OutOfMemoryError during pagination with Join Fetch in Spring Data JPA
0 reputation · 29 Jan 2023, 23:18 UTC
0 reputation · 29 Jan 2023, 23:18 UTC
When implementing pagination for large datasets using Spring Data JPA with Hibernate, developers often use JOIN FETCH to optimize entity loading and avoid the N+1 select problem. However, combining fetch joins with Pageable parameters can lead to unexpected memory consumption.
The core issue arises when Hibernate cannot translate the fetch join and pagination request into a single SQL query with LIMIT and OFFSET clauses. In such cases, Hibernate may retrieve the entire result set into memory to perform the pagination manually.
What are the specific conditions that trigger Hibernate to switch from database-level pagination to in-memory pagination when using fetch joins? Is there a documented configuration to prevent this behavior and force a database-level limit?
When a query contains a JOIN FETCH and is executed with a Pageable, Hibernate will *always* materialise the entire result set before applying the page boundaries. The JPA specification forbids applying LIMIT/OFFSET to a fetch join because it would break the integrity of the entity graph. Consequently, Hibernate loads every row that matches the join, stores the entities in the persistence context, and then slices the collection in memory. This is the source of the OutOfMemoryError you see.
JOIN FETCH (or an @EntityGraph that expands an association in the same statement).Pageable (or any JPA setFirstResult/setMaxResults).LIMIT/OFFSET because the fetch join would produce duplicate root rows.No. Hibernate deliberately refuses to apply pagination to a fetch join. The spec states that a fetch join is a *fetch* operation, not a query operation, and therefore must not be subject to result‑set limits. The only knobs you have are:
hibernate.max_fetch_depth – limits how deep Hibernate will traverse associations, but does not affect pagination.In practice the fix is to redesign the query:
Pageable, then access associations lazily or via a separate JOIN FETCH query that is not paged.@EntityGraph with distinct and a separate SELECT for the associations. This keeps eager loading but respects pagination.hibernate.generate_statistics=true.
statistics.getEntityLoadCount() – it should equal the total rows, not the page size.
In short, Hibernate’s behaviour is by design, not a bug. There is no switch to override it; you must refactor the query or projection to keep pagination at the database level.
Use comments to ask for clarification. Post a solution as an answer.
35,275 reputation · 30 Jan 2023, 02:16 UTC
When a JOIN FETCH is combined with a Pageable, Hibernate will only apply LIMIT/OFFSET if the generated SQL can safely perform the slicing before the fetch join. In practice this fails when the query contains ORDER BY on fetched associations, multiple collection fetches, or requires DISTINCT to deduplicate rows. In those cases Hibernate loads every matching row into memory and then slices the result set client‑side.
Work‑around: select only the root entity IDs (or project to a DTO) in the first pass, apply setFirstResult/setMaxResults, and then run a second JOIN FETCH query to hydrate the associations. This keeps the database in charge of the pagination limits.