What is Criteria API and When to Use It
The JPA Criteria API is a programmatic, strongly-typed (Type-Safe) API designed for the dynamic construction of object queries. Rather than concatenating static JPQL strings, de...
The JPA Criteria API is a programmatic, strongly-typed (Type-Safe) API designed for the dynamic construction of object queries. Rather than concatenating static JPQL strings, developers build an abstract syntax tree of the query in Java code using classes and methods. The primary production domain for Criteria API is dynamic filtering and multi-criteria search engines (e.g., e-commerce product catalogs, administrative dashboards), where WHERE predicates are evaluated and assembled conditionally at runtime.
🟢 Junior Level
Core Concept and Foundational Components
Unlike raw JPQL strings ("SELECT u FROM User u WHERE u.age > 18"), where typos in field names are only caught at runtime when the database query fails, Criteria API queries are validated by the Java compiler:
// 1. CriteriaBuilder — Central factory for expressions, predicates, and queries
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
// 2. CriteriaQuery<T> — Query structure defining result type
CriteriaQuery<User> cq = cb.createQuery(User.class);
// 3. Root<T> — Query entry point (equivalent to SQL 'FROM users u')
Root<User> user = cq.from(User.class);
// 4. Assembling SELECT and WHERE clauses
cq.select(user)
.where(cb.greaterThan(user.get("age"), 18));
// 5. Query execution
List<User> adults = entityManager.createQuery(cq).getResultList();
Key Building Blocks
CriteriaBuilder: The core factory. Creates predicates (cb.equal(),cb.like(),cb.between()), aggregations (cb.count(),cb.max()), and theCriteriaQueryitself.CriteriaQuery<T>: The query definition. Sets the projection (select), filtering (where), sorting (orderBy), and grouping (groupBy).Root<T>: The query root representing the base entity being scanned.Predicate: A boolean expression evaluating totrueorfalse, combined into theWHEREclause.
🟡 Middle Level
Dynamic Filtering with Optional Parameters
The primary operational advantage of Criteria API is modular condition composition based on nullable input filters:
public List<User> searchUsers(String name, Integer minAge, String status) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<User> cq = cb.createQuery(User.class);
Root<User> root = cq.from(User.class);
List<Predicate> predicates = new ArrayList<>();
if (name != null && !name.isBlank()) {
predicates.add(cb.like(cb.lower(root.get("name")), "%" + name.toLowerCase() + "%"));
}
if (minAge != null) {
predicates.add(cb.greaterThanOrEqualTo(root.get("age"), minAge));
}
if (status != null) {
predicates.add(cb.equal(root.get("status"), status));
}
// Combine predicates using logical AND:
cq.where(predicates.toArray(new Predicate[0]));
cq.orderBy(cb.asc(root.get("name")));
return entityManager.createQuery(cq).getResultList();
}
Canonical Type-Safety: JPA Metamodel (User_)
Referencing properties with string literals (root.get("name")) remains vulnerable to typos and breaks during IDE refactoring.
To achieve 100% compile-time type safety, include the hibernate-jpamodelgen annotation processor, which generates static metamodel classes during compilation:
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<scope>provided</scope>
</dependency>
Generated static metamodel class:
@StaticMetamodel(User.class)
public abstract class User_ {
public static volatile SingularAttribute<User, Long> id;
public static volatile SingularAttribute<User, String> name;
public static volatile SingularAttribute<User, Integer> age;
}
Now attribute references are verified at compile time:
// A typo in the field name fails compilation immediately!
predicates.add(cb.greaterThanOrEqualTo(root.get(User_.age), minAge));
🔴 Senior Level
The Specification<T> Pattern in Spring Data JPA
In production enterprise architectures, raw Criteria API is too verbose. Spring Data JPA encapsulates it into the functional Specification<T> abstraction:
public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {
}
Creating reusable, atomic specifications:
public final class UserSpecs {
public static Specification<User> hasName(String name) {
return (root, query, cb) ->
name == null ? null : cb.like(cb.lower(root.get(User_.name)), "%" + name.toLowerCase() + "%");
}
public static Specification<User> isAdult() {
return (root, query, cb) -> cb.greaterThanOrEqualTo(root.get(User_.age), 18);
}
public static Specification<User> hasRole(String roleName) {
return (root, query, cb) -> {
Join<User, Role> roleJoin = root.join(User_.roles);
return cb.equal(roleJoin.get(Role_.name), roleName);
};
}
}
Combining filters in the service layer:
Specification<User> spec = Specification.where(UserSpecs.isAdult())
.and(UserSpecs.hasName(request.getName()));
// Automatically supports pagination and issues optimized SQL:
Page<User> result = userRepository.findAll(spec, pageable);
The Fetch Join Disaster in Spring Data JPA Pagination
A notorious production bug occurs when combining Specification fetch joins with paginated queries (Pageable):
public static Specification<Order> fetchItems() {
return (root, query, cb) -> {
// ❌ RUNTIME CRASH DURING PAGINATION!
root.fetch(Order_.items, JoinType.LEFT);
return null;
};
}
Why It Fails:
- Spring Data JPA’s
findAll(Specification, Pageable)executes two separate queries:- The data query:
SELECT o FROM Order o ... - The total count query:
SELECT COUNT(o) FROM Order o ...
- The data query:
- The JPA specification disallows
FETCH JOINwithinCOUNTqueries because joining collections skews row counts. Hibernate throws:org.hibernate.query.SemanticException: query specified join fetching, but the query result type is not the interface of the entity.
Senior Solution:
Conditionally apply the fetch join only when the query result is not a count scalar:
public static Specification<Order> safeFetchItems() {
return (root, query, cb) -> {
// Apply FETCH only if this is NOT a COUNT query!
Class<?> resultType = query.getResultType();
if (resultType != Long.class && resultType != long.class) {
root.fetch(Order_.items, JoinType.LEFT);
}
return null;
};
}
4 Tricky Questions
1. What is the fundamental difference between root.join() and root.fetch() in Criteria API?
Answer:
root.join("items"): Performs an SQLJOINexclusively to enable filtering on child attributes (where(cb.equal(join.get("price"), 100))). The child entities are not materialized in memory; their collection remains an uninitialized lazy proxy (PersistentBag). Accessing it outside the transaction triggers $N+1$ queries.root.fetch("items"): Generates an SQLJOIN FETCH, loading and initializing the child collection in a single query. The returnedFetch<X, Y>contract historically prohibited downstreamWHEREjoins, though Hibernate 6 allows subsequent joins on fetch paths.
2. How do you construct a DTO projection in Criteria API without using CriteriaQuery<Tuple>?
Answer:
- Use the
cb.construct()factory method:CriteriaQuery<UserDto> cq = cb.createQuery(UserDto.class); Root<User> root = cq.from(User.class); cq.select(cb.construct( UserDto.class, root.get(User_.id), root.get(User_.name), root.get(User_.email) )); - The
UserDtoclass must provide a public constructor matching the exact number, types, and sequence of the projected expressions. - This creates a direct column-level database query and bypasses entity tracking in the First-Level Cache.
3. Why is Criteria API often considered inferior to QueryDSL or jOOQ in modern development?
Answer:
- Extreme Verbosity (Boilerplate): A moderate query requires 3–4 times more lines of code in Criteria API compared to JPQL, QueryDSL, or jOOQ.
- Poor Readability: The relational query structure is obscured beneath repetitive scaffolding (
CriteriaBuilder,Root,Path, and verbose type casts). - Limited SQL-99/Advanced Feature Coverage: Unlike jOOQ, Criteria API is restricted to the JPA specification and cannot express complex window functions, recursive CTEs, or vendor-specific SQL constructs without awkward
cb.function()escapes.
4. What happens if you pass an empty array of predicates predicates.toArray(new Predicate[0]) to cq.where()?
Answer:
- Passing an empty array to
cq.where()is equivalent to omitting theWHEREclause entirely. - Hibernate generates an unqualified SQL statement:
SELECT * FROM users. - No exception is thrown; the query successfully retrieves all rows in the target table.
🎯 Interview Cheat Sheet
30-Second Elevator Pitch
“JPA Criteria API is a programmatic, type-safe API for building database queries dynamically in Java code. Its primary use case is multi-filter search forms where
WHEREpredicates must be assembled conditionally at runtime. Combined withhibernate-jpamodelgen, it provides compile-time property validation via static metamodels (User_). In Spring Boot, Criteria API is best leveraged via theSpecification<T>pattern. For static queries, Criteria API should be avoided due to excessive verbosity; for complex dynamic querying, QueryDSL and jOOQ offer vastly superior readability and developer ergonomics.”
Key Production Metrics & Indicators
- CriteriaQuery Construction Overhead: Dynamic predicate tree construction consumes measurable CPU cycles under high-concurrency request loads (thousands of RPS).
- Dual Query Latency on Paginated Specifications: Excessive execution times caused by unoptimized companion
COUNTqueries in Spring Data’sfindAll(Specification, Pageable).
Red Flags (What NOT to Say)
- ❌ “I write all static queries in Criteria API to ensure compile-time safety.” — Unnecessary boilerplate and reduced maintainability; use JPQL or Spring Data derived queries for static queries.
- ❌ “Criteria API bypasses Hibernate and talks directly to the database driver.” — False. Criteria API generates a JPA query tree that compiles through Hibernate’s SQM engine into SQL.
- ❌ “Using raw string properties like
root.get("title")is standard in Criteria API.” — Defeats the primary purpose of Criteria API; always use generated static metamodels (User_). - ❌ “I attach
root.fetch()inside Specifications without checking if it’s a count query.” — Breaks paginated queries by injectingFETCH JOINintoCOUNTstatements.
Related Topics
- What is JPQL and How Does It Differ from SQL — Query language fundamentals
- How to Use JOIN FETCH to Solve the N+1 Problem — Solving query storms
- What is Projection in JPA — Projection mechanisms
- How to Properly Use @OneToMany and @ManyToOne — Association mapping