2026'da Spring Data JPA N+1 Sorgu Çözümleri: Fetch Join ve EntityGraph
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.
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.
Çö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.
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.
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 Data JPA 3.x, EntityGraph yönetimi ve optimize edilmiş sorgular için iyileştirmeler sunar.
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.
@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.
Sonuç
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.
N+1 Önleme Kontrol Listesi:
- ✅ Ç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
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 loglama: Logback ve JSON ile üretim ortamında yapılandırılmış loglar
Spring Boot yapılandırılmış loglama için kapsamlı rehber. Logback JSON yapılandırması, izleme için MDC, üretimde en iyi uygulamalar ve ELK Stack entegrasyonu.