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

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

В отличие от строкового JPQL ("SELECT u FROM User u WHERE u.age > 18"), в котором опечатка в имени поля обнаруживается только в Runtime при обращении к БД, Criteria API компилир...

JPA Criteria API — это программный, типобезопасный (Type-Safe) API для динамического конструирования объектных запросов к базе данных. Вместо написания статичных строковых запросов JPQL разработчик собирает дерево абстрактного синтаксиса запроса с помощью Java-методов и классов. Главная область применения Criteria API — динамический поиск с множеством опциональных фильтров (Search Forms / Filters), где количество условий WHERE формируется на лету во время выполнения программы.


🟢 Junior Level

Что такое Criteria API и базовые компоненты

В отличие от строкового JPQL ("SELECT u FROM User u WHERE u.age > 18"), в котором опечатка в имени поля обнаруживается только в Runtime при обращении к БД, Criteria API компилируется компилятором Java:

// 1. CriteriaBuilder — фабрика выражений, предикатов и функций
CriteriaBuilder cb = entityManager.getCriteriaBuilder();

// 2. CriteriaQuery<T> — структура запроса с типизированным результатом
CriteriaQuery<User> cq = cb.createQuery(User.class);

// 3. Root<T> — корневая сущность (аналог FROM User u в SQL)
Root<User> user = cq.from(User.class);

// 4. Сборка условий WHERE и SELECT
cq.select(user)
  .where(cb.greaterThan(user.get("age"), 18));

// 5. Исполнение запроса
List<User> adults = entityManager.createQuery(cq).getResultList();

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

  • CriteriaBuilder: Центральная фабрика. Создает предикаты (cb.equal(), cb.like(), cb.between()), агрегатные функции (cb.count(), cb.max()) и сам объект запроса.
  • CriteriaQuery<T>: Каркас будущего запроса. Определяет возвращаемый тип (select), секцию where, сортировку (orderBy) и группировку (groupBy).
  • Root<T>: Точка отсчета запроса. Представляет собой сканируемую таблицу сущности.
  • Predicate: Условие фильтрации (логическое выражение true/false), подставляемое в секцию WHERE.

🟡 Middle Level

Динамическая фильтрация с опциональными параметрами

Главное преимущество Criteria API — удобство добавления условий в зависимости от переданных пользователем фильтров:

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

    // Объединяем предикаты через логическое AND:
    cq.where(predicates.toArray(new Predicate[0]));
    cq.orderBy(cb.asc(root.get("name")));

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

Каноническая типобезопасность: JPA Metamodel (User_)

Вызов root.get("name") опирается на строковый литерал и не защищает от опечаток.

Для достижения 100% Type-Safety подключается процессор аннотаций hibernate-jpamodelgen, который на этапе компиляции генерирует канонические метамодели:

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

Сгенерированный класс метамодели:

@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;
}

Теперь обращение к полям валидируется компилятором:

// Опечатка в имени поля User_.agx вызовет ошибку компиляции!
predicates.add(cb.greaterThanOrEqualTo(root.get(User_.age), minAge));

🔴 Senior Level

Паттерн Specification в Spring Data JPA

В реальных Enterprise-приложениях чистый Criteria API слишком многословен. Spring Data JPA инкапсулирует его в элегантный функциональный интерфейс Specification<T>:

public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {
}

Создание переиспользуемых атомарных спецификаций:

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

Композиция фильтров в контроллере/сервисе:

Specification<User> spec = Specification.where(UserSpecs.isAdult())
    .and(UserSpecs.hasName(request.getName()));

// Автоматически учитывает пагинацию и генерирует оптимальный SQL:
Page<User> result = userRepository.findAll(spec, pageable);

Ловушка Fetch Join в Spring Data JPA Page<T>

Критическая production-ошибка при использовании Specification с пагинацией:

public static Specification<Order> fetchItems() {
    return (root, query, cb) -> {
        // ❌ ПАДЕНИЕ В RUNTIME ПРИ ПАГИНАЦИИ!
        root.fetch(Order_.items, JoinType.LEFT);
        return null;
    };
}

Почему это падает:

  1. Метод findAll(Specification, Pageable) выполняет два запроса:
    • Запрос данных: SELECT o FROM Order o ...
    • Запрос общего количества: SELECT COUNT(o) FROM Order o ...
  2. Спецификация JPA прямо запрещает наличие FETCH JOIN в COUNT-запросах (смысл подсчета строк ломается при джойне коллекций). Hibernate выбрасывает ошибку: org.hibernate.query.SemanticException: query specified join fetching, but the query result type is not the interface of the entity.

