JPA Specifications provide a powerful and flexible way to handle complex query scenarios. they offer type safety, reusability, and allow for the construction of dynamic queries based on runtime conditions.
In this article, I will show step by step how you can configure, implement and use JPA Specifications in your Spring Boot project.
Prerequisite
In order to follow this tutorial to the end, you should have some knowledge about:
- Java 8 or newer releases
- Maven
- Docker
- SQL Database
What are JPA Specifications
JPA has introduces a criteria API that you can use to build queries programmatically. By writing a criteria
, you define the where clause of a query for a domain class.
Spring Data JPA takes the concept of a specification from Eric Evan’s book, “Domain-Driven Design” — following the same semantics and providing an API to define such specifications with the JPA criteria API.
To use specifications, you can extend your repository interface with the JpaSpecificationExecutor
interface, as follows:
public interface EmployeeRepository extends CrudRepository<Employee, UUID>,
JpaSpecificationExecutor<Employee> {
//...
}
The additional interface has methods that let you run specifications in various ways. For example, the findAll
method returns all entities that match the specification, as shown in the following example:
List<T> findAll(Specification<T> spec);
The Specification
interface is defined as follows:
public interface Specification<T> {
Predicate withPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder);
}
Specifications can easily be used to build an extensible set of predicates on top of an entity that then can be combined and used with JpaRepository
without the need to declare a query (method) for every needed combination.
Why using JPA Specifications
Using JPA specifications is beneficial when you need a more dynamic and flexible way to construct queries in your application, as opposed to using static queries with the @Query
annotation.
JPA specifications allow you to dynamically build complex queries based on certain criteria at runtime. This can be especially useful when you have a variety of search parameters that can change or when you need to apply or add different criteria based on user input.
JPA specifications support pagination and sorting out of the box, making it easier to implement these features in your queries. This can be particularly useful when dealing with large datasets or when you need to display query results in a paginated format.
Here are some reasons why this might happen:
-
Join Fetches
: When using JPA Specifications to construct queries that involve joining multiple entities, the generated SQL query may result in multiple selects to fetch data from different tables. This can lead to many database round trips, increasing query execution time.-
Lazy Loading
: If your entities have lazy loading associations, the JPA Specifications query may trigger additional select statements to fetch associated entities lazily. This can result in the N+1 query problem, where multiple additional queries are executed to fetch related entities one at a time.-
Predicate Construction
: The dynamic nature of JPA Specifications allows for the construction of complex query predicates based on various criteria. As a result, the generated SQL query may include multiple conditions and subqueries, leading to increased query complexity and potentially more database calls.-
Subquery Generation
: JPA Specifications can involve the generation of subqueries to handle nested conditions or advanced filtering logic. Subqueries can result in additional selects being executed to fetch the necessary data for the main query.Database Configuration
Before diving into Spring Boot part, you have to configure a postgres docker database for the demo practice.
In a folder on your computer, create a folder named postgres-db and add 3 files named :
- docker-compose.yml
- postgres.dockerfile
- .env
In the docker-compose.yml
file add the following configuration
services:
postgres:
build:
context: .
dockerfile: postgres.dockerfile
image: "postgres-tutorials"
container_name: ${PG_CONTAINER_NAME}
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
PGDATA: ${PGDATA}
volumes:
- dbtuto:/data/postgres-tuto
ports:
- "5432:5432"
restart: unless-stopped
volumes:
dbtuto:
external: true
In the postgres.dockerfile
add the following configuration
FROM postgres:15.1-alpine
LABEL author="Your name here"
LABEL description="Postgres Image for demo"
LABEL version="1.0"
COPY *.sql /docker-entrypoint-initdb.d/
In the .env
file add the following configuration
PG_CONTAINER_NAME='postgres-tuto'
POSTGRES_USER='tuto'
POSTGRES_PASSWORD='admingres'
POSTGRES_DB='database'
PGDATA='/data/postgres-tuto'
Now that you have configured all your database elements, you can run the following command to start your docker database container
docker-compose up -d
Practice
To maintain a certain consistency, and for ease of understanding. I've decided to use the Layered Architecture for most of my tutorials on Spring Boot.
So, in most of my articles on this blog, I'll use this mini-project structure:
src
â”” main
â”” java
â”” controller # for handling the HTTP Requests
â”” entity # entity need to be managed by JPA
â”” repository # for accessing data from database
â”” service # for business logic implementation
â”” resources
â”” application.yml # configurations of our app
pom.xml # dependency management file
If you’re curious about a more detailed presentation of spring boot project architecture, and you want to know how all these layers work under the hood, check out this article that I wrote.
Now that I’ve set this reminder, we can continue on this tutorial.
Dependencies
In order to run our production code for this tutorial, you will need to add the following additional dependencies in your pom.xml
file.
<dependencies>
<!-- Springboot dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- -->
<!-- for JPA Specifications -->
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
<version>4.0.2</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>4.0.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>1.0.0.Final</version>
</dependency>
<!-- -->
<!-- for Databases management -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.5.1</version>
</dependency>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
<version>6.4.2.Final</version>
</dependency>
<!-- -->
<!-- for Logging -->
<dependency>
<groupId>org.tuxdude.logback.extensions</groupId>
<artifactId>logback-colorizer</artifactId>
<version>1.0.1</version>
<type>jar</type>
</dependency>
<!-- -->
<!-- Additional dependencies -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.github.javafaker</groupId>
<artifactId>javafaker</artifactId>
<version>1.0.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.16.1</version>
</dependency>
<!-- -->
</dependencies>
App Config
Once your database container is up and running, in your application.yml
file, you can add the following configurations to make your app work smoothly with your database and also having some logs printed in your IDE terminal for database operations.
spring:
datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://localhost:5432/database
username: tuto
password: admingres
jpa:
hibernate:
ddl-auto: create-drop
show-sql: true
properties:
hibernate:
format_sql: true
logging:
level:
org:
hibernate.orm.jdbc.bind : trace
springframework: info
hibernate: info
server:
port: 8000
Entity
We will use Hibernate to define the model of the entity in Java, and it will create the SQL-related queries for our DDL. You can create a package named entity and create a Record
class.
Then we will define a name for the table in our database with Jakarta persistence API like this
//...
@Table(name = "employee")
public class Record extends BaseEntity {
//...
So the complete code for the entity will look like this
package com.example.demo.entity;
import jakarta.persistence.*;
import lombok.*;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.math.BigDecimal;
import java.util.UUID;
@Getter
@Setter
@Entity
@Builder
@ToString
@AllArgsConstructor
@Table(name = "employee")
@EntityListeners(AuditingEntityListener.class)
public class Record extends BaseEntity {
public Record() {
// No-args constructor for Hibernate Reflection
}
@Id
@Column(nullable = false)
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@Column(name = "fullname")
private String fullName;
@Column(name = "age")
private Integer age;
@Column(name = "address")
private String address;
@Column(name = "salary")
private BigDecimal salary;
}
Here is the BaseEntity
class where we define the shared properties by all the entities in your project
package com.example.demo.entity;
import jakarta.persistence.Column;
import jakarta.persistence.MappedSuperclass;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.io.Serializable;
import java.time.LocalDateTime;
@MappedSuperclass
abstract class BaseEntity implements Serializable {
@CreationTimestamp
@Column(updatable = false, name = "created_at")
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}
Repository
Create a package called repository, then create a file RecordRepository
and add the code below
package com.example.demo.repository;
import com.example.demo.entity.Record;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;
import java.util.UUID;
@Repository
public interface RecordRepository extends JpaRepository<Record, UUID>,
JpaSpecificationExecutor<Record> {
}
Specifications
Now take the case we want to retrieve all employees having high salaries (for example: salary > 65000), we will do something like this.
private static Specification<Record> employeesHavingHighSalary() {
return (root, query, builder) -> builder.greaterThan(
root.get(Record_.SALARY), BigDecimal.valueOf(65000)
);
}
The Record_
type is a metamodel type generated using the JPA Metamodel generator (see the Hibernate implementation’s documentation for an example).
As I mentioned in the dependency section above, you have to add the following dependencies in your pom.xml.
<!-- for JPA Specifications -->
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
<version>4.0.2</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>4.0.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>1.0.0.Final</version>
</dependency>
<!-- -->
Then run the following command to generate the Record_ metamodel.
mvn clean install
The expression, Record_.SALARY
, assumes the Record
has a salary
attribute. Besides that, we have expressed some criteria on a business requirement abstraction level and created executable Specifications
. So a client might use a Specification
as follows:
List<Record> employees = recordRepository.findAll(employeesHavingHighSalary());
Why not create a query for this kind of data access?
Using a single Specification
does not gain a lot of benefit over a plain query declaration. The power of specifications really shines when you combine them to create new Specification
objects.
You can achieve this through the default methods of Specification
JPA provides to build expressions similar to the following:
List<Record> youngEmployeesWithHighSalaries = recordRepository
.findAll(employeesHavingHighSalary().and(employeeAreYoungWorkers(27)))
Specifications offers some “glue-code” default methods to chain and combine Specification instances. These methods let you extend your data access layer by creating new Specification implementations and combining them with already existing implementations.
For the example above, we were searching for employees having high salaries and being young at the same time. It is pretty easy to achieve that with specifications rather than using plain old SQL queries.
Specifications Builder
The best way to use specification is to use it with the builder pattern inspired by Joshua Blosh.
So, If we want to use JPA Specifications in our service layer, we will create a new class called EmployeeSpecs
, and add the following code.
package com.example.demo.specification;
import com.example.demo.entity.Record;
import com.example.demo.entity.Record_;
import org.springframework.data.jpa.domain.Specification;
import java.math.BigDecimal;
import static java.util.Objects.nonNull;
public class EmployeeSpecs {
private EmployeeSpecs() {
// Enforcing object construction through builder
}
public static class Builder {
private Specification<Record> havingHighSalaries;
private Specification<Record> areYoungWorkers;
public Builder() {
// Default implementation ignored
}
public Builder havingHighSalaries() {
this.havingHighSalaries = employeesHavingHighSalary();
return this;
}
public Builder areYoungWorkers(Integer age) {
if (nonNull(age)) this.areYoungWorkers = employeeAreYoungWorkers(age);
return this;
}
public Specification<Record> build() {
return Specification.where(this.areYoungWorkers)
.and(this.havingHighSalaries);
}
private static Specification<Record> employeesHavingHighSalary() {
return (root, query, builder) -> builder.greaterThan(
root.get(Record_.SALARY), BigDecimal.valueOf(65000)
);
}
private static Specification<Record> employeeAreYoungWorkers(Integer age) {
return (root, query, builder) -> builder.lessThan(
root.get(Record_.AGE), age
);
}
}
}
This class exposes 2 specifications, the first one for retrieving employees who have high salaries and the second one for retrieving employees who are young workers.
In our RecordService we can call them to retrieve wanted information.
Service
Create a package called service, then create a file RecordService
and add the code below
package com.example.demo.service;
import com.example.demo.entity.Record;
import com.example.demo.repository.RecordRepository;
import com.example.demo.specification.EmployeeSpecs;
import lombok.RequiredArgsConstructor;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Objects;
@Service
@RequiredArgsConstructor
public class RecordService {
private final RecordRepository repository;
public List<Record> findAllHighSalary() {
Specification<Record> specifications = new EmployeeSpecs.Builder()
.havingHighSalaries()
.build();
return repository.findAll(specifications);
}
public List<Record> findAllYoungEmployeesWithHighSalaries(Integer age) {
if (Objects.isNull(age)) return findAllHighSalary();
Specification<Record> specifications = new EmployeeSpecs.Builder()
.havingHighSalaries()
.areYoungWorkers(age)
.build();
return repository.findAll(specifications);
}
}
Controller
Create a package controller and add the following file
package com.example.demo.controller;
import com.example.demo.entity.Record;
import com.example.demo.service.RecordService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/employees")
public class RecordController {
private final RecordService service;
public RecordController(RecordService service) {
this.service = service;
}
@GetMapping
public ResponseEntity<List<Record>> getYoungEmployeeWithHighSalaries(@RequestParam(value = "age", required = false) Integer age) {
return new ResponseEntity<>(service.findAllYoungEmployeesWithHighSalaries(age), HttpStatus.OK);
}
}
App Runner
Once you have successfully configured all your application files, you can now create a package called runner and in that package create the file AppRunner
, add the following in it.
The AppRunner class serves for loading our demo database with dummy data so that we can retrieve them with the Rest API we expose through our controller.
package com.example.demo.runner;
import com.example.demo.entity.Record;
import com.example.demo.repository.RecordRepository;
import com.github.javafaker.Faker;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Random;
@Component
@RequiredArgsConstructor
public class AppRunner implements CommandLineRunner {
private final RecordRepository repository;
@Override
public void run(String... args) {
int numberOfRecords = 30;
final Faker faker = new Faker(Locale.UK);
final List<Record> employees = new ArrayList<>();
while (numberOfRecords > 0) {
var employee = Record.builder()
.fullName(faker.name().fullName())
.address(faker.address().fullAddress())
.age(getRandomNumberBetween(26, 40))
.salary(generateRandomSalary())
.build();
employees.add(employee);
numberOfRecords--;
}
repository.saveAll(employees);
}
private static BigDecimal generateRandomSalary() {
return BigDecimal.valueOf(getRandomNumberBetween(40, 120) * 1000L);
}
private static int getRandomNumberBetween(int min, int max) {
return new Random().nextInt((max - min) + 1) + min;
}
}
Now, you can start your Spring Boot application with this following command.
mvn spring-boot:run
Demonstration
Firstly, we will test that with our specification, we can retrieve all employees with high salaries. Like we mention in our specification, it’s all employees that salaries are greater than 65000.
root.get(Record_.SALARY), BigDecimal.valueOf(65000)
To do so, we have to use our URL without providing the age request param.
We can see that our specifications return all the employees that salaries are greater than 65000.
By providing age in the request params, with our specification, we can get all employees who are young and having high salaries.
That’s it, the power of specifications in action.
I hope you enjoyed reading this, and I'm curious to hear if this tutorial helped you. Please let me know your thoughts below in the comments. Don't forget to subscribe to my newsletter to avoid missing my upcoming blog posts.
You can also find me here LinkedIn • Twitter • GitHub or Medium
Wrap up
In conclusion, JPA Specifications provide a powerful and flexible way to handle complex query scenarios. As demonstrated, they offer type safety, reusability, and allow for the construction of dynamic queries based on runtime conditions.
This makes them particularly useful when dealing with complex or changing query criteria. By encapsulating query logic within specifications, developers can create reusable building blocks for constructing queries, making the code more maintainable and easier to test.
This tutorial has provided a practical guide on how to use JPA Specifications effectively in your Spring Boot applications. You can all the source code on my GitHub down below.