# 16 — Criteria API: type-safe queries with the real generated metamodel [← Previous: 15 — HQL queries](15-hql-queries.md) | [Back to README →](../README.md) | [Next: 17 — Bootstrapping EntityManager →](17-entitymanager-bootstrap.md) Backs ankurm.com post 4880 (Criteria API). Everything below comes from [`CriteriaQueryTest`](../src/test/java/com/ankurm/hibernatedemo/query/CriteriaQueryTest.java), sharing the same [`Employee`](../src/main/java/com/ankurm/hibernatedemo/query/Employee.java)/[`Department`](../src/main/java/com/ankurm/hibernatedemo/query/Department.java) entities as chapter 15. `Employee_` and `Department_` are **real, generated** static metamodel classes, produced at build time by `hibernate-jpamodelgen`, wired into [`pom.xml`](../pom.xml)'s `maven-compiler-plugin` via `annotationProcessorPaths` (not a plain dependency -- that's the setting that actually triggers annotation processing during `javac`, confirmed by inspecting `target/generated-sources/annotations/` after a build and finding real `Employee_.java`/`Department_.java` files there, not hand-written stand-ins). ## String paths work, but they're not type-checked The most basic Criteria query builds a predicate from a plain string field name, exactly like a map lookup: ```java CriteriaBuilder cb = em.getCriteriaBuilder(); CriteriaQuery cr = cb.createQuery(Employee.class); Root root = cr.from(Employee.class); Predicate salaryGreater = cb.gt(root.get("salary"), 90_000.0); Predicate nameLike = cb.like(root.get("lastName"), "%o%"); cr.select(root).where(cb.and(salaryGreater, nameLike)).orderBy(cb.asc(root.get("lastName"))); ``` ``` stringPathPredicates: [Byron, Hopper, Torvalds] ``` **Read that result carefully**: `salary > 90000` alone would already return exactly Byron, Hopper, and Torvalds -- `lastName LIKE '%o%'` doesn't narrow the set further here, because all three of those last names genuinely contain an `o`. Both predicates are doing real work in general, they just happen to agree on this particular seed data; the test asserts the actual combined result, not an assumption about which predicate "mattered." `root.get("salary")` compiles even if `salary` were misspelled -- the typo would only surface at runtime, as an `IllegalArgumentException` when Hibernate tries to resolve the path. That's the whole reason the static metamodel exists. Source: [`CriteriaQueryTest`](../src/test/java/com/ankurm/hibernatedemo/query/CriteriaQueryTest.java). Raw output: [`docs/output/criteria-predicates-and-metamodel.txt`](output/criteria-predicates-and-metamodel.txt). Going deeper: - [Jakarta Persistence 3.2 Criteria API spec, §6](https://jakarta.ee/specifications/persistence/3.2/jakarta-persistence-spec-3.2#criteria-api) (`rel="nofollow"`) ## The static metamodel is compiler-checked, and it's real The identical query, rewritten through `Employee_`: ```java cr.select(root) .where(cb.gt(root.get(Employee_.salary), 90_000.0)) .orderBy(cb.asc(root.get(Employee_.lastName))); ``` ``` staticMetamodel: [Byron, Hopper, Torvalds] ``` Same result as the string-path version above -- proving `Employee_.salary` and `Employee_.lastName` are genuinely wired to the same underlying attributes, not just present and unused. Misspell `Employee_.salery` and the build fails at `javac`, not at test time three months later when someone renames the `salary` field and forgets the string literal thirty call sites away. Joins work the same way, through the metamodel's generated association fields: ```java Join department = root.join(Employee_.department, JoinType.INNER); cr.select(root).where(cb.equal(department.get(Department_.name), "Engineering")); ``` ``` joinViaMetamodel: 3 engineering employees ``` Source: [`CriteriaQueryTest`](../src/test/java/com/ankurm/hibernatedemo/query/CriteriaQueryTest.java). Raw output: [`docs/output/criteria-predicates-and-metamodel.txt`](output/criteria-predicates-and-metamodel.txt). The one-time setup cost -- one `annotationProcessorPaths` block -- is the whole tradeoff. Once it's in place, every entity gets its `_`-suffixed metamodel class for free on every build; nothing in application code has to opt in per-entity. Going deeper: - [Chapter 04's annotations-vs-XML chapter](04-annotations-vs-xml.md) if metamodel generation from XML-mapped entities matters for your setup - [`hibernate-jpamodelgen` on Maven Central](https://mvnrepository.com/artifact/org.hibernate.orm/hibernate-jpamodelgen) (`rel="nofollow"`) ## `root.join()` vs `root.fetch()` -- the same trap as HQL's bare `JOIN` This is chapter 15's `JOIN` vs `JOIN FETCH` distinction, in Criteria API form, and it's just as easy to get wrong here because `root.join()` *looks* like it should load the association: ```java root.join(Employee_.department, JoinType.INNER); // filters, does NOT eagerly load // ... after the query runs and results are touched: ``` ``` rootJoinVsFetch: join+touch=3 statements, fetch+touch=1 statement ``` `root.join()` alone still costs one extra SELECT per distinct department touched afterward -- identical N+1 shape to HQL's bare `JOIN`. `root.fetch()` is the actual fix, and it needs `.distinct(true)` on the query to avoid duplicate rows when the fetched association is a collection (harmless but wasteful here since `department` is `@ManyToOne`, kept for the habit): ```java root2.fetch(Employee_.department, JoinType.INNER); cr2.select(root2).distinct(true); ``` That second query costs exactly one statement total, even after touching every returned entity's department. Source: [`CriteriaQueryTest`](../src/test/java/com/ankurm/hibernatedemo/query/CriteriaQueryTest.java). Raw output: [`docs/output/criteria-predicates-and-metamodel.txt`](output/criteria-predicates-and-metamodel.txt). > **Trap**: `Join` and `Fetch` are different interfaces in the Criteria API (`root.join()` returns > a `Join`, `root.fetch()` returns a `Fetch`), which is part of why it's easy to reach for the > wrong one -- your IDE will happily autocomplete either. Reach for `fetch()` specifically when > you intend to read the association afterward; reach for `join()` when it's purely a filter. ## Aggregation and subqueries `CriteriaBuilder.avg()` on a metamodel path, against a `Double`-typed query: ```java CriteriaQuery avgQuery = cb.createQuery(Double.class); Root avgRoot = avgQuery.from(Employee.class); avgQuery.select(cb.avg(avgRoot.get(Employee_.salary))); ``` ``` aggregation: average salary = 85600.0 ``` Hand-computed from the seed data ((95000+98000+92000+72000+71000)/5 = 85600) and asserted to match, not just observed. A correlated-by-value subquery, finding employees above the company-wide average: ```java Subquery sub = mainQuery.subquery(Double.class); Root subRoot = sub.from(Employee.class); sub.select(cb.avg(subRoot.get(Employee_.salary))); mainQuery.select(empRoot).where(cb.gt(empRoot.get(Employee_.salary), sub)); ``` ``` subquery: above-average earners (avg=85600) = [Byron, Hopper, Torvalds] ``` **Worth reading twice**: the naive assumption is "only the single highest earner clears the average" -- that's wrong here. The two Marketing salaries (72000 and 71000) pull the company-wide average down to 85600, well below every Engineering salary, so all three Engineering employees clear it, not just Grace Hopper at the top. This is a real example of why "above average" queries need the actual average computed, not eyeballed -- an earlier draft of this test asserted the wrong single-employee result and was caught by actually computing the average by hand and comparing. `or()` combines predicates with a varargs overload -- worth calling out only because it's easy to reach for `cb.equal(...).or(...)` chaining instead and get confused about operator precedence: ```java cb.or(cb.equal(root.get(Employee_.status), "INACTIVE"), cb.equal(root.get(Employee_.lastName), "Hamilton")) ``` ``` orPredicate: [Torvalds, Hamilton] ``` Source: [`CriteriaQueryTest`](../src/test/java/com/ankurm/hibernatedemo/query/CriteriaQueryTest.java). Raw output: [`docs/output/criteria-aggregation-and-subquery.txt`](output/criteria-aggregation-and-subquery.txt). ## `CriteriaUpdate` and `CriteriaDelete` -- bulk operations, type-safe The same bulk-operation semantics as chapter 15's HQL `UPDATE`/`DELETE` (single SQL statement, bypasses the persistence context for already-loaded entities), expressed through `CriteriaBuilder.createCriteriaUpdate()`/`createCriteriaDelete()` instead of a query string: ```java CriteriaUpdate update = cb.createCriteriaUpdate(Employee.class); Root updRoot = update.from(Employee.class); update.set(Employee_.salary, cb.prod(updRoot.get(Employee_.salary), 1.1)) .where(cb.equal(updRoot.get(Employee_.department).get(Department_.id), engineeringId)); int updated = em.createQuery(update).executeUpdate(); ``` ``` criteriaUpdate: 3 rows updated, Ada's new salary = 104500.00000000001 ``` `cb.prod(...)` is Criteria's typed multiplication (`salary * 1.1`), and the trailing `.00000000001` on the result is ordinary `double` floating-point representation, not a bug -- `95000.0 * 1.1` doesn't land on an exact binary fraction, which is exactly why the assertion in the test uses an offset-based comparison instead of exact equality. ```java CriteriaDelete delete = cb.createCriteriaDelete(Employee.class); Root delRoot = delete.from(Employee.class); delete.where(cb.isNull(delRoot.get(Employee_.department))); int deleted = em.createQuery(delete).executeUpdate(); ``` ``` criteriaDelete: deleted=1, remaining=5 ``` Source: [`CriteriaQueryTest`](../src/test/java/com/ankurm/hibernatedemo/query/CriteriaQueryTest.java). Raw output: [`docs/output/criteria-bulk-update-delete.txt`](output/criteria-bulk-update-delete.txt). > **Trap**: `CriteriaUpdate`/`CriteriaDelete` need their own `Root`, created via > `update.from(...)`/`delete.from(...)` -- you cannot reuse a `Root` from a `CriteriaQuery` > built earlier in the same method, even against the same entity type. They're different root > instances tied to different query objects. Going deeper: - Chapter 15's [bulk `UPDATE`/`DELETE` bypass the persistence context](15-hql-queries.md#bulk-update-and-delete-bypass-the-persistence-context) -- identical caveat, different API surface ## When to reach for Criteria over HQL Neither API is strictly "better" -- they solve different problems: | | HQL | Criteria API | |---|---|---| | Readability for a fixed, known query | Higher -- reads like SQL | Lower -- more ceremony per query | | Compile-time safety | None -- typos in path expressions fail at runtime | Full, with the generated metamodel | | Dynamically building predicates (search filters, optional criteria) | Painful -- string concatenation or conditional clause-building | Natural -- build up `Predicate`s in a loop, combine with `cb.and()`/`cb.or()` | | Startup validation via `@NamedQuery` | Yes (see chapter 14) | No equivalent | The dynamic-predicate case is where Criteria earns its ceremony: a search endpoint with five optional filter fields is a genuinely painful HQL string-building exercise and a clean loop of `if (filter != null) predicates.add(cb.equal(...))` in Criteria. ## Summary | Claim | Verified value | |---|---| | `root.get("string")` vs `root.get(Employee_.field)` | Both produce identical results; only the metamodel version is compiler-checked | | `Employee_`/`Department_` | Real, generated by `hibernate-jpamodelgen` via `annotationProcessorPaths`, not hand-written | | `root.join()` | Filters via SQL join; does NOT eagerly load the association | | `root.fetch()` | Loads the association in the same SELECT | | `cb.avg()` | Matches hand-computed average exactly | | Subquery for "above average" | All three Engineering employees qualify -- Marketing salaries pull the average down further than expected | | `CriteriaUpdate`/`CriteriaDelete` | Same bulk-operation semantics as HQL `UPDATE`/`DELETE`: single statement, bypasses the persistence context | [← Previous: 15 — HQL queries](15-hql-queries.md) | [Back to README →](../README.md) | [Next: 17 — Bootstrapping EntityManager →](17-entitymanager-bootstrap.md)