The previous part of this tutorial described how we can create database queries with named queries.
This tutorial has already taught us how we can create static database queries with Spring Data JPA. However, when we are writing real-life applications, we have to be able to create dynamic database queries as well.
This blog post describes how we can create dynamic database queries by using the JPA Criteria API. We will also implement a search function that has two requirements:
- It must return todo entries whose title or description contains the given search term.
- The search must be case-insensitive.
Let’s start by ensuring that Maven creates the JPA static metamodel classes when we compile our project.
Additional Reading:
If you are not familiar with Spring Data JPA, you should read the following blog posts before you continue reading this blog post:
- Spring Data JPA Tutorial: Introduction provides a quick introduction to Spring Data JPA and gives an overview of the Spring Data repository interfaces.
- Spring Data JPA Tutorial: Getting the Required Dependencies describes how you can get the required dependencies.
- Spring Data JPA Tutorial: Configuration describes how you can configure the persistence layer of a Spring application that uses Spring Data JPA.
Creating the JPA Static Metamodel Classes
A static metamodel consists of classes that describe the entity and embeddable classes found from our domain model. These metamodel classes provide static access to the metadata that describes the attributes of our domain model classes.
We want to use these classes because they give us the possibility to create type-safe criteria queries, but we don’t want to create them manually.
Luckily, we can create these classes automatically by using the Maven Processor Plugin and the JPA Static Metamodel Generator. We can configure these tools by following these steps:
- Add the Maven Processor Plugin (version 2.2.4) declaration to the plugins section of the pom.xml file.
- Configure the dependencies of this plugin and add the JPA static metamodel generator dependency (version 4.3.8) to the plugin’s dependencies section.
- Create an execution that invokes the plugin’s process goal in the generate-sources phase of the Maven default lifecycle.
- Ensure that the plugin runs only the org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor. This annotation processor scans our entities and embeddable classes, and creates the static metamodel classes.
The configuration of the Maven Processor Plugin looks as follows:
1<plugin>
2 <groupId>org.bsc.maven</groupId>
3 <artifactId>maven-processor-plugin</artifactId>
4 <version>2.2.4</version>
5 <executions>
6 <execution>
7 <id>process</id>
8 <goals>
9 <goal>process</goal>
10 </goals>
11 <phase>generate-sources</phase>
12 <configuration>
13 <processors>
14 <processor>org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor</processor>
15 </processors>
16 </configuration>
17 </execution>
18 </executions>
19 <dependencies>
20 <dependency>
21 <groupId>org.hibernate</groupId>
22 <artifactId>hibernate-jpamodelgen</artifactId>
23 <version>4.3.8.Final</version>
24 </dependency>
25 </dependencies>
26</plugin>
Additional Reading:
When we compile our project, the invoked annotation processor creates the JPA static metamodel classes to the target/generated-sources/apt directory. Because our domain model has only one entity, the annotation processor creates only one class called Todo_. The source code of the Todo_ class looks as follows:
1package net.petrikainulainen.springdata.jpa.todo;
2
3import java.time.ZonedDateTime;
4import javax.annotation.Generated;
5import javax.persistence.metamodel.SingularAttribute;
6import javax.persistence.metamodel.StaticMetamodel;
7
8@Generated(value = "org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor")
9@StaticMetamodel(Todo.class)
10public abstract class Todo_ {
11
12 public static volatile SingularAttribute<Todo, ZonedDateTime> creationTime;
13 public static volatile SingularAttribute<Todo, String> createdByUser;
14 public static volatile SingularAttribute<Todo, ZonedDateTime> modificationTime;
15 public static volatile SingularAttribute<Todo, String> modifiedByUser;
16 public static volatile SingularAttribute<Todo, String> description;
17 public static volatile SingularAttribute<Todo, Long> id;
18 public static volatile SingularAttribute<Todo, String> title;
19 public static volatile SingularAttribute<Todo, Long> version;
20
21}
Let’s move and find out how we can create database queries with the JPA criteria API.
Creating Database Queries With the JPA Criteria API
We can create database queries with the JPA Criteria API by following these steps:
- Modify the repository interface to support queries that use the JPA Criteria API.
- Specify the conditions of the invoked database query.
- Invoke the database query.
Let’s get started.
Modifying the Repository Interface
The JpaSpecificationExecutor<T> interface declares the methods that can be used to invoke database queries that use the JPA Criteria API. This interface has one type parameter T that describes the type of the queried entity.
In other words, if we need to modify our repository interface to support database queries that use the JPA Criteria API, we have to follow these steps:
- Extend the JpaSpecificationExecutor<T> interface.
- Set the type of the managed entity.
Example: The only Spring Data JPA repository of our example application (TodoRepository) manages Todo objects. After we have modified this repository to support criteria queries, its source code looks as follows:
1import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
2import org.springframework.data.repository.Repository;
3
4interface TodoRepository extends Repository<Todo, Long>, JpaSpecificationExecutor<Todo> {
5}
After we have extended the JpaSpeciticationExecutor interface, the classes that use our repository interface get access to the following methods:
- The long count(Specification<T> spec) method returns the number of objects that fulfil the conditions specified by the Specification<T> object given as a method parameter.
- The List<T> findAll(Specification<T> spec) method returns objects that fulfil the conditions specified by the Specification<T> object given as a method parameter.
- The T findOne(Specification<T> spec) method returns an object that fulfils the conditions specified by the Specification<T> object given as a method parameter.
The JpaSpecificationExecutor<T> interface declares also two other methods that are used to sort and paginate objects that fulfil the conditions specified by the Specification<T> object. We will talk more about these methods when we learn to sort and paginate our query results.
Additional Reading:
Let’s find out how we can specify the conditions of the invoked database query.
Specifying the Conditions of the Invoked Database Query
We can specify the conditions of the invoked database query by following these steps:
- Create a new Specification<T> object.
- Set the type of the queried entity as the value of the type parameter (T).
- Specify the conditions by implementing the toPredicate() method of the Specification<T> interface.
Example 1:
If we have to create a criteria query that returns Todo objects, we have to create the following specification:
1new Specification<Todo>() {
2 @Override
3 public Predicate toPredicate(Root<Todo> root,
4 CriteriaQuery<?> query,
5 CriteriaBuilder cb) {
6 //Create the query by using the JPA Criteria API
7 }
8}
Additional Reading:
- Dynamic, typesafe queries in JPA 2.0
- JPA Criteria API by samples - Part I
- JPA Criteria API by samples - Part II
- JPA 2 Criteria API Tutorial
- The Javadoc of the CriteriaBuilder interface
- The Javadoc of the CriteriaQuery interface
- The Javadoc of the Predicate interface
- The Javadoc of the Root<X> interface
- The Javadoc of the Specification<T> interface
The obvious next question is:
Where should we create these Specification<T> objects?
I argue that we should create our Specification<T> objects by using specification builder classes because:
- We can put our query generation logic into one place. In other words, we don’t litter the source code of our service classes (or other components) with the query generation logic.
- We can create reusable specifications and combine them in the classes that invoke our database queries.
Example 2:
If we need to create a specification builder class that constructs Specification<Todo> objects, we have to follow these steps:
- Create a final TodoSpecifications class. The name of this class isn’t important, but I like to use the naming convention: [The name of the queried entity class]Specifications.
- Add a private constructor the created class. This ensures that no one can instantiate our specification builder class.
- Add static specification builder methods to this class. In our case, we will add only one specification builder method (hasTitle(String title)) to this class and implement it by returning a new Specification<Todo> object.
The source code of the TodoSpecifications class looks as follows:
1import org.springframework.data.jpa.domain.Specification;
2
3import javax.persistence.criteria.CriteriaBuilder;
4import javax.persistence.criteria.CriteriaQuery;
5import javax.persistence.criteria.Predicate;
6import javax.persistence.criteria.Root;
7
8final class TodoSpecifications {
9
10 private TodoSpecifications() {}
11
12 static Specification<Todo> hasTitle(String title) {
13 return new Specification<Todo>() {
14 @Override
15 public Predicate toPredicate(Root<Todo> root,
16 CriteriaQuery<?> query,
17 CriteriaBuilder cb) {
18 //Create the query here.
19 }
20 }
21 }
22}
If we use Java 8, we can clean up the implementation of the hasTitle(String title) method by using lambda expressions. The source code of our new specification builder class looks as follows:
1import org.springframework.data.jpa.domain.Specification;
2
3import javax.persistence.criteria.CriteriaBuilder;
4import javax.persistence.criteria.CriteriaQuery;
5import javax.persistence.criteria.Predicate;
6import javax.persistence.criteria.Root;
7
8import org.springframework.data.jpa.domain.Specification;
9
10final class TodoSpecifications {
11
12 private TodoSpecifications() {}
13
14 static Specification<Todo> hasTitle(String title) {
15 return (root, query, cb) -> {
16 //Create query here
17 };
18 }
19}
Let’s find out how we can invoke the created database query.
Invoking the Created Database Query
After we have specified the conditions of the invoked database query by creating a new Specification<T> object, we can invoke the database query by using the methods that are provided by the JpaSpecificationExecutor<T> interface.
The following examples demonstrates how we can invoke different database queries:
Example 1:
If we want to get the number of Todo objects that have the title ‘foo’, we have to create and invoke our database query by using this code:
1Specification<Todo> spec = TodoSpecifications.hasTitle("foo");
2long count = repository.count(spec);
Example 2:
If we want to the get a list of Todo objects that have the title ‘foo’, we have to create and invoke our database query by using this code:
1Specification<Todo> spec = TodoSpecifications.hasTitle("foo");
2List<Todo> todoEntries = repository.findAll(spec);
Example 3:
If we want to get the Todo object whose title is ‘foo’, we have to create and invoke our database query by using this code:
1Specification<Todo> spec = TodoSpecifications.hasTitle("foo");
2List<Todo> todoEntries = repository.findOne(spec);
If we need to create a new specification that combines our existing specifications, we don’t have to add a new method to our specification builder class. We can simply combine our existing specifications by using the Specifications<T> class. The following examples demonstrates how we can use that class:
Example 4:
If we have specifications A and B, and we want to create a database query that returns Todo objects which fulfil the specification A and the specification B, we can combine these specifications by using the following code:
1Specification<Todo> specA = ...
2Specification<Todo> specB = ...
3List<Todo> todoEntries = repository.findAll(
4 Specifications.where(specA).and(specB)
5);
Example 5:
If we have specifications A and B, and we want to create a database query that returns Todo objects which fulfil the specification A or the specification B, we can combine these specifications by using the following code:
1Specification<Todo> specA = ...
2Specification<Todo> specB = ...
3Lis<Todo> todoEntries = repository.findAll(
4 Specifications.where(specA).or(specB)
5);
Example 6:
If we have specifications A and B, and we want to create a database query that returns Todo objects which fulfil the specification A but not the specification B, we can combine these specifications by using the following code:
1Specification<Todo> specA = ...
2Specification<Todo> specB = ...
3List<Todo> searchResults = repository.findAll(
4 Specifications.where(specA).and(
5 Specifications.not(specB)
6 )
7);
Additional Reading:
Let’s move on and find out how we can implement the search function.
Implementing the Search Function
We can implement our search function by following these steps:
- Modify our repository interface to support criteria queries.
- Create the specification builder class that creates Specification<Todo> objects.
- Implement the service method that uses our specification builder class and invokes the created database queries by using our repository interface.
Let’s start by modifying our repository interface.
Modifying Our Repository Interface
We can make the necessary modifications to our repository interface by following these steps:
- Extend the JpaSpecificationExecutor<T> interface.
- The type of the queried entity to Todo.
The source code of our repository interface looks as follows:
1import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
2import org.springframework.data.repository.Repository;
3
4import java.util.List;
5import java.util.Optional;
6
7interface TodoRepository extends Repository<Todo, Long>, JpaSpecificationExecutor<Todo> {
8
9 void delete(Todo deleted);
10
11 List<Todo> findAll();
12
13 Optional<Todo> findOne(Long id);
14
15 void flush();
16
17 Todo save(Todo persisted);
18}
Let’s move on and create the specification builder class.
Creating the Specification Builder Class
We can create a specification builder class that fulfils the requirements of our search function by following these steps:
- Create the specification builder class and ensure that it cannot be instantiated.
- Create a private static getContainsLikePattern(String searchTerm) method and implement it by following these rules: - If the searchTerm is null or empty, return the String “%”. This ensures that if the search term is not given, our specification builder class will create a specification that returns all todo entries. - If the search isn’t null or empty, transform the search term into lowercase and return the like pattern that fulfils the requirements of our search function.
- Add a static titleOrDescriptionContainsIgnoreCase(String searchTerm) method to the specification builder class and set its return type to Specification<Todo>.
- Implement this method by following these steps: 1. Create a Specification<Todo> object that selects todo entries whose title or description contains the given search term. 2. Return the created Specification<Todo> object.
The source code or our specification builder class looks as follows:
1import org.springframework.data.jpa.domain.Specification;
2
3final class TodoSpecifications {
4
5 private TodoSpecifications() {}
6
7 static Specification<Todo> titleOrDescriptionContainsIgnoreCase(String searchTerm) {
8 return (root, query, cb) -> {
9 String containsLikePattern = getContainsLikePattern(searchTerm);
10 return cb.or(
11 cb.like(cb.lower(root.<String>get(Todo_.title)), containsLikePattern),
12 cb.like(cb.lower(root.<String>get(Todo_.description)), containsLikePattern)
13 );
14 };
15 }
16
17 private static String getContainsLikePattern(String searchTerm) {
18 if (searchTerm == null || searchTerm.isEmpty()) {
19 return "%";
20 }
21 else {
22 return "%" + searchTerm.toLowerCase() + "%";
23 }
24 }
25}
Let’s find out how we can implement the service method that creates and invokes our database query.
Implementing the Service Method
The first thing that we have to do is to create an interface called TodoSearchService. This interface declares one method called findBySearchTerm(). This method takes the search term as a method parameter and returns a list of TodoDTO objects. The source code of the TodoSearchService interface looks as follows:
1import java.util.List;
2
3public interface TodoSearchService {
4
5 List<TodoDTO> findBySearchTerm(String searchTerm);
6}
We can implement this interface by following these steps:
- Create a RepositoryTodoSearchService class, implement the TodoSearchService interface, and annotate the class with the @Service annotation.
- Add a private final TodoRepository field to the created class.
- Create a constructor that injects a TodoRepository object to the created field by using constructor injection.
- Override the findBySearchTerm() method. Annotate the method with the @Transactional annotation and ensure that the transaction is read-only.
- Implement the findBySearchTerm() method by following these steps: 1. Get the Specification<Todo> object by invoking the static titleOrDescriptionContainsIgnoreCase() method of the TodoSpecifications class. 2. Get the todo entries whose title or description contains the given search term by invoking the findAll() method of the JpaSpecificationExecutor interface. Pass the created Specification<Todo> object as a method parameter. 3. Transform the list of Todo objects into a list of TodoDTO objects and return the created list.
The source of our service class looks as follows:
1import org.springframework.beans.factory.annotation.Autowired;
2import org.springframework.data.jpa.domain.Specification;
3import org.springframework.stereotype.Service;
4import org.springframework.transaction.annotation.Transactional;
5
6import java.util.List;
7
8import static net.petrikainulainen.springdata.jpa.todo.TodoSpecifications.titleOrDescriptionContainsIgnoreCase;
9
10@Service
11final class RepositoryTodoSearchService implements TodoSearchService {
12
13 private final TodoRepository repository;
14
15 @Autowired
16 public RepositoryTodoSearchService(TodoRepository repository) {
17 this.repository = repository;
18 }
19
20 @Transactional(readOnly = true)
21 @Override
22 public List<TodoDTO> findBySearchTerm(String searchTerm) {
23 Specification<Todo> searchSpec = titleOrDescriptionContainsIgnoreCase(searchTerm);
24 List<Todo> searchResults = repository.findAll(searchSpec);
25 return TodoMapper.mapEntitiesIntoDTOs(searchResults);
26 }
27}
Additional Reading:
Let’s move on and find out when we should create our database queries by using the JPA Criteria API.
Why Should We Use the JPA Criteria API?
This tutorial has already taught us how we can create database queries by using the method names of our query methods, the @Query annotation, and named queries. The problem of these query generation methods is that we cannot use them if we have to create dynamic queries (i.e queries that don’t have a constant number of conditions).
If we need to create dynamic queries, we have to create these queries programmatically, and using the JPA Criteria API is one way to do it. The pros of using the JPA Criteria API are:
- It supports dynamic queries.
- If we have an existing application that uses the JPA Criteria API, it is easy to refactor it to use Spring Data JPA (if we want to).
- It is the standard way to create dynamic queries with the Java Persistence API (this doesn’t necessarily matter, but sometimes it does matter).
That sounds impressive. Unfortunately, the JPA Criteria API has one big problem:
It is very hard to implement complex queries and even harder to read them.
That is why I think that we should use criteria queries only when it is absolutely necessary (and we cannot use Querydsl).
Let’s move on and summarize what we have learned from this blog post.
Summary
This blog post has taught us six things:
- We can create the JPA static metamodel classes by using the Maven Processor Plugin.
- If we want to invoke queries that use the JPA Criteria API, our repository interface must extend the JpaSpecificationExecutor<T> interface.
- We can specify the conditions of our database queries by creating new Specification<T> objects.
- We should create our Specification<T> objects by using specification builder classes.
- We can combine Specification<T> objects by using the methods provided by the Speficications<T> class.
- We should use criteria queries only when we don’t have a choice.
The next part of this tutorial describes how we can create database queries with Querydsl.
P.S. You can get the example application of this blog post from Github.