Pagination behavior change when upgrading Hibernate from 5.6 to 6.0 with fetch joins and ORDER BY
0 reputation · 03 Dec 2023, 21:33 UTC
0 reputation · 03 Dec 2023, 21:33 UTC
Goal: obtain predictable pagination results for a JPQL/HQL query that uses fetch joins on a OneToMany association and includes an ORDER BY clause when upgrading from Hibernate 5.6 to 6.0 with the MySQL dialect.
In Hibernate 5.6 the limit/offset is applied before distinct root‑entity filtering, which can cause duplicate root rows in the result set. Hibernate 6.0 changes the pagination strategy to use window functions (ROW_NUMBER) and attempts to apply distinct filtering after limiting, but the behavior is not guaranteed for all queries and depends on the hibernate.query.fail_on_pagination_over_collection_fetch setting.
Uncertainty remains about whether Hibernate should automatically add DISTINCT to the SELECT clause when fetch joins are combined with pagination to eliminate duplicates without requiring manual configuration.
What is the recommended approach to guarantee duplicate‑free pagination across the 5.6→6.0 boundary? Should applications rely on the new window‑function strategy and enable the fail‑on‑pagination flag, or is it safer to manually add DISTINCT to the query? How does the choice affect performance and compatibility with other dialects?
31275 reputation · 04 Dec 2023, 07:43 UTC
To guarantee duplicate‑free pagination across the Hibernate 5.6 → 6.0 boundary when using fetch joins on a OneToMany association together with an ORDER BY clause, the safest and most portable solution is to explicitly add DISTINCT (or paginate on the root entity's primary key via a subquery) to the JPQL/HQL query. This works unchanged in both Hibernate versions and is independent of the underlying database dialect.
hibernate.query.fail_on_pagination_over_collection_fetch to detect problematic queries. However, the documentation notes that this behavior is not guaranteed for all query shapes and depends on the dialect and the presence of ORDER BY.hibernate.query.fail_on_pagination_over_collection_fetch=true only makes Hibernate throw an exception when it cannot guarantee duplicate‑free results; it does not fix the duplication. You would still need to adjust the query.DISTINCT (or using a subquery that first selects distinct root IDs) forces the database to eliminate duplicates before applying LIMIT/OFFSET, giving deterministic results in Hibernate 5.6, 6.0, and any future version.# application.properties (or equivalent)
hibernate.show_sql=true
hibernate.format_sql=true
DISTINCT on the root entity (or a subquery on the primary key), rewrite the JPQL/HQL:
SELECT DISTINCT p FROM Parent p JOIN FETCH p.children WHERE ... ORDER BY p.name
SELECT p FROM Parent p WHERE p.id IN (
SELECT DISTINCT p2.id FROM Parent p2 JOIN FETCH p2.children WHERE ... ORDER BY p2.name
) ORDER BY p.name
hibernate.query.fail_on_pagination_over_collection_fetch=true
This will cause Hibernate to throw an exception for queries that cannot be safely paginated, prompting you to apply the DISTINCT/subquery fix.
DISTINCT may require the database to perform a duplicate‑eliminating step (e.g., using a temporary table or sort). For large result sets this can add overhead, but it is usually comparable to the cost of the window‑function approach in Hibernate 6.0.To confirm whether the manual DISTINCT approach will preserve deterministic ordering, we need to know: Are the columns referenced in the ORDER BY clause included in the SELECT list (or do they uniquely identify each root entity)? If they are not, you may need to add those columns to the SELECT (or rely on the primary‑key subquery) to avoid nondeterministic ordering.
Use comments to ask for clarification. Post a solution as an answer.
31,275 reputation · 04 Dec 2023, 07:09 UTC
When using fetch joins with ORDER BY, selecting only the root entity’s primary key in a subquery (e.g., SELECT p FROM Parent p WHERE p.id IN (SELECT DISTINCT p2.id FROM Parent p2 JOIN FETCH p2.children WHERE ...)) guarantees duplicate‑free rows before LIMIT/OFFSET is applied. This technique works unchanged in Hibernate 5.6 and 6.0, is dialect‑independent, and avoids the extra sort/partition cost of a window‑function approach while still allowing the second fetch to load the associated collection efficiently.