Introduction
When building REST APIs, we often need to filter data based on various conditions — like searching users by name, email, or registration date. Writing separate queries for every combination can quickly become a nightmare.
That’s where JPA Specifications and CriteriaBuilder come in! They allow us to build dynamic queries based on incoming parameters, without cluttering the codebase.
In this primer, you’ll learn how to:
• Use Spring Data JPA’s Specification interface.
• Dynamically create queries using CriteriaBuilder.
• Combine multiple filters cleanly.
• Build a search API that takes flexible filtering options.
Prerequisites
Before starting, ensure you have the following:
• Basic knowledge of Spring Boot and Spring Data JPA.
• A working Spring Boot project with JPA configured.
• A database set up (e.g., H2, MySQL, PostgreSQL).
Project Structure
We’ll use a simple User entity as an example:
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
private String role;
private LocalDate registrationDate;
// Getters and Setters
}
Step 1: Add Spring Data JPA Dependency
Add the Spring Data JPA dependency to your pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
This allows us to use JPA repositories and specifications in the project.
Step 2: Extend JpaSpecificationExecutor in Repository
Next, update your UserRepository to extend JpaSpecificationExecutor:
public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {
}
This step ensures that your repository can support dynamic queries using JPA Specifications.
Step 3: Define the Specification
Create a utility class UserSpecifications that will build our dynamic queries.
public class UserSpecifications {
public static Specification<User> hasName(String name) {
return (root, query, cb) -> name == null ? null : cb.like(cb.lower(root.get("name")), "%" + name.toLowerCase() + "%");
}
public static Specification<User> hasEmail(String email) {
return (root, query, cb) -> email == null ? null : cb.like(cb.lower(root.get("email")), "%" + email.toLowerCase() + "%");
}
public static Specification<User> hasRole(String role) {
return (root, query, cb) -> role == null ? null : cb.equal(root.get("role"), role);
}
public static Specification<User> registeredAfter(LocalDate date) {
return (root, query, cb) -> date == null ? null : cb.greaterThanOrEqualTo(root.get("registrationDate"), date);
}
}
In each method, we return a Specification<User>. If the parameter is null, it returns null, and the filter is ignored.
Step 4: Combine Specifications Dynamically
Now, let’s create a service method to combine the filters based on user input:
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public List<User> searchUsers(String name, String email, String role, LocalDate registeredAfter) {
Specification<User> spec = Specification.where(UserSpecifications.hasName(name))
.and(UserSpecifications.hasEmail(email))
.and(UserSpecifications.hasRole(role))
.and(UserSpecifications.registeredAfter(registeredAfter));
return userRepository.findAll(spec);
}
}
The Specification.where() method creates the base query, and .and() is used to combine filters.
Step 5: Expose it via REST Controller
To make our dynamic query accessible via API, we’ll create a REST endpoint:
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/search")
public List<User> searchUsers(
@RequestParam(required = false) String name,
@RequestParam(required = false) String email,
@RequestParam(required = false) String role,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate registeredAfter) {
return userService.searchUsers(name, email, role, registeredAfter);
}
}
Now, you can make requests like:
GET /api/users/search?name=John&role=ADMIN
This API supports dynamic filtering, applying only the filters that have values.
Why Use Specification + CriteriaBuilder?
Benefits of Using Specification and CriteriaBuilder:
• Cleaner Code: Avoid cluttering the repository with numerous query methods.
• Reusable Filters: Easily reuse query components across multiple parts of the application.
• Flexibility: Dynamic filtering based on any combination of parameters.
• Type-Safe: Avoid raw SQL or JPQL, which can be error-prone.