BackendBench
Coffee

Unravel โ€บ Java concepts โ€บ Persistence and JPA

Persistence

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

Mental model

A list of customers is a stack of folders; each folder only has a sticky note saying "orders are in the basement".

What happens, in order
  1. 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.
  2. 2Loading 100 orders and then calling order.getItems() on each runs 1 query for the orders plus 100 for the itemsThat is "N+1".
  3. 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.
Remember
Watch out
Looping over entities and touching a lazy relationFixing LazyInitializationException by making every relation eagerSerializing entities and triggering lazy loads inside Jackson
Example (highlighted lines explain it)
// 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
  1. 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:

Part of a learning path

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.