Что такое 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;
};
}
Почему это падает:
- Метод
findAll(Specification, Pageable)выполняет два запроса:- Запрос данных:
SELECT o FROM Order o ... - Запрос общего количества:
SELECT COUNT(o) FROM Order o ...
- Запрос данных:
- Спецификация 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?
Ответ:
root.join("items"): Выполняет SQLJOINдля возможности фильтрации по дочерним колонкам (where(cb.equal(join.get("price"), 100))). Дочерние объектыitemsне материализуются в памяти и остаются неинициализированными ленивыми прокси (PersistentBag). При обращении к коллекции возникнет проблема $N+1$.root.fetch("items"): Выполняет SQLJOIN FETCH, за один запрос загружая в память и инициализируя дочернюю коллекцию. При этом возвращаемыйFetch<X, Y>интерфейс ограничен: от него нельзя строить дальнейшие предикаты фильтрации в JPA 2.1 (в JPA 2.2 / Hibernate 6Fetchразрешает последующие джойны).
2. Как в Criteria API построить DTO-проекцию без использования CriteriaQuery<Tuple>?
Ответ:
- Используется фабричный метод
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) )); - Класс
UserDtoобязан иметь публичный конструктор с точно совпадающими по типам и порядку аргументами. - Этот подход формирует в СУБД прямой запрос только выбранных колонок без материализации сущностей в
PersistenceContext.
3. Почему Criteria API считается менее предпочтительным инструментом по сравнению с QueryDSL или jOOQ?
Ответ:
- Многословность (Boilerplate): Базовый запрос на Criteria API требует в 3–4 раза больше строк кода, чем эквивалент на JPQL или QueryDSL.
- Плохая читаемость: Структура SQL-запроса теряется за нагромождением
CriteriaBuilder,Root,Pathи кастов типов. - Отсутствие поддержки части синтаксиса СУБД: В отличие от jOOQ, Criteria API ограничен спецификацией JPA и не умеет работать со сложными оконными функциями, рекурсивными CTE и специфическими функциями баз данных без громоздких оберток
cb.function().
4. Что произойдет, если в cq.where() передать пустой массив предикатов predicates.toArray(new Predicate[0])?
Ответ:
- Если список предикатов пуст, вызов
cq.where(new Predicate[0])эквивалентен отсутствию секцииWHERE. - Hibernate сгенерирует SQL-запрос
SELECT * FROM tableбез каких-либо условий фильтрации. - Ошибки или исключения не произойдет, запрос вернет все строки таблицы.
🎯 Шпаргалка для интервью
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.