Batch-fetch lazy collections for a bounded parent set when one collection join would multiply rows or break paging.
JPA collection batch fetching: reduce N+1 without a giant join
Why loading one page still fans out
A receipt list query fetches 47 headers. Rendering each header’s line count by touching a lazy collection can issue one more SELECT per header. Fetch plans need to match the response shape: a to-one join is often cheap, while joining many to-many or one-to-many collections multiplies rows. Hibernate batch fetching groups pending lazy loads into fewer secondary queries, preserving the original parent query.
Keep the parent set bounded
For a targeted association, @BatchSize can bound how many uninitialized collections Hibernate requests per secondary query. A global default fetch batch size is broader and may change unrelated screens; measure it before adopting. Batch fetching still loads collection contents, so a receipt with enormous line history needs a separate paged child endpoint or aggregate query. Page versus Slice remains a choice about count work, not collection size.
Verify SQL shape
Run a representative 47-parent page with collection access inside an active service transaction. Compare SELECT count, returned row count, memory and p95 latency with the unbatched path. Then repeat with skewed parents, including one with thousands of lines. A small query count can still be slow if each query returns too much data. Open-in-view off helps catch accidental view-layer lazy loading in tests.
Implementation contract
@Entity
class ReceiptHeader {
@OneToMany(mappedBy = "receipt", fetch = FetchType.LAZY)
@org.hibernate.annotations.BatchSize(size = 47)
private List<ReceiptLine> lines = new ArrayList<>();
}Cost and verification
For 47 parents, batching may replace up to 47 extra selects with a small number of IN-style secondary loads. Database parameter limits and child-row volume still matter; measure both trips and bytes.
Common Mistakes
- Do not use collection batch fetching as a substitute for child pagination.
- Do not switch every association to eager loading to remove N+1.
- Do not validate only SELECT count while ignoring rows returned.
Read next
Spring JPA fetch plans: measure N+1 queries before changing mappings, Spring Data JPA Page versus Slice: pay for totals only when needed, Spring Boot Open EntityManager in View: close the response-time query gap, Spring Data JPA entity graph for a bounded to-one read, Spring Data JPA count query for a joined Page.
