Your endpoint returns 100 orders and somehow runs 101 SQL queries. Your database CPU is pinned, latency is terrible, and the code looks completely normal. Welcome to the N+1 problem โ the single most common JPA performance bug.
The Symptom
List<Order> orders = orderRepository.findAll(); // 1 query for (Order order : orders) { System.out.println(order.getCustomer().getName()); // 1 query EACH }
One query loads the orders. Then each getCustomer() triggers another query to lazily load that order's customer. One hundred orders means one hundred extra queries โ N+1 total.
Why It Happens
JPA relationships are lazy by default (a good default โ it avoids loading data you never use). But "lazy" means "load it the moment you touch it." Touching it inside a loop means loading it once per iteration. Hibernate is faithfully doing what you asked; you just asked one row at a time.
Fix 1: Fetch Join
Tell JPQL to load the association in the same query:
@Query("SELECT o FROM Order o JOIN FETCH o.customer") List<Order> findAllWithCustomer();
One query, one join, all customers loaded. This is the most direct fix.
Fix 2: Entity Graph
Keep your method declarative and attach a graph:
@EntityGraph(attributePaths = {"customer"}) List<Order> findAll();
Same effect as a fetch join, without hand-writing JPQL.
Fix 3: Batch Fetching
When you truly need lazy loading, tell Hibernate to load lazy associations in batches instead of one at a time:
@BatchSize(size = 50) @ManyToOne(fetch = FetchType.LAZY) private Customer customer;
Now 100 customers load in 2 queries (50 each) instead of 100.
Prevention
- โธEnable SQL logging in dev (
spring.jpa.show-sql=true) and watch the query count on every endpoint. - โธAdd an assertion in tests that a request issues a bounded number of queries.
- โธTreat any per-row query inside a loop as a red flag in review.
The takeaway: N+1 is not a bug in JPA โ it is lazy loading meeting a loop. Fetch the association up front with a join or entity graph, or batch it, and 101 queries collapse back into one or two.