Avoiding HHH000104 Warning with Deep Pagination and Join Fetch in Hibernate
0 reputation · 03 Aug 2020, 02:47 UTC
0 reputation · 03 Aug 2020, 02:47 UTC
In a Hibernate‑based Java application we need to retrieve a large dataset page by page while eagerly loading associated collections via a JPQL join fetch. The goal is to keep memory usage bounded even when the offset exceeds 1,000 rows.
Hibernate provides setFirstResult and setMaxResults on Query or TypedQuery to express offset and limit. When a join fetch is used, the framework emits the warning HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!, indicating that the limit is applied after the join fetch and all rows are loaded into memory before trimming.
It is unclear whether applying setFirstResult/setMaxResults before the join fetch (e.g., via a subquery) eliminates the warning and improves performance, or whether alternatives such as ScrollableResults or avoiding join fetch altogether are more appropriate. The documentation does not explicitly state the best practice for deep pagination with join fetch in Hibernate 6.x.
LIMIT/OFFSET before the join fetch prevent the HHH000104 warning?ScrollableResults a viable alternative for deep pagination when a join fetch is required?35275 reputation · 03 Aug 2020, 03:44 UTC
Applying setFirstResult/setMaxResults before a JOIN FETCH via a subquery prevents the HHH000104 warning because Hibernate paginates the root entity IDs first and then fetches associations only for that bounded set. ScrollableResults does not eliminate the warning or the memory risk; the JDBC driver still processes the full joined result set before Hibernate trims it in memory. For deep pagination in Hibernate 6.x, the safest pattern is to avoid JOIN FETCH on the paginated query and instead load collections separately.
The HHH000104 warning is confirmed Hibernate behavior: when a JPQL query contains a collection JOIN FETCH and pagination, Hibernate executes the SQL join without a database-level limit, loads every row into the persistence context, and only then applies the offset and limit in memory. This is why deep offsets remain expensive even with ScrollableResults.
The subquery approach is a practical workaround, not a change to Hibernate's core fetch strategy. It works because the outer query no longer paginates a collection fetch directly; the inner query limits the root IDs, and the outer query joins only those IDs.
setFirstResult and setMaxResults. This pushes LIMIT/OFFSET to the database.SELECT r, c FROM Root r JOIN r.collection c WHERE r.id IN :ids. This keeps each result set bounded by the page size.@BatchSize (or use @Fetch(FetchMode.SUBSELECT)) so Hibernate issues a small number of follow-up queries instead of one large join.When the collection must be eagerly initialized in one query, restrict the root IDs first:
SELECT DISTINCT r FROM Root r
JOIN FETCH r.collection c
WHERE r.id IN (
SELECT r2.id FROM Root r2
ORDER BY r2.id
)
Apply setFirstResult/setMaxResults to the inner subquery (or the outer query if your provider supports it). Verify with JDBC logging that the database receives the limit before the join. Note that DISTINCT is still required because a root with N collection elements produces N joined rows.
hibernate.show_sql and JDBC row-count logging to confirm the database—not Hibernate—is applying the limit.ScrollableResults streams the JDBC cursor, but if the SQL still contains the unconstrained join, the driver and Hibernate still materialize the full result set before trimming.LazyInitializationException.One detail would change the recommendation: do you need the collection fully initialized before the transaction closes, or can it be loaded in a subsequent batch? If it must be initialized eagerly, use the ID-subquery pattern; if a follow-up query is acceptable, the two-step or @BatchSize approach is simpler and more scalable.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.