N+1 Problemi

Java dünyasında performansın en büyük düşmanlarından biri olan N+1 Problemi, aslında veri tabanı ile kurduğumuz iletişimin verimsiz yönetilmesinden kaynaklanır. Paylaştığın özet, bu sorunu ve çözüm yollarını harika bir şekilde açıklıyor.

Sektör standartlarına uygun, ölçeklenebilir ve sürdürülebilir bir yapı kurmak adına bu konuyu biraz daha derinleştirelim:


N+1 Problemi Nedir?

N+1 sorunu; sistemin ana nesne için 1 sorgu attıktan sonra, bu nesneye bağlı her bir alt ilişkili (child) veri için ayrı ayrı N adet sorgu daha çalıştırmasıdır. Eğer 1.000 kayıt çekiyorsanız, arka planda 1.001 sorgu çalışması uygulamanızın yanıt süresini (latency) dramatik şekilde artırır.

Stratejik Yaklaşımlar ve Çözümler

Kurumsal seviyede bir Java projesinde nasıl daha “bakımı kolay” (maintainable) hale getirebiliriz?

1. JOIN FETCH (JPQL Kullanımı)

En kesin ve performanslı çözümdür. INNER JOIN veya LEFT JOIN kullanarak veriyi tek seferde çeker.

  • Avantajı: SQL düzeyinde kontrol sağlar.
  • Dikkat: Birden fazla Collection (örneğin hem orders hem de addresses) için aynı anda JOIN FETCH kullanmak, Cartesian Product (kartezyen çarpım) hatasına ve bellek sorunlarına yol açabilir.

2. @EntityGraph

Daha deklaratif bir yaklaşımdır. Sorguyu yazarken değil, ihtiyaca göre hangi alanların yükleneceğini belirtmenizi sağlar.

  • Avantajı: Dinamik olarak farklı “fetch planları” oluşturmanıza imkan tanır.
  • Ölçeklenebilirlik: @NamedEntityGraph kullanarak bu tanımları merkezi bir yerde (Entity üzerinde) tutabilir, kodun okunabilirliğini artırabilirsiniz.

3. Batch Fetching (Performans Sigortası)

Eğer her senaryoda JOIN FETCH yazmak istemiyorsanız, Hibernate’in Batch Size özelliğini kullanabilirsiniz.

Properties

spring.jpa.properties.hibernate.default_batch_fetch_size=20

spring.jpa.show-sql=true

Bu ayar sayesinde, N adet sorgu yerine veriler 20’şerli gruplar halinde çekilir. Bu, N+1’i tamamen yok etmese de etkisini ciddi oranda azaltır.


Altın Kurallar

Sürdürülebilir bir mimari için şu prensipler izlenmelidir:

DurumTercih Edilen StratejiNeden?
Varsayılan Fetch TipiLAZYBellek kullanımını optimize eder ve gereksiz veri çekilmesini önler.
Raporlama/Liste SayfalarıJOIN FETCHVerinin tümüne ihtiyaç duyulduğunda en hızlı yöntemdir.
Dinamik Sorgular@EntityGraphEsneklik sağlar, aynı metodun farklı ihtiyaçlara göre çalışmasına izin verir.
ManyToOne / OneToOneLAZY (Manuel)Varsayılan EAGER olduğu için her zaman (fetch = FetchType.LAZY) olarak belirtilmelidir.

E-Tablolar’a aktar

Geliştirme Süreci İçin İpucu

Performans sorunlarını canlı ortamda (Production) fark etmek yerine, test aşamasında tespit etmek için SQL loglarını takip etmek kritiktir. Ancak daha profesyonel bir yaklaşım için, unit testler sırasında sorgu sayısını doğrulayan kütüphaneler kullanarak testable bir yapı kurabilirsin.

Unutma; en hızlı kod, hiç çalıştırılmayan koddur; en hızlı sorgu ise tek seferde tüm ihtiyacı getiren sorgudur.

The trap? When you load a list of 100 customers and then access their orders in a loop:

List<Customer> customers = customerRepo.findAll(); // 1 query
for (Customer c : customers) {
c.getOrders().size(); // 100 more queries!
}
// Total: 101 SQL queries = performance disaster

Solutions that actually work:

// 1. JOIN FETCH in JPQL
@Query(“SELECT c FROM Customer c JOIN FETCH c.orders”)
List<Customer> findAllWithOrders();

// 2. @EntityGraph
@EntityGraph(attributePaths = {“orders”})
List<Customer> findAll();

Rule of thumb:
✅ Keep FetchType.LAZY as default
✅ Use JOIN FETCH or @EntityGraph when you know you need related data
✅ Enable Hibernate SQL logging to detect N+1 early
❌ Never switch everything to EAGER — that’s trading N+1 for over-fetching

Bir cevap yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

This site uses Akismet to reduce spam. Learn how your comment data is processed.