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,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)
|
||||
Reference in New Issue
Block a user