2011-09-19

First Look at Querydsl with JPA 2

Today I integrated integrated Querydsl into a java webapp that I recently started coding.

The project was already using JPA 2 (Hibernate). I used JPQL to implement an initial set of finder methods for some simple use-cases, but I decided to explore alternatives to JPQL when I reached a use-case that required me to build a query dynamically based on a search form that contains about half a dozen search fields.

In the past, before JPA 2, I used to use Hibernate's proprietary criteria API in these cases, and my search methods would dynamically build a criteria with code like this:

1
2
3
4
5
6
7
8
public List<User> findAll(UserSearchForm userSearchForm) {
    Criteria criteria = getSession().createCriteria(User.class);
    if( userSearchForm.getLastName() != null ) {
        criteria.add( Restrictions.eq("lastName", userSearchForm.getLastName()) );
    }
    // ... more conditional checks + more restrictions
    return criteria.list();
}

JPA 2 has a criteria API, and I spent a little time today reading documentation to learn the new API. After reading it over for a while, I concluded that I don't think the JPA 2 criteria API is productive for human consumption.

If I were to implement the above sample code with the JPA 2 criteria API, I believe it would read something like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// NOTE: I did not test this code. This is an example typed into my text editor
// freehand.
public List<User> findAll(UserSearchForm userSearchForm) {
    CriteriaBuilder cb = em().getCriteriaBuilder;           // CriteriaBuilder: class #1
    CriteriaQuery<User> cq = cb.createQuery(User.class);    // CriteriaQuery: class #2
    Root<User> user = cq.from(User.class);                  // Root: class #3

    List<Predicate> allCriteria = new ArrayList<Predicate>();   // Predicate: class #4
    if (userSearchForm.getLastName() != null) {
        // ParameterExpression: class #5
        ParameterExpression<String> px = cb.parameter(String.class, "lastName");
        allCriteria.add(cb.equal(user.get("lastName"), px));
    }
    // ... more

    // no error checking for 0 case
    if(allCriteria.size() == 1) {
        cq.where(allCriteria.get(0));
    } else {
        cq.where(cb.and( allCriteria.toArray(new Predicate[allCriteria.size()])));
    }

    TypedQuery<User> q = em.createQuery(cq);                // TypedQuery: class #7
    if( userSearchForm.getLastName() != null )
        q.setParameter("lastName", userSearchForm.getLastName());
    // ... more
    return q.getResultList();
}

The JPA 2 criteria API also supports using the canonical metamodel to create criteria queries with type-safe component expressions. I'm not going to provide an example here because the major classes used by the typesafe API are the same (CriteriaQuery, Root, CriteriaBuilder, etc), and because examples are easy enough to find with a simple web search.

While the API is very flexible, to me the component-classes feel like AST node classes designed to be written and read by a machine interpreter rather than a human. IMHO, it fails the StringBuilder litmus test. If I rewrote it to dynamically build a JPQL query using a StringBuilder, I believe it would be shorter and more readable. When APIs designed to build expressions require the use of many classes and verbose statements even for simple cases, the resultant code is hard to read. Code that is hard to read is hard to debug, and code that is hard to debug is more likely to contain bugs (it also takes longer to write, longer to test, etc.) Basically, this API is screaming for a human-friendly DSL facade.

Enter Querydsl. The mysema blog alread has a nice comparison of JPA 2 Criteria and Querydsl queries, but I'll briefly write an example of how the above code would look in Querydsl:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// again, this is not tested. let me know if you spot an error
public List<User> findAll(UserSearchForm userSearchForm) {
    QUser user = QUser.user;
    JPQLQuery query = queryFrom(user);  // queryFrom is just new JPAQuery(em()).from(user)
    if (userSearchForm.getLastName() != null) {
        query.where(user.lastName.eq(userSearchForm.getLastName()));
    }
    // ... apply more restrictions conditionally
    return query.list(user);
}

Note that, while Querydsl uses a method-chaining fluent interface, most (all?) methods that can be invoked on Querydsl's JPQLQuery will mutate the query object (i.e., you do not need to write query = query.where(predicateA); query = query.where(predicateB); ... when building a query with multiple statements) . So we can chain together all the method calls into a compound statement, or we can build a query object with multiple statements. This option gives us terse, simple code for simple cases while still supporting more complex cases that require building a query dynamically based on a series of conditions.

Well, this blog post has more to do with my reaction toward the JPA 2 Criteria API than it has to do with Querydsl. Tomorrow, I'll write a post about some gotchas that I ran into with Querydsl + JPA.

No comments:

Post a Comment