Add Hibernate 7 batches 2-6, batch 7, and batch 8: mapping styles, JPA annotations, natural IDs, @Immutable, stored procedures, in-memory test databases, JNDI mocking, proxies, associations, temporal mapping, named queries, HQL, Criteria API, EntityManager bootstrapping, Ehcache 3 L2 cache configuration, HikariCP connection pooling, Hibernate Validator CDI integration, aggregate functions, sorting, pagination, interceptors, and Hibernate Search 8 (Hibernate 7.4.5.Final + Spring Boot 4.1.1 + JDK 25)
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
# 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<Employee> cr = cb.createQuery(Employee.class);
|
||||
Root<Employee> 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<Employee, Department> 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<Double> avgQuery = cb.createQuery(Double.class);
|
||||
Root<Employee> 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<Double> sub = mainQuery.subquery(Double.class);
|
||||
Root<Employee> 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<Employee> update = cb.createCriteriaUpdate(Employee.class);
|
||||
Root<Employee> 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<Employee> delete = cb.createCriteriaDelete(Employee.class);
|
||||
Root<Employee> 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)
|
||||
Reference in New Issue
Block a user