Что такое Criteria API в Hibernate и когда его следует использовать?
Criteria API строит типобезопасные JPA-запросы программно через CriteriaBuilder и Root. Основной сценарий — динамические фильтры с опциональными параметрами; для статических запросов JPQL читается проще.
Criteria API в Hibernate
Criteria API — это типобезопасный программный способ построения JPA-запросов в Java. В отличие от JPQL/HQL, запрос строится через объекты Java, что позволяет компилятору проверять правильность имён полей и типов. Это особенно ценно для динамических запросов, где набор фильтров заранее неизвестен.
Базовый пример: простой SELECT
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Product> cq = cb.createQuery(Product.class);
Root<Product> root = cq.from(Product.class);
cq.select(root)
.where(cb.equal(root.get("category"), "Electronics"));
List<Product> results = entityManager.createQuery(cq).getResultList();
Типобезопасность через метамодель (JPA Metamodel)
JPA генерирует статическую метамодель (Product_) из аннотированных сущностей. Это устраняет строковые имена полей и даёт compile-time проверку:
// Metamodel-класс генерируется автоматически процессором аннотаций
// (hibernate-jpamodelgen в зависимостях Maven/Gradle)
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Product> cq = cb.createQuery(Product.class);
Root<Product> root = cq.from(Product.class);
Predicate categoryFilter = cb.equal(root.get(Product_.category), "Electronics");
Predicate priceFilter = cb.lessThan(root.get(Product_.price), new BigDecimal("1000"));
cq.select(root).where(cb.and(categoryFilter, priceFilter));
Динамические фильтры — главный сценарий
Criteria API незаменим, когда фильтры применяются опционально (например, форма поиска с необязательными полями):
public List<Product> findProducts(String category, BigDecimal maxPrice, Boolean inStock) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Product> cq = cb.createQuery(Product.class);
Root<Product> root = cq.from(Product.class);
List<Predicate> predicates = new ArrayList<>();
if (category != null) {
predicates.add(cb.equal(root.get(Product_.category), category));
}
if (maxPrice != null) {
predicates.add(cb.lessThanOrEqualTo(root.get(Product_.price), maxPrice));
}
if (Boolean.TRUE.equals(inStock)) {
predicates.add(cb.greaterThan(root.get(Product_.stockQuantity), 0));
}
cq.select(root).where(cb.and(predicates.toArray(new Predicate[0])));
return entityManager.createQuery(cq).getResultList();
}
JOIN и агрегация
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Object[]> cq = cb.createQuery(Object[].class);
Root<Order> order = cq.from(Order.class);
Join<Order, Customer> customer = order.join(Order_.customer, JoinType.INNER);
cq.multiselect(
customer.get(Customer_.name),
cb.count(order),
cb.sum(order.get(Order_.totalAmount))
).groupBy(customer.get(Customer_.id), customer.get(Customer_.name))
.having(cb.greaterThan(cb.count(order), 5L));
List<Object[]> results = entityManager.createQuery(cq).getResultList();
Spring Data JPA Specification
В Spring Data JPA Criteria API инкапсулируется в Specification<T> для переиспользования и комбинирования предикатов:
public class ProductSpecs {
public static Specification<Product> hasCategory(String category) {
return (root, query, cb) -> cb.equal(root.get(Product_.category), category);
}
public static Specification<Product> priceLessThan(BigDecimal max) {
return (root, query, cb) -> cb.lessThan(root.get(Product_.price), max);
}
}
// Использование:
repository.findAll(ProductSpecs.hasCategory("Electronics")
.and(ProductSpecs.priceLessThan(new BigDecimal("500"))));
Подводные камни
- Многословность: Criteria API значительно многословнее JPQL. Для статических запросов JPQL или @Query читается намного проще — не применяйте Criteria API везде.
- Отсутствие метамодели: без подключения
hibernate-jpamodelgenк annotation processor приходится использовать строковые имена полей, что убивает главное преимущество — типобезопасность. - Root vs Join смешение: неверное использование
root.get("association")вместоroot.join()не создаёт JOIN в SQL, а генерирует implicit join или CROSS JOIN. - N+1 с fetch join: чтобы загрузить коллекцию через Criteria API, нужен явный
root.fetch(), иначе коллекция остаётся lazy и порождает N+1. - COUNT-запрос при пагинации: при использовании
setFirstResult/setMaxResultsс JOIN FETCH Hibernate не может выполнить эффективный COUNT — нужно строить отдельный CriteriaQuery для подсчёта. - Кэширование query plan: Hibernate кэширует план запроса; если динамически создавать разные структуры CriteriaQuery для каждого запроса, кэш разрастётся. Унифицируйте структуру и варьируйте только predicates.
- distinct с коллекциями: при JOIN на @OneToMany без
cq.distinct(true)родительские сущности дублируются в результате.
Common mistakes
- Путать термин «criteria api» с соседним механизмом Hibernate.
- Не называть границу lifecycle, transaction, thread или request для «criteria api».
- Игнорировать production-эффекты «criteria api»: latency, SQL shape, memory, security или observability.
What the interviewer is testing
- Попросить объяснить механизм «criteria api» на минимальном примере.
- Проверить, видит ли кандидат failure mode и диагностику для «criteria api».
- Уточнить, какие настройки или API меняют «criteria api» в реальном сервисе.