Improving Your Experience With CriteriaAPI Using Builder Pattern and JPA Static Metamodel - Part III

Written by alexandermakeev | Published 2022/10/04
Tech Story Tags: java | hibernate | spring | spring-data | spring-data-jpa | jpa | database | sql

TLDRIn this chapter, we will continue implementing an extension for the Criteria API using Builder pattern and JPA Static Metamodel Generator to reduce complexity with explicitness and to increase readabilityvia 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 parts:

In this chapter, we provide the IN (subquery) operator, for example:

List<Order> ordersToSpainWithActiveMembership = criteriaApiHelper.select(Order.class)
        .in(Order_.customer,
                subquery(CustomerMembership.class)
                        .isTrue(CustomerMembership_.isActive)
                        .select(CustomerMembership_.customer),
                subquery(CustomerAddress.class)
                        .equal(CustomerAddress_.country, Country_.name, "Spain")
                        .select(CustomerAddress_.customer)
        )
        .findAll();

This query should fetch orders for the customers matching two subqueries: the customers with an active membership and the customers who live in Spain.

1. SubqueryPredicate

First of all, to store the javax.persistence.criteria.Subquery details, we define the SubqueryPredicate interface. It will help us to construct the javax.persistence.criteria.Subquery the same way as QueryPredicate, which we created earlier to construct the javax.persistence.criteria.Predicate:

@FunctionalInterface
public interface SubqueryPredicate<P> {
    Subquery<P> apply(CommonAbstractCriteria criteria, CriteriaBuilder cb);
}

2. SubQuery

In this step, we create the SubQuery interface to be able to filter the subquery using predicates methods from the inheriting BaseQuery interface:

public interface SubQuery<R, Q extends SubQuery<R, Q>> extends BaseQuery<R, Q> {
}

Then after we provided the necessary filters with BaseQuery methods, in order to finish a subquery, we may need to specify the custom subquery’s path to select that will be used for the IN operator.

For example, with the following select() method, we can select the Customer entity from the root CustomerAddress entity type:

subquery(CustomerAddress.class)
        .equal(CustomerAddress_.country, Country_.name, "Spain")
        .equal(CustomerAddress_.city, City_.name, "Madrid")
        .select(CustomerAddress_.customer)

Let’s provide the necessary select() methods with a different set of a parent-child relations chain, returning the SubqueryPredicate instance, which we can use later to construct the javax.persistence.criteria.Subquery:

public interface SubQuery<R, Q extends SubQuery<R, Q>> extends BaseQuery<R, Q> {
    SubqueryPredicate<R> select();

    <P> SubqueryPredicate<P> select(SingularAttribute<R, P> attribute);

    <P1, P2> SubqueryPredicate<P2> select(SingularAttribute<R, P1> attribute1, SingularAttribute<P1, P2> attribute2);

    <P1, P2, P3> SubqueryPredicate<P3> select(SingularAttribute<R, P1> attribute1, SingularAttribute<P1, P2> attribute2, SingularAttribute<P2, P3> attribute3);
}

Next, let’s create the implementation of the SubQuery interface, and implement the defined select() methods:

public class SubQueryImpl<R> extends BaseQueryImpl<R, SubQueryImpl<R>> implements SubQuery<R, SubQueryImpl<R>> {
}

When we construct a subquery, first we specify the Root type as the entity that we use to start a query from. Therefore, the constructor should accept the Root type:

public class SubQueryImpl<R> extends BaseQueryImpl<R, SubQueryImpl<R>> implements SubQuery<R, SubQueryImpl<R>> {
    private final Class<R> type;

    public SubQueryImpl(Class<R> type) {
        this.type = type;
    }

    @Override
    protected SubQueryImpl<R> self() {
        return this;
    }
}

Next, we implement the defined select() methods. Each method should return an instance of SubqueryPredicate:

new SubqueryPredicate<R>() {
    @Override
    public Subquery<R> apply(CommonAbstractCriteria criteria, CriteriaBuilder cb) {
        Subquery<R> subquery = criteria.subquery(type);
        Root<R> subqueryRoot = subquery.from(type);
        subquery
                .select(subqueryRoot)
                .where(buildPredicates(subquery, predicates, cb, subqueryRoot));
        return subquery;
    }
};

The complete implementation of all the defined select() methods:

public class SubQueryImpl<R> extends BaseQueryImpl<R, SubQueryImpl<R>> implements SubQuery<R, SubQueryImpl<R>> {
    ...

