Hibernate pagination with fetch‑join: incomplete pages after setFirstResult/setMaxResults
0 reputation · 11 May 2021, 23:14 UTC
0 reputation · 11 May 2021, 23:14 UTC
Retrieve a paginated list of root entities using setFirstResult and setMaxResults while eagerly fetching associated collections.
When a join‑fetch is used, Hibernate applies the limit/offset after the join operation. This can cause rows that belong to the next page to be discarded, leading to incomplete pages or duplicate root entities if a DistinctResultTransformer is not applied.
Does Hibernate guarantee distinct root entities when pagination is combined with join‑fetch, or must developers explicitly apply a distinct transformer or switch to a different pagination strategy?
setFirstResult/setMaxResults with a join‑fetch?ScrollableResults or keyset pagination over the standard offset/limit approach in this context?35775 reputation · 12 May 2021, 08:40 UTC
Hibernate does not automatically deduplicate root entities when you combine setFirstResult/setMaxResults with a join‑fetch. You must explicitly request distinct results (e.g., via DistinctResultTransformer, JPQL/HQL DISTINCT, or CriteriaBuilder distinct) to guarantee each parent appears only once per page.
When a join‑fetch is present, Hibernate first builds the SQL join, then applies the LIMIT and OFFSET clauses to the resulting rows. If a parent has multiple child rows, those rows occupy multiple positions in the result set. Cutting the set with offset/limit can therefore discard some child rows, causing the parent to appear fewer times than expected or to be omitted entirely from a page.
LIMIT/OFFSET after the FROM … JOIN … section for all major dialects (MySQL, PostgreSQL, Oracle, SQL Server, DB2).DISTINCT (or DistinctResultTransformer) collapses duplicate parent rows before the limit/offset is applied, giving the expected number of distinct parents per page.SELECT DISTINCT p FROM Parent p JOIN FETCH p.children WHERE …criteria.setResultTransformer(CriteriaSpecification.DISTINCT_ROOT_ENTITY) (or criteria.distinct(true) in newer versions).SELECT DISTINCT … and map the result.hibernate.show_sql=true) to confirm that LIMIT/OFFSET appears after the join.If you need deep pagination (large offsets) or want to avoid the offset‑performance penalty, consider:
WHERE p.id > :lastId ORDER BY p.id LIMIT :pageSize. This works safely with join‑fetch and distinct because the offset is not used.Use these approaches when the page size is small but the offset can become large, or when you observe missing or duplicate root entities despite using distinct.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.