GraphQL resolver yazımı: argümanlar, asenkron akış ve DataLoader ile N+1 çözümü
Beceri rehberi

GraphQL resolver yazımı: argümanlar, asenkron akış ve DataLoader ile N+1 çözümü

Hazırlayan: Mindset Institute EditörlüğüGüncellendi: 5 dk okuma

GraphQL resolver nasıl yazılır: parent, args, context ve info argümanları, asenkron çalışma, N+1 sorununun nedeni ve DataLoader ile gruplama çözümü.

Resolver ne zaman çalışır ve ne üretir?

GraphQL şemasındaki her alanın arkasında, sunucu geliştiricisinin yazdığı bir resolver (çözümleyici) fonksiyonu vardır. Bir alan çalıştırılacağı zaman o alanın resolver'ı çağrılır ve bir sonraki değeri üretir. Alan skaler ya da enum döndürüyorsa o dal tamamlanır; nesne döndürüyorsa sorgudaki iç içe seçim bu nesne üzerinde devam eder ve yaprak değerlere ulaşılana kadar sürer.

Yürütme kök alanlardan başlar: Query, Mutation ya da Subscription alanları. Kök alanın üst nesnesi olmadığı için resolver genellikle argümanlara ve bağlama bakarak veri çeker. Alt alanlarda ise resolver'a önceki nesne verilir.

Resolver'ın argümanları: parent, args, context, info

Referans uygulamada bir resolver şu argümanları alır: • obj (parent): bir önceki alanın döndürdüğü nesne. Kök Query alanlarında çoğunlukla kullanılmaz. • args: alana sorguda verilen argümanlar; örneğin order(id: ...) çağrısındaki id. • context: her resolver'a aynı biçimde aktarılan değer. Oturumdaki kullanıcı, veritabanı bağlantısı, veri yükleyiciler gibi istek düzeyindeki bilgi buradan taşınır. • info: sorgu ve şema hakkındaki alan bazlı bilgi; ileri düzey durumlar dışında nadiren gerekir.

Örnek bir resolver haritası:

const resolvers = { Query: { order: (parent, args, context) => context.db.orders.findById(args.id), }, Order: { customer: (order, args, context) => context.db.customers.findById(order.customerId), }, };

Burada Query.order kök alandır ve args ile context kullanır. Order.customer ise parent olarak gelen siparişin müşteri kimliğini kullanır.

Varsayılan resolver ve asenkron çalışma

Sunucu kütüphaneleri, bir alan için resolver yazılmadıysa parent nesnede aynı isimli özelliği okuyup döndürür. Dolayısıyla Order.status gibi doğrudan veri alanları için resolver yazmanız gerekmez. Resolver'ı yalnızca değer hesaplanıyor, başka bir kaynaktan geliyor ya da dönüştürülüyorsa yazın.

Resolver Promise döndürebilir; GraphQL bu Promise'i bekler ve alt alanlara ondan sonra geçer. Kardeş alanlar eşzamanlı çözülebilir; bir nesnenin birkaç alanı aynı anda hesaplanır. Sorgu alanları eşzamanlı çalışabilirken mutation alanları yan etki taşıdığı için sırayla yürütülür. Bu yüzden bir istekte birden fazla mutation gönderiyorsanız sıranın korunduğunu varsayabilirsiniz.

Liste döndüren bir alanda her öğe ayrı ayrı çözülür; öğelerin alt alanları da tek tek resolver'dan geçer. Sorunun kökü tam burada başlar.

Resolver'ları test etmek de kolaydır: resolver sıradan bir fonksiyon olduğundan, parent, args ve sahte bir context vererek doğrudan çağırıp dönen değeri doğrulayabilirsiniz. Veritabanı yerine context içine koyduğunuz sahte bir veri erişim nesnesi, hem testi hızlandırır hem de toplu fonksiyonun doğru anahtarlarla çağrıldığını denetlemenizi sağlar.

N+1 sorunu nasıl oluşur?

Örneğin bir sorgu siparişlerin listesini ve her siparişin müşteri adını istesin. Sipariş listesi için bir veritabanı sorgusu çalışır. Sonra liste içindeki her siparişte Order.customer resolver'ı ayrı bir müşteri sorgusu çalıştırır. Sipariş sayısı kadar ek sorgu eklenir; yani bir liste sorgusu artı sipariş sayısı kadar sorgu. Buna N+1 sorunu denir.

Sorun, geliştirme veritabanında az kayıtla fark edilmez; üretimde liste büyüdükçe yanıt süresi ve veritabanı yükü artar. Belirtisi, sorgu günlüğünde aynı tablonun aynı biçimde art arda çok sayıda sorgulanmasıdır. Tanılamak için bir istek sırasında çalışan sorguları sayın.

DataLoader ile gruplama ve önbellekleme

DataLoader, veri çekme katmanında kullanılan genel bir yardımcıdır. Gruplama (batching) ve önbellek sağlar. Gruplama: olay döngüsünün tek bir turunda yapılan tek tek load çağrılarını birleştirip toplu fonksiyonunuzu bir kez çalıştırır. Önbellek tarafında ise aynı anahtarı aynı istek içinde tekrar yüklemek toplu fonksiyona yeniden gitmez.

Toplu fonksiyonun sözleşmesi katıdır: anahtarlar dizisi alır, aynı uzunlukta ve aynı sırada bir değerler dizisi döndürür. Veritabanı sonucu sırasız dönse bile her değer kendi anahtarının indeksinde olmalıdır; bulunamayan kayıt için null ya da hata verilir.

const customerLoader = new DataLoader(async (ids) => { const rows = await db.customers.findByIds(ids); return ids.map((id) => rows.find((r) => r.id === id) || null); });

Resolver artık şöyle yazılır:

Order: { customer: (order, args, context) => context.loaders.customer.load(order.customerId), },

Listedeki bütün siparişlerin müşteri istekleri tek bir toplu sorguda birleşir.

Yükleyiciyi her istek için yeniden oluşturun

DataLoader önbelleği yalnızca tek bir istek süresince geçerli olmalıdır. Farklı kullanıcıların yetkileri farklı olabilir; ortak bir yükleyici paylaşırsanız bir kullanıcının görebildiği veri başka bir kullanıcıya sızabilir. Bu yüzden yükleyiciler her istek başında context nesnesi oluşturulurken yaratılır ve istek bitince atılır. DataLoader, Redis gibi paylaşımlı bir önbelleğin yerine geçmez; amacı istek içi tekrarı ve sorgu sayısını azaltmaktır.

Federasyonlu mimarilerde de aynı mantık geçerlidir: bir varlığı çözen referans resolver'ı bir sorguda çok kez çağrılabilir, bu yüzden orada da gruplama gerekir.

Resolver yazarken sık yapılan hatalar

  • Her alanda veritabanına gitmek: ilişkili veriyi parent ile birlikte çekebiliyorsanız (join) bunu kök resolver'da yapın, yalnızca gerçekten gereken alanlarda ek sorgu çalıştırın.
  • Yetki kontrolünü yalnızca kök alanda yapmak: iç içe alanlar başka yollardan da erişilebilir; kritik verilerde alan düzeyinde kontrolü ya da iş mantığı katmanında kontrolü değerlendirin.
  • Resolver'a iş mantığını gömmek: resolver'ı ince tutun; iş kuralları ayrı servis katmanında dursun, test edilebilir olsun.
  • Toplu fonksiyonda sıra ve uzunluk sözleşmesini bozmak: yanlış kullanıcıya yanlış veri döner.
  • Hataları yutmak: istemcinin hatayı görmesi gerekiyorsa uygun hata ya da şemada ifade edilmiş sonuç tipi döndürün.
  • Derin sorgulara sınır koymamak: iç içe sorgular kontrolsüz derinleşebilir; derinlik ve karmaşıklık sınırı performans ve güvenlik için gerekir.

Bu beceriyi Mindset programlarında nerede çalışırsınız?

GraphQL ile API Geliştirme eğitimi, REST deneyimi olup GraphQL sunucusu yazacak arka yüz geliştiricilerine yöneliktir; Çözümleyici Geliştirme ve Performans ve Güvenlik modülleri, bu sayfadaki resolver yazımı, N+1 sorunu ve sorgu sınırlandırma konularının çalışıldığı yerlerdir. Apollo GraphQL ile İstemci ve Sunucu Geliştirme eğitimi, Apollo Server ile sunucu geliştirmeyi kapsar ve sunucu tarafında çalışacak ekipler için uygundur.

Takımlarda yükleyici ve context kurulumunun ortak bir şablonda toplanması, her geliştiricinin aynı hatayı yeniden yapmasını önler.

İlgili Mindset programı

GraphQL ile API Geliştirme

Sık sorulanlar

Sık sorulan sorular

Her alan için resolver yazmak zorunda mıyım?

Hayır. Resolver tanımlamadığınız alanlar için kütüphane, parent nesnede aynı isimli özelliği okuyup döndürür. Resolver'ı yalnızca değeri hesaplamanız, başka bir kaynaktan almanız ya da dönüştürmeniz gerektiğinde yazın. Örneğin kullanıcı nesnesindeki ad alanı için gerek yoktur; ad ile soyadı birleştiren tam ad alanı için gerekir.

DataLoader'ı global olarak tek sefer oluşturabilir miyim?

Oluşturmamalısınız. DataLoader istek başına oluşturulmalıdır; çünkü önbelleği kullanıcıya özel yetkilere duyarlı veriyi tutabilir. Global bir yükleyici bir kullanıcının görebildiği veriyi başka bir kullanıcıya sızdırabilir ve bayat veri tutabilir. Yaygın kalıp, her istekte yükleyicileri oluşturup context nesnesine koymaktır.

N+1 sorununu yalnızca DataLoader mı çözer?

Hayır. İlişkili veriyi kök resolver'da tek sorguda (join ya da toplu sorgu) çekmek de çözümdür. DataLoader, alt alan resolver'larının bağımsız çağrılarını otomatik gruplamak istediğinizde ve veri kaynağı tek tek isteklere dayalı olduğunda uygundur.

Mutation'lar neden sırayla çalışır?

Mutation yan etki taşır; sıra sonucu etkileyebilir. Bu yüzden GraphQL mutation alanlarını sırayla yürütür, sorgu alanları ise eşzamanlı çözülebilir. Örneğin aynı istekte önce sepete ürün ekleyip sonra ödemeyi başlatan iki mutation varsa ikincisi birincinin tamamlanmasını bekler.

GraphQL Resolver Yazma ve N+1 Sorununu Çözme