    @Override
    public SubqueryPredicate<R> select() {
        return (query, cb) -> {
            Subquery<R> subquery = query.subquery(type);
            Root<R> subqueryRoot = subquery.from(type);
            subquery
                    .select(subqueryRoot)
                    .where(buildPredicates(subquery, predicates, cb, subqueryRoot));
            return subquery;
        };
    }

    @Override
    public <P> SubqueryPredicate<P> select(SingularAttribute<R, P> attribute) {
        return (query, cb) -> {
            Subquery<P> subquery = query.subquery(attribute.getJavaType());
            Root<R> subqueryRoot = subquery.from(type);
            subquery
                    .select(subqueryRoot.get(attribute))
                    .where(buildPredicates(subquery, predicates, cb, subqueryRoot));
            return subquery;
        };
    }

    @Override
    public <P1, P2> SubqueryPredicate<P2> select(SingularAttribute<R, P1> attribute1, SingularAttribute<P1, P2> attribute2) {
        return (query, cb) -> {
            Subquery<P2> subquery = query.subquery(attribute2.getJavaType());
            Root<R> subqueryRoot = subquery.from(type);
            subquery
                    .select(subqueryRoot.get(attribute1).get(attribute2))
                    .where(buildPredicates(subquery, predicates, cb, subqueryRoot));
            return subquery;
        };
    }

    @Override
    public <P1, P2, P3> SubqueryPredicate<P3> select(SingularAttribute<R, P1> attribute1, SingularAttribute<P1, P2> attribute2, SingularAttribute<P2, P3> attribute3) {
        return (query, cb) -> {
            Subquery<P3> subquery = query.subquery(attribute3.getJavaType());
            Root<R> subqueryRoot = subquery.from(type);
            subquery
                    .select(subqueryRoot.get(attribute1).get(attribute2).get(attribute3))
                    .where(buildPredicates(subquery, predicates, cb, subqueryRoot));
            return subquery;
        };
    }
}

Now, we can compose a subquery using the SubQueryImpl and build an instance of the SubqueryPredicate. But to apply the IN operator, we should define the in() filter in the BaseQuery interface.

3. IN Operator

In this section, we will define the absent IN filter in the BaseQuery interface. This method should accept the SubqueryPredicate<R> varargs argument as the selected value of SubQuery:

public interface BaseQuery<R, Q extends BaseQuery<R, Q>> {
   ...

   Q in(SubqueryPredicate<R>... values);
}

Next, we can specify additional in() methods accepting SingularAttribute parameters to reach the nested relation which should be used in the IN operator.

In the following example, we use the in() filter to match the Address’s city entity with the Location subquery, returning the City entity:

.in(Address_.city,
    subquery(Location.class)
        .isTrue(Location_.hasPriority)
        .select(Location_.city)
)

Let’s define these additional in() methods accepting a different set of SingularAttribute parent-child arguments, allowing us to use nested relations:

public interface BaseQuery<R, Q extends BaseQuery<R, Q>> {
   ...

   <V> Q in(SingularAttribute<R, V> attribute, SubqueryPredicate<V>... values);

   <P, V> Q in(SingularAttribute<R, P> attribute1, SingularAttribute<P, V> attribute2, SubqueryPredicate<V>... values);

   <P1, P2, V> Q in(SingularAttribute<R, P1> attribute1, SingularAttribute<P1, P2> attribute2, SingularAttribute<P2, V> attribute3, SubqueryPredicate<V>... values);
}

Next, let’s implement defined in() methods in the BaseQueryImpl. Each method should first map the SubqueryPredicate varargs parameters to the javax.persistence.criteria.Subquery instances using  SubqueryPredicate#apply() method and, after that, create a predicate with the IN operator for the path provided by SingularAttribute parameters:

Predicate[] predicates = Arrays.stream(subqueryPredicates)
        .map(t -> t.apply(CommonAbstractCriteria, CriteriaBuilder))
        .map(subquery -> root.get(attribute).in(subquery))
        .toArray(Predicate[]::new);

Now we can apply the CriteriaBuilder#and() to make a conjunction of the obtained predicates and finally, construct an instance of the QueryPredicate:

