A tiny, copy‑pastable project that reproduces the N+1 problem and shows 3 safe fixes:
- Baseline (trigger N+1):
findAll()
then accessauthor.getBooks()
in a loop. - Fix #1:
JOIN FETCH
query - Fix #2:
@EntityGraph(attributePaths = "books")
- (Bonus)
hibernate.default_batch_fetch_size
is configured to help with to‑one proxies.
mvn -v # Java 21 + Maven 3.9+ recommended
mvn test -q
The tests assert query counts using Hibernate Statistics
:
baselineShowsNPlusOne()
expects >= 6 statements with our seed data (5 authors).joinFetchFixUsesSingleQuery()
andentityGraphFixUsesSingleQuery()
expect <= 2 statements.
Author
↔Book
with@OneToMany
/@ManyToOne (LAZY)
AuthorRepository
with:findAllWithBooksJoinFetch()
—select distinct a from Author a left join fetch a.books
findAllWithBooksGraph()
—@EntityGraph(attributePaths="books")
- IDs-then-fetch helpers to implement paging without N+1
DemoService
— methods that reproduce/fix N+1 and expose a helper to read the currentprepareStatement
countapplication.yml
—open-in-view: false
, SQL logging, andhibernate.default_batch_fetch_size: 50
application-test.yml
—hibernate.generate_statistics: true
data.sql
— seeds 5 authors × 3 books
When you need pagination with a to-many association, use the two-step pattern:
- Page parent IDs (
Page<Long>
) - Fetch the graph with
join fetch
forwhere id in :ids
See AuthorRepository.pageIdsByNamePrefix(...)
and fetchGraphByIds(ids)
.
Join fetching a collection duplicates parent rows in SQL. select distinct
avoids exploding row counts
and lets Hibernate deduplicate entities safely.
Happy debugging! If you want me to wire this into your existing service with concrete entity names, say the word.