JPA Criteria API with Lambda expressions

Java Persistence API (JPA) has two ways of querying database – JPQL and Criteria API. JPQL queries are written as simple strings, usually easy to read, but as strings, they cannot be checked by a Java compiler. If an entity attribute gets renamed, all queries using it break without giving any warning (tests can help). JPA Criteria API is better in that, especially when used with generated entity metadata classes. But the Criteria API has its own issues – it’s rather hard to read and write and it’s easy to forget if you don’t work with it for a while.

Reasons

I was thinking about how to make Criteria API code easier. The problem is that you usually need several objects to build a query and the calls cannot be streamlined. it’s possible to write a callback interface with a method getting all the necessary parameters to build a query, but that’s just making a new API on top of the existing one without it being significantly easier. Then I realized that Java 8 comes with Lambda expression support. It allows us to write basic functions without a need to wrap them in classes. Lambda expressions can be used as method parameters where an object implementing a single method interface is expected.

Implementation

With Lambda expressions, it’s possible to write a database query on one line with good readability and compiler checks. So to combine the best of both JPQL and Criteria API. Here is an example of a method that makes it possible:

public <T> List<T> findWhere(Class<T> entityClass, PredicateBuilder<T> pb) {
    CriteriaBuilder cb = em().getCriteriaBuilder();
    CriteriaQuery<T> q = cb.createQuery(entityClass);
    Root<T> root = q.from(entityClass);
    ParameterExpression<Integer> p = cb.parameter(Integer.class);
    CriteriaQuery<T> criteriaQuery = q.select(root);
    criteriaQuery.where(pb.build(cb, root));
    TypedQuery<T> typedQuery = em.createQuery(criteriaQuery);
    return typedQuery.getResultList();
}

public interface PredicateBuilder<T> {
    Predicate build(CriteriaBuilder criteriaBuilder, Root<T> root);
}

… and its usage:

findWhere(Car.class, (cb, root) -> (cb.equal(root.get(Car_.colour), colour)));

This was a simple query. But what if you want to set up result ordering or use to use some other advanced functionality? For that, it’s necessary to have access to a CriteriaQuery. It’s easy to make a three parameter method interface and call the method like this:

findWhere(Car.class, (cb, root, query)
    -> (query.where(cb.equal(root.get(Car_.colour), colour))
             .orderBy(cb.desc(root.get(Car_.id)))));

The code is not so nice anymore, byt it’s still much better than a pure Criateria API method. For more complex queries, it’s possible to use lambda expressions with multiple statements:

findWhere(Car.class, (criteriaBuilder, root) -> {
        root.fetch(Driver_.cars);
        return criteriaBuilder.isMember(car, root.get(Driver_.cars));
});

And for even more control, you can write a new createQuery method (similar to findWhere) which will return a TypedQuery instead of the final result:

createQuery(Car.class, (cb, root) -> (cb.equal(root.get(Car_.colour), colour)))
        .setMaxResults(10)
        .setFistResult(5)
        .getResultList();

Conclusion

Lambda queries can useful for making JPA code nicer. For complicated queries, it’s probably better to use plain Criteria API or JPQL, but many other queries can be simplified using Lambda expressions. I have prepared a working demo project you can download and modify to your taste. It’s a standalone application requiring only Maven for building it.

Tento obsah bol zaradený v Java. Zálohujte si trvalý odkaz.

Nie je možné pridávať komentáre.