HibernateMiddleTechnical

Что такое 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» в реальном сервисе.

Sources

Related topics