Using Spring Data JPA Specification

Spring Data JPA was created primarily to allow easy query creation with query generation by method name. However, sometimes, we need to create complex queries and cannot take advantage of a query generator.

The Spring Data JPA provides a repository programming model that starts with an interface per managed domain object. Defining these interfaces serves two purposes: first, by extending the JpaRepository, we get a bunch of generic CRUD methods, like save, findAll, delete, and so on. Second, this will allow the Spring Data JPA repository infrastructure to scan the classpath for this interface and create a Spring bean for it. A typical repository interface will look something like this:

public interface CustomerRepository extends JpaRepository<Customer, Long> {
  Customer findByEmailAddress(String emailAddress);
  List<Customer> findByLastname(String lastname, Sort sort);
  Page<Customer> findByFirstname(String firstname, Pageable pageable);
}

To Create Complex Queries, Why Specification?

Yes, complex queries can be build using the Criteria API. To understand why we should use specification, let’s consider a simple business requirement. We will implement this requirement using Criteria API and after with specification.

Here’s is the use case: on a customer’s birthday, we want to send a voucher to all long-term customers. How do we retrieve one that matches?

We have two predicates:

  • Birthday
  • Long-term customer — let us assume that the customer created an account at least two years ago.

Here’s how it would with the implementation using the JPA 2.0 Criteria API:

LocalDate today = new LocalDate();

CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<Customer> query = builder.createQuery(Customer.class);
Root<Customer> root = query.from(Customer.class);

Predicate hasBirthday = builder.equal(root.get(Customer_.birthday), today);
Predicate isLongTermCustomer = builder.lessThan(root.get(Customer_.createdAt), today.minusYears(2); 
query.where(builder.and(hasBirthday, isLongTermCustomer));
em.createQuery(query.select(root)).getResultList();

In the above code,

LocalDate

The main problem with this code is that predicates are not easy to externalize and reuse because you need to set up the CriteriaBuilder, CriteriaQuery , and Root first. Also, code readability is poor because it is hard to quickly infer the intent of the code.

Specification

To be able to define reusable predicates, we introduced the specification interface that is derived from concepts introduced in Eric Evans’ Domain Driven Design book. It defines a specification as a predicate over an entity, which is exactly what our Specification interface represents. This actually only consists of a single method:

public interface Specification<T> {
  Predicate toPredicate(Root<T> root, CriteriaQuery query, CriteriaBuilder cb);
}

When using Java 8, the code becomes very clear and easy to understand.

public CustomerSpecifications {
  public static Specification<Customer> customerHasBirthday() {
return (root, query, cb) ->{ 
return cb.equal(root.get(Customer_.birthday), today);
};
 }

 public static Specification<Customer> isLongTermCustomer() {
return (root, query, cb) ->{ 
        return cb.lessThan(root.get(Customer_.createdAt), new LocalDate.minusYears(2));
};
}
}

A client can now perform the following:

customerRepository.findAll(hasBirthday());
customerRepository.findAll(isLongTermCustomer());

Here, the basic implementation will prepare the CriteriaQueryRoot , and  CriteriaBuilder for you, apply the predicate created by the given specification, and execute the query.

We just created the reusable predicates that can be individually executed. We can combine these individual predicates to meet our business requirement. We have a helper class, specifications, that provides and(…) and  or(…) methods to concatenate atomic specifications.

customerRepository.findAll(where(customerHasBirthday()).and(isLongTermCustomer()));

This reads fluently, improving readability as well as providing additional flexibility when compared to the use of the JPA Criteria API alone. The only caveat here is that coming up with the specification implementation requires quite some coding effort.

Here are some benefits of specifications:

  1. All "basic" queries are already implemented, i.e. findById, save, and delete
  2. Pagination works out of the box. You can pass a Pageable object simply from the Controller to the Service to your Repository, and it just works (even sorting)!
  3. Using Spring's Specification API is a little bit more simple than plain JPA stuff because you just create predicates and don't have to mess around with the  EntityManager and PersistenceContext .

If you have anything that you want to add or share, then please leave a message in the comment section below.

Happy learning!