skip to content

Criteria API

Building queries as typed object trees for dynamic, composable filters. Interviewers ask when Criteria beats string JPQL — dynamic search screens — and what the static metamodel buys you over string attribute names.

part ofHibernateoverview, primer and where to startread it →
on this pageshow

questions

5

Walk through how you build and execute a simple query with the JPA Criteria API — what roles do CriteriaBuilder, CriteriaQuery, Root and TypedQuery each play?

level: juniorimportance: must knowfreq 55%

answer

  1. cb → cq → root → select/where → em.createQuery
  2. Root = FROM entry + path navigator
  3. where() replaces, doesn't append
  4. values become bind parameters, not literals
  5. same translator as JPQL, same SQL

basics

~20 s

EntityManager.getCriteriaBuilder() gives a CriteriaBuilder, the factory for everything. It creates a CriteriaQuery<T> that fixes the result type. Root<E> is the FROM entity and the handle for attribute paths. You set select/where/orderBy, then em.createQuery(cq) returns an executable TypedQuery<T>.

solid answer

~50 s

The Criteria API builds a query as an object tree instead of a string. 1. `CriteriaBuilder cb = em.getCriteriaBuilder()` — the factory for queries, predicates, expressions and functions. 2. `CriteriaQuery<Book> cq = cb.createQuery(Book.class)` — declares the **result type** of the query. 3. `Root<Book> book = cq.from(Book.class)` — adds a FROM clause entry; the `Root` is also the navigation handle: `book.get("title")` is a `Path` you feed into predicates. 4. Compose the clauses: `cq.select(book).where(cb.equal(book.get("status"), Status.ACTIVE)).orderBy(cb.asc(book.get("title")))`. 5. `TypedQuery<Book> q = em.createQuery(cq)` — turns the tree into an executable query; from here it behaves exactly like a JPQL `TypedQuery`: `setParameter`, `setFirstResult/setMaxResults`, `getResultList`, `getSingleResult`. The result type of `createQuery` need not be the entity — `cb.createQuery(String.class)` with `cq.select(book.get("title"))` returns titles, and `cb.createTupleQuery()` returns multi-column `Tuple` rows. Hibernate translates that tree into the same SQL a JPQL query would produce; there is no separate execution engine.

code

java · 11 lines
java
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Book> cq = cb.createQuery(Book.class);
Root<Book> book = cq.from(Book.class);

cq.select(book)
  .where(cb.equal(book.get("status"), Status.ACTIVE))
  .orderBy(cb.asc(book.get("title")));

List<Book> result = em.createQuery(cq)
        .setMaxResults(50)
        .getResultList();

go deeper

for a junior

Name the four objects and their order — builder, query, root, TypedQuery — and show a where/orderBy on a single entity.

for a middle

Add the non-entity result types (single path, Tuple, cb.construct) and the where()-replaces gotcha, and explain that criteria and JPQL share the same translator.

for a senior

Talk about parameters vs inline values for reusable query definitions, root ownership per query, and when programmatic building is worth the verbosity.

for a principal

Frame it as a query-construction strategy: which layer owns predicate composition, how teams keep criteria code readable, and the maintenance cost of hand-built query trees versus declarative strings.

