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:
2026-09-20 06:06:42 +00:00
committed by Claude
commit 8568c0ce6c
330 changed files with 23668 additions and 0 deletions
+105
View File
@@ -0,0 +1,105 @@
# 23 — Pagination
[← Previous: 22 — Sorting](22-sorting.md) | [Next: 24 — Interceptors →](24-interceptors.md) | [Back to README →](../README.md)
Backs the rewrite of ankurm.com post 4890 (pagination).
## `setFirstResult`/`setMaxResults` translate to the dialect's real syntax
```java
query.setFirstResult(2);
query.setMaxResults(2);
```
generates, on H2:
```sql
... order by a1_0.sequence offset ? rows fetch first ? rows only
```
Different dialects render this differently (`LIMIT ... OFFSET ...` on MySQL/PostgreSQL,
`OFFSET ... FETCH ...` on SQL Server/H2, `ROWNUM` tricks on older Oracle) — the JPA-level API is
the same everywhere; only the generated SQL shape changes.
[`PaginationTest.java`](../src/test/java/com/ankurm/hibernatedemo/pagination/PaginationTest.java) —
[output](output/23-limit-offset.txt)
## `ScrollableResults` with `ScrollMode.FORWARD_ONLY`
```java
try (ScrollableResults<Article> results = session.createQuery(hql, Article.class)
.setReadOnly(true)
.scroll(ScrollMode.FORWARD_ONLY)) {
while (results.next()) {
Article a = results.get();
// process one row at a time
}
}
```
No `List<Article>` holding every row is built by application code — rows are pulled from the JDBC
`ResultSet` one at a time as `next()`/`get()` are called. Whether this actually avoids loading the
whole result set into memory server-side too depends on the JDBC driver's own fetch-size
behavior, not on this API alone.
[Test](../src/test/java/com/ankurm/hibernatedemo/pagination/PaginationTest.java) —
[output](output/23-scrollable-forward-only.txt)
## `JOIN FETCH` + pagination: it depends on what you order by
The often-repeated claim is that `join fetch` over a `to-many` association combined with
`setFirstResult`/`setMaxResults` always falls back to loading everything into memory and
paginating in application code, logging a warning. Tested directly against
`hibernate-core-7.4.5.Final.jar`, this is only half true:
- **Ordering by a column on the root entity** (`order by a.sequence`): Hibernate 7.4.5's query
translator paginates a *derived subquery* of root ids first (its own `OFFSET`/`FETCH`), then
joins the fetched collection onto that already-paginated set of ids. No in-memory fallback, no
warning.
- **Ordering by a column on the fetched collection itself** (`order by c.body`, where `c` is the
joined collection alias): the "paginate the root ids first" trick can't work, because the sort
key isn't a root-entity column. This is the query shape that reproduces the real in-memory
fallback.
```sql
-- root-ordered: paginates a derived subquery of ids, then joins
select a1_0.id, c1_0.article_id, ... from (
select distinct a1_0.id, a1_0.sequence, a1_0.title from article a1_0
where ... order by a1_0.sequence offset ? rows fetch first ? rows only
) a1_0 join comment c1_0 on a1_0.id = c1_0.article_id order by a1_0.sequence
```
The warning's real message and code, read directly out of `QueryLogging.i18n.properties` in the
jar:
```
HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory
```
Not `HHH000104`, a code sometimes quoted for this that belongs to a different, older message
entirely — checked with `javap` against `QueryLogging_$logger.class`, not assumed from a search
result.
[Tests](../src/test/java/com/ankurm/hibernatedemo/pagination/PaginationTest.java) —
[root-ordered output](output/23-joinfetch-root-order-no-warning.txt),
[collection-ordered output](output/23-joinfetch-collection-order-warning.txt)
## Keyset (seek) pagination
```sql
select a from Article a where a.id > :lastId order by a.id asc
```
with `setMaxResults(pageSize)` and no `setFirstResult` at all. Each page's `WHERE` clause carries
the previous page's last id, so the database never has to count-and-skip rows the way `OFFSET`
does — the cost of fetching page 500 is the same as fetching page 1.
[Test](../src/test/java/com/ankurm/hibernatedemo/pagination/PaginationTest.java) —
[output](output/23-keyset-seek.txt)
## The total-count-query pattern
A "Page 2 of 7" UI needs two separate queries — a `COUNT` and a `LIMIT`/`OFFSET` `SELECT` — not
one query doing both; SQL has no way to return a page of rows and the total matching count in a
single result set without a window function trick most codebases don't bother with for this.
[Test](../src/test/java/com/ankurm/hibernatedemo/pagination/PaginationTest.java) —
[output](output/23-total-count-pattern.txt)
## Going deeper
- Deep `OFFSET` pagination degrades because the database still has to *generate and discard*
every skipped row before reaching the page — keyset pagination sidesteps this entirely, at the
cost of not supporting arbitrary "jump to page N" navigation.
- `Slice`/`Page` abstractions (Spring Data) wrap the count-query pattern automatically; knowing
the two-query shape underneath explains why a `Pageable` with `unpaged()` sort still issues a
`COUNT`.
- [Hibernate ORM 7.4 User Guide — pagination](https://docs.jboss.org/hibernate/orm/7.4/userguide/html_single/Hibernate_User_Guide.html#pagination)