Spring Data JPA gives us simple methods like findByName()
or custom queries using @Query
. But when you need to build dynamic, complex queries at runtime (especially with multiple filters), Criteria API is the most powerful and flexible tool.
This post will walk you through:
- What is the JPA Criteria API
- Why use it instead of JPQL or native SQL
- How to use it in Spring Boot
- Advanced use cases like joins, predicates, pagination
- Best practices

π§ What is Criteria API?
The JPA Criteria API allows you to construct queries programmatically in Java using a type-safe, object-oriented API.
Itβs like building SQL queries without writing actual SQL.
β‘ Why Use Criteria API?
When to Use | Why |
---|---|
Dynamic Filters | Build queries with optional parameters |
Complex Joins and Subqueries | Easier than writing JPQL manually |
Type Safety | No syntax errors at runtime |
Compile-Time Checking | Refactor-friendly |
π§± Basic Building Blocks
CriteriaBuilder
: Factory for query partsCriteriaQuery
: Represents a query objectRoot
: Represents table/entityPredicate
: WHERE conditions
π§ͺ Example Setup
Let’s use a User
entity:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
@Entity public class User { @Id @GeneratedValue private Long id; private String name; private String email; private String status; @ManyToOne private Department department; // Getters and setters } |
1 2 3 4 5 6 7 8 9 |
@Entity public class Department { @Id @GeneratedValue private Long id; private String name; } |
π§ 1. Basic Criteria Query
1 2 3 4 5 6 7 8 9 10 |
public List<User> findUsersByStatus(String status) { CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery<User> query = cb.createQuery(User.class); Root<User> user = query.from(User.class); Predicate condition = cb.equal(user.get("status"), status); query.select(user).w<strong>h</strong>ere(condition); return entityManager.createQuery(query).getResultList(); |
π§© 2. Multiple Conditions (Dynamic Filters)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public List<User> findUsers(String name, String email, String status) { CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery<User> query = cb.createQuery(User.class); Root<User> user = query.from(User.class); List<Predicate> predicates = new ArrayList<>(); if (name != null) { predicates.add(cb.like(user.get("name"), "%" + name + "%")); } if (email != null) { predicates.add(cb.equal(user.get("email"), email)); } if (status != null) { predicates.add(cb.equal(user.get("status"), status)); } query.select(user).w<strong>h</strong>ere(cb.and(predicates.toArray(new Predicate[0]))); return entityManager.createQuery(query).getResultList(); } |
β This is useful for building dynamic search filters in real-world applications.
π 3. Using Joins (e.g., User β Department)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public List<User> findUsersByDepartment(String deptName) { CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery<User> query = cb.createQuery(User.class); Root<User> user = query.from(User.class); Join<User, Department> department = user.join("department"); Predicate predicate = cb.equal(department.get("name"), deptName); query.select(user).w<strong>h</strong>ere(predicate); return entityManager.createQuery(query).getResultList(); } |
π 4. Sorting and Pagination
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public List<User> getUsersSortedAndPaged(int page, int size, String sortField) { CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery<User> query = cb.createQuery(User.class); Root<User> user = query.from(User.class); query.orderBy(cb.asc(user.get(sortField))); TypedQuery<User> typedQuery = entityManager.createQuery(query); typedQuery.setFirstResult(page * size); typedQuery.setMaxResults(size); return typedQuery.getResultList(); } |
π― 5. Using CriteriaQuery for Count
Useful for pagination metadata:
1 2 3 4 5 6 7 8 |
public long countUsersWithStatus(String status) { CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery<Long> query = cb.createQuery(Long.class); Root<User> user = query.from(User.class); query.<strong>select</strong>(cb.count(user)).where(cb.equal(user.get("status"), status)); return entityManager.createQuery(query).getSingleResult(); } |
π§ 6. CriteriaBuilder with IN Clause
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public List<User> findUsersInDepartments(List<String> deptNames) { CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery<User> query = cb.createQuery(User.class); Root<User> user = query.from(User.class); Join<User, Department> department = user.join("department"); Predicate predicate = department.get("name").in(deptNames); query.<strong>select</strong>(user).where(predicate); return entityManager.createQuery(query).getResultList(); } |
ποΈ 7. Reusable Utility for Dynamic Filtering
A reusable method to build Predicate
list dynamically:
1 2 3 4 5 6 7 8 9 10 11 12 |
private List<Predicate> buildFilters(CriteriaBuilder cb, Root<User> root, Map<String, Object> filters) { List<Predicate> predicates = new ArrayList<>(); filters.forEach((field, value) -> { if (value != null) { predicates.add(cb.equal(root.get(field), value)); } }); return predicates; } |
Then use it in your repository:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public List<User> searchUsers(Map<String, Object> filters) { CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery<User> query = cb.createQuery(User.class); Root<User> user = query.from(User.class); List<Predicate> predicates = buildFilters(cb, user, filters); query.<strong>select</strong>(user).where(cb.and(predicates.toArray(new Predicate[0]))); return entityManager.createQuery(query).getResultList(); } |
β Best Practices
Practice | Why |
---|---|
β
Use Metamodel for type safety (via @StaticMetamodel ) | Prevents string-based errors |
β Encapsulate criteria logic into service or helper | Keeps repository clean |
β Use Criteria for complex, dynamic queries | Better than string-based JPQL |
β Avoid mixing Criteria and native queries | Keep query styles consistent |
β Donβt use Criteria for simple queries | Named queries or method names are easier |
π§ͺ Summary
- Criteria API is powerful for type-safe, dynamic queries
- Useful for filtering, joins, pagination, and sorting
- Consider using Specifications if you use Spring Data JPA (alternative abstraction)
- Donβt overuse it for basic queries