We are still cooking the magic in the way!
The Criteria API
The Criteria API
JPQL is expressive and readable, but it has a fundamental weakness: your query is a plain string. A typo in an entity name, a misspelled field, or a malformed join clause compiles cleanly and only blows up at runtime. The Criteria API solves this by letting you build queries programmatically — as a tree of Java objects — so the compiler and your IDE can validate every part of it before the app ever runs.
The Criteria API lives in the jakarta.persistence.criteria package and is tightly integrated with the JPA EntityManager. In Spring Boot 3 with Hibernate 6 you already have everything you need on the classpath — there is nothing extra to add.
The Three Core Objects
Every Criteria query is assembled from three collaborating objects:
CriteriaBuilder— the factory. You get one from theEntityManager. It creates query objects, expressions, predicates, and ordering clauses. Think of it as the JPQL keyword set made into a Java API.CriteriaQuery<T>— the query definition. It carries the result type, theFROMclause, theWHEREpredicate, ordering, and grouping. OneCriteriaQueryper logical query.Root<T>— the entity range variable, equivalent to the alias inFROM Order o. It gives you typed access to the entity's persistent attributes viaget("fieldName")or, with the Metamodel,get(Order_.status).
Root from the same CriteriaQuery it will be used in — mixing roots across query objects is a common mistake that produces a runtime exception.
Building a Simple SELECT Query
The pattern is always: obtain CriteriaBuilder → create CriteriaQuery → add Root → configure predicates → execute with TypedQuery.
Notice the result of em.createQuery(cq) is a typed TypedQuery<Order>. Unlike the string-based createQuery(String), the compiler knows the return type; no unchecked cast needed.
Adding Ordering and Pagination
Ordering and pagination attach to either the CriteriaQuery or the TypedQuery:
setMaxResults over in-memory truncation. The JPA provider translates setMaxResults / setFirstResult into the database-native pagination syntax (LIMIT/OFFSET on MySQL and PostgreSQL, FETCH FIRST on DB2, ROWNUM on older Oracle). The database discards rows before they travel across the network, which matters enormously at scale.
Combining Multiple Predicates
Real queries filter on several conditions. CriteriaBuilder.and() and cb.or() combine predicates exactly like SQL AND/OR. Each call returns a new Predicate — you compose them like building a boolean expression tree.
cb.and(Predicate...) accepts a vararg so you can pass as many conditions as you need. The equivalent cb.or() is used for OR-groups. You can nest them to arbitrary depth.
Joining Related Entities
Joins in the Criteria API are explicit objects of type Join<Z, X>. You navigate from the Root using join(), specifying the relationship field name and optionally the join type.
For a fetch join (which loads the association eagerly to avoid N+1) call order.fetch("items") instead of order.join("items"). A Fetch is not a Join, but it accepts the same JoinType parameter and produces the same SQL JOIN ... FETCH semantics.
setMaxResults causes an in-memory limit warning. When a fetch join produces a one-to-many result set and you also paginate, Hibernate has to fetch all rows into memory and paginate there — it cannot push the LIMIT to SQL safely. The fix is to paginate by entity ID in a first query, then fetch associations in a second query scoped to those IDs.
Scalar and Count Queries
Not every query returns full entities. To count rows, change the result type to Long and use cb.count():
The same pattern works for cb.sum(), cb.max(), cb.avg() and other aggregate expressions — just change the generic type parameter on CriteriaQuery to match what the aggregate returns.
Why Choose the Criteria API Over JPQL?
- Compile-time safety — typos in field names fail to compile, not at 2 AM in production.
- Dynamic queries — you can conditionally add predicates in a loop, which is impossible with a string template without messy concatenation.
- IDE support — auto-complete on
CriteriaBuildermethods andRoot.get()paths (even more with the Metamodel, covered next lesson). - Refactoring safety — if you rename an entity field, the compiler tells you every broken query location immediately.
The trade-off is verbosity. A five-line JPQL query becomes fifteen lines of Criteria code. For fixed, read-heavy queries JPQL is often clearer; for queries built dynamically from user input (search screens with optional filters) the Criteria API is the right tool.
Summary
The Criteria API builds JPA queries as composable Java objects rather than strings. The three core types — CriteriaBuilder, CriteriaQuery<T>, and Root<T> — map directly to the SELECT, FROM, and WHERE structure of SQL. You add predicates with cb.equal(), cb.greaterThan() and their variants, combine them with cb.and()/cb.or(), and execute via a typed TypedQuery. Joins are explicit Join objects; fetch joins use fetch(). The next lesson introduces the JPA Metamodel, which replaces string field names like "status" with generated type-safe constants, making Criteria queries even more robust.