The JPA Criteria API provides a type-safe, programmatic way to build queries. Unlike HQL (string-based), Criteria queries are built using Java objects and are checked at compile time - typos in property names cause compile errors, not runtime errors.
The Criteria API is especially useful for building dynamic queries where the conditions are not known at compile time (e.g., search filters).
import jakarta.persistence.criteria.*;
Session session = sessionFactory.openSession();
// Get CriteriaBuilder from session
CriteriaBuilder cb = session.getCriteriaBuilder();
// Create CriteriaQuery for User
CriteriaQuery<User> cq = cb.createQuery(User.class);
// Define the root entity (FROM User u)
Root<User> root = cq.from(User.class);
// SELECT * FROM users (no WHERE)
cq.select(root);
List<User> allUsers = session.createQuery(cq).getResultList();
// WHERE u.role = 'ADMIN'
Predicate roleAdmin = cb.equal(root.get("role"), "ADMIN");
cq.where(roleAdmin);
List<User> admins = session.createQuery(cq).getResultList();
// WHERE u.age >= 18 AND u.active = true
Predicate ageCheck = cb.greaterThanOrEqualTo(root.get("age"), 18);
Predicate activeCheck = cb.isTrue(root.get("active"));
cq.where(cb.and(ageCheck, activeCheck));
// ORDER BY u.lastName ASC
cq.orderBy(cb.asc(root.get("lastName")));
// Execute
List<User> result = session.createQuery(cq).getResultList();
session.close();
// Dynamic search query - conditions added based on non-null parameters
public List<User> searchUsers(String name, String email, String role,
int page, int pageSize) {
Session session = sessionFactory.openSession();
CriteriaBuilder cb = session.getCriteriaBuilder();
CriteriaQuery<User> cq = cb.createQuery(User.class);
Root<User> root = cq.from(User.class);
List<Predicate> predicates = new ArrayList<>();
// Add conditions only if parameters are provided
if (name != null && !name.isEmpty()) {
predicates.add(cb.like(cb.lower(root.get("username")),
"%" + name.toLowerCase() + "%"));
}
if (email != null && !email.isEmpty()) {
predicates.add(cb.like(root.get("email"), "%" + email + "%"));
}
if (role != null && !role.isEmpty()) {
predicates.add(cb.equal(root.get("role"), role));
}
// Combine all predicates with AND
if (!predicates.isEmpty()) {
cq.where(cb.and(predicates.toArray(new Predicate[0])));
}
cq.orderBy(cb.asc(root.get("username")));
// Pagination
List<User> results = session.createQuery(cq)
.setFirstResult(page * pageSize) // Offset
.setMaxResults(pageSize) // Limit
.getResultList();
session.close();
return results;
}
// Count query for pagination
public Long countUsers(String role) {
Session session = sessionFactory.openSession();
CriteriaBuilder cb = session.getCriteriaBuilder();
CriteriaQuery<Long> cq = cb.createQuery(Long.class);
Root<User> root = cq.from(User.class);
cq.select(cb.count(root));
if (role != null) {
cq.where(cb.equal(root.get("role"), role));
}
Long count = session.createQuery(cq).uniqueResult();
session.close();
return count;
}
Criteria API is useful when optional filters, reusable predicates, and typed paths make a query genuinely dynamic.
It is usually less readable than HQL for a fixed query. Use it when runtime choices change joins or predicates, and still inspect the generated SQL so type safety does not hide inefficient execution.
Build each validated filter as a Predicate, then combine groups with cb.and or cb.or in the same structure as the business rule. A flat list can silently change A AND (B OR C) into (A AND B) OR C. Treat an absent filter differently from a filter whose value is empty or invalid.
Do not accept entity property names directly from a request. Map supported sort and filter names to known metamodel paths so user input cannot choose an arbitrary field or join.
Create joins once and reuse them when several predicates target the same association. Collection joins can duplicate root rows, so use distinct only after understanding the SQL and count-query effect. A DTO or Tuple projection can avoid loading complete entities for a read-only result.
Use a subquery when the condition is naturally an existence or aggregate test, but compare its plan with a join. Type-safe construction does not guarantee efficient SQL; inspect the generated statement and database execution plan.
Pagination requires a deterministic order that ends with a unique tie-breaker. Build the count query deliberately because fetch joins and projections from the result query may not belong in it. For deep pages, evaluate keyset pagination when an indexed continuation value is available.
Test no filters, each filter alone, grouped filters, invalid values, no matches, duplicate join rows, and both sort directions. Assert results and query count, then review SQL with realistic cardinality.
Criteria is useful when filters, joins, sorting, and projections are assembled dynamically from optional inputs.
Pagination without a deterministic ORDER BY can return rows in different orders between executions.
Create predicates only for validated, present filter values, then combine them with the intended AND or OR grouping. Empty strings, unbounded date ranges, and user-controlled property names should be normalized before query construction.
Explore 500+ free tutorials across 20+ languages and frameworks.