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

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

TLDRIn this chapter, we write an extension for the Criteria API using Builder pattern and JPA Static Metamodel Generator to increase readability and reduce read complexity with explicitness.via the TL;DR App

In this tutorial, I want to show you a way of how to perform persistence operations with Criteria API in a more convenient form using Builder pattern and JPA Static Metamodel Generator.

The full source code is available over on Github.

1 Spring Data JPA

In the Java world, the most popular way to work with relational databases is to use Spring Data JPA. It is quite a sophisticated framework that allows you to instantly get started working with the database. The most compelling feature in the Spring Data is the Repository.  All you need to do to access the database table is to create a Java interface and extend it from the CrudRepository with the defined domain class and with the id type of the domain class as type arguments:

public interface CustomerRepository extends CrudRepository<Customer, Long> {
}

For each declared interface, Spring will create an appropriate bean, allowing you to easily use it with Dependency Injection. With the CrudRepository, this interface will provide basic implementations for CRUD operations for the managed entity class. For example, the following read operations will allow you to retrieve all the rows and a specific row by id:

package org.springframework.data.repository;

public interface CrudRepository<T, ID> extends Repository<T, ID> {
    …

    Iterable<T> findAll();

    Optional<T> findById(ID id);	

    …
}

If you want to filter the data with specific predicates, Spring Repository allows you to define your own methods with the query derivation mechanism using entity’s fields and keywords:

public interface CustomerRepository extends CrudRepository<Customer, Long> {
    List<Customer> findByLastName(String lastName);

    List<Customer> findByZipCode(String zipCode);

    List<Customer> findByLastNameAndZipCode(String lastName, String zipCode);
}

It looks very clear and convenient. But there are few cons and pitfalls you should be aware of:

  1. Typo mistakes

Using query derivation mechanism language to compose a query, you can really minimize your code with supported keywords. Modern Intellij IDEA will help you to build such a query quite easily:

@Entity
class Customer {
    @Id
    private Long id;
    private String zipCode;
}

interface CustomerRepository extends CrudRepository<Customer, Long> {
    List<Customer> findAllByZip(String zipCode);
}

If we construct a query using wrong or non-existing field names, like zip instead of zipCode, when we run this code we will probably see an exception, saying that no property zip is found for the Customer type:

Caused by: org.springframework.data.mapping.PropertyReferenceException: No property 'zip' found for type 'Customer' Did you mean ''id''

But if we change the argument type of this field, for example to Integer, we will not receive any exception during start up:

@Entity
class Customer {
    @Id
    private Long id;
    private String zipCode;
}

interface CustomerRepository extends CrudRepository<Customer, Long> {
    List<Customer> findAllByZipCode(Integer zipCode);
}

You’ll receive an exception only when you manually invoke this method passing specific value:

java.lang.IllegalArgumentException: Parameter value [90001] did not match expected type [java.lang.String (n/a)]

There is no way to mock this part of code. Therefore, you should be very careful when you compose a query or when you refactor an entity’s field type.

  1. Read complexity

Query derivation mechanism language is quite useful, but it becomes difficult to read if you deal with a complex query containing a deep nested join. Let’s say we want to get Order entities filtered by address attributes:

List<Order> findAllByCustomerAddressAddressLineLikeAndCustomerAddressCityNameAndCustomerAddressCityCountryName(String addressLine, String city, String country);

Sure, you can simplify this query with JPQL using the @Query annotation over this method. But in this case, you will lose all the privileges of the query derivation mechanism with proper validation when Spring starts up:

public interface OrderRepository extends CrudRepository<Order, Long> {
    @Query("SELECT o FROM Order o " +
            " INNER JOIN o.customer cr" +
            " INNER JOIN cr.address a" +
            " INNER JOIN a.city ct" +
            " INNER JOIN ct.country cn" +
            " WHERE a.addressLine LIKE :address_line AND ct.name = :city AND cn.name = :country")
    List<Order> filterByAddress(@Param("address_line") String addressLine, @Param("city") String city, @Param("country") String country);
}

The other scenario is to use the Criteria API by extending your Repository with JpaSpecificationExecutor interface and allowing you to access the Root, CriteriaQuery and CriteriaBuilder within the Specification interface:

