Interview question
Fix and regression-test a JPA N+1 query
Uses a bounded entity graph for a to-one association and proves the repair with Hibernate query statistics.
TL;DR
Uses a bounded entity graph for a to-one association and proves the repair with Hibernate query statistics.
Lazy loading, EntityGraph, query boundaries, DTO mapping, Hibernate statistics, persistence-context control, and measured regression tests.
Practice the problem like a real interview: restate, reason, implement, and test.
An order-list service loads 100 orders and maps order.getCustomer().getName(), producing one order query plus customer queries. Fix this specific to-one N+1 without making the association globally eager. Add a regression test that fails when query count grows with the number of orders.
Next in Java & Spring Practical Labs: JPA keyset pagination
Before: 1 order query + N customer queries
After: 1 bounded query fetching orders and customer
Test: many orders/customer rows -> prepared statement count remains 1
@EntityGraph(attributePaths = "customer") to that method.interface OrderRepository extends org.springframework.data.jpa.repository.JpaRepository<Order, Long> {
@org.springframework.data.jpa.repository.EntityGraph(attributePaths = "customer")
java.util.List<Order> findTop100ByStatusOrderByCreatedAtDesc(OrderStatus status);
}
@org.springframework.stereotype.Service
class PaidOrderQuery {
private final OrderRepository orders;
PaidOrderQuery(OrderRepository orders) { this.orders = orders; }
@org.springframework.transaction.annotation.Transactional(readOnly = true)
java.util.List<OrderRow> latestPaid() {
return orders.findTop100ByStatusOrderByCreatedAtDesc(OrderStatus.PAID).stream()
.map(order -> new OrderRow(
order.getId(), order.getCustomer().getName(), order.getTotal()))
.toList();
}
}
// In a PostgreSQL integration test with hibernate.generate_statistics=true:
entityManager.flush();
entityManager.clear();
var statistics = entityManager.getEntityManagerFactory()
.unwrap(org.hibernate.SessionFactory.class).getStatistics();
statistics.clear();
var rows = query.latestPaid();
org.assertj.core.api.Assertions.assertThat(rows).hasSize(20);
org.assertj.core.api.Assertions.assertThat(statistics.getPrepareStatementCount()).isEqualTo(1);
The service materializes O(n) bounded results. The repair changes round trips from O(n) to O(1) for this association, though the joined result and object materialization remain O(n).
EAGER affects every query and still does not guarantee one SQL statement.Spot a weak answer, missing edge case, or clearer explanation? Send it in.
Say thanks with a standalone, one-time $5 contribution. No account required.