Hibernate Pagination: Transitioning from setFirstResult to ScrollableResults for Large Collections
0 reputation · 26 Sept 2026, 22:34 UTC
0 reputation · 26 Sept 2026, 22:34 UTC
Hibernate ORM provides setFirstResult() and setMaxResults() to handle result set pagination. While these methods generate dialect-specific SQL (such as LIMIT/OFFSET), they can trigger significant performance issues when combined with join fetch on OneToMany collections. In these scenarios, Hibernate may perform pagination in memory to avoid Cartesian product inconsistencies, potentially leading to OutOfMemoryError.
For applications processing massive datasets where deep pagination is required, the ScrollableResults interface offers a mechanism to stream results without loading the entire set into the persistence context.
Given the risk of memory-based pagination in Hibernate 6.x, what are the specific criteria for choosing ScrollableResults over standard offset-based pagination when collection joins are necessary? How does the persistence context management differ between these two approaches during large-scale data retrieval?
Choose ScrollableResults when you must stream very large, read-only result sets—especially with deep offsets—and the query includes join fetch on a OneToMany collection. In Hibernate 6.x, offset pagination with setFirstResult()/setMaxResults() normally maps to SQL LIMIT/OFFSET, but a collection fetch join can prevent the database from applying the limit safely because of Cartesian product duplication. Hibernate may then paginate in memory, which increases heap use and can trigger OutOfMemoryError. ScrollableResults avoids materializing the full page by holding a JDBC cursor and fetching rows incrementally.
Prefer offset pagination when you need a bounded, random-access page of managed entities, when the data is mutable and must remain in the persistence context for automatic dirty checking and flush, or when the offset is small enough that database-side paging is efficient.
With offset pagination, every entity on the requested page is loaded into the persistence context and stays managed for the life of the session unless you explicitly evict it. For large pages this keeps many managed instances in memory.
ScrollableResults lets you decouple retrieval from retention. You can mark the query read-only and call session.clear() or evict() periodically while iterating, keeping the persistence context small. The trade-off is that evicted entities lose automatic dirty checking; if you need to update them later, offset pagination is safer.
setFirstResult()/setMaxResults() with a ScrollableResults loop.ScrollableResults scroll = session.createQuery(hql, Root.class)
.setReadOnly(true)
.scroll();
int count = 0;
while (scroll.next()) {
Root r = (Root) scroll.get();
// process r
if (++count % 100 == 0) {
session.clear();
}
}
scroll.close();
Because pagination behavior with fetch joins is dialect- and patch-version sensitive, verify locally:
LIMIT/OFFSET is actually pushed to the database for your current offset query.Do the entities need to remain managed for dirty checking and flush after retrieval? If yes, offset pagination with smaller pages is the safer default; if no, ScrollableResults with periodic clearing is the better fit for large collection joins.
Use comments to ask for clarification. Post a solution as an answer.
6,950 reputation · 26 Sept 2026, 23:51 UTC
Switching to ScrollableResults does not remove the Cartesian product created by a join fetch on a OneToMany collection. The JDBC cursor still returns one row per collection element, so the same parent entity can appear multiple times as you scroll. In Hibernate 6.x, verify whether the generated SQL includes DISTINCT or whether your iteration logic must skip already-processed parent identifiers; otherwise you may process duplicate parents and still hold excessive persistence-context state unless you evict or clear periodically.