The problem: you load N parent rows with one query, then accessing a lazy association on each parent triggers one more query per parent, so N+1 queries in total. For example, listing 100 orders and calling order.getItems() in a loop runs 101 queries.
How to spot it:
- Enable SQL logging (spring.jpa.show-sql or better, a datasource proxy) or Hibernate statistics, and watch for repeated similar queries.
- Tests that assert the number of queries catch regressions.
Fixes:
- Fetch join in JPQL: select o from Order o join fetch o.items where ... loads parents and children in one query.
- @EntityGraph(attributePaths = "items") on a repository method, which is declarative and reusable.
- Batch fetching: @BatchSize(size = 50) or hibernate.default_batch_fetch_size loads children in groups with IN queries.
- DTO projections: select only the fields the API needs, avoiding entity graphs entirely for read-heavy endpoints.
Cautions:
- Fetch-joining several collections at once creates a cartesian product; split queries or use batch fetching.
- Paginating with collection fetch joins happens in memory; paginate parent ids first, then fetch children.