orderRepository.findAll((Specification<Order>) (root, query, cb) -> {
    Path<Address> addressPath = root.get(Order_.customer).get(Customer_.address);
    Path<City> cityPath = addressPath.get(Address_.city);
    return cb.and(
            cb.equal(addressPath.get(Address_.addressLine), "%" + addressLine + "%"),
            cb.equal(cityPath.get(City_.name), city),
            cb.equal(cityPath.get(City_.country).get(Country_.name), country)
    );
});

  1. Explicitness

Using Spring Data JPA, we have to create a repository for each table. Explicitness is always good, but it’s quite problematic if you have about 200 tables. In this case, you need to create 200 repositories accordingly, even if you need only one query per repository.

2 Extension implementation

My idea is to eliminate all these issues by creating an extension for the Criteria API:

select()
    .equal(Order_.status, OrderStatus.Shipped)
    .like(Order_.customer, Customer_.address, Address_.addressLine, “11 Aleksandr Pushkin St”)
    .equal(Order_.customer, Customer_.address, Address_.city, City_.name, “Tbilisi”)
    .findAll();

  1. First of all, to exclude all the typo errors, we will use the JPA Static Metamodel with an explicit parent-child path chain and with generic types to eliminate any errors during compilation. Each argument will accept an entity's attribute, providing only logical relations with a corresponding data value at the end:
equal(Order_.customer, Customer_.address, Address_.city, City_.country, Country_.name, “Georgia”)

  1. To reduce read complexity and eliminate nested path duplication, we can use the Builder pattern with common paths:
select()
    .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. To get rid of redundant explicitness by creating a separate repository for each entity, our extension will accept the entity type of the table that we use to start the query from. By passing the entity type explicitly, we can compose queries from any place in the code:
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();

Now, let’s dive into the code and try to build a simple implementation. Our extension will provide the ability to select, update and delete rows.

2.1 BaseQuery

First, we add the BaseQuery interface that will contain basic operations for selecting, updating and deleting queries. This interface should have two generic types: R as the Root type and Q extends BaseQuery<R, Q> as the BaseQuery implementation itself, which we can use as a return type for Builder pattern methods:

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

Next, we add the first method for the equal operation. It will accept SingularAttribute as the entity field that we want to compare and the comparing value itself:

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

	<V> Q equal(SingularAttribute<R, V> attribute, V value);
}

SingularAttribute provides two generic types: the first one is the entity type, and the second is the type of the entity’s represented attribute. Therefore, we put R as the first generic type, and we add an extra V generic type as the second type, equal to the value type. Please note that if we want to provide type safety, the value should have the same data type as the represented attribute.

Next, we can add the second overloaded implementation of the equal filter, that will contain a subsequent parent-child relation to allow you to compare a nested attribute. In this case, we need to add an extra generic type P for the in-between relation:

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

    <P, V> Q equal(SingularAttribute<R, P> attribute1, SingularAttribute<P, V> attribute2, V value);
}

Lastly, we will add the third overloaded equal method with an extra P1 and P2 generic types to allow you to reach an attribute available after two subsequent parent-child relations:

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

    <P1, P2, V> Q equal(SingularAttribute<R, P1> attribute1, SingularAttribute<P1, P2> attribute2, SingularAttribute<P2, V> attribute3, V value);
}

Now, we are ready to initialize BaseQuery using the Criteria API. This class again should have two generic types: the Root type and the BaseQueryImpl implementation itself, which we can use as a return type for Builder pattern methods:

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

Before we initialize the equal implementations, we first need to define the collection of predicates. Our query will have nested criteria predicates with postponed initialization. Therefore, we introduce the QueryPredicate interface with the following arguments: CommonAbstractCriteria, CriteriaBuilder and Path to allow us to create the necessary predicate:

@FunctionalInterface
public interface QueryPredicate<R> {
    Predicate apply(CommonAbstractCriteria criteria, CriteriaBuilder cb, Path<R> root);
}

Next, we introduce a collection of QueryPredicate as BaseQueryImpl field and a method that will convert this collection to a collection of javax.persistence.criteria.Predicate:

public class BaseQueryImpl<R, Q extends BaseQueryImpl<R, Q>> implements BaseQuery<R, Q> {
    protected final Collection<QueryPredicate<R>> predicates;

    public BaseQueryImpl() {
        this.predicates = new ArrayList<>();
    }

    protected <T> Predicate[] buildPredicates(CommonAbstractCriteria criteria, Collection<QueryPredicate<T>> predicates,
                                              CriteriaBuilder cb, Path<T> root) {
        return predicates
                .stream()
                .map(t -> t.apply(criteria, cb, root))
                .toArray(Predicate[]::new);
    }
}

