In this tutorial, we will continue implementing an extension for the Criteria API using the Builder pattern and JPA Static Metamodel Generator. Please check the previous part:
Improving your experience with Criteria API using Builder pattern and JPA Static Metamodel - Part I
The full source code is available over on Github.
In this part, we will improve the BaseQuery interface to allow you to use the or/and predicates with a nested sub-path chain, providing us the ability to switch from this block of code:
List<Order> listOfOrders = criteriaApiHelper.select(Order.class)
.equal(Order_.state, OrderStatus.Shipped)
.like(Order_.customer, Customer_.address, Address_.addressLine, "11 Aleksandr Pushkin St")
.equal(Order_.customer, Customer_.address, Address_.city, City_.name, "Tbilisi")
.equal(Order_.customer, Customer_.address, Address_.city, City_.country, Country_.name, “Georgia”)
.findAll();
To this:
List<Order> listOfOrders = criteriaApiHelper.select(Order.class)
.equal(Order_.status, OrderStatus.Shipped)
.and(Order_.customer, Customer_.address,
like(Address_.addressLine, "11 Aleksandr Pushkin St"),
and(Address_.city,
equal(City_.name, "Tbilisi"),
equal(City_.country, Country_.name, "Georgia")
)
)
.findAll();
First of all, we need to define a way to build nested or/and predicates. It’s quite problematic to split the builder chain if we need to specify multiple nested predicates within one or/and predicate. To resolve this, we can delegate this nested predicates logic to the additional QueryPart
class that can hold its own predicates separately outside of the main builder chain:
public class QueryPart<R> extends BaseQueryImpl<R, QueryPart<R>> {
@Override
protected QueryPart<R> self() {
return this;
}
}
Next, let’s introduce new methods in the BaseQuery
interface for the or/and predicates, accepting QueryPart<R>
varargs arguments:
public interface BaseQuery<R, Q extends BaseQuery<R, Q>> {
…
Q and(QueryPart<R>... partQueries);
Q or(QueryPart<R>... partQueries);
}
Also, if we want to specify multiple predicates for one nested table, we can define a parent-child attributes chain to specify the nested relation only once:
public interface BaseQuery<R, Q extends BaseQuery<R, Q>> {
...
<P> Q and(SingularAttribute<R, P> attribute, QueryPart<P>... partQueries);
<P1, P2> Q and(SingularAttribute<R, P1> attribute1, SingularAttribute<P1, P2> attribute2, QueryPart<P2>... partQueries);
<P1, P2, P3> Q and(SingularAttribute<R, P1> attribute1, SingularAttribute<P1, P2> attribute2, SingularAttribute<P2, P3> attribute3, QueryPart<P3>... partQueries);
<P> Q or(SingularAttribute<R, P> attribute, QueryPart<P>... partQueries);
<P1, P2> Q or(SingularAttribute<R, P1> attribute1, SingularAttribute<P1, P2> attribute2, QueryPart<P2>... partQueries);
<P1, P2, P3> Q or(SingularAttribute<R, P1> attribute1, SingularAttribute<P1, P2> attribute2, SingularAttribute<P2, P3> attribute3, QueryPart<P3>... partQueries);
}
Next, let’s implement these defined methods. To build all the QueryPart
-s, we should combine nested predicates using the BaseQueryImpl#buildPredicates()
method and pass the result to the CriteriaBuilder#and()
method:
Predicate[] predicates = Arrays.stream(partQueries)
.map(partQuery -> cb.and(buildPredicates(CriteriaQuery, partQuery.predicates, CriteriaBuilder, Root)))
.toArray(Predicate[]::new);
But, as you may see, we can’t access the CriteriaQuery
, CriteriaBuilder
and Root
parameters. For this reason, we have provided the QueryPredicate
interface in the previous part:
@FunctionalInterface
public interface QueryPredicate<R> {
Predicate apply(CommonAbstractCriteria criteria, CriteriaBuilder cb, Path<R> root);
}
public abstract class BaseQueryImpl<R, Q extends BaseQueryImpl<R, Q>> implements BaseQuery<R, Q> {
protected final Collection<QueryPredicate<R>> predicates;
...
}
Therefore, to reach the CriteriaQuery
, CriteriaBuilder
and Root
we can just create an instance of QueryPredicate
:
new QueryPredicate<R>() {
@Override
public Predicate apply(CommonAbstractCriteria criteria, CriteriaBuilder cb, Path<R> root) {
Predicate[] predicates = Arrays.stream(partQueries)
.map(partQuery -> cb.and(buildPredicates(criteria, partQuery.predicates, cb, root)))
.toArray(Predicate[]::new);
...
}
};
Lastly, to build and return the Predicate
instance, we should pass the predicates to the CriteriaBuilder#and()
for the and predicate and to the CriteriaBuilder#or()
for the or predicate accordingly:
public abstract class BaseQueryImpl<R, Q extends BaseQueryImpl<R, Q>> implements BaseQuery<R, Q> {
...
@SafeVarargs
@Override
public final Q and(QueryPart<R>... partQueries) {
predicates.add((criteria, cb, root) -> {
Predicate[] predicates = Arrays.stream(partQueries)
.map(partQuery -> cb.and(buildPredicates(criteria, partQuery.predicates, cb, root)))
.toArray(Predicate[]::new);
return cb.and(predicates);
});
return self();
}
@SafeVarargs
@Override
public final Q or(QueryPart<R>... partQueries) {
predicates.add((criteria, cb, root) -> {
Predicate[] predicates = Arrays.stream(partQueries)
.map(partQuery -> cb.or(buildPredicates(criteria, partQuery.predicates, cb, root)))
.toArray(Predicate[]::new);
return cb.or(predicates);
});
return self();
}
}
Next, let’s implement methods with the nested parent-child relations chain accepting additional SingularAttribute
arguments. The only difference with these methods is that you should reach and pass the required Path
instance using the provided attributes to the BaseQueryImpl#buildPredicates()
method:
public abstract class BaseQueryImpl<R, Q extends BaseQueryImpl<R, Q>> implements BaseQuery<R, Q> {
...
@SafeVarargs
@Override
public final <P> Q and(SingularAttribute<R, P> attribute, QueryPart<P>... partQueries) {
predicates.add((criteria, cb, root) -> {
Predicate[] predicates = Arrays.stream(partQueries)
.map(partQuery -> cb.and(buildPredicates(criteria, partQuery.predicates, cb, root.get(attribute))))
.toArray(Predicate[]::new);
return cb.and(predicates);
});
return self();
}
@SafeVarargs
@Override
public final <P1, P2> Q and(SingularAttribute<R, P1> attribute1, SingularAttribute<P1, P2> attribute2, QueryPart<P2>... partQueries) {
predicates.add((criteria, cb, root) -> {
Predicate[] predicates = Arrays.stream(partQueries)
.map(partQuery -> cb.and(buildPredicates(criteria, partQuery.predicates, cb, root.get(attribute1).get(attribute2))))
.toArray(Predicate[]::new);
return cb.and(predicates);
});
return self();
}
@SafeVarargs
@Override
public final <P1, P2, P3> Q and(SingularAttribute<R, P1> attribute1, SingularAttribute<P1, P2> attribute2, SingularAttribute<P2, P3> attribute3, QueryPart<P3>... partQueries) {
predicates.add((criteria, cb, root) -> {
Predicate[] predicates = Arrays.stream(partQueries)
.map(partQuery -> cb.and(buildPredicates(criteria, partQuery.predicates, cb, root.get(attribute1).get(attribute2).get(attribute3))))
.toArray(Predicate[]::new);
return cb.and(predicates);
});
return self();
}
@SafeVarargs
@Override
public final <P> Q or(SingularAttribute<R, P> attribute, QueryPart<P>... partQueries) {
predicates.add((criteria, cb, root) -> {
Predicate[] predicates = Arrays.stream(partQueries)
.map(partQuery -> cb.or(buildPredicates(criteria, partQuery.predicates, cb, root.get(attribute)))).toArray(Predicate[]::new);
return cb.or(predicates);
});
return self();
}
@SafeVarargs
@Override
public final <P1, P2> Q or(SingularAttribute<R, P1> attribute1, SingularAttribute<P1, P2> attribute2, QueryPart<P2>... partQueries) {
predicates.add((criteria, cb, root) -> {
Predicate[] predicates = Arrays.stream(partQueries)
.map(partQuery -> cb.or(buildPredicates(criteria, partQuery.predicates, cb, root.get(attribute1).get(attribute2)))).toArray(Predicate[]::new);
return cb.or(predicates);
});
return self();
}
@SafeVarargs
@Override
public final <P1, P2, P3> Q or(SingularAttribute<R, P1> attribute1, SingularAttribute<P1, P2> attribute2,
SingularAttribute<P2, P3> attribute3, QueryPart<P3>... partQueries) {
predicates.add((criteria, cb, root) -> {
Predicate[] predicates = Arrays.stream(partQueries)
.map(partQuery -> cb.or(buildPredicates(criteria, partQuery.predicates, cb, root.get(attribute1).get(attribute2).get(attribute3)))).toArray(Predicate[]::new);
return cb.or(predicates);
});
return self();
}
}
Now, we can finish the implementation by providing static methods in the CriteriaApiHelper
, allowing us to create QueryPart
interfaces from any part of the code:
public class CriteriaApiHelper {
...
public static <R, V> QueryPart<R> equal(SingularAttribute<R, V> attribute, V value) {
return new QueryPart<R>()
.equal(attribute, value);
}
public static <R, V> QueryPart<R> notEqual(SingularAttribute<R, V> attribute, V value) {
return new QueryPart<R>()
.notEqual(attribute, value);
}
public static <R> QueryPart<R> isTrue(SingularAttribute<R, Boolean> attribute) {
return new QueryPart<R>()
.isTrue(attribute);
}
public static <R> QueryPart<R> isFalse(SingularAttribute<R, Boolean> attribute) {
return new QueryPart<R>()
.isFalse(attribute);
}
public static <R, V> QueryPart<R> isNull(SingularAttribute<R, V> attribute) {
return new QueryPart<R>()
.isNull(attribute);
}
public static <R, V> QueryPart<R> isNotNull(SingularAttribute<R, V> attribute) {
return new QueryPart<R>()
.isNotNull(attribute);
}
public static <R, V extends Comparable<? super V>> QueryPart<R> greaterThan(SingularAttribute<R, V> attribute, V value) {
return new QueryPart<R>()
.greaterThan(attribute, value);
}
public static <R, V extends Comparable<? super V>> QueryPart<R> lessThan(SingularAttribute<R, V> attribute,
V value) {
return new QueryPart<R>()
.lessThan(attribute, value);
}
public static <R> QueryPart<R> like(SingularAttribute<R, String> attribute, String value) {
return new QueryPart<R>()
.like(attribute, value);
}
@SafeVarargs
public static <R> QueryPart<R> and(QueryPart<R>... partQueries) {
return new QueryPart<R>()
.and(partQueries);
}
@SafeVarargs
public static <R> QueryPart<R> or(QueryPart<R>... partQueries) {
return new QueryPart<R>()
.or(partQueries);
}
}
In this part, we slightly improved the extension for the Criteria API. Now we can perform or/and predicates with the common parent-child relation:
import static org.example.criteria.api.helper.CriteriaApiHelper.or;
import static org.example.criteria.api.helper.CriteriaApiHelper.equal;
import static org.example.criteria.api.helper.CriteriaApiHelper.like;
List<Order> listOfOrders = criteriaApiHelper.select(Order.class)
.equal(Order_.status, OrderStatus.Shipped)
.and(Order_.customer, Customer_.address,
like(Address_.addressLine, "11 Aleksandr Pushkin St"),
or(Address_.city,
equal(City_.name, "Tbilisi"),
equal(City_.country, Country_.name, "Georgia")
)
)
.findAll();