Question 27 · Section 16

What is Criteria API and When to Use It

Criteria API is a type-safe way of building JPQL queries programmatically. It's especially useful for dynamic queries with many optional filters, where static JPQL would be inco...

Language versions: English Russian Ukrainian

Overview

Criteria API is a type-safe way of building JPQL queries programmatically. It’s especially useful for dynamic queries with many optional filters, where static JPQL would be inconvenient.


Junior Level

What is Criteria API

Criteria API - type-safe way to build JPQL queries programmatically.

// JPQL - string query
"SELECT u FROM User u WHERE u.age > 18"

// Criteria API - type-safe
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<User> query = cb.createQuery(User.class);
Root<User> root = query.from(User.class);
query.select(root).where(cb.gt(root.get("age"), 18));

List<User> users = entityManager.createQuery(query).getResultList();

Main Components

CriteriaBuilder     - factory for creating expressions
CriteriaQuery<T>    - the query itself
Root<T>             - root entity (FROM clause)
Predicate           - WHERE condition

Simple Example

CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<User> query = cb.createQuery(User.class);
Root<User> root = query.from(User.class);

// SELECT u FROM User u WHERE u.age > 18
query.select(root)
     .where(cb.greaterThan(root.get("age"), 18));
     // String-based access - NOT type-safe. For type-safety use Metamodel:
     // root.get(User_.age)

List<User> users = entityManager.createQuery(query).getResultList();

Middle Level

When to Use

Criteria API when:
- Dynamic queries (many optional filters)
- Type-safety needed (compilation catches errors)
- Safe refactoring

JPQL when:
- Simple static queries
- Easier to read
- Less code

Dynamic Query with Filters

public List<User> findUsers(String name, Integer minAge, String status) {
    CriteriaBuilder cb = entityManager.getCriteriaBuilder();
    CriteriaQuery<User> query = cb.createQuery(User.class);
    Root<User> root = query.from(User.class);

    List<Predicate> predicates = new ArrayList<>();

    if (name != null) {
        predicates.add(cb.like(root.get("name"), "%" + name + "%"));
    }
    if (minAge != null) {
        predicates.add(cb.ge(root.get("age"), minAge));
    }
    if (status != null) {
        predicates.add(cb.equal(root.get("status"), status));
    }

    query.where(predicates.toArray(new Predicate[0]));
    query.orderBy(cb.asc(root.get("name")));

    return entityManager.createQuery(query).getResultList();
}

With Metamodel (Type-Safe)

// Metamodel generated automatically
@StaticMetamodel(User.class)
public abstract class User_ {
    public static volatile SingularAttribute<User, String> name;
    public static volatile SingularAttribute<User, Integer> age;
    public static volatile SingularAttribute<User, String> status;
}

// Type-safe query
root.get(User_.age)  // instead of root.get("age")

JOIN in Criteria API

CriteriaQuery<Order> query = cb.createQuery(Order.class);
Root<Order> root = query.from(Order.class);
Join<Order, User> userJoin = root.join("user");

query.select(root)
     .where(cb.equal(userJoin.get("name"), "John"));

Senior Level

Specification Pattern (Spring Data JPA)

public class UserSpecifications {

    public static Specification<User> hasName(String name) {
        return (root, query, cb) ->
            cb.like(root.get(User_.name), "%" + name + "%");
    }

    public static Specification<User> hasMinAge(int minAge) {
        return (root, query, cb) ->
            cb.greaterThanOrEqualTo(root.get(User_.age), minAge);
    }

    public static Specification<User> hasStatus(String status) {
        return (root, query, cb) ->
            cb.equal(root.get(User_.status), status);
    }
}

// Usage
Specification<User> spec = Specification
    .where(UserSpecifications.hasName("John"))
    .and(UserSpecifications.hasMinAge(18))
    .and(UserSpecifications.hasStatus("ACTIVE"));

List<User> users = userRepository.findAll(spec);

Subqueries

// Simplified example - in real code subqueries can be much more complex

CriteriaQuery<User> query = cb.createQuery(User.class);
Root<User> userRoot = query.from(User.class);

// Subquery
Subquery<Long> subquery = query.subquery(Long.class);
Root<Order> orderRoot = subquery.from(Order.class);
subquery.select(cb.max(orderRoot.get("total")))
        .where(cb.equal(orderRoot.get("user").get("id"), userRoot.get("id")));

query.select(userRoot)
     .where(cb.greaterThan(userRoot.get("totalSpent"), subquery));

Fetch Joins

CriteriaQuery<Order> query = cb.createQuery(Order.class);
Root<Order> root = query.from(Order.class);
root.fetch("items", JoinType.LEFT);  // FETCH JOIN

query.select(root)
     .distinct(true);  // eliminate duplicates

Metamodel Generation

<!-- pom.xml -->
<dependency>
    <groupId>org.hibernate.orm</groupId>
    <artifactId>hibernate-jpamodelgen</artifactId>
    <scope>provided</scope>
</dependency>

// Generates User_, Order_, etc. automatically

Best Practices

Criteria API for dynamic queries
Metamodel for type-safety
JPQL for simple static queries
Spring Data JPA Specification
Fetch joins for related data
Distinct for duplicates

Criteria API for simple queries (excessive)
Without metamodel (string-based prone to errors)
String concat in Criteria - query plan cache pollution and unreadability. Values are still parameterized, SQL injection unlikely.
Ignoring distinct on fetch joins

Interview Cheat Sheet

Must know:

  • Criteria API - type-safe way of building JPQL queries programmatically
  • Main components: CriteriaBuilder, CriteriaQuery, Root, Predicate
  • Best for dynamic queries with many optional filters
  • Metamodel (User_) provides full type-safety - errors at compile time
  • Specification pattern (Spring Data JPA) - reusable criteria
  • For simple static queries, JPQL is simpler and more readable

Frequent follow-up questions:

  • Criteria API vs JPQL - when which? Criteria API for dynamic queries (optional filters), JPQL for static
  • Why Metamodel? root.get(“age”) - string-based, runtime errors; root.get(User_.age) - type-safe, compile-time errors
  • What does Specification pattern give? Criteria composition: where(hasName).and(hasMinAge), code reuse
  • Does Criteria API support subqueries and fetch joins? Yes - Subquery for subqueries, root.fetch() for fetch joins

Red flags (DO NOT say):

  • “Criteria API for simple SELECT” - excessive, JPQL simpler
  • “Without Metamodel - I use strings” - runtime errors on refactoring
  • “String concat in Criteria” - query plan cache pollution, unreadability
  • “I ignore distinct on fetch joins” - duplicates in results

Related topics:

  • [[26. What is JPQL and How Does It Differ from SQL]]
  • [[28. How to Use JOIN FETCH to Solve the N+1 Problem]]
  • [[29. What is Projection in JPA]]
  • [[23. How to Properly Use @OneToMany and @ManyToOne]]