@OneToMany Fetch Strategies — JOIN, SUBSELECT, SELECT, BatchSize
Fast recall
Quick revision
Core answer
@OneToMany. SELECT (default LAZY) — 1 query for parents + 1 per parent's collection on access (the classic N+1). JOIN (`@Fetch(JOIN)` / `JOIN FETCH`) — one query with LEFT JOIN; rows multiply (Cartesian) and pagination breaks (HHH90003004 — Hibernate paginates in memory). SUBSELECT (`@Fetch(SUBSELECT)`) — parent query + one follow-up WHERE parent_id IN (SELECT ... FROM original_query); works with pagination. `@BatchSize(50)` — reduces N+1 to ceil(N/50) + 1 via WHERE parent_id IN (?, ?, ?) batches; works with pagination. `FetchType.EAGER` on `@OneToMany` is always wrong — it forces JOIN on every parent load and silently produces Cartesian products.Awareness of N+1 and the trade-offs between fetch strategies, including pagination compatibility
Using `FetchType.EAGER` on `@OneToMany` thinking it avoids N+1 — instead it causes a JOIN that multiplies rows (Cartesian product)
Explain why JOIN fetch breaks pagination, when SUBSELECT or BatchSize is preferable, and the global `default_batch_fetch_size` setting
Interview-ready guide
Interview answer
Hibernate has four practical fetch strategies for @OneToMany collections.
SELECT — the default LAZY:
- One query for parents, one extra per parent's collection on access — N+1
- Acceptable only when N is small or access is rare
JOIN FETCH:
- One query with LEFT JOIN — zero N+1
- Rows multiply — Cartesian product with multiple collections
- Pagination breaks —
HHH90003004, Hibernate paginates in memory
SUBSELECT (`@Fetch(SUBSELECT)`):
- One query for parents + one follow-up
WHERE parent_id IN (SELECT...) - Works with pagination, no Cartesian risk
- Best when you always need the collection across the entire page
`@BatchSize(50)`:
- Reduces N+1 to
ceil(N/50)+1via batched IN-clause queries - Pagination-safe, the pragmatic default
- Set globally with
hibernate.default_batch_fetch_size: 50
Avoid:
FetchType.EAGERon@OneToMany— silently produces Cartesian products- JOIN FETCH of two
@OneToMany—MultipleBagFetchExceptionor row explosion - JOIN FETCH + Pageable on a collection — Hibernate paginates in memory
@OneToMany Fetch Strategies — JOIN, SUBSELECT, SELECT, BatchSize
Direct Answer
Hibernate supports four practical fetch strategies for @OneToMany. SELECT (default LAZY) — 1 query for parents + 1 per parent's collection on access (the classic N+1). JOIN (@Fetch(JOIN) / JOIN FETCH) — one query with LEFT JOIN; rows multiply (Cartesian) and pagination breaks (HHH90003004 — Hibernate paginates in memory). SUBSELECT (@Fetch(SUBSELECT)) — parent query + one follow-up WHERE parent_id IN (SELECT ... FROM original_query); works with pagination. @BatchSize(50) — reduces N+1 to ceil(N/50) + 1 via WHERE parent_id IN (?, ?, ?) batches; works with pagination. FetchType.EAGER on @OneToMany is always wrong — it forces JOIN on every parent load and silently produces Cartesian products.
Common Mistakes & Pitfalls
Using FetchType.EAGER on @OneToMany thinking it avoids N+1 — instead it causes a JOIN that multiplies rows (Cartesian product)
Check your understanding
Answer these without looking back at the article.
- Why does Hibernate log `HHH90003004` when you use JOIN FETCH with pagination?
- How does `@BatchSize` work and how do you set a global default in Spring Boot?
- When would you choose an `@EntityGraph` over a JPQL JOIN FETCH?
- Why is `MultipleBagFetchException` thrown and how do you work around it?
Related interview prep categories
2Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.