Improving your experience with Criteria API using Builder pattern and JPA Static Metamodel - Part II

Written by alexmakeev1995 | Published 2022/10/03
Tech Story Tags: java | hibernate | jpa | spring | spring-data-jpa | sql | generics | learn-to-code

TLDRIn this tutorial, we continue implementing an extension for the Criteria API using the Builder pattern and JPA Static Metamodel Generator. 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:. listOfOrders = criteriaApiHelper.select(Order) to listOf-Orders. The full source code is available over on GitHub.via the TL;DR App

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();

1. Implementation

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);
    }
}

2 Wrapping up

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();


Written by alexmakeev1995 | Senior SWE at Layermark
Published by HackerNoon on 2022/10/03