new QueryPredicate<R>() {
    @Override
    public Predicate apply(CommonAbstractCriteria criteria, CriteriaBuilder cb, Path<R> root) {
        Predicate[] predicates = Arrays.stream(subqueryPredicates)
                .map(t -> t.apply(criteria, cb))
                .map(subquery -> root.get(attribute).in(subquery))
                .toArray(Predicate[]::new);
        return cb.and(predicates);
    }
};

There is the complete implementation of all in() methods:

public abstract class BaseQueryImpl<R, Q extends BaseQueryImpl<R, Q>> implements BaseQuery<R, Q> {
    ...

    @SafeVarargs
    @Override
    public final Q in(SubqueryPredicate<R>... values) {
        predicates.add((criteria, cb, root) ->
                cb.and(
                        Arrays.stream(values)
                                .map(t -> t.apply(criteria, cb))
                                .map(root::in)
                                .toArray(Predicate[]::new)
                )
        );
        return self();
    }

    @SafeVarargs
    @Override
    public final <V> Q in(SingularAttribute<R, V> attribute, SubqueryPredicate<V>... values) {
        predicates.add((criteria, cb, root) ->
                cb.and(
                        Arrays.stream(values)
                                .map(t -> t.apply(criteria, cb))
                                .map(subquery -> root.get(attribute).in(subquery))
                                .toArray(Predicate[]::new)
                )
        );
        return self();
    }

    @SafeVarargs
    @Override
    public final <P, V> Q in(SingularAttribute<R, P> attribute1, SingularAttribute<P, V> attribute2, SubqueryPredicate<V>... values) {
        predicates.add((criteria, cb, root) ->
                cb.and(
                        Arrays.stream(values)
                                .map(t -> t.apply(criteria, cb))
                                .map(subquery -> root.get(attribute1).get(attribute2).in(subquery))
                                .toArray(Predicate[]::new)
                )
        );
        return self();
    }



    @SafeVarargs
    @Override
    public final <P1, P2, V> Q in(SingularAttribute<R, P1> attribute1, SingularAttribute<P1, P2> attribute2,
                                  SingularAttribute<P2, V> attribute3, SubqueryPredicate<V>... values) {
        predicates.add((criteria, cb, root) ->
                cb.and(
                        Arrays.stream(values)
                                .map(t -> t.apply(criteria, cb))
                                .map(subquery -> root.get(attribute1).get(attribute2).get(attribute3).in(subquery))
                                .toArray(Predicate[]::new)
                )
        );
        return self();
    }

}

Lastly, to finish the implementation, we can provide static in() methods, allowing us to create nested IN predicates within the and/or filters.

For example, the following query will retrieve the orders with a customer matching any of the provided subqueries:

List<Order> ordersToSpainOrWithActiveMembership = criteriaApiHelper.select(Order.class)
        .or(Order_.customer,
                in(
                        subquery(CustomerDetails.class)
                                .isTrue(CustomerDetails_.isActiveMembership)
                                .select(CustomerDetails_.customer)
                ),
                in(Customer_.address,
                        subquery(AddressDetails.class)
                                .equal(AddressDetails_.country, Country_.name, "Spain")
                                .select(AddressDetails_.address)
                )
        )
        .findAll();

There is the implementation for all the static in() methods in the CriteriaApiHelper class:

public class CriteriaApiHelper {
    ...
	
    @SafeVarargs
    public static <R> QueryPart<R> in(SubqueryPredicate<R>... values) {
        return new QueryPart<R>()
                .in(values);
    }

    @SafeVarargs
    public static <R, V> QueryPart<R> in(SingularAttribute<R, V> attribute, SubqueryPredicate<V>... values) {
        return new QueryPart<R>()
                .in(attribute, values);
    }

    @SafeVarargs
    public static <R, P, V> QueryPart<R> in(SingularAttribute<R, P> attribute1, SingularAttribute<P, V> attribute2,
                                            SubqueryPredicate<V>... values) {
        return new QueryPart<R>()
                .in(attribute1, attribute2, values);
    }

    @SafeVarargs
    public static <R, P1, P2, V> QueryPart<R> in(SingularAttribute<R, P1> attribute1, SingularAttribute<P1, P2> attribute2,
                                                 SingularAttribute<P2, V> attribute3, SubqueryPredicate<V>... values) {
        return new QueryPart<R>()
                .in(attribute1, attribute2, attribute3, values);
    }
}

4. Wrapping Up

In this part, we slightly improved extension for the Criteria API. Now we can perform the IN operator.


Written by alexandermakeev | Senior SWE at Layermark
Published by HackerNoon on 2022/10/04