Skip to content
Chapter 32 · Spring Boot

JPA Part 10: The Specification API ​

The Criteria API from part 9 works, but it has two problems. The Specification API from Spring Data JPA solves both.

Problem 1: duplicated conditions ​

java
public List<UserDetails> getUserDetailsByPhoneCriteriaAPI(Long phoneNo) {
    CriteriaBuilder cb = entityManager.getCriteriaBuilder();
    CriteriaQuery<UserDetails> crQuery = cb.createQuery(UserDetails.class);
    Root<UserDetails> user = crQuery.from(UserDetails.class);
    crQuery.select(user);

    // the same condition may be needed in many methods and classes
    Predicate predicate = cb.equal(user.get("phone"), phoneNo);
    crQuery.where(predicate);

    return entityManager.createQuery(crQuery).getResultList();
}

The same predicate ("phone equals ...") tends to be written again in every method that needs it.

Specification ​

A Specification<T> is a functional interface for one condition. Its only abstract method builds the predicate:

java
Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb);

and it has helpers to combine conditions:

MethodSQL
toPredicate(...)the condition itself (abstract; you implement it)
spec1.and(spec2)cond1 AND cond2
spec1.or(spec2)cond1 OR cond2
Specification.not(spec1)NOT cond1

Put the conditions in one class, as lambdas:

java
public class UserSpecification {

    public static Specification<UserDetails> equalsPhone(Long phoneNo) {
        return (root, query, cb) -> cb.equal(root.get("phone"), phoneNo);
    }

    public static Specification<UserDetails> likeName(String name) {
        return (root, query, cb) -> cb.like(root.get("name"), "%" + name + "%");
    }
}

Any method can now reuse them:

java
Specification<UserDetails> specification = UserSpecification.equalsPhone(phoneNo);
Predicate predicate = specification.toPredicate(userRoot, crQuery, cb);
crQuery.where(predicate);

Problem 2: boilerplate ​

Even with the condition moved out, look at what is left:

java
public List<UserDetails> getUserDetailsByPhoneSpecificationAPI(Long phoneNo) {
    CriteriaBuilder cb = entityManager.getCriteriaBuilder();                 // manage a CriteriaBuilder
    CriteriaQuery<UserDetails> crQuery = cb.createQuery(UserDetails.class);  // manage a CriteriaQuery
    Root<UserDetails> userRoot = crQuery.from(UserDetails.class);            // manage a Root
    crQuery.select(userRoot);

    Specification<UserDetails> specification = UserSpecification.equalsPhone(phoneNo);
    crQuery.where(specification.toPredicate(userRoot, crQuery, cb));         // manage the Predicate

    TypedQuery<UserDetails> query = entityManager.createQuery(crQuery);      // execute it
    query.setFirstResult(0);
    query.setMaxResults(5);
    return query.getResultList();
}

All we really want to say is: which table (and joins), which columns, and which conditions. Creating the objects, building and running the query should be JPA's job.

JpaSpecificationExecutor ​

Extend it in the repository, which stays empty:

java
@Repository
public interface UserDetailsRepository extends JpaRepository<UserDetails, Long>,
                                               JpaSpecificationExecutor<UserDetails> {
}

It adds methods that take a Specification: findAll(spec), findAll(spec, pageable), findAll(spec, sort), findOne(spec), count(spec), exists(spec). Inside, Spring Data does exactly the boilerplate above:

java
// Spring Data's code, simplified
protected <S extends T> TypedQuery<S> getQuery(Specification<S> spec, Class<S> domainClass, Sort sort) {
    CriteriaBuilder builder = entityManager.getCriteriaBuilder();
    CriteriaQuery<S> query = builder.createQuery(domainClass);

    Root<S> root = query.from(domainClass);
    if (spec != null) {
        Predicate predicate = spec.toPredicate(root, query, builder);
        if (predicate != null) {
            query.where(predicate);
        }
    }
    query.select(root);

    if (sort.isSorted()) {
        query.orderBy(toOrders(sort, root, builder));
    }
    return entityManager.createQuery(query);
}

Note the if (predicate != null): a specification may return null, meaning "no condition".

Using it ​

java
public class UserSpecification {

    public static Specification<UserDetails> equalsPhone(Long phoneNo) {
        return (root, query, cb) -> cb.equal(root.get("phone"), phoneNo);
    }

    public static Specification<UserDetails> likeName(String name) {
        return (root, query, cb) -> cb.like(root.get("name"), "%" + name + "%");
    }

    public static Specification<UserDetails> joinAddress() {
        return (root, query, cb) -> {
            root.join("userAddress", JoinType.INNER);
            return null;   // no condition, only the join
        };
    }
}
java
public List<UserDetails> getUserDetailsByPhoneSpecificationAPI() {
    Specification<UserDetails> result = Specification.where(UserSpecification.joinAddress())
            .and(UserSpecification.equalsPhone(123L))
            .and(UserSpecification.likeName("AA"));

    return userDetailsRepository.findAll(result);
}

The query becomes SELECT ... FROM user_details JOIN user_address ... WHERE phone = 123 AND user_name LIKE '%AA%'. and and or combine left and right sides, resolving nested combinations recursively.

Specifications are meant for conditions. There is no dedicated method for joins, so joinAddress is a small trick: it adds the join to the root and returns null for "no condition".

Pagination and sorting work as usual:

java
Page<UserDetails> page = userDetailsRepository.findAll(
        UserSpecification.equalsPhone(123L),
        PageRequest.of(0, 5, Sort.by("name")));

Compare with the same query written with the Criteria API:

java
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<UserDetails> crQuery = cb.createQuery(UserDetails.class);
Root<UserDetails> user = crQuery.from(UserDetails.class);
user.join("userAddress", JoinType.INNER);
crQuery.select(user);
Predicate predicate1 = cb.equal(user.get("phone"), 123L);
Predicate predicate2 = cb.like(user.get("name"), "%AA%");
crQuery.where(cb.and(predicate1, predicate2));
return entityManager.createQuery(crQuery).getResultList();

The specification version reads as the conditions themselves, and each condition is written once and reused.

Next: That completes data access with JPA, and with it the core of an application: controllers, beans, configuration, transactions and persistence. The next module is security. Chapter 33 starts with what security must stop: it demonstrates the common web attacks (CSRF, XSS, SQL injection) and CORS.

Check yourself ​

  1. What two problems of Criteria API code does the Specification API solve?
  2. What is a Specification<T>, and which method do you implement?
  3. How do you combine specifications with AND, OR and NOT?
  4. What must the repository extend to accept specifications, and which methods does that add?
  5. What does a specification that returns null mean?
  6. How do you add a join with specifications?
  7. How do you get a sorted page of results for a specification?
Answers
  1. Duplicated conditions (the same predicate in many places) and boilerplate (CriteriaBuilder, CriteriaQuery, Root, execution).
  2. A functional interface for one condition; toPredicate(root, query, cb).
  3. spec1.and(spec2), spec1.or(spec2), Specification.not(spec1).
  4. JpaSpecificationExecutor<T>: findAll(spec), findAll(spec, pageable), findAll(spec, sort), findOne, count, exists.
  5. No condition; Spring Data adds no WHERE part for it.
  6. In a specification that calls root.join(...) and returns null.
  7. repository.findAll(spec, PageRequest.of(page, size, Sort.by("field"))).