Spring Data JPA N+1-Oplossingen in 2026: Fetch Join en EntityGraph
Volledige gids voor het detecteren en oplossen van het N+1-probleem in Spring Data JPA. Fetch join, @EntityGraph, batch fetching en strategieën voor queryprestaties.

Het N+1-probleem vormt een van de meest voorkomende prestatievalkuilen in JPA. Een onschuldige query om 100 bestellingen op te halen kan 101 SQL-queries veroorzaken: één voor de bestellingen en daarna één voor elke bijbehorende klant. Deze stille vermenigvuldiging van queries verslechtert de prestaties en overbelast de database.
Een endpoint dat 50 artikelen met hun auteurs retourneert kan door N+1 van 10ms naar 500ms springen. Vroege detectie voorkomt kritieke productieproblemen.
Het N+1-Probleem in JPA Begrijpen
Het N+1-probleem treedt op wanneer JPA een collectie van entiteiten laadt en vervolgens een extra query uitvoert voor elke entiteit om de bijbehorende associaties te laden. Dit gedrag komt voort uit de standaard lazy loading van @OneToMany- en @ManyToMany-relaties.
Neem een klassiek model met bestellingen en klanten. Elke bestelling behoort tot een klant, en deze relatie gebruikt standaard lazy loading.
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String orderNumber;
private LocalDateTime createdAt;
// ManyToOne relationship is lazy by default since JPA 2.0
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id")
private Customer customer;
// OneToMany relationship lazy by default
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
private List<OrderItem> items = new ArrayList<>();
// Getters and setters omitted
}@Entity
@Table(name = "customers")
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
// Getters and setters omitted
}Wanneer een query bestellingen ophaalt en vervolgens de naam van de klant opvraagt, voert Hibernate voor elke bestelling een extra query uit.
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
public List<OrderDto> getAllOrders() {
// 1 query: SELECT * FROM orders
List<Order> orders = orderRepository.findAll();
// For each order, accessing customer triggers a query
return orders.stream()
.map(order -> new OrderDto(
order.getId(),
order.getOrderNumber(),
// N queries: SELECT * FROM customers WHERE id = ?
order.getCustomer().getName()
))
.toList();
}
}Voor 100 bestellingen voert deze code 101 SQL-queries uit. De Hibernate-logs onthullen dit destructieve patroon.
-- Query 1: fetch orders
SELECT o.id, o.order_number, o.created_at, o.customer_id FROM orders o
-- Queries 2-101: fetch each customer
SELECT c.id, c.name, c.email FROM customers c WHERE c.id = 1
SELECT c.id, c.name, c.email FROM customers c WHERE c.id = 2
SELECT c.id, c.name, c.email FROM customers c WHERE c.id = 3
-- ... 97 more identical queriesHet N+1-Probleem Detecteren met Hibernate-Logs
De eerste stap bestaat uit het inschakelen van SQL-logs om problematische queries te identificeren. De volgende configuratie toont elke door Hibernate uitgevoerde query.
# application.yml
spring:
jpa:
show-sql: true
properties:
hibernate:
# Format SQL for better readability
format_sql: true
# Display session statistics (queries, time)
generate_statistics: true
logging:
level:
# Detailed SQL query logging
org.hibernate.SQL: DEBUG
# Display prepared statement parameters
org.hibernate.orm.jdbc.bind: TRACEHibernate-statistieken bieden een waardevolle samenvatting aan het einde van elke transactie.
Session Metrics {
23421 nanoseconds spent acquiring 1 JDBC connection;
0 nanoseconds spent releasing 0 JDBC connections;
1254789 nanoseconds spent preparing 101 JDBC statements;
15478963 nanoseconds spent executing 101 JDBC statements;
0 nanoseconds spent executing 0 JDBC batches;
}Het aantal van 101 JDBC-statements voor een eenvoudige bestellijst signaleert duidelijk een N+1-probleem.
SQL-logs en statistieken hebben invloed op de prestaties. Deze opties moeten in productie uitgeschakeld blijven en gereserveerd zijn voor ontwikkel- en testomgevingen.
Oplossing 1: Fetch Join met JPQL
Fetch join laadt associaties in één enkele SQL-query via een join. Deze expliciete aanpak lost N+1 op door alle benodigde gegevens in één keer op te halen.
public interface OrderRepository extends JpaRepository<Order, Long> {
// Explicit fetch join to load customers
@Query("SELECT o FROM Order o JOIN FETCH o.customer")
List<Order> findAllWithCustomer();
// Multiple fetch join for several associations
@Query("SELECT o FROM Order o " +
"JOIN FETCH o.customer c " +
"JOIN FETCH o.items i")
List<Order> findAllWithCustomerAndItems();
// Fetch join with WHERE condition
@Query("SELECT o FROM Order o " +
"JOIN FETCH o.customer c " +
"WHERE o.createdAt > :since")
List<Order> findRecentOrdersWithCustomer(
@Param("since") LocalDateTime since
);
}Fetch join transformeert N+1-queries in één geoptimaliseerde query.
-- Single query with join
SELECT o.id, o.order_number, o.created_at, o.customer_id,
c.id, c.name, c.email
FROM orders o
JOIN customers c ON o.customer_id = c.idDe service gebruikt nu de geoptimaliseerde methode zonder de bedrijfscode aan te passen.
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
public List<OrderDto> getAllOrders() {
// Single query with join
List<Order> orders = orderRepository.findAllWithCustomer();
// No additional queries
return orders.stream()
.map(order -> new OrderDto(
order.getId(),
order.getOrderNumber(),
order.getCustomer().getName() // Already loaded
))
.toList();
}
}Klaar om je Spring Boot gesprekken te halen?
Oefen met onze interactieve simulatoren, flashcards en technische tests.
Oplossing 2: @EntityGraph voor Declaratieve Controle
De annotatie @EntityGraph biedt een declaratief alternatief voor fetch join. Deze definieert welke associaties moeten worden geladen zonder aangepaste JPQL te schrijven.
public interface OrderRepository extends JpaRepository<Order, Long> {
// Inline EntityGraph with attributePaths
@EntityGraph(attributePaths = {"customer"})
List<Order> findAll();
// EntityGraph with multiple attributes
@EntityGraph(attributePaths = {"customer", "items"})
List<Order> findByCreatedAtAfter(LocalDateTime since);
// Named EntityGraph referencing entity definition
@EntityGraph(value = "Order.withCustomerAndItems")
List<Order> findByCustomerId(Long customerId);
// Combination with custom query
@EntityGraph(attributePaths = {"customer"})
@Query("SELECT o FROM Order o WHERE o.orderNumber LIKE :prefix%")
List<Order> findByOrderNumberPrefix(@Param("prefix") String prefix);
}Benoemde EntityGraphs worden direct op de entiteit gedefinieerd voor hergebruik in meerdere repositories.
@Entity
@Table(name = "orders")
@NamedEntityGraph(
name = "Order.withCustomer",
attributeNodes = @NamedAttributeNode("customer")
)
@NamedEntityGraph(
name = "Order.withCustomerAndItems",
attributeNodes = {
@NamedAttributeNode("customer"),
@NamedAttributeNode("items")
}
)
@NamedEntityGraph(
name = "Order.full",
attributeNodes = {
@NamedAttributeNode("customer"),
@NamedAttributeNode(value = "items", subgraph = "items-product")
},
subgraphs = @NamedSubgraph(
name = "items-product",
attributeNodes = @NamedAttributeNode("product")
)
)
public class Order {
// Fields unchanged
}De subgraph maakt het laden van geneste associaties mogelijk. Het bovenstaande voorbeeld laadt de bestellingen, hun items en het product van elk item in één query.
Oplossing 3: Batch Fetching voor Collecties
Batch fetching biedt een alternatief voor fetch join voor @OneToMany-collecties. In plaats van elke collectie afzonderlijk te laden, groepeert Hibernate de queries in batches.
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String orderNumber;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id")
private Customer customer;
// Batch fetching on the collection
@OneToMany(mappedBy = "order")
@BatchSize(size = 25)
private List<OrderItem> items = new ArrayList<>();
}Met @BatchSize(size = 25) laadt Hibernate items voor 25 bestellingen tegelijk in plaats van 1. Voor 100 bestellingen daalt het aantal queries van 101 naar 5.
-- Without batch fetching: 100 queries
SELECT * FROM order_items WHERE order_id = 1
SELECT * FROM order_items WHERE order_id = 2
-- ... 98 more queries
-- With @BatchSize(size = 25): 4 queries
SELECT * FROM order_items WHERE order_id IN (1, 2, 3, ..., 25)
SELECT * FROM order_items WHERE order_id IN (26, 27, 28, ..., 50)
SELECT * FROM order_items WHERE order_id IN (51, 52, 53, ..., 75)
SELECT * FROM order_items WHERE order_id IN (76, 77, 78, ..., 100)De globale configuratie van de batchgrootte geldt voor alle collecties in de applicatie.
# application.yml
spring:
jpa:
properties:
hibernate:
# Global default batch size
default_batch_fetch_size: 25Batch fetching past bij gevallen waarin fetch join een te groot Cartesisch product genereert. Voor een bestelling met 10 items en 5 betalingen retourneert fetch join 50 rijen. Batch fetching voert 2 afzonderlijke, efficiëntere queries uit.
Vergelijking van Laadstrategieën
Elke strategie biedt voordelen afhankelijk van de gebruikscontext. De volgende tabel vat de optimale gebruiksgevallen samen.
| Strategie | Gebruikscontext | Voordelen | Nadelen |
|----------|----------|------------|---------------|
| Fetch Join | @ManyToOne-relaties | Enkele SQL-query | Cartesisch product met collecties |
| @EntityGraph | Declaratief laden | Herbruikbaar, leesbaar | Minder flexibel dan JPQL |
| Batch Fetching | @OneToMany-collecties | Vermijdt Cartesisch product | Meerdere queries |
| Subselect | Zelden geraadpleegde collecties | Laadt alleen wanneer nodig | Gecorreleerde subquery |
De subselect-strategie laadt de hele collectie bij de eerste toegang tot een element.
@OneToMany(mappedBy = "order")
@Fetch(FetchMode.SUBSELECT)
private List<OrderItem> items = new ArrayList<>();-- Generated subselect query
SELECT * FROM order_items
WHERE order_id IN (SELECT id FROM orders WHERE created_at > ?)N+1 Vermijden met DTO-Projecties
DTO-projecties bieden een radicale maar effectieve aanpak. Door alleen de noodzakelijke kolommen te selecteren, vermijden projecties volledig het laden van entiteiten en hun associaties.
public record OrderSummaryDto(
Long orderId,
String orderNumber,
String customerName,
String customerEmail
) {}public interface OrderRepository extends JpaRepository<Order, Long> {
// DTO projection with constructor
@Query("SELECT new com.example.dto.OrderSummaryDto(" +
"o.id, o.orderNumber, c.name, c.email) " +
"FROM Order o JOIN o.customer c")
List<OrderSummaryDto> findAllOrderSummaries();
// DTO projection with condition
@Query("SELECT new com.example.dto.OrderSummaryDto(" +
"o.id, o.orderNumber, c.name, c.email) " +
"FROM Order o JOIN o.customer c " +
"WHERE o.createdAt > :since")
List<OrderSummaryDto> findRecentOrderSummaries(
@Param("since") LocalDateTime since
);
}Deze aanpak genereert een optimale SQL-query zonder de overhead van entity-mapping.
SELECT o.id, o.order_number, c.name, c.email
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE o.created_at > ?Geavanceerde Configuratie met Spring Data JPA
Spring Data JPA 3.x introduceert verbeteringen voor het beheer van EntityGraph en geoptimaliseerde queries.
public interface OrderRepository extends JpaRepository<Order, Long> {
// Dynamic EntityGraph with Specification
@EntityGraph(attributePaths = {"customer"})
List<Order> findAll(Specification<Order> spec);
// Pagination with EntityGraph
@EntityGraph(attributePaths = {"customer"})
Page<Order> findByCustomerNameContaining(
String name,
Pageable pageable
);
// Slice for efficient pagination
@EntityGraph(attributePaths = {"customer"})
Slice<Order> findByCreatedAtBefore(
LocalDateTime date,
Pageable pageable
);
}Voorwaardelijk laden maakt het mogelijk om verschillende strategieën toe te passen afhankelijk van de context.
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final EntityManager entityManager;
public List<Order> getOrdersWithGraph(String graphName) {
// Dynamic EntityGraph retrieval
EntityGraph<?> graph = entityManager
.getEntityGraph(graphName);
return entityManager
.createQuery("SELECT o FROM Order o", Order.class)
.setHint("jakarta.persistence.loadgraph", graph)
.getResultList();
}
public List<Order> getOrdersForListing() {
// Minimal graph for listing
return getOrdersWithGraph("Order.withCustomer");
}
public List<Order> getOrdersForDetail() {
// Full graph for detail view
return getOrdersWithGraph("Order.full");
}
}Begin met oefenen!
Test je kennis met onze gespreksimulatoren en technische tests.
Prestatietests om N+1 te Detecteren
Geautomatiseerde tests verifiëren de afwezigheid van N+1-problemen door het aantal uitgevoerde SQL-queries te tellen.
@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE)
class OrderRepositoryPerformanceTest {
@Autowired
private OrderRepository orderRepository;
@Autowired
private EntityManager entityManager;
@PersistenceContext
private EntityManager em;
private Statistics statistics;
@BeforeEach
void setUp() {
// Enable Hibernate statistics
Session session = entityManager.unwrap(Session.class);
SessionFactory factory = session.getSessionFactory();
statistics = factory.getStatistics();
statistics.setStatisticsEnabled(true);
statistics.clear();
}
@Test
void findAllWithCustomer_shouldExecuteSingleQuery() {
// Given: 50 orders in database
createTestOrders(50);
entityManager.clear();
statistics.clear();
// When: fetch with fetch join
List<Order> orders = orderRepository.findAllWithCustomer();
// Then: single query executed
assertThat(orders).hasSize(50);
assertThat(statistics.getQueryExecutionCount())
.as("Should execute only 1 query with fetch join")
.isEqualTo(1);
}
@Test
void findAll_withoutOptimization_triggersNPlus1() {
// Given: 50 orders in database
createTestOrders(50);
entityManager.clear();
statistics.clear();
// When: fetch without optimization
List<Order> orders = orderRepository.findAll();
// Access customers to trigger lazy loading
orders.forEach(o -> o.getCustomer().getName());
// Then: N+1 queries (51 instead of 1)
assertThat(statistics.getQueryExecutionCount())
.as("Should detect N+1 problem")
.isGreaterThan(1);
}
private void createTestOrders(int count) {
for (int i = 0; i < count; i++) {
Customer customer = new Customer();
customer.setName("Customer " + i);
customer.setEmail("customer" + i + "@test.com");
entityManager.persist(customer);
Order order = new Order();
order.setOrderNumber("ORD-" + i);
order.setCustomer(customer);
order.setCreatedAt(LocalDateTime.now());
entityManager.persist(order);
}
entityManager.flush();
}
}De assertie op getQueryExecutionCount() zorgt ervoor dat optimalisaties behouden blijven tijdens code-evolutie.
Conclusie
Het N+1-probleem vormt een grote prestatie-uitdaging in Spring Data JPA, maar er bestaan verschillende effectieve oplossingen. Fetch join en @EntityGraph lossen de meeste gevallen op door associaties in één query te laden. Batch fetching biedt een alternatief voor grote collecties waar fetch join een Cartesisch product genereert.
Checklist voor N+1-Preventie:
- ✅ SQL-logs inschakelen tijdens ontwikkeling om meerdere queries te detecteren
- ✅
JOIN FETCHgebruiken voor vaak geraadpleegde@ManyToOne-relaties - ✅
@EntityGraphtoepassen voor herbruikbaar declaratief laden - ✅
@BatchSizeconfigureren voor grote@OneToMany-collecties - ✅ DTO-projecties verkiezen voor alleen-lezen-bewerkingen
- ✅ Tests schrijven die het aantal uitgevoerde queries verifiëren
- ✅ SQL-logs en statistieken uitschakelen in productie
- ✅ Kritieke endpoints regelmatig profileren met Hibernate-metrics
Tags
Delen
Gerelateerde artikelen

Spring GraphQL Sollicitatiegesprek: Resolvers, DataLoaders en Oplossingen voor het N+1-probleem
Voorbereiding op Spring GraphQL sollicitatiegesprekken met deze complete gids. Resolvers, DataLoaders, omgaan met het N+1-probleem, mutaties en best practices voor technische vragen.

Spring Boot 3.4 Virtual Threads: Interviewvragen en Performance-Benchmarks
Beheers Java 21 Virtual Threads met Spring Boot 3.4: 15 interviewvragen, performance-benchmarks en migratiepatronen om technische gesprekken te slagen.

Spring Boot logging in 2026: gestructureerde logs in productie met Logback en JSON
Volledige gids voor gestructureerde logging in Spring Boot. Logback JSON-configuratie, MDC voor tracing, best practices in productie en integratie met ELK Stack.