💾 Section 16 · Question #27

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 the CriteriaQuery itself.
  • 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 to true or false, combined into the WHERE clause.

🟡 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:

  1. 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 ...
  2. The JPA specification disallows FETCH JOIN within COUNT queries 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:

  1. root.join("items"): Performs an SQL JOIN exclusively 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.
  2. root.fetch("items"): Generates an SQL JOIN FETCH, loading and initializing the child collection in a single query. The returned Fetch<X, Y> contract historically prohibited downstream WHERE joins, 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:

  1. 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)
    ));
    
  2. The UserDto class must provide a public constructor matching the exact number, types, and sequence of the projected expressions.
  3. 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:

  1. Extreme Verbosity (Boilerplate): A moderate query requires 3–4 times more lines of code in Criteria API compared to JPQL, QueryDSL, or jOOQ.
  2. Poor Readability: The relational query structure is obscured beneath repetitive scaffolding (CriteriaBuilder, Root, Path, and verbose type casts).
  3. 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:

  1. Passing an empty array to cq.where() is equivalent to omitting the WHERE clause entirely.
  2. Hibernate generates an unqualified SQL statement: SELECT * FROM users.
  3. 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 WHERE predicates must be assembled conditionally at runtime. Combined with hibernate-jpamodelgen, it provides compile-time property validation via static metamodels (User_). In Spring Boot, Criteria API is best leveraged via the Specification<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 COUNT queries in Spring Data’s findAll(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 injecting FETCH JOIN into COUNT statements.