Configuring Hibernate join‑fetch pagination with distinct results in Red Hat JBoss EAP
0 reputation · 25 Jun 2020, 09:30 UTC
0 reputation · 25 Jun 2020, 09:30 UTC
Goal: Retrieve paginated lists of parent entities with their child associations using Hibernate’s setFirstResult and setMaxResults together with left join fetch, while ensuring each parent appears only once per page.
Constraints: The application runs on Red Hat JBoss EAP 7.x (Hibernate ORM 5.0‑5.6), where the default limit/offset behavior can produce duplicate root entities when join fetching is used. The team must decide whether to rely on a ResultTransformer such as DistinctRootEntityResultTransformer, adopt a keyset‑based (seek) pagination strategy, or use ScrollableResults, considering the effects of high write traffic on result stability and JDBC connection consumption.
Specific questions: Should a DistinctRootEntityResultTransformer be applied to eliminate duplicates without affecting page size? Is keyset‑based pagination more suitable for workloads with frequent inserts/updates? What are the resource trade‑offs between using ScrollableResults and limit/offset with a transformer in a concurrent environment?
In Red Hat JBoss EAP 7.x (Hibernate 5.x), combining setFirstResult/setMaxResults with a JOIN FETCH on a collection causes a fundamental conflict. Because a join fetch multiplies the number of rows returned by the database (one row per child entity), Hibernate cannot apply LIMIT and OFFSET at the SQL level without truncating the parent entities' child collections.
When this occurs, Hibernate triggers the HHH000104 warning and performs in-memory pagination. This means the entire result set is loaded into the JVM before being sliced, which can lead to OutOfMemoryError on large datasets.
To ensure pagination happens at the database level while still retrieving child associations, avoid the JOIN FETCH in the initial paginated query. Instead, use a two-step approach:
setFirstResult and setMaxResults.JOIN FETCH to load the parents and their children (e.g., WHERE p.id IN (:ids)).| Strategy | Effectiveness | Trade-off |
|---|---|---|
DistinctRootEntityResultTransformer |
Removes duplicates in the Java list. | Does not fix the in-memory pagination issue; the database still sends all rows. |
| Keyset Pagination (Seek) | Highly stable for high-write traffic. | Requires a strictly ordered, unique column; more complex to implement than offset. |
ScrollableResults |
Reduces JVM memory pressure. | Holds a JDBC cursor open longer, increasing connection consumption and potential lock contention. |
To verify that your configuration is avoiding in-memory pagination, enable SQL logging in your JBoss EAP standalone configuration or via log4j/logback:
logger.org.hibernate.SQL = DEBUG
Verification Checklist:
LIMIT and OFFSET (or the dialect equivalent).HHH000104 warning.setMaxResults value exactly, regardless of the number of children.To refine this recommendation: Are the child collections typically small (e.g., < 50 items) or potentially massive? If they are small, @BatchSize may be a simpler alternative to the two-step pattern.
Use comments to ask for clarification. Post a solution as an answer.
35,275 reputation · 25 Jun 2020, 18:22 UTC
An often-overlooked detail is that setMaxResults applies to the raw JDBC row count, not the number of distinct parent entities after deduplication. When left join fetch produces a Cartesian product, the database returns all matching rows before Hibernate’s DistinctRootEntityResultTransformer collapses them. This means a page configured for 20 results may contain fewer than 20 parents, and the Nth page can shift unpredictably if the underlying data changes between queries. For workloads where exact page boundaries matter, a keyset approach using a monotonically increasing identifier and indexed sort column avoids this pagination drift entirely, though it requires a stable ordering key and does not support arbitrary page jumps.