Senior-решение: Проверять тип результата запроса перед добавлением fetch:

public static Specification<Order> safeFetchItems() {
    return (root, query, cb) -> {
        // Добавляем FETCH только если это НЕ COUNT-запрос!
        Class<?> resultType = query.getResultType();
        if (resultType != Long.class && resultType != long.class) {
            root.fetch(Order_.items, JoinType.LEFT);
        }
        return null;
    };
}

4 Tricky Questions

1. В чем разница между вызовами root.join() и root.fetch() в Criteria API?

Ответ:

  1. root.join("items"): Выполняет SQL JOIN для возможности фильтрации по дочерним колонкам (where(cb.equal(join.get("price"), 100))). Дочерние объекты items не материализуются в памяти и остаются неинициализированными ленивыми прокси (PersistentBag). При обращении к коллекции возникнет проблема $N+1$.
  2. root.fetch("items"): Выполняет SQL JOIN FETCH, за один запрос загружая в память и инициализируя дочернюю коллекцию. При этом возвращаемый Fetch<X, Y> интерфейс ограничен: от него нельзя строить дальнейшие предикаты фильтрации в JPA 2.1 (в JPA 2.2 / Hibernate 6 Fetch разрешает последующие джойны).

2. Как в Criteria API построить DTO-проекцию без использования CriteriaQuery<Tuple>?

Ответ:

  1. Используется фабричный метод cb.construct():
    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. Класс UserDto обязан иметь публичный конструктор с точно совпадающими по типам и порядку аргументами.
  3. Этот подход формирует в СУБД прямой запрос только выбранных колонок без материализации сущностей в PersistenceContext.

3. Почему Criteria API считается менее предпочтительным инструментом по сравнению с QueryDSL или jOOQ?

Ответ:

  1. Многословность (Boilerplate): Базовый запрос на Criteria API требует в 3–4 раза больше строк кода, чем эквивалент на JPQL или QueryDSL.
  2. Плохая читаемость: Структура SQL-запроса теряется за нагромождением CriteriaBuilder, Root, Path и кастов типов.
  3. Отсутствие поддержки части синтаксиса СУБД: В отличие от jOOQ, Criteria API ограничен спецификацией JPA и не умеет работать со сложными оконными функциями, рекурсивными CTE и специфическими функциями баз данных без громоздких оберток cb.function().

4. Что произойдет, если в cq.where() передать пустой массив предикатов predicates.toArray(new Predicate[0])?

Ответ:

  1. Если список предикатов пуст, вызов cq.where(new Predicate[0]) эквивалентен отсутствию секции WHERE.
  2. Hibernate сгенерирует SQL-запрос SELECT * FROM table без каких-либо условий фильтрации.
  3. Ошибки или исключения не произойдет, запрос вернет все строки таблицы.

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

30-секундный ответ (Elevator Pitch)

«JPA Criteria API — это программный типобезопасный способ построения объектных запросов. Его главное назначение — динамические запросы с опциональными фильтрами, где статический JPQL превратился бы в громоздкую конкатенацию строк. В связке с hibernate-jpamodelgen он обеспечивает полную проверку типов во время компиляции через статические метамодели (User_). В Spring Data JPA Criteria API оборачивается в удобный паттерн Specification. Для статичных запросов использовать Criteria API не рекомендуется из-за избыточной многословности; лучшей альтернативой для сложных динамических сценариев служат QueryDSL или jOOQ».

Ключевые метрики для Production-мониторинга

  • Длительность фазы сборки CriteriaQuery: сложные графы предикатов могут потреблять заметные миллисекунды CPU при высокой частоте запросов (RPS).
  • Дублирование запросов при пагинации: мониторинг времени генерации COUNT-запросов при использовании JpaSpecificationExecutor.

Красные флаги (Чего говорить НЕЛЬЗЯ)

  • ❌ «Я пишу все запросы в проекте исключительно на Criteria API для типобезопасности» — Избыточный оверхед и ухудшение читаемости кода; для статики лучше JPQL/HQL.
  • ❌ «Criteria API работает напрямую с таблицами БД, как SQL» — Criteria API оперирует метамоделью JPA-сущностей.
  • ❌ «Использую строковые имена полей root.get(“username”) в Criteria без метамодели» — Теряется главное преимущество — типобезопасность на этапе компиляции.
  • ❌ «Ставлю root.fetch() в Specification без проверки типа запроса при пагинации» — Сломает автоматический COUNT-запрос Spring Data.

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