Spring Data JPA N+1 Sorgu Çözümleri: Fetch Join, EntityGraph ve Hibernate 7.4
Spring Data JPA'da N+1 sorununu tespit etme ve düzeltme rehberi. Fetch join, @EntityGraph, batch fetching ve sorgu performansı stratejileri.

N+1 sorunu, JPA'da en yaygın performans tuzaklarından birini temsil eder. 100 siparişi getirmek için yapılan masum bir sorgu, 101 SQL sorgusunu tetikleyebilir: bir tane siparişler için ve ardından her ilişkili müşteri için bir tane. Bu sessiz sorgu çoğalması performansı düşürür ve veritabanını aşırı yükler.
Yazarlarıyla birlikte 50 makale döndüren bir endpoint, N+1 nedeniyle 10ms'den 500ms'ye çıkabilir. Erken tespit, kritik üretim sorunlarını önler.
JPA'da N+1 Sorununu Anlamak
N+1 sorunu, JPA bir varlık koleksiyonunu yüklediğinde ve ardından ilişkilerini yüklemek için her varlık için ek bir sorgu yürüttüğünde meydana gelir. Bu davranış, @OneToMany ve @ManyToMany ilişkilerinin varsayılan tembel yüklemesinden kaynaklanır.
Siparişler ve müşterilerle klasik bir model düşünülebilir. Her sipariş bir müşteriye aittir ve bu ilişki varsayılan olarak tembel yüklemeyi kullanır.
@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
}Bir sorgu siparişleri getirip ardından müşteri adına eriştiğinde, Hibernate her sipariş için ek bir sorgu yürütür.
@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();
}
}100 sipariş için bu kod 101 SQL sorgusu yürütür. Hibernate logları bu yıkıcı kalıbı ortaya çıkarır.
-- 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 queriesHibernate Logları ile N+1 Sorununu Tespit Etme
İlk adım, sorunlu sorguları tanımlamak için SQL loglarını etkinleştirmektir. Aşağıdaki yapılandırma, Hibernate tarafından yürütülen her sorguyu görüntüler.
# 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 istatistikleri, her işlem sonunda değerli bir özet sağlar.
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;
}Basit bir sipariş listesi için 101 JDBC ifadesi sayısı, açıkça bir N+1 sorununun göstergesidir.
SQL logları ve istatistikler performansı etkiler. Bu seçenekler üretimde devre dışı bırakılmalı ve geliştirme ile test ortamlarına ayrılmalıdır.
Çözüm 1: JPQL ile Fetch Join
Fetch join, ilişkileri tek bir SQL sorgusunda bir join aracılığıyla yükler. Bu açık yaklaşım, gerekli tüm verileri tek seferde getirerek N+1'i çözer.
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, N+1 sorgularını tek bir optimize edilmiş sorguya dönüştürür.
-- 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.idServis artık iş kodunu değiştirmeden optimize edilmiş yöntemi kullanır.
@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();
}
}Spring Boot mülakatlarında başarılı olmaya hazır mısın?
İnteraktif simülatörler, flashcards ve teknik testlerle pratik yap.
Çözüm 2: Bildirimsel Kontrol için @EntityGraph
@EntityGraph ek açıklaması, fetch join'e bildirimsel bir alternatif sunar. Özel JPQL yazmadan hangi ilişkilerin yükleneceğini tanımlar. JPA en iyi uygulamalarına genel bir bakış için Spring Data JPA temelleri modülüne bakılabilir.
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);
}Adlandırılmış EntityGraph'lar, birden fazla repository'de yeniden kullanılmak üzere doğrudan varlık üzerinde tanımlanır.
@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
}Subgraph, iç içe ilişkilerin yüklenmesini sağlar. Yukarıdaki örnek, siparişleri, kalemlerini ve her kalemin ürününü tek bir sorguda yükler.
Hibernate 7, ek açıklama ayrıntısını azaltan basitleştirilmiş bir metin tabanlı @NamedEntityGraph sözdizimi sunar:
@Entity
@Table(name = "orders")
@org.hibernate.annotations.NamedEntityGraph(
name = "Order.full",
graph = "customer, items(product)"
)
public class Order {
// Fields unchanged
}Bu tek satırlık tanım, ayrıntılı iç içe @NamedAttributeNode ve @NamedSubgraph ek açıklamalarının yerini alır.
Çözüm 3: Koleksiyonlar için Batch Fetching
Batch fetching, @OneToMany koleksiyonları için fetch join'e bir alternatif sunar. Her koleksiyonu ayrı ayrı yüklemek yerine, Hibernate sorguları gruplara ayırır.
@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<>();
}@BatchSize(size = 25) ile Hibernate, 1 yerine bir seferde 25 sipariş için kalemleri yükler. 100 sipariş için sorgu sayısı 101'den 5'e düşer.
-- 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)Genel batch boyutu yapılandırması, uygulamadaki tüm koleksiyonlara uygulanır.
# application.yml
spring:
jpa:
properties:
hibernate:
# Global default batch size
default_batch_fetch_size: 25Batch fetching, fetch join'in çok büyük bir Kartezyen çarpım oluşturduğu durumlar için uygundur. 10 kalem ve 5 ödeme içeren bir sipariş için fetch join 50 satır döndürür. Batch fetching, 2 ayrı ve daha verimli sorgu yürütür.
Hibernate 7.4'te Fetch Join ile Sayfalama
Fetch join'lerin uzun süredir devam eden bir sınırlaması, sayfalama ile uyumsuzluğuydu. setMaxResults() yöntemini koleksiyon fetch join'leriyle birleştirmek, Hibernate'i tüm eşleşen satırları yüklemeye ve ardından limitleri bellekte uygulamaya zorladı. firstResult/maxResults specified with collection fetch; applying in memory uyarısı bu performans sorununu gösteriyordu.
Hibernate 7.4 bu sorunu çözüyor. Koleksiyon fetch join'leriyle sayfalama artık iç içe sorgular kullanarak veritabanı seviyesinde çalışıyor. Hibernate önce sınırlı üst varlık tanımlayıcıları kümesini belirliyor, ardından yalnızca bu üst satırlar için ilişkili koleksiyonları getiriyor.
public interface OrderRepository extends JpaRepository<Order, Long> {
@Query("SELECT o FROM Order o JOIN FETCH o.items")
Page<Order> findAllWithItems(Pageable pageable);
@Query("SELECT o FROM Order o JOIN FETCH o.customer JOIN FETCH o.items")
Slice<Order> findAllWithCustomerAndItems(Pageable pageable);
}Üretilen SQL artık veritabanı seviyesinde sayfalama için alt sorgu kullanıyor:
-- Hibernate 7.4: database-level pagination with fetch join
SELECT o.*, i.*
FROM orders o
JOIN order_items i ON o.id = i.order_id
WHERE o.id IN (
SELECT id FROM orders
ORDER BY created_at DESC
LIMIT 10 OFFSET 0
)Bu iyileştirme, üst-alt ilişkilerini görüntüleyen sayfalanmış endpoint'ler için bellek kullanımını önemli ölçüde azaltır. Spring Boot 4.1, Hibernate 7.4.5 içerir ve bu optimizasyon varsayılan olarak kullanılabilir.
Hâlâ önceki Hibernate sürümlerindeki uygulamalar için iki aşamalı getirme deseni en güvenli yaklaşım olmaya devam eder: önce sayfalama ile üst ID'leri getirin, ardından bu belirli ID'ler için ilişkileri yükleyin.
Yükleme Stratejilerinin Karşılaştırılması
Her strateji, kullanım bağlamına bağlı olarak avantajlar sunar. Aşağıdaki tablo, en uygun kullanım durumlarını özetler.
| Strateji | Kullanım Durumu | Avantajları | Dezavantajları |
|---|---|---|---|
| Fetch Join | @ManyToOne ilişkileri | Tek SQL sorgusu | Koleksiyonlarla Kartezyen çarpım |
| @EntityGraph | Bildirimsel yükleme | Yeniden kullanılabilir, okunabilir | JPQL'den daha az esnek |
| Batch Fetching | @OneToMany koleksiyonları | Kartezyen çarpımı önler | Birden fazla sorgu |
| Subselect | Nadiren erişilen koleksiyonlar | Yalnızca gerektiğinde yükler | İlişkili alt sorgu |
Subselect stratejisi, herhangi bir öğeye ilk erişimde tüm koleksiyonu yükler.
@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 > ?)DTO Projeksiyonları ile N+1'den Kaçınma
DTO projeksiyonları radikal ama etkili bir yaklaşım sunar. Yalnızca gerekli sütunları seçerek, projeksiyonlar varlıkların ve ilişkilerinin yüklenmesinden tamamen kaçınır. Bu desen, JPA sorguları modülünde ele alındığı gibi, okuma ağırlıklı endpoint'ler için özellikle uygundur.
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
);
}Bu yaklaşım, varlık eşleme yükü olmadan optimum bir SQL sorgusu üretir.
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 > ?Spring Data JPA ile Gelişmiş Yapılandırma
Spring Boot 4.x'teki Spring Data JPA, gelişmiş EntityGraph yönetimi ve sorgu optimizasyonu sağlar. Bu getirme stratejilerini karmaşık iş mantığıyla birleştirirken transaction propagation konusunu anlamak önemlidir.
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
);
}Koşullu yükleme, bağlama göre farklı stratejiler uygulamayı sağlar.
@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");
}
}Pratik yapmaya başla!
Mülakat simülatörleri ve teknik testlerle bilgini test et.
N+1'i Tespit Etmek için Performans Testleri
Otomatik testler, yürütülen SQL sorgularının sayısını sayarak N+1 sorunlarının yokluğunu doğrular. Kapsamlı test stratejileri için Spring Boot için Testcontainers rehberine bakılabilir.
@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();
}
}getQueryExecutionCount() üzerindeki iddia, kod evrimi sırasında optimizasyonların yerinde kalmasını sağlar.
Kaynaklar
- Hibernate 7.4 What's New – fetch join'lerle sayfalama artık veritabanı seviyesinde güvenli
- Spring Boot 4.1 Release Notes – Hibernate 7.4.5.Final ile birlikte geliyor
- Hibernate ORM Documentation – getirme stratejileri ve EntityGraph için resmi referans
Spring Data JPA için N+1 Önleme Kontrol Listesi
N+1 sorunu Spring Data JPA'da büyük bir performans zorluğu temsil eder, ancak birkaç etkili çözüm mevcuttur. Fetch join ve @EntityGraph, ilişkileri tek bir sorguda yükleyerek çoğu durumu çözer. Batch fetching, fetch join'in Kartezyen çarpım oluşturduğu büyük koleksiyonlar için bir alternatif sunar. Hibernate 7.4 ile fetch join'lerle sayfalama artık veritabanı seviyesinde çalışıyor.
Üretimde uygulanması gerekenler:
- Çoklu sorguları tespit etmek için geliştirme ortamında SQL loglarını etkinleştirin
- Sık erişilen
@ManyToOneilişkileri içinJOIN FETCHkullanın - Yeniden kullanılabilir bildirimsel yükleme için
@EntityGraphuygulayın - Büyük
@OneToManykoleksiyonları için@BatchSizeyapılandırın - Salt okunur işlemler için DTO projeksiyonlarını tercih edin
- Yürütülen sorgu sayısını doğrulayan testler yazın
- Üretimde SQL loglarını ve istatistikleri devre dışı bırakın
- Kritik endpoint'leri Hibernate metrikleri ile düzenli olarak profilleyin
- Hibernate 7.4 sayfalama düzeltmelerinden yararlanmak için Spring Boot 4.1+ sürümüne yükseltin
Spring Boot kodundaki hatayı bulabilir misin?
Gerçek bir kod parçası, gizli bir hata, günde bir deneme. Denemek için hesap gerekmez.

