How to use Stream API and Lambda functions for flexible JPA criteria queries construction.
One of the things I found interesting and evolutionary after 2 years away from actual coding is the way of constructing JPA criteria queries using object-oriented approach. How I got involved in this you can read here.
Approach
Simple enough:
1.Define criteria methods
The methods return
[java]
org.springframework.data.jpa.domain.Specification
[/java]
and trough Lambda functions define the JPA criterias.
[java]
public Specification<User> activeUsers(long from) {
return (root, query, cb) -> {
Subquery<Integer> sq = query.subquery(Integer.class);
return cb.and(
cb.greaterThanOrEqualTo(root.get(User_.createdDate), new Date(from)),
cb.isTrue(root.get(User_.enabled)),
cb.exists(sq.select(cb.literal(1)).where(cb.equal(root,
sq.from(RssReadTask.class).get(RssReadTask_.user)))));
};
}
public Specification<User> proUsers() {
return (root, query, cb) -> {
final String proUserPermissionsName = ProUserPermissions.class.getSimpleName();
return cb.and(
cb.or(
cb.equal(root.get(User_.userLevel), proUserPermissionsName),
cb.equal(root.get(User_.userLevel),
proUserPermissionsName.toLowerCase())
)
);
};
}
public Specification<User> userByCreatedRange(Long from, Long to) {
return (root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
if (from != null && from > 0) {
predicates.add(cb.greaterThanOrEqualTo(root.get(User_.createdDate),
new Date(from)));
}
if (to != null && to > 0) {
predicates.add(cb.lessThanOrEqualTo(root.get(User_.createdDate),
new Date(to)));
}
Predicate[] p = predicates.toArray(new Predicate[predicates.size()]);
return p.length == 0 ? null : p.length == 1 ? p[0] : cb.and(p);
};
}
[/java]
2. Define some helper methods and classes
[java]
private <T> CriteriaQuery<T> applyFilter(CriteriaQuery<T> query, Class<?> cls,
Specification filter, Root<?> root, CriteriaBuilder cb) {
Predicate[] where = Stream.of(filter)
.filter(f -> f != null)
.map(new FilterPredicateFunction<>(root, query, cb))
.filter(f -> f != null)
.toArray(Predicate[]::new);
return where.length > 0 ? query.where(where) : query;
}
private static class FilterPredicateFunction<T> implements java.util.function.
Function<Specification, Predicate> {
private final Root<T> root;
private final CriteriaQuery<?> query;
private final CriteriaBuilder cb;
public FilterPredicateFunction(Root<T> root, CriteriaQuery<?> query,
CriteriaBuilder cb) {
this.root = root;
this.query = query;
this.cb = cb;
}
@Nullable
@Override
@SuppressWarnings("unchecked")
public Predicate apply(@Nullable Specification input) {
if (input instanceof Specification) {
return input.toPredicate(root, query, cb);
} else {
throw new IllegalArgumentException();
}
}
}
@Override
public Specification and(Specification… filters) {
return (root, query, cb) ->
cb.and(Arrays.asList(filters)
.stream()
.filter(f -> f != null)
.map(f -> (f.toPredicate(root, query, cb))
.filter(f -> f != null)
.toArray(Predicate[]::new));
}
[/java]
3.Use
The following method queries the DB for all the users that have more than one RssReadTask object associated, have certain permission object a associated and are created in a specified time range. Adding different criterias based on input parameters is quite easy.
[java]
@RequestMapping(value = "/count", method = RequestMethod.GET, params = {"activeOnly"})
public Long count(
@RequestParam(value = "from", defaultValue = "0") long from,
@RequestParam(value = "to", defaultValue = "0") long to,
@RequestParam(value = "activeOnly", defaultValue = "false") boolean activeOnly,
@RequestParam(value = "proOnly", defaultValue = "false") boolean proOnly) {
List<org.springframework.data.jpa.domain.Specification> filterList
= new ArrayList<>();
if (activeOnly) {
filterList.add(activeUsers(from));
}
if (proOnly) {
filterList.add(proUsers());
}
filterList.add(userByCreatedRange(from, to)))
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Long> query = cb.createQuery(Long.class);
Root<User> root = query.from(User.class);
return em.createQuery(applyFilter(query.select(cb.count(root)), User.class,
and(filterList), root, cb, false)).getSingleResult();
}
[/java]
Conclusion
The best thing about the approach is that all the criteria methods activeUsers, proUsers, userByCreatedRange are reusable and can be applied in more complex queries using the Java 8 Streams API.