Hibernate pagination behavior with collection join fetches
0 reputation · 28 Jan 2020, 04:20 UTC
0 reputation · 28 Jan 2020, 04:20 UTC
Hibernate provides setFirstResult() and setMaxResults() to handle database-level pagination. When these methods are used in conjunction with JOIN FETCH on a collection, the ORM must ensure that the number of root entities returned matches the requested page size, regardless of the number of child records joined.
There is a known conflict where the SQL result set contains duplicate root entities due to the join, making it impossible for the database to apply a precise LIMIT or OFFSET without risking truncated entity graphs. This often triggers the HHH000104 warning, indicating that pagination is being performed in memory after the full result set is retrieved.
Given a requirement for strict memory constraints on the JVM, it is necessary to determine the optimal configuration to avoid in-memory pagination while maintaining the efficiency of a single query fetch.
35275 reputation · 28 Jan 2020, 08:17 UTC
Hibernate cannot safely apply SQL LIMIT or OFFSET when a JOIN FETCH on a collection is present. Because a single root entity can produce multiple rows in a SQL result set (one for each child record), a database-level limit would truncate the result set mid-entity, leading to incomplete object graphs. To avoid this data corruption, Hibernate retrieves the entire result set into the JVM heap and performs pagination in memory, which triggers the HHH000104 warning.
To maintain strict JVM memory constraints, separate the identification of the page from the retrieval of the data:
setFirstResult() and setMaxResults() here. This ensures the database handles the pagination boundary.JOIN FETCH to retrieve the entities and their collections, filtering by the IDs obtained in the first step via an IN clause.// Step 1: Paginated ID retrieval (Database-level limit)
List<Long> ids = entityManager.createQuery("select u.id from User u", Long.class)
.setFirstResult(0)
.setMaxResults(20)
.getResultList();
// Step 2: Full graph fetch for specific IDs
List<User> users = entityManager.createQuery("select u from User u join fetch u.orders where u.id in :ids", User.class)
.setParameter("ids", ids)
.getResultList();
| Approach | Pros | Cons |
|---|---|---|
| Two-Step Fetch | Constant memory footprint; safe for large datasets. | Two network round-trips; IN clause size limits. |
| In-Memory Pagination | Single query; simpler implementation. | High risk of OutOfMemoryError; scales poorly. |
Assumption: This solution assumes the page size is small enough to fit within the database's maximum IN clause limit (e.g., 1,000 elements). If your page size exceeds this, you must partition the ID list into smaller batches for the second query.
To provide a more tailored recommendation, please specify if you are using a specific Hibernate version (e.g., 5.x or 6.x) as some newer versions offer different optimization paths for window functions.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.