In addition, we’ll introduce abstract self() method that should return this instance for each implementation:

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

    //subclasses must override this method to return "this"
    protected abstract Q self();
}

Now, we are ready to create defined equal implementations. Each implementation should retrieve each required Path and accumulate an equal predicate in the predicates collection:

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

    @Override
    public <V> Q equal(SingularAttribute<R, V> attribute, V value) {
        predicates.add((criteria, cb, root) -> cb.equal(root.get(attribute), value));
        return self();
    }

    @Override
    public <P, V> Q equal(SingularAttribute<R, P> attribute1, SingularAttribute<P, V> attribute2, V value) {
        predicates.add((criteria, cb, root) -> cb.equal(root.get(attribute1).get(attribute2), value));
        return self();
    }

    @Override
    public <P1, P2, V> Q equal(SingularAttribute<R, P1> attribute1, SingularAttribute<P1, P2> attribute2,
                               SingularAttribute<P2, V> attribute3, V value) {
        predicates.add((criteria, cb, root) -> cb.equal(root.get(attribute1).get(attribute2).get(attribute3), value));
        return self();
    }
}

You can add additional equal statements on your own accepting a chain of 3+ subsequent parent-child relations as well as predicates for other operators, like notEqual, like, greaterThan, lessThan, isNull, etc.

2.2 SelectQuery

To perform select statements, we will create the SelectQuery interface extending the BaseQuery:

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

This interface will mostly contain the terminating methods that will return the final result from a database. The first terminating method will be findAll() returning the list of R (Root type):

public interface SelectQuery<R, Q extends SelectQuery<R, Q>> extends BaseQuery<R, Q> {
    List<R> findAll();
}

Next, we will provide two overloaded findAll() methods to select nested join table:

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

    <P> List<P> findAll(SingularAttribute<R, P> attribute);

    <P1, P2> List<P2> findAll(SingularAttribute<R, P1> attribute1, SingularAttribute<P1, P2> attribute2);
}

The second group of terminating interfaces we implement is getOne(), which will return a single result or throw an exception if there is no result found:

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

    R getOne();

    <P> P getOne(SingularAttribute<R, P> attribute);

    <P1, P2> P2 getOne(SingularAttribute<R, P1> attribute1, SingularAttribute<P1, P2> attribute2);
}

The third terminating group we implement is findOne(), which will return a single result wrapped in the Optional:

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

    Optional<R> findOne();

    <P> Optional<P> findOne(SingularAttribute<R, P> attribute);

    <P1, P2> Optional<P2> findOne(SingularAttribute<R, P1> attribute1, SingularAttribute<P1, P2> attribute2);
}

The next terminating group we can provide is the count() method that will return number of rows matching the query:

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

    long count();

    <P> long count(SingularAttribute<R, P> attribute);

    <P1, P2> long count(SingularAttribute<R, P1> attribute1, SingularAttribute<P1, P2> attribute2);
}

Next, let’s add non-terminating interfaces to order the rows. Methods without Order argument will provide the ascending order:

public interface SelectQuery<R, Q extends SelectQuery<R, Q>> extends BaseQuery<R, Q> {
    ...
 
    <P> SelectQuery<R, Q> order(SingularAttribute<R, P> attribute);

    <P> SelectQuery<R, Q> order(SingularAttribute<R, P> attribute, Order order);

    <P1, P2> SelectQuery<R, Q> order(SingularAttribute<R, P1> attribute1, SingularAttribute<P1, P2> attribute2);

    <P1, P2> SelectQuery<R, Q> order(SingularAttribute<R, P1> attribute1, SingularAttribute<P1, P2> attribute2, Order order);

    <P1, P2, P3> SelectQuery<R, Q> order(SingularAttribute<R, P1> attribute1, SingularAttribute<P1, P2> attribute2, SingularAttribute<P2, P3> attribute3);

    <P1, P2, P3> SelectQuery<R, Q> order(SingularAttribute<R, P1> attribute1, SingularAttribute<P1, P2> attribute2, SingularAttribute<P2, P3> attribute3, Order order);
}