## What the Criteria API is JPQL is a query *language*: you write a string, and the provider parses it at runtime. The Criteria API is the same query model expressed as **Java objects you assemble programmatically**. Instead of `"select b from Book b where b.status = :s"`, you build a small tree of nodes: a query node, a from node, a comparison node. Hibernate then renders that tree to SQL exactly as it renders a parsed JPQL string — same translator, same SQL, same performance characteristics. The API lives in `jakarta.persistence.criteria` (`javax.persistence.criteria` before Jakarta EE 9). ## The four objects **CriteriaBuilder** — obtained from `EntityManager.getCriteriaBuilder()` (or the `EntityManagerFactory`). It is the factory for *everything*: `createQuery`, `equal`, `like`, `and`, `or`, `greaterThan`, `count`, `sum`, `upper`, `asc`, `desc`, `construct`, subqueries. If you need an operator, look for it on the builder first. **CriteriaQuery<T>** — the query itself, with `T` fixed as the result type. It holds the clauses: `select`, `multiselect`, `where`, `groupBy`, `having`, `orderBy`, `distinct`. Setter-style methods return the query so they chain. A `CriteriaQuery` is a *definition*, not an execution: you can build it once and execute it many times, but you cannot mix roots between two different `CriteriaQuery` instances — a `Root` belongs to the query that created it, which is why a paginated list query and its matching count query each need their own root. **Root<E>** — created by `cq.from(Entity.class)`, it represents one entry in the FROM clause (a query can have several roots, producing a cross join constrained by the WHERE clause). It is the entry point for **paths**: `root.get("author").get("name")` navigates a to-one association and renders as an implicit join. Paths are typed as `Path<Y>`/`Expression<Y>`; when the compiler cannot infer the type from a string key you sometimes need `root.<String>get("title")` or the static metamodel. **TypedQuery<T>** — `em.createQuery(criteriaQuery)` hands the tree to the provider and returns the ordinary JPA execution handle. Everything you know about executing JPQL applies: `getResultList()`, `getSingleResult()`/`getResultStream()`, `setMaxResults`, `setLockMode`, hints. ## Selecting something other than the entity - Single column: `cb.createQuery(String.class)` + `cq.select(root.get("title"))`. - Several columns as a tuple: `cb.createTupleQuery()` + `cq.multiselect(root.get("id"), root.get("title"))`, then read `tuple.get(0, Long.class)`. - Aggregate: `cb.createQuery(Long.class)` + `cq.select(cb.count(root))`. - A DTO: `cq.select(cb.construct(BookView.class, root.get("id"), root.get("title")))` — the object-tree equivalent of a constructor expression. If you call `cq.from(...)` and never call `select`, and the result type matches the single root's type, providers default the selection to that root — but write `select` explicitly; it costs nothing and removes ambiguity. ## Parameters and literals Two styles. You can pass a plain Java value: `cb.equal(root.get("status"), Status.ACTIVE)` — Hibernate turns it into a **JDBC bind parameter**, not an inlined literal, so it is safe from injection and friendly to statement caching. Or you can declare an explicit `ParameterExpression<String> p = cb.parameter(String.class, "title")`, use it in the predicate, and bind at execution with `query.setParameter("title", value)`. Use explicit parameters when you want to build the `CriteriaQuery` once and execute it repeatedly with different values; use inline values for the common build-per-request case. ## Why it exists The payoff is not aesthetics — Criteria code is more verbose than JPQL. It is that the query is **data you can manipulate**: add a predicate inside an `if`, hand a `List<Predicate>` around, generate ORDER BY from a sort field, refactor an attribute name and let the compiler find the usages (with the static metamodel). For a query whose shape is fixed, string JPQL is shorter and easier to read, and most teams keep those in JPQL or named queries. ## Common mistakes Treating clause calls as accumulative: a second `cq.where(...)` **replaces** the first, so combine predicates yourself with `cb.and(...)`. Reusing a `Root` from another `CriteriaQuery`. Expecting `Criteria` (the old `org.hibernate.Criteria` session API) — that is a different, removed API; the JPA Criteria API is the survivor.

  • Can you reuse the same Root object in a second CriteriaQuery, for example to build a matching count query for a paginated list?
    No. A Root belongs to the CriteriaQuery that created it, and mixing them yields an invalid tree or a provider error. For a count query you create a second CriteriaQuery<Long>, call from() again to get a fresh root, and rebuild the predicates against that root. The usual pattern is a helper method that takes a Root and a CriteriaBuilder and returns the List<Predicate>, so the same filtering logic feeds both queries.
  • When you write cb.equal(root.get("status"), Status.ACTIVE) with a literal Java value, does Hibernate inline that value into the SQL?
    No — by default it renders a JDBC bind parameter and binds the value, so the SQL text stays constant and is safe from injection and friendly to the statement cache. Explicit ParameterExpression objects exist for a different reason: they let you build the CriteriaQuery once and execute it many times with different values via setParameter. Hibernate does expose settings that change literal handling, but the default is binding.
  • Calling cq.where(a) and then cq.where(b) — what does the query end up filtering on?
    Only b. The clause setters replace rather than accumulate, which is a classic source of silently wrong results. To combine you build the predicates yourself and pass them together: cq.where(cb.and(a, b)), or cq.where(predicateArray), which the spec treats as conjunction.

JPQL is writing a sentence; the Criteria API is assembling that same sentence from labelled Lego blocks — clumsier to read, but you can hand pieces around and snap on another clause at runtime.

saying these in an interview costs you the question

  • Thinking the Criteria API produces different or slower SQL than the equivalent JPQL string
  • Believing repeated where() calls accumulate as AND
  • Confusing the JPA Criteria API with the removed legacy org.hibernate.Criteria session API
  • Assuming literal values passed to cb.equal are string-concatenated into SQL (an injection risk)
  • Claiming a CriteriaQuery is executable on its own, without em.createQuery

context

open as a page

A search endpoint accepts five optional filter fields, any combination of which may be present. How would you build that query with the JPA Criteria API, and why is that preferable to concatenating a JPQL string?

level: middleimportance: must knowfreq 60%

basics

~20 s

Create one CriteriaQuery and Root, then collect a List<Predicate>: for each filter that is non-null, add the matching predicate. Finish with cq.where(list.toArray(new Predicate[0])), which ANDs them. No string building, no dangling AND, values bound as parameters.

open as a page

What is the JPA static metamodel — generated classes such as Customer_ with fields like Customer_.name — how is it produced, and what does it give you over passing attribute names as plain strings?

level: middleimportance: should knowfreq 35%

basics

~20 s

An annotation processor generates a companion class per entity (Customer_) holding typed attribute descriptors. Using root.get(Customer_.name) instead of root.get("name") gives compile-time checking of the attribute name and its Java type, so renames and type mistakes break the build instead of failing at runtime.

open as a page

When would you reach for the JPA Criteria API instead of a string JPQL query, and when is Criteria the wrong tool? Be concrete about the costs of each.

level: seniorimportance: should knowfreq 45%

basics

~20 s

Use Criteria when the query shape varies at runtime — optional filters, dynamic sorting, composable predicates — or when compile-time attribute safety matters. Use string JPQL for fixed-shape queries: far more readable, reviewable, pasteable. Same translator, same SQL, so it is a maintainability call, not performance.

open as a page

Using CriteriaBuilder, how do you express an inner join to a related entity and a correlated subquery, and what is the difference between calling join() and fetch() on a Root?

level: seniorimportance: nice to knowfreq 30%

basics

~20 s

root.join("customer", JoinType.INNER) returns a Join you can navigate and filter on. root.fetch("lines") returns a Fetch — it loads the association eagerly but is not usable in WHERE, so you cannot filter through it (people cast Fetch to Join to reuse it). Subqueries: cq.subquery(Long.class), its own from(), correlate() for the outer root, then cb.exists / cb.in.

open as a page