Hibernate Pagination Limits with Collection Join Fetches
0 reputation · 24 May 2022, 23:09 UTC
0 reputation · 24 May 2022, 23:09 UTC
Hibernate provides setFirstResult() and setMaxResults() to handle dataset pagination, delegating the specific SQL syntax to the configured Dialect. This mechanism is intended to restrict the number of rows returned by the database to optimize memory and network usage.
A known behavior occurs when pagination is combined with join fetches on @OneToMany or @ManyToMany collections. Because the resulting Cartesian product can distort the row count, Hibernate may shift the pagination process from the database level to the application level.
This shift is typically indicated by the HHH000104 warning, signifying that the entire result set is loaded into memory before the specified limits are applied. This creates a significant risk of OutOfMemoryError when dealing with large datasets.
35775 reputation · 24 May 2022, 23:19 UTC
Maintain database‑level pagination by splitting the work into two queries: first fetch only the root entity identifiers with the desired setFirstResult()/setMaxResults(), then load the full entities and their collections using those IDs.
JOIN FETCH is applied to a @OneToMany or @ManyToMany collection, Hibernate cannot push LIMIT/OFFSET (or dialect equivalent) down to the database because doing so could cut off rows belonging to the same root entity.HHH000104: firstResult/maxResults specified with collection fetch; applying in memory! and causes the entire matching result set to be materialized in the JVM before pagination is applied.OutOfMemoryError for large data sets and degrades performance linearly with the number of rows fetched.The Cartesian product produced by the join fetch multiplies rows for each collection element. Applying a limit after the join would arbitrarily discard some collection rows, breaking the object graph. Hibernate therefore chooses to fetch all rows and paginate in memory.
List ids = session.createQuery(
"select e.id from RootEntity e where …", Long.class)
.setFirstResult(offset)
.setMaxResults(pageSize)
.list();List results = session.createQuery(
"select distinct e from RootEntity e
left join fetch e.collection
where e.id in :ids", RootEntity.class)
.setParameter("ids", ids)
.setHint("org.hibernate.readOnly", false) // optional
.list();WHERE clause like e.id > :lastId ordered by the same key.setFirstResult) still requires the database to scan and skip preceding rows, which grows linearly with the offset. Even with the two‑query approach, the first query suffers this cost.WHERE clause that starts after the last retrieved key. Its cost is roughly constant regardless of page number, making it far more efficient for large data sets.@BatchSize) to limit the amount of data transferred.To decide whether the two‑query method or keyset pagination is preferable, we need to know the typical number of collection elements per root entity. If each root owns a large collection, alternative strategies (DTOs, batch fetch) may be required.
Use comments to ask for clarification. Post a solution as an answer.
35,775 reputation · 25 May 2022, 06:38 UTC
An often-overlooked detail: when setMaxResults or setFirstResult is used alongside a join fetch on a collection, Hibernate does not push LIMIT/OFFSET to the database. Instead, the full result set is transferred over the network and materialized in the JVM heap before the pagination limits are enforced. The setMaxResults value only truncates the returned Java list, not the data received from the database. This means network I/O scales with the un-paginated result size, and the HHH000104 warning logs when this in-memory fallback occurs. For large collections, this can cause memory pressure even if the application only processes a small page.