Що таке 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.