Unravel โบ Java concepts โบ Persistence and JPA
Lazy loading and the N+1 query problem
Relations between entities are often loaded lazily, one extra query on first access.
Learn these first
At a glance
A list of customers is a stack of folders; each folder only has a sticky note saying "orders are in the basement".
- 1@OneToMany is lazy by default and @ManyToOne is eager by defaultA lazy relation is a proxy that runs a query the first time you touch it.
- 2Loading 100 orders and then calling order.getItems() on each runs 1 query for the orders plus 100 for the itemsThat is "N+1".
- 3Fixes: a join fetch in the query (join fetch o.items), an @EntityGraph, or batch fetching (@BatchSize) that loads many children per queryChoose per use case rather than making everything eager.
- Accessing a lazy relation after the transaction (or session) is closed throws LazyInitializationException
- A join fetch of two collections in one query multiplies rows (a cartesian product) and can break paging; fetch collections in separate steps or with batching
- For a lazy collection Hibernate puts a PersistentBag that implements List and initializes on first read; for a lazy single entity it creates a bytecode-generated subclass proxy that loads the target on the first non-id method call
// 1 query for the orders, then one more per order for its items: N+1List<Order> orders = orderRepository.findAll();for (Order o : orders) { total += o.getItems().size();}// one query instead: select o from Order o join fetch o.items- line 2 1 query for the orders, then one more per order for its items: N+1
Read the full explanation (about 1 min)
Relations between entities are often loaded lazily, one extra query on first access. Touching them in a loop causes one query per row (N+1), the most common JPA performance bug.
A mental model
A list of customers is a stack of folders; each folder only has a sticky note saying "orders are in the basement". Opening each note sends a runner downstairs. Ten folders means ten trips, where one trip could have fetched everything.
How it works
@OneToMany is lazy by default and @ManyToOne is eager by default. A lazy relation is a proxy that runs a query the first time you touch it.
Loading 100 orders and then calling order.getItems() on each runs 1 query for the orders plus 100 for the items. That is "N+1".
Fixes: a join fetch in the query (join fetch o.items), an @EntityGraph, or batch fetching (@BatchSize) that loads many children per query. Choose per use case rather than making everything eager.
Going deeper
Accessing a lazy relation after the transaction (or session) is closed throws LazyInitializationException. The cure is to load what you need inside the service method and map to a DTO, not to widen the session.
A join fetch of two collections in one query multiplies rows (a cartesian product) and can break paging; fetch collections in separate steps or with batching.
Turn on SQL logging in development (spring.jpa.show-sql=true or hibernate.generate_statistics) and count queries in a test for the hot endpoints.
Under the hood
For a lazy collection Hibernate puts a PersistentBag that implements List and initializes on first read; for a lazy single entity it creates a bytecode-generated subclass proxy that loads the target on the first non-id method call.
Common mistakes
- Looping over entities and touching a lazy relation.
- Fixing LazyInitializationException by making every relation eager.
- Serializing entities and triggering lazy loads inside Jackson.
How Unravel recognises it
When you paste code, Unravel looks for these signs of lazy loading and the n+1 query problem. Spotting them yourself is a good habit when you read unfamiliar code:
- Annotations:
@OneToMany@ManyToOne@ManyToMany@OneToOne@EntityGraph@BatchSize@Fetch - Types:
FetchType
Part of a learning path
- How a Spring request really works ยท previous: JPA entities ยท next: Spring Data repositories
- JPA and Hibernate in depth ยท previous: JPA relationships, cascade and orphan removal ยท next: Persistence context, flush and dirty checking
Related concepts
Frequently asked questions
What is Lazy loading and the N+1 query problem?
Relations between entities are often loaded lazily, one extra query on first access. Touching them in a loop causes one query per row (N+1), the most common JPA performance bug.
What are the most common mistakes with Lazy loading and the N+1 query problem?
Looping over entities and touching a lazy relation. Fixing LazyInitializationException by making every relation eager. Serializing entities and triggering lazy loads inside Jackson.
How can I see Lazy loading and the N+1 query problem in real code?
Paste your own Java into Unravel and it will point out where lazy loading and the n+1 query problem appears, explain how it works at the detail level you choose, and draw the flow of the code around it.