Yazan:
Anthony Fillion-MailletSharpSkill kurucusu
10 yılı aşkın süredir fullstack geliştirici. SharpSkill’i yönetiyor ve burada yayımlanan her şeyden sorumlu.
2 Eylül 2026 tarihinde güncellendi
Etiketler
Paylaş
İlgili makaleler

Spring GraphQL Mülakatı: Resolver'lar, DataLoader'lar ve N+1 Problemi Çözümleri
Bu kapsamlı kılavuzla Spring GraphQL mülakatlarına hazırlanın. Resolver'lar, DataLoader'lar, N+1 problemi yönetimi, mutation'lar ve teknik sorular için en iyi uygulamalar.

Spring Boot 3.4 Virtual Threads: Mülakat Soruları ve Performans Karşılaştırmaları
Java 21 Virtual Threads'i Spring Boot 3.4 ile öğrenin: 15 mülakat sorusu, performans karşılaştırmaları ve teknik mülakatlarda fark yaratacak göç desenleri.

2026'da Spring Boot Gözlemlenebilirlik: OpenTelemetry, Dağıtık İzleme ve Mülakat Soruları
OpenTelemetry ve Micrometer Tracing ile Spring Boot gözlemlenebilirliği hakkında kapsamlı rehber. Dağıtık izleme kurulumu, Observation API, OTLP export yapılandırması ve teknik mülakat hazırlığı.