Lastly, if you want to initialize Lazy relations, you can add fetch() methods:

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

    <P> SelectQuery<R, Q> fetch(SingularAttribute<R, P> attribute);

    <P1, P2> SelectQuery<R, Q> fetch(SingularAttribute<R, P1> attribute1, SingularAttribute<P1, P2> attribute2);

    <P1, P2, P3> SelectQuery<R, Q> fetch(SingularAttribute<R, P1> attribute1, SingularAttribute<P1, P2> attribute2, SingularAttribute<P2, P3> attribute3);
}

Now we have enough interfaces to perform select statements. You can add any other interfaces on your own including pagination, exists and so on. Let’s finish the implementation for the declared methods by creating the SelectQueryImpl class:

public class SelectQueryImpl<R> extends BaseQueryImpl<R, SelectQueryImpl<R>> implements SelectQuery<R, SelectQueryImpl<R>> {
}

This class will contain the following fields: javax.persistence.EntityManager, javax.persistence.criteria.CriteriaBuilder, javax.persistence.criteria.CriteriaQuery, javax.persistence.criteria.Root and the List of javax.persistence.criteria.Order:

public class SelectQueryImpl<R> extends BaseQueryImpl<R, SelectQueryImpl<R>> implements SelectQuery<R, SelectQueryImpl<R>> {
    private final EntityManager em;
    private final CriteriaBuilder cb;
    private final CriteriaQuery query;
    private final Root<R> root;
    private final List<Order> orderList;
}

Next, let’s declare a constructor. To initialize Criteria fields, we need the EntityManager and the root R entity type:

public class SelectQueryImpl<R> extends BaseQueryImpl<R, SelectQueryImpl<R>> implements SelectQuery<R, SelectQueryImpl<R>> {
    ...

    public SelectQueryImpl(EntityManager em, Class<R> type) {
        this.em = em;
        this.cb = em.getCriteriaBuilder();
        this.query = cb.createQuery();
        this.root = query.from(type);
        this.orderList = new ArrayList<>();
    }

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

Before implementing declared terminating and non-terminating interfaces, we will add a private method that will accept javax.persistence.criteria.Selection argument and return built TypedQuery:

public class SelectQueryImpl<R> extends BaseQueryImpl<R, SelectQueryImpl<R>> implements SelectQuery<R, SelectQueryImpl<R>> {
    ...

    private <T> TypedQuery<T> buildQuery(Selection<T> selection) {
        CriteriaQuery<T> resultQuery = query
                .select(selection)
                .orderBy(orderList);
        if (!predicates.isEmpty()) {
            resultQuery = resultQuery.where(buildPredicates(query, predicates, cb, root));
        }
        return em.createQuery(resultQuery);
    }
}

This method will build our query by selecting the required table path, providing predicates and orderList.

Now we are ready to implement required interfaces. To implement the first group for the findAll() interfaces, we just need to pass desired table path to the buildQuery() method and call getResultList():

public class SelectQueryImpl<R> extends BaseQueryImpl<R, SelectQueryImpl<R>> implements SelectQuery<R, SelectQueryImpl<R>> {
    ...

    @Override
    public List<R> findAll() {
        return buildQuery(root)
                .getResultList();
    }

    @Override
    public <P> List<P> findAll(SingularAttribute<R, P> attribute) {
        return buildQuery(root.get(attribute))
                .getResultList();
    }

    @Override
    public <P1, P2> List<P2> findAll(SingularAttribute<R, P1> attribute1, SingularAttribute<P1, P2> attribute2) {
        return buildQuery(root.get(attribute1).get(attribute2))
                .getResultList();
    }
}

The second implementation is for the getOne() interface, which will act the same except for the fact that we should limit the result by one row and call the getSingleResult():

public class SelectQueryImpl<R> extends BaseQueryImpl<R, SelectQueryImpl<R>> implements SelectQuery<R, SelectQueryImpl<R>> {
    ...

    @Override
    public R getOne() {
        return buildQuery(root)
                .setMaxResults(1)
                .getSingleResult();
    }

    @Override
    public <P> P getOne(SingularAttribute<R, P> attribute) {
        return buildQuery(root.get(attribute))
                .setMaxResults(1)
                .getSingleResult();
    }

    @Override
    public <P1, P2> P2 getOne(SingularAttribute<R, P1> attribute1, SingularAttribute<P1, P2> attribute2) {
        return buildQuery(root.get(attribute1).get(attribute2))
                .setMaxResults(1)
                .getSingleResult();
    }
}

The next realization for the findOne() can be implemented using getOne() method with try/catch block:

public class SelectQueryImpl<R> extends BaseQueryImpl<R, SelectQueryImpl<R>> implements SelectQuery<R, SelectQueryImpl<R>> {
    ...

    @Override
    public Optional<R> findOne() {
        try {
            return Optional.of(getOne());
        } catch (NoResultException e) {
            return Optional.empty();
        }
    }

    @Override
    public <P> Optional<P> findOne(SingularAttribute<R, P> attribute) {
        try {
            return Optional.of(getOne(attribute));
        } catch (NoResultException e) {
            return Optional.empty();
        }
    }

    @Override
    public <P1, P2> Optional<P2> findOne(SingularAttribute<R, P1> attribute1, SingularAttribute<P1, P2> attribute2) {
        try {
            return Optional.of(getOne(attribute1, attribute2));
        } catch (NoResultException e) {
            return Optional.empty();
        }
    }
}

The last terminating group is count(), which will return count of rows matching the query:

public class SelectQueryImpl<R> extends BaseQueryImpl<R, SelectQueryImpl<R>> implements SelectQuery<R, SelectQueryImpl<R>> {
    ...

    @Override
    public long count() {
        return buildQuery(cb.count(root))
                .getSingleResult();
    }

    @Override
    public <P> long count(SingularAttribute<R, P> attribute) {
        return buildQuery(cb.count(root.get(attribute)))
                .getSingleResult();
    }

    @Override
    public <P1, P2> long count(SingularAttribute<R, P1> attribute1, SingularAttribute<P1, P2> attribute2) {
        return buildQuery(cb.count(root.get(attribute1).get(attribute2)))
                .getSingleResult();
    }
}

Lastly, we need to implement non-terminating interfaces for the order() and fetch() methods:

public class SelectQueryImpl<R> extends BaseQueryImpl<R, SelectQueryImpl<R>> implements SelectQuery<R, SelectQueryImpl<R>> {
    ...

    @Override
    public <P> SelectQueryImpl<R> order(SingularAttribute<R, P> attribute) {
        return order(attribute, Order.ASC);
    }

    @Override
    public <P> SelectQueryImpl<R> order(SingularAttribute<R, P> attribute, Order sort) {
        javax.persistence.criteria.Order order = sort == Order.ASC ? cb.asc(root.get(attribute)) : cb.desc(root.get(attribute));
        orderList.add(order);
        return this;
    }

    @Override
    public <P1, P2> SelectQueryImpl<R> order(SingularAttribute<R, P1> attribute1,
                                             SingularAttribute<P1, P2> attribute2) {
        return order(attribute1, attribute2, Order.ASC);
    }

    @Override
    public <P1, P2> SelectQueryImpl<R> order(SingularAttribute<R, P1> attribute1,
                                             SingularAttribute<P1, P2> attribute2, Order sort) {
        Path<P2> path = root.get(attribute1).get(attribute2);
        javax.persistence.criteria.Order order = sort == Order.ASC ? cb.asc(path) : cb.desc(path);
        orderList.add(order);
        return this;
    }

    @Override
    public <P1, P2, P3> SelectQueryImpl<R> order(SingularAttribute<R, P1> attribute1,
                                                 SingularAttribute<P1, P2> attribute2,
                                                 SingularAttribute<P2, P3> attribute3) {
        return order(attribute1, attribute2, attribute3, Order.ASC);
    }

    @Override
    public <P1, P2, P3> SelectQueryImpl<R> order(SingularAttribute<R, P1> attribute1,
                                                 SingularAttribute<P1, P2> attribute2,
                                                 SingularAttribute<P2, P3> attribute3, Order sort) {
        Path<P3> path = root.get(attribute1).get(attribute2).get(attribute3);
        javax.persistence.criteria.Order order = sort == Order.ASC ? cb.asc(path) : cb.desc(path);
        orderList.add(order);
        return this;
    }

    @Override
    public <P> SelectQueryImpl<R> fetch(SingularAttribute<R, P> attribute) {
        root.fetch(attribute);
        return this;
    }

    @Override
    public <P1, P2> SelectQueryImpl<R> fetch(SingularAttribute<R, P1> attribute1, SingularAttribute<P1, P2> attribute2) {
        root.fetch(attribute1).fetch(attribute2);
        return this;
    }

    @Override
    public <P1, P2, P3> SelectQueryImpl<R> fetch(SingularAttribute<R, P1> attribute1, SingularAttribute<P1, P2> attribute2,
                                                 SingularAttribute<P2, P3> attribute3) {
        root.fetch(attribute1).fetch(attribute2).fetch(attribute3);
        return this;
    }
}

2.3 UpdateQuery

To perform update statements, we will create the UpdateQuery interface extending the BaseQuery:

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

This query will contain only two methods: the first one to set the attributes we want to update and the second one is the terminating method that will return the number of updated rows:

public interface UpdateQuery<R, Q extends UpdateQuery<R, Q>> extends BaseQuery<R, Q> {
    <V> UpdateQuery<R, Q> set(SingularAttribute<R, V> attribute, V value);

    int execute();
}

Implementation for the UpdateQuery should be quite straightforward except for the fact, now we should use CriteriaUpdate instead of CriteriaQuery:

public class UpdateQueryImpl<R> extends BaseQueryImpl<R, UpdateQueryImpl<R>> implements UpdateQuery<R, UpdateQueryImpl<R>> {
    private final EntityManager em;
    private final CriteriaBuilder cb;
    private final CriteriaUpdate<R> query;
    private final Root<R> root;

    public UpdateQueryImpl(EntityManager em, Class<R> type) {
        this.em = em;
        this.cb = em.getCriteriaBuilder();
        this.query = cb.createCriteriaUpdate(type);
        this.root = query.from(type);
    }

    @Override
    public <V> UpdateQueryImpl<R> set(SingularAttribute<R, V> attribute, V value) {
        query.set(attribute, value);
        return this;
    }

    @Override
    public int execute() {
        if (!predicates.isEmpty()) {
            query.where(buildPredicates(query, predicates, cb, root));
        }
        return em.createQuery(query)
                .executeUpdate();
    }

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

2.4 DeleteQuery

Lastly, to perform delete statements, we will create the DeleteQuery interface extending the BaseQuery:

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

DeleteQuery will have only one terminating method, returning the number of deleted rows:

public interface DeleteQuery<R, Q extends DeleteQuery<R, Q>> extends BaseQuery<R, Q> {
    int execute();
}

Implementation for the DeleteQuery with the CriteriaDelete:

public class DeleteQueryImpl<R> extends BaseQueryImpl<R, DeleteQueryImpl<R>> implements DeleteQuery<R, DeleteQueryImpl<R>> {
    private final EntityManager em;
    private final CriteriaBuilder cb;
    private final CriteriaDelete<R> query;
    private final Root<R> root;

    public DeleteQueryImpl(EntityManager em, Class<R> type) {
        this.em = em;
        this.cb = em.getCriteriaBuilder();
        this.query = cb.createCriteriaDelete(type);
        this.root = query.from(type);
    }

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

    @Override
    public int execute() {
        if (!predicates.isEmpty()) {
            query.where(buildPredicates(query, predicates, cb, root));
        }
        return em.createQuery(query)
                .executeUpdate();
    }
}

2.5 CriteriaApiHelper

In order to create any of the BaseQuery implementations, we should pass an instance of EntityManager every time we want to perform a query. To simplify this injection process, we can declare an extra class that will hold the EntityManager:

public class CriteriaApiHelper {
    private final EntityManager em;

    public CriteriaApiHelper(EntityManager em) {
        this.em = em;
    }
}

Now we can just initialize methods for BaseQuery by creating a suitable implementation for each query type:

public class CriteriaApiHelper {
    …

    public <T> SelectQueryImpl<T> select(Class<T> type) {
        return new SelectQueryImpl<>(em, type);
    }

    public <T> UpdateQueryImpl<T> update(Class<T> type) {
        return new UpdateQueryImpl<>(em, type);
    }

    public <T> DeleteQueryImpl<T> delete(Class<T> type) {
        return new DeleteQueryImpl<>(em, type);
    }
}

3 Wrapping up

Now you can perform select, update and delete queries using CriteriaApiHelper:

List<Order> listOfOrders = criteriaApiHelper.select(Order.class)
        .equal(Order_.status, 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();

In order to see the entities with an underscore at the end, you should include the JPA Static Metamodel Generator library:

<dependency>
    <groupId>org.hibernate</groupId>
	<artifactId>hibernate-jpamodelgen</artifactId>
    <scope>provided</scope>
</dependency>

After that, you need to rebuild your project and mark the target/generated-sources/annotations package as the Generated Sources Root:

In the next chapters, we will implement nested expressions providing or/and predicates and sub queries.


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