Вопрос 27 · Раздел 16

Что такое Criteria API и когда его использовать

Criteria API — type-safe способ построения JPQL запросов программно. Он особенно полезен для динамических запросов с множеством опциональных фильтров, где статический JPQL был б...

Версии по языкам: English Russian Ukrainian

Обзор

Criteria API — type-safe способ построения JPQL запросов программно. Он особенно полезен для динамических запросов с множеством опциональных фильтров, где статический JPQL был бы неудобен.


🟢 Junior Level

Что такое Criteria API

Criteria API — type-safe способ строить JPQL запросы программно.

// JPQL — строковый запрос
"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();

Основные компоненты

CriteriaBuilder      фабрика для создания выражений
CriteriaQuery<T>     сам запрос
Root<T>              корневая сущность (FROM clause)
Predicate            условие WHERE

Простой пример

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 — НЕ type-safe. Для type-safety используйте Metamodel:
     // root.get(User_.age)

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

🟡 Middle Level

Когда использовать

✅ Criteria API когда:
- Динамические запросы (много опциональных фильтров)
- Type-safe нужен (компиляция ловит ошибки)
- Рефакторинг безопасен

❌ JPQL когда:
- Простые статические запросы
- Легче читать
- Меньше кода

Динамический запрос с фильтрами

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();
}

С Metamodel (type-safe)

// Metamodel генерируется автоматически
@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 запрос
root.get(User_.age)  // вместо root.get("age")

JOIN в 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);
    }
}

// Использование
Specification<User> spec = Specification
    .where(UserSpecifications.hasName("John"))
    .and(UserSpecifications.hasMinAge(18))
    .and(UserSpecifications.hasStatus("ACTIVE"));

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

Subqueries

// Упрощённый пример — в реальном коде subquery могут быть значительно сложнее

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);  // убрать дубликаты

Генерация Metamodel

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

// Генерирует User_, Order_, и т.д. автоматически

Best Practices

✅ Criteria API для динамических запросов
✅ Metamodel для type-safety
✅ JPQL для простых статических запросов
✅ Spring Data JPA Specification
✅ Fetch joins для связанных данных
✅ Distinct для дубликатов

❌ Criteria API для простых запросов (избыточно)
❌ Без metamodel (string-based prone to errors)
❌ String concat в Criteria — риск query plan cache pollution и нечитаемость. Значения всё равно параметризуются, SQL injection маловероятен.
❌ Игнорирование distinct при fetch joins

🎯 Шпаргалка для интервью

Обязательно знать:

  • Criteria API — type-safe способ построения JPQL запросов программно
  • Основные компоненты: CriteriaBuilder, CriteriaQuery, Root, Predicate
  • Лучше всего для динамических запросов с множеством опциональных фильтров
  • Metamodel (User_) обеспечивает полную type-safety — ошибки на компиляции
  • Specification pattern (Spring Data JPA) — переиспользуемые критерии
  • Для простых статических запросов JPQL проще и чита

Частые уточняющие вопросы:

  • Criteria API vs JPQL — когда что? Criteria API для динамических запросов (опциональные фильтры), JPQL для статических
  • Зачем Metamodel? root.get(“age”) — string-based, ошибки runtime; root.get(User_.age) — type-safe, ошибки на компиляции
  • Specification pattern что даёт? Композиция критериев: where(hasName).and(hasMinAge), переиспользование кода
  • Criteria API поддерживает subqueries и fetch joins? Да — Subquery для подзапросов, root.fetch() для fetch joins

Красные флаги (НЕ говорить):

  • «Criteria API для простых SELECT» — избыточно, JPQL проще
  • «Без Metamodel — использую строки» — runtime errors при рефакторинге
  • «String concat в Criteria» — query plan cache pollution, нечитаемость
  • «Игнорирую distinct при fetch joins» — дубликаты в результатах

Связанные темы:

  • [[26. Что такое JPQL и чем он отличается от SQL]]
  • [[28. Как использовать JOIN FETCH для решения проблемы N+1]]
  • [[29. Что такое projection в JPA]]
  • [[23. Как правильно использовать @OneToMany и @ManyToOne]]