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