Spring Data JPA Query With Arbitrary AND Clauses Example
Building dynamic queries is a common requirement in modern applications, especially when filtering data based on flexible and user-driven criteria. Spring Data JPA provides several mechanisms to achieve this, enabling us to construct queries dynamically without writing complex and repetitive SQL statements. In this article, we delve into three key approaches for creating dynamic queries in Spring Data JPA with arbitrary AND clauses: QueryDSL, Query by Example, and Query by Specification.
1. Scenario
This guide demonstrates constructing dynamic queries using QueryDSL with two related entities in a one-to-many relationship. We will dynamically build queries based on arbitrary conditions to filter data across these entities. We will use the following entities:
Department
: Represents a company department.Employee
: Represents an employee belonging to a department (oneDepartment
can have manyEmployees
).
Users will query employees with filters on both Employee
and Department
fields.
2. Setting Up QueryDSL in Your Project
To begin, we need to configure QueryDSL in our project. QueryDSL is a powerful library that allows us to build type-safe SQL-like queries using Java. It integrates smoothly with Spring Data JPA and supports dynamic query creation.
Maven Dependencies
Add the following dependencies to your pom.xml
file:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | < dependencies > < dependency > < groupId >com.querydsl</ groupId > < artifactId >querydsl-jpa</ artifactId > < version >5.1.0</ version > < classifier >jakarta</ classifier > </ dependency > < dependency > < groupId >com.querydsl</ groupId > < artifactId >querydsl-apt</ artifactId > < version >5.1.0</ version > < classifier >jakarta</ classifier > < scope >provided</ scope > </ dependency > </ dependencies > < build > < plugins > < plugin > < groupId >com.mysema.maven</ groupId > < artifactId >apt-maven-plugin</ artifactId > < version >1.1.3</ version > < executions > < execution > < phase >generate-sources</ phase > < goals > < goal >process</ goal > </ goals > < configuration > < outputDirectory >target/generated-sources</ outputDirectory > < processor >com.querydsl.apt.jpa.JPAAnnotationProcessor</ processor > </ configuration > </ execution > </ executions > </ plugin > </ plugins > </ build > |
The first dependency here is querydsl-jpa
. This is the core QueryDSL JPA module that provides support for building type-safe queries with JPA entities. The version 5.1.0
is specified, and the jakarta
classifier ensures compatibility with the Jakarta Persistence API (javax.persistence
was renamed to jakarta.persistence
starting from Jakarta EE 9).
The second dependency is querydsl-apt
, which is responsible for generating the QueryDSL meta-classes (e.g., QEmployee
, QDepartment
) during the build process. The scope
is set to provided
because this dependency is needed only at compile time and doesn’t need to be included in the final packaged artifact. Like the previous dependency, it also uses the jakarta
classifier to ensure compatibility with Jakarta Persistence.
Build Plugin
This plugin, apt-maven-plugin
, is used to process annotations in the Java source code and generate additional classes. In this case, it generates the QueryDSL JPA meta-model classes from the JPA entities. The plugin’s version is 1.1.3
.
The plugin is configured to run in the generate-sources
phase of the Maven build lifecycle, ensuring that the meta-model classes are generated before compilation. The goals
section specifies the process
goal, which triggers annotation processing. The configuration includes:
outputDirectory
: Specifies where the generated source files should be placed, in this case,target/generated-sources
.processor
: Defines the annotation processor to use, which in this case iscom.querydsl.apt.jpa.JPAAnnotationProcessor
. This processor reads the JPA annotations from your entities and generates QueryDSL classes based on them.
3. Defining Entities
In our scenario, we have two entities: Department
and Employee
. A Department
has many Employees
, establishing a one-to-many relationship.
Department Entity
01 02 03 04 05 06 07 08 09 10 11 12 13 | @Entity public class Department { @Id @GeneratedValue (strategy = GenerationType.IDENTITY) private Long id; private String name; @OneToMany (mappedBy = "department" , cascade = CascadeType.ALL) private List<Employee> employees; // Getters and Setters } |
The Department
entity represents a company department. It includes:
- A primary key
id
. - A
name
field for the department name. - A
List<Employee>
field annotated with@OneToMany
to establish the one-to-many relationship. ThemappedBy
attribute indicates that thedepartment
field in theEmployee
entity owns the relationship.
Employee Entity
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 | @Entity public class Employee { @Id @GeneratedValue (strategy = GenerationType.IDENTITY) private Long id; private String firstName; private String lastName; private Integer age; @ManyToOne @JoinColumn (name = "department_id" ) private Department department; // Getters and Setters } |
The Employee
entity represents an employee within a department. It includes:
- A primary key
id
. - Fields for
firstName
,lastName
, andage
. - A
department
field annotated with@ManyToOne
to establish the relationship back to theDepartment
entity. The@JoinColumn
annotation specifies the foreign key column asdepartment_id
.
4. Generating QueryDSL Meta-Model
After defining the entities, compile your project using Maven. QueryDSL will generate meta-model classes (QEmployee
and QDepartment
). These classes allow you to reference entity fields like employee.firstName
or department.name
in a type-safe manner when building queries.
4.1 Using QuerydslPredicateExecutor
for Simplified Dynamic Queries
A powerful feature of QueryDSL is its integration with Spring Data JPA through the QuerydslPredicateExecutor
interface. This interface allows us to execute dynamic queries by combining predicates programmatically. In this section, we’ll demonstrate how to apply QuerydslPredicateExecutor
to the Employee
and Department
entities in our application.
4.1.1 Defining the Employee Repository
To enable QuerydslPredicateExecutor
, update the EmployeeRepository
interface to extend it alongside JpaRepository
. This provides built-in support for dynamic query execution using QueryDSL predicates.
1 2 3 | public interface EmployeeRepository extends JpaRepository, QuerydslPredicateExecutor { } |
The JpaRepository<Employee, Long>
offers basic CRUD operations and pagination support for the Employee
entity, while the QuerydslPredicateExecutor<Employee>
allows the direct execution of QueryDSL predicates using the findAll
method.
4.1.2 Building and Executing Predicates
With the repository configured, we can now dynamically use QueryDSL to build predicates. Here’s an example of querying employees who satisfy multiple criteria:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 | @Service public class EmployeeQueryService { private final EmployeeRepository employeeRepository; public EmployeeQueryService(EmployeeRepository employeeRepository) { this .employeeRepository = employeeRepository; } public List<Employee> findEmployeesByMandatoryCriteria(String lastName, Integer age, String departmentName) { QEmployee qEmployee = QEmployee.employee; // Build the predicate with AND clauses BooleanExpression predicate = qEmployee.lastName.endsWithIgnoreCase(lastName) .and(qEmployee.age.eq(age)) .and(qEmployee.department.name.eq(departmentName)); // Execute the query and return results return (List<Employee>) employeeRepository.findAll(predicate); } } |
This code defines a service class, EmployeeQueryService
, which uses QueryDSL to construct dynamic queries for filtering employees based on various criteria. It demonstrates the flexibility of QueryDSL in building predicates dynamically and highlights the use of .and()
clauses to combine multiple conditions logically.
The above code dynamically constructs a query using QueryDSL predicates without any conditional checks. The BooleanExpression
object predicate
starts with the first condition, qEmployee.lastName.endsWithIgnoreCase(lastName)
, filtering employees whose last name ends with the specified string (case-insensitive). Additional filters are appended to the predicate using the .and()
method, which combines conditions logically to ensure all must be true. For example, the second condition qEmployee.age.eq(age)
restricts the results to employees with an exact age match, while the third condition, qEmployee.department.name.eq(departmentName)
, filters by department name.
The .and()
method plays a pivotal role by chaining these conditions into a single predicate, forming a cohesive query that can be executed in one call to the repository. By passing the combined predicate to the findAll
method of QuerydslPredicateExecutor
, the repository efficiently retrieves only those employees meeting all specified criteria. This code is ideal for scenarios where all filters are mandatory, eliminating the need for conditional checks.
4.2 Exposing the Service through a REST Endpoint
To make this query accessible, you can expose it through a REST controller:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 | @RestController public class EmployeeController { private final EmployeeQueryService employeeQueryService; public EmployeeController(EmployeeQueryService employeeQueryService) { this .employeeQueryService = employeeQueryService; } @GetMapping ( "/employees" ) public List<Employee> getEmployees( @RequestParam String lastName, @RequestParam Integer age, @RequestParam String departmentName) { return employeeQueryService.findEmployeesByMandatoryCriteria(lastName, age, departmentName); } } |
The code below shows how to add sample data to the database for testing the endpoint:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | @SpringBootApplication public class DynamicJpaQuerydslApplication implements CommandLineRunner { private final EmployeeRepository employeeRepository; private final DepartmentRepository departmentRepository; public DynamicJpaQuerydslApplication(EmployeeRepository employeeRepository, DepartmentRepository departmentRepository) { this .employeeRepository = employeeRepository; this .departmentRepository = departmentRepository; } public static void main(String[] args) { SpringApplication.run(DynamicJpaQuerydslApplication. class , args); } @Override public void run(String... args) { Department sales = new Department(1L, "Sales" , null ); Department marketing = new Department(2L, "Marketing" , null ); departmentRepository.save(sales); departmentRepository.save(marketing); Employee johnSmith = new Employee(1L, "Thomas" , "Smith" , 25 , sales); Employee aliceJohnson = new Employee(2L, "Alice" , "Franklin" , 30 , marketing); Employee bobSmith = new Employee(3L, "Bob" , "Fish" , 25 , sales); employeeRepository.save(johnSmith); employeeRepository.save(aliceJohnson); employeeRepository.save(bobSmith); } } |
4.3 Testing the Endpoint
Once the application is running, we can test the endpoint using a tool like Postman or a browser. For example, the following request on a browser (http://localhost:8080/employees?lastName=Smith&age=25&departmentName=Sales
) retrieves employees matching the criteria:
Response (JSON):
5. Query by Example (QBE)
Spring Data JPA supports Query by Example (QBE), allowing us to dynamically construct queries based on a sample entity (example). This is particularly useful for building filters without explicitly writing queries. To support Query by Example, the repository interface must extend both JpaRepository
and QueryByExampleExecutor
.
Here is how we can integrate QBE into the application:
1 2 | public interface EmployeeRepository extends JpaRepository<Employee, Long>, QueryByExampleExecutor<Employee> { } |
Implement the Service Layer
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | @Service public class EmployeeQueryByExampleService { @Autowired EmployeeRepository employeeRepository; public List<Employee> findEmployeesByExample(String lastName, Integer age, String departmentName) { Employee probe = new Employee(); probe.setLastName(lastName); probe.setAge(age); Department department = new Department(); department.setName(departmentName); probe.setDepartment(department); // Create an Example with matching rules ExampleMatcher matcher = ExampleMatcher.matching() .withIgnorePaths( "id" ) // Ignore ID field .withIgnoreNullValues() // Ignore null values .withStringMatcher(ExampleMatcher.StringMatcher.ENDING); // Match strings by ending Example<Employee> example = Example.of(probe, matcher); return employeeRepository.findAll(example); } } |
Query by Example works by creating a probe, which is a sample entity with fields set to the desired query values. An ExampleMatcher
is used to define matching rules, such as ignoring null values and specifying how string comparisons are handled. The probe and matcher are combined into an Example
object, which serves as a query definition. This Example
is then passed to the findAll
method, allowing dynamic query execution without the need to write explicit SQL.
6. Query by Specification
Spring Data JPA also supports Specifications, a more advanced and flexible way to build dynamic queries programmatically using the Specification
interface. To enable Specifications, update the repository by extending the interface with JpaSpecificationExecutor
in addition to JpaRepository
to support building and executing Specifications. This enables dynamic query construction based on flexible criteria.
Update the Repository and add JpaSpecificationExecutor
to the repository to enable Specifications.
1 2 | public interface EmployeeRepository extends JpaRepository<Employee, Long>, JpaSpecificationExecutor<Employee> { } |
Next, create a custom Specification
class for building predicates dynamically.
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 | public class EmployeeSpecifications { public static Specification<Employee> hasLastName(String lastName) { return (root, query, criteriaBuilder) -> criteriaBuilder.like(root.get( "lastName" ), "%" + lastName); } public static Specification<Employee> hasAge(Integer age) { return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get( "age" ), age); } public static Specification<Employee> hasDepartmentName(String departmentName) { return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.join( "department" ).get( "name" ), departmentName); } } |
The EmployeeSpecifications
class provides reusable methods to build dynamic query predicates using Spring Data JPA’s Specification
interface and the CriteriaBuilder
. The hasLastName
method uses criteriaBuilder.like
to generate a predicate for matching employee last names with a wildcard pattern. The hasAge
method leverages criteriaBuilder.equal
to create a condition for exact age matching. Similarly, the hasDepartmentName
method uses criteriaBuilder.equal
in conjunction with root.join
to filter employees based on their department name by performing a join between the Employee
and Department
entities. Each method dynamically constructs criteria for flexible query building.
Next, Use the Specification
class to combine multiple dynamic conditions with .and()
or .or()
like this:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 | @Service public class EmployeeQueryBySpecificationService { @Autowired EmployeeRepository employeeRepository; public List<Employee> findEmployeesBySpecification(String lastName, Integer age, String departmentName) { Specification<Employee> spec = Specification.where( null ); // Add dynamic specifications spec = spec.and(EmployeeSpecifications.hasLastName(lastName)); spec = spec.and(EmployeeSpecifications.hasAge(age)); spec = spec.and(EmployeeSpecifications.hasDepartmentName(departmentName)); return employeeRepository.findAll(spec); } } |
These two approaches – Query by Example and Query by Specification, offer alternatives to dynamic query building, complementing QueryDSL for different use cases in our Spring Data JPA application.
7. Conclusion
In this article, we explored dynamic query building in Spring Data JPA using a variety of approaches, including QueryDSL, Query by Example, and Query by Specification. Each method provides a unique and effective way to construct queries dynamically, catering to different use cases. We demonstrated how to use the .and
operator in QueryDSL for combining multiple conditions seamlessly, enabling type-safe and expressive queries. Similarly, Query by Example and Query by Specification offer declarative and reusable ways to build complex filtering criteria without writing explicit SQL.
8. Download the Source Code
This article focused on building a Spring Data JPA query with arbitrary AND clauses for dynamic and flexible filtering.
You can download the full source code of this example here: spring data jpa query arbitrary and clauses