GraphQL'de N+1 Problemi ve DataLoader ile Çözümü
GraphQL'in esnek sorgulama gücü, beraberinde sinsi bir performans tuzağını da getirir: N+1 problemi. Bir istemci, ilişkili verileri tek bir sorguda isteyebilir; ancak sunucu, her bir üst kayıt için ayrı bir veritabanı sorgusu çalıştırırsa, 100 kayıtlık bir liste 101 sorguya dönüşür. İşte DataLoader, bu sorunu toplu yükleme ve önbellekleme ile çözen standart araçtır.
1. N+1 Problemi Nedir ve GraphQL'de Neden Belirgindir?
N+1 problemi, bir liste sorgusunun ardından her bir öğe için ayrı bir ilişkili veri sorgusu çalıştırılmasıdır. Örneğin, 10 gönderi ve her gönderinin yazarını getiren bir GraphQL sorgusu düşünün:
graphql
{
posts {
id
title
author { name }
}
}
Eğer posts alanı 10 kayıt dönerse ve her author alanı yazarı ayrı bir sorguyla çekerse, toplam 11 sorgu çalışır: 1 sorgu gönderiler için, 10 sorgu yazarlar için . Gönderi sayısı arttıkça sorgu sayısı da doğrusal olarak artar; bu, veritabanı bağlantılarını, CPU'yu ve yanıt süresini olumsuz etkiler .
GraphQL'de bu durum daha belirgindir çünkü her alan (field) bağımsız bir resolver (çözümleyici) tarafından çözülür ve GraphQL motoru, resolver'lar arasında otomatik bir koordinasyon veya toplu sorgu mekanizması sunmaz .
2. DataLoader: Toplu Yükleme ve Önbellekleme
DataLoader, Facebook tarafından geliştirilen ve GraphQL ekosisteminde standart hale gelen bir yardımcı kütüphanedir. İki temel teknikle çalışır :
-
Batching (Toplu Yükleme): Aynı anda gelen
.load(id)çağrılarını toplar ve tek birbatchLoadFn(keys)çağrısına dönüştürür. Böylece N ayrı sorgu yerine 1 toplu sorgu çalışır. -
Caching (Önbellekleme): İstek ömrü boyunca aynı anahtar için yapılan tekrarlı
.load(id)çağrılarını önbellekten yanıtlar; veritabanına tekrar gitmez.
Kritik Kural: DataLoader örnekleri her istek için ayrı ayrı oluşturulmalıdır (request-scoped). Global bir örnek kullanmak, kullanıcılar arasında veri sızıntısına yol açar .
3. .NET'te DataLoader: Hot Chocolate ve GreenDonut
.NET ekosisteminde GraphQL için en yaygın kullanılan sunucu olan Hot Chocolate, DataLoader implementasyonu olarak GreenDonut kütüphanesini içerir . GreenDonut, Facebook'un DataLoader'ının C# portudur ve BatchDataLoader, GroupedDataLoader gibi hazır sınıflar sunar.
A. BatchDataLoader (Bire-Bir İlişkiler)
Her anahtar için tek bir değer dönen ilişkilerde (ör. Post → Author) kullanılır.
csharp
public class AuthorDataLoader : BatchDataLoader<int, Author>
{
private readonly IAuthorRepository _repository;
public AuthorDataLoader(
IAuthorRepository repository,
IBatchScheduler batchScheduler,
DataLoaderOptions options)
: base(batchScheduler, options)
{
_repository = repository;
}
protected override async Task<IReadOnlyDictionary<int, Author>> LoadBatchAsync(
IReadOnlyList<int> keys,
CancellationToken cancellationToken)
{
// Tek sorgu: WHERE Id IN (keys)
var authors = await _repository.GetByIdsAsync(keys, cancellationToken);
return authors.ToDictionary(a => a.Id);
}
}
Resolver'da Kullanım:
csharp
[ExtendObjectType(typeof(Post))]
public class PostResolvers
{
public Task<Author> GetAuthor(
[Parent] Post post,
AuthorDataLoader loader,
CancellationToken ct)
{
return loader.LoadAsync(post.AuthorId, ct);
}
}
Bu yapılandırmayla, 10 gönderinin yazarı için 1 toplu sorgu çalışır; 10 ayrı sorgu değil.
B. GroupedDataLoader (Bire-Çok İlişkiler)
Bir anahtarın birden fazla değere bağlı olduğu ilişkilerde (ör. Bike → BikeParts) kullanılır .
csharp
public class BikePartsByBikeIdDataLoader : GroupedDataLoader<int, BikePart>
{
protected override async Task<ILookup<int, BikePart>> LoadGroupedBatchAsync(
IReadOnlyList<int> keys,
CancellationToken cancellationToken)
{
// Tek sorgu: WHERE BikeId IN (keys)
var parts = await _repository.GetByBikeIdsAsync(keys, cancellationToken);
return parts.ToLookup(p => p.BikeId);
}
}
Gerçek bir projede bu geçiş, 100 bisiklet için sorgu sayısını 201+'dan 3'e düşürmüş ve karmaşıklığı O(N)'den O(1)'e indirmiştir .
4. DataLoader Kullanırken Sık Yapılan Hatalar
| Hata | Sonuç | Çözüm |
|---|---|---|
| Global (singleton) loader | Kullanıcılar arası veri sızıntısı | Her istek için yeni loader |
.load() çağrılarını tek tek await etmek |
Toplu yükleme gerçekleşmez | Task.WhenAll(ids.Select(id => loader.LoadAsync(id))) kullanın |
| Batch fonksiyonunda anahtar sırasını korumamak | Yanlış eşleme | Sonuçları anahtar sırasına göre döndürün |
| Resolver'ların bir kısmında loader, bir kısmında doğrudan sorgu | Parçalı batch'ler, düşük performans | Tüm ilişkili resolver'larda tutarlı loader kullanın |
5. DataLoader Her Zaman Yeterli mi? Alternatifler
DataLoader, resolver seviyesinde N+1'i çözer; ancak tek bir set tabanlı SQL sorgusu (JOIN) veya ORM eager loading (Include) bazı durumlarda daha basit ve daha hızlı olabilir . Eğer erişim deseni öngörülebilirse, veriyi tek sorguda çekmek, resolver'lara loader eklemekten daha temizdir. DataLoader, özellikle ilişkiler dinamikse ve sorgu şekli istemciye göre değişiyorsa değerlidir.
Sonuç
GraphQL'de N+1 problemi, esnek sorgulamanın doğal bir yan etkisidir ve göz ardı edildiğinde veritabanını sessizce çökertebilir. DataLoader, toplu yükleme ve istek bazlı önbellekleme ile bu sorunu zarifçe çözer. .NET'te Hot Chocolate/GreenDonut, BatchDataLoader ve GroupedDataLoader ile bu çözümü standartlaştırır. Unutmayın: DataLoader'ı doğru kullanmanın anahtarı, her istek için yeni örnek oluşturmak ve tüm ilişkili resolver'larda tutarlı biçimde kullanmaktır; yarım uygulanan bir DataLoader, hiç kullanmamaktan daha kötü sonuçlar doğurabilir.