Criteria API
AdvancedThe Criteria API builds type-safe dynamic queries programmatically using metamodel classes; more verbose than JPQL but catches column/type errors at compile time.
Overview
The JPA Criteria API allows building queries programmatically using a fluent builder pattern and statically-generated metamodel classes. Unlike JPQL strings — which fail only at runtime — Criteria queries fail at compile time when field names change, making them safer for complex dynamic search scenarios where filters are optional. The metamodel is generated by a JPA annotation processor (hibernate-jpamodelgen) and produces classes like Order_ with typed field descriptors (SingularAttribute<Order, String>) for each entity field. The API is verbose but predictable; for most Spring Boot applications, Spring Data Specifications wrap Criteria API in a cleaner fluent interface.
Static metamodel and basic Criteria query
Configure the Hibernate metamodel generator to produce Order_ classes. These provide compile-time-safe field references for building Criteria queries.
<!-- pom.xml — metamodel generator -->
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>6.4.0.Final</version>
<scope>provided</scope>
</dependency>
// Generated: Order_.java (do not edit — auto-generated)
// @StaticMetamodel(Order.class)
// public abstract class Order_ {
// public static volatile SingularAttribute<Order, Long> id;
// public static volatile SingularAttribute<Order, String> status;
// public static volatile SingularAttribute<Order, BigDecimal> amount;
// public static volatile ListAttribute<Order, OrderItem> items;
// }
// Basic Criteria query
public List<Order> findByStatus(String status) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Order> query = cb.createQuery(Order.class);
Root<Order> root = query.from(Order.class);
query.select(root)
.where(cb.equal(root.get(Order_.status), status))
.orderBy(cb.desc(root.get(Order_.amount)));
return em.createQuery(query)
.setMaxResults(100)
.getResultList();
}Dynamic multi-filter search
Criteria API shines when query conditions are optional. Build predicates conditionally and combine them with cb.and().
public List<Order> searchOrders(OrderSearchRequest req) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Order> query = cb.createQuery(Order.class);
Root<Order> root = query.from(Order.class);
List<Predicate> predicates = new ArrayList<>();
// Each filter is optional — only add predicates for non-null params
if (req.getStatus() != null) {
predicates.add(cb.equal(root.get(Order_.status), req.getStatus()));
}
if (req.getMinAmount() != null) {
predicates.add(cb.ge(root.get(Order_.amount), req.getMinAmount()));
}
if (req.getCustomerId() != null) {
predicates.add(cb.equal(root.get(Order_.customerId), req.getCustomerId()));
}
if (req.getFromDate() != null) {
predicates.add(cb.greaterThanOrEqualTo(
root.get(Order_.createdAt), req.getFromDate()));
}
if (req.getItemSku() != null) {
// Join to items sublist
Join<Order, OrderItem> items = root.join(Order_.items, JoinType.INNER);
predicates.add(cb.equal(items.get(OrderItem_.sku), req.getItemSku()));
}
query.where(cb.and(predicates.toArray(new Predicate[0])));
query.orderBy(cb.desc(root.get(Order_.createdAt)));
return em.createQuery(query)
.setFirstResult(req.getPage() * req.getSize())
.setMaxResults(req.getSize())
.getResultList();
}Spring Data Specifications (recommended wrapper)
Spring Data JpaSpecificationExecutor wraps Criteria API with a cleaner interface. Specifications are composable (and, or, not) and reusable across repository methods.
// Repository
public interface OrderRepository extends JpaRepository<Order, Long>,
JpaSpecificationExecutor<Order> {}
// Reusable Specification components
public class OrderSpecifications {
public static Specification<Order> hasStatus(String status) {
return (root, query, cb) ->
status == null ? null : cb.equal(root.get(Order_.status), status);
}
public static Specification<Order> amountAtLeast(BigDecimal min) {
return (root, query, cb) ->
min == null ? null : cb.ge(root.get(Order_.amount), min);
}
public static Specification<Order> createdAfter(LocalDateTime from) {
return (root, query, cb) ->
from == null ? null : cb.greaterThanOrEqualTo(root.get(Order_.createdAt), from);
}
}
// Usage: compose specs with and()
Specification<Order> spec = Specification
.where(OrderSpecifications.hasStatus(req.getStatus()))
.and(OrderSpecifications.amountAtLeast(req.getMinAmount()))
.and(OrderSpecifications.createdAfter(req.getFromDate()));
Page<Order> orders = orderRepository.findAll(spec,
PageRequest.of(req.getPage(), req.getSize(),
Sort.by(Sort.Direction.DESC, "createdAt")));Key Points to Remember
- 1Criteria API provides compile-time type safety; JPQL strings fail at runtime when field names change.
- 2The Hibernate JPA metamodel generator (hibernate-jpamodelgen) produces entity_class_ files with SingularAttribute/ListAttribute fields.
- 3Build predicates conditionally and combine with cb.and() for dynamic optional-filter search endpoints.
- 4Spring Data Specifications are the recommended abstraction — cleaner than raw Criteria, composable with .and()/.or().
- 5CriteriaQuery<T> vs CriteriaQuery<Tuple>: use Tuple for multi-column projections that do not map to a single entity.
- 6For very complex reports, native SQL is often simpler than deep Criteria nesting — choose the right tool for readability.
Interview Questions
Sign in to ask AriaWhat advantage does the Criteria API provide over JPQL strings?
How does the JPA metamodel (Order_) help build type-safe queries?
How would you implement a dynamic search endpoint with 5 optional filters using Specifications?
What is the difference between JoinType.INNER and JoinType.LEFT in a Criteria JOIN and when would you use each?
When would you choose native SQL over the Criteria API for a complex query?
Ask Aria about Criteria API
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.