Ders 28 / 34
Çözücüler
Çözücünün dört girdisi, varsayılan çözücünün ne yaptığı, yürütmenin düzey düzey ilerlemesi ve bir sorgunun ürettiği çözücü çağrısı ile veri kaynağı turu sayısının ölçülmesi.
İçindekiler
Çalıştırıcı her alan için bir çözücü çağırdı ama çözücülerin nasıl yazıldığı konuşulmadı.
Ödünç kaydının uye alanı için ayrı bir çözücü yazılmıştı, id alanı için yazılmamıştı;
ikisi arasındaki fark, GraphQL yürütmesinin belkemiğidir.
Bu ders çözücü imzasını tanımlar, varsayılan çözücünün ne yaptığını gösterir ve bir sorgunun kaç çözücü çağrısı ürettiğini sayar. Son sayı, sonraki dersin konusunu doğuracak.
Çözücünün Dört Girdisi
Çözücü, tek bir alanın değerini üreten işlevdir ve dört girdi alır.
- Kaynak, üst düzeyin döndürdüğü değerdir.
Odunc.uyeçözücüsünün kaynağı, bir üst düzeydeki ödünç kaydıdır. Kök alanlarda kaynak yoktur. - Argümanlar, alanın argümanlarıdır; değişkenler bu noktada çözülmüştür, çözücü
$iddiye bir şey görmez. - Bağlam, istek boyunca ortak olan veridir: kimlik bilgisi, veritabanı bağlantısı, istek başına önbellek. Alanlar arasında taşınan tek yatay kanal budur.
- Bilgi, çağrının nerede yapıldığını söyler: hangi alan, hangi üst tip, yanıttaki hangi yol.
Çalıştırıcının bu dördünü nasıl ürettiği aşağıda görünür. Yeni olan iki şey var: bilgi
nesnesi ve her çağrının bildirildiği izleyici kancası.
// calistirici.mjs — denetlenmis bir belgeyi calistirir // Her alan icin bir cozucu cagrilir; cozucu dort girdi alir: // kaynak ust duzeyin dondurdugu deger // argumanlar alanin argumanlari (degiskenler cozulmus) // baglam istek boyunca ortak veri // bilgi { alan, ustTip, yol } — cagriyi yerinde tanimlar import { tipCoz, alanTanimi, olasiTipler } from "./sema.mjs"; const YAPRAK = new Set(["skaler", "sirali"]); // Degisken bildirimlerini gelen degerlerle birlestirir. function degiskenleriCoz(islem, gelen) { const d = {}; for (const b of islem.degiskenler) { const v = gelen[b.ad] ?? b.varsayilan; if (v === undefined && b.tip.endsWith("!")) throw new Error(`$${b.ad} değişkeni zorunlu`); d[b.ad] = v; } return d; } const argCoz = (argumanlar, dv) => Object.fromEntries(Object.entries(argumanlar).map(([a, d]) => [a, d.tur === "degisken" ? dv[d.ad] : d.deger])); // Parcalari ve satir ici parcalari acar; somut tipe uymayan dallari atar. function duzle(sema, belge, tipAdi, secim, birikim = []) { for (const s of secim) { if (s.tur === "alan") { birikim.push(s); continue; } const parca = s.tur === "parcaYayimi" ? belge.parcalar[s.ad] : s; const kosul = s.tur === "parcaYayimi" ? parca.tip : s.tip; if (kosul === tipAdi || olasiTipler(sema, kosul).includes(tipAdi)) duzle(sema, belge, tipAdi, parca.secim, birikim); } return birikim; } async function secimCalistir(o, tipAdi, secim, kaynak, yol, sirali = false) { const alanlar = duzle(o.sema, o.belge, tipAdi, secim); const sonuc = {}; for (const a of alanlar) sonuc[a.takma] = null; // anahtar sirasi sorgudan gelir const isle = async (a) => { sonuc[a.takma] = await alanCalistir(o, tipAdi, a, kaynak, `${yol}.${a.takma}`); }; if (sirali) for (const a of alanlar) await isle(a); else await Promise.all(alanlar.map(isle)); return sonuc; } async function alanCalistir(o, tipAdi, alan, kaynak, yol) { const tanim = alanTanimi(o.sema.tipler[tipAdi], alan.ad); const ozel = o.cozucular[tipAdi]?.[alan.ad]; const cozucu = ozel ?? ((k) => k?.[alan.ad]); // varsayilan cozucu: ayni adli alani okur const bilgi = { alan: alan.ad, ustTip: tipAdi, yol, varsayilan: !ozel }; o.izleyici?.(bilgi); const deger = await cozucu(kaynak, argCoz(alan.argumanlar, o.dv), o.baglam, bilgi); return tamamla(o, tanim.tip, deger, alan.secim, yol); } async function tamamla(o, tipDizgi, deger, secim, yol) { if (deger === null || deger === undefined) return null; const t = tipCoz(tipDizgi); if (t.liste) return Promise.all(deger.map((d, i) => tamamla(o, t.ad, d, secim, `${yol}[${i}]`))); const tip = o.sema.tipler[t.ad]; if (YAPRAK.has(tip.tur)) return deger; // Arayuz ve birlesimde somut tip cozuculerden sorulur. const somut = tip.tur === "nesne" ? t.ad : o.cozucular.__tip[t.ad](deger); return secimCalistir(o, somut, secim, deger, yol); } export async function calistir(sema, cozucular, belge, { islemAdi, degiskenler = {}, baglam = {}, izleyici } = {}) { const islem = islemAdi ? belge.islemler.find((i) => i.ad === islemAdi) : belge.islemler[0]; const o = { sema, cozucular, belge, dv: degiskenleriCoz(islem, degiskenler), baglam, izleyici }; const kok = islem.tur === "mutation" ? sema.mutasyon : sema.sorgu; return { data: await secimCalistir(o, kok, islem.secim, null, "", islem.tur === "mutation") }; }
Varsayılan Çözücü
alanCalistir içindeki tek satır belirleyicidir: yazılmış bir çözücü yoksa çalıştırıcı
(k) => k?.[alan.ad] işlevini kullanır. Bu varsayılan çözücüdür ve kaynaktan aynı adlı
alanı okur.
Ödünç kaydının id, durum ve iadeTarihi alanları için çözücü yazılmamıştır, çünkü
veri kaynağından gelen nesne bu alanları zaten aynı adlarla taşır. uye alanı için
yazılmıştır, çünkü kayıtta yalnız uyeId vardır ve üye nesnesine dönüşmesi bir arama
gerektirir.
Kural şudur: çözücü yalnız veri şekli ile şema şekli ayrıştığında yazılır. Bu, veri kaynağının şemaya benzemesi hâlinde çözücü sayısını çarpıcı biçimde azaltır ve şemayı veri kaynağının bir kopyası olmaya iter — dikkat edilmesi gereken bir eğilimdir, çünkü şema tüketicinin gördüğü arayüzdür, deponun değil.
Aşağıdaki veri kaynağı her erişimi ayrıca sayar; böylece çözücü çağrısı ile veri kaynağı turu birbirinden ayırt edilebilir.
// veri.mjs — bellek ici veri; her erisim sayilir export const sayac = { kitap: 0, uye: 0, odunc: 0 }; const KITAPLAR = new Map([ ["978-0262033848", { isbn: "978-0262033848", baslik: "Algoritmalara Giris", yazar: "Cormen", kopya: 1 }], ["978-0201896831", { isbn: "978-0201896831", baslik: "Programlama Sanati", yazar: "Knuth", kopya: 2 }], ["978-0131103627", { isbn: "978-0131103627", baslik: "C Programlama Dili", yazar: "Kernighan", kopya: 3 }], ]); const UYELER = new Map([ ["U-1001", { id: "U-1001", olusturma: "2024-02-11", ad: "Ayse Yilmaz" }], ["U-1002", { id: "U-1002", olusturma: "2025-06-03", ad: "Deniz Kaya" }], ]); const ODUNCLER = new Map([ ["O-1", { id: "O-1", olusturma: "2026-01-04", uyeId: "U-1001", durum: "ACIK", iadeTarihi: "2026-03-20", kalemler: [{ isbn: "978-0262033848", sube: "merkez" }, { isbn: "978-0201896831", sube: "sahil" }] }], ["O-2", { id: "O-2", olusturma: "2026-01-19", uyeId: "U-1002", durum: "ACIK", iadeTarihi: "2026-04-02", kalemler: [{ isbn: "978-0131103627", sube: "tepe" }] }], ["O-3", { id: "O-3", olusturma: "2026-02-02", uyeId: "U-1001", durum: "KAPALI", iadeTarihi: "2026-02-28", kalemler: [{ isbn: "978-0201896831", sube: "merkez" }] }], ]); // Tek kayit getiren erisimler: her cagri bir "veri kaynagi turu" sayilir. export const kitapGetir = (isbn) => { sayac.kitap++; return KITAPLAR.get(isbn); }; export const uyeGetir = (id) => { sayac.uye++; return UYELER.get(id); }; export const oduncListesi = (uyeId) => { sayac.odunc++; return [...ODUNCLER.values()].filter((o) => !uyeId || o.uyeId === uyeId); }; export const oduncGetir = (id) => { sayac.odunc++; return ODUNCLER.get(id); }; export const sifirla = () => { sayac.kitap = 0; sayac.uye = 0; sayac.odunc = 0; }; export const COZUCULER = { __tip: { Kayit: (d) => (d.kalemler ? "Odunc" : "Uye") }, Sorgu: { odunc: (_, a) => oduncGetir(a.id) ?? null, oduncler: () => oduncListesi(), uye: (_, a) => uyeGetir(a.kimlik) ?? null, }, // id, olusturma, durum, iadeTarihi icin cozucu yok: varsayilan cozucu isini gorur. Odunc: { uye: (o) => uyeGetir(o.uyeId) }, Kalem: { kitap: (k) => kitapGetir(k.isbn) }, Uye: { oduncler: (u) => oduncListesi(u.id) }, };
Yürütmenin Biçimi
// kosu.mjs — cozucu cagrilarini izler, sirasini gosterir ve sayar import { ayristir } from "./ayristirici.mjs"; import { calistir } from "./calistirici.mjs"; import { SEMA } from "./sema.mjs"; import { COZUCULER, sayac, sifirla } from "./veri.mjs"; async function izle(baslik, sorgu, ayrintili) { const iz = []; sifirla(); const sonuc = await calistir(SEMA, COZUCULER, ayristir(sorgu), { izleyici: (b) => iz.push(b) }); console.log(`\n=== ${baslik} ===`); if (ayrintili) { for (const b of iz) console.log(` ${(b.ustTip + "." + b.alan).padEnd(16)}${b.varsayilan ? "varsayılan" : "yazılmış "} ${b.yol}`); } else { // Derinlige gore grupla: derinlik, cagri sayisi, hangi alanlar const duzey = new Map(); for (const b of iz) { const d = b.yol.split(".").length - 1; if (!duzey.has(d)) duzey.set(d, []); duzey.get(d).push(`${b.ustTip}.${b.alan}`); } console.log(" derinlik çağrı alanlar"); for (const [d, liste] of [...duzey].sort((a, b) => a[0] - b[0])) { console.log(` ${String(d).padStart(8)} ${String(liste.length).padStart(5)} ${[...new Set(liste)].join(", ")}`); } } const ozel = iz.filter((b) => !b.varsayilan).length; console.log(` toplam çözücü çağrısı: ${iz.length} (yazılmış ${ozel}, varsayılan ${iz.length - ozel})`); console.log(` veri kaynağı turu: kitap=${sayac.kitap} üye=${sayac.uye} ödünç=${sayac.odunc} toplam=${sayac.kitap + sayac.uye + sayac.odunc}`); return sonuc; } await izle("küçük sorgu", `{ oduncler { id uye { ad } } }`, true); const sonuc = await izle("büyük sorgu", `{ oduncler { id durum uye { ad } kalemler { sube kitap { baslik yazar } } } }`, false); console.log(`\nilk kayıt: ${JSON.stringify(sonuc.data.oduncler[0])}`);
=== küçük sorgu ===
Sorgu.oduncler yazılmış .oduncler
Odunc.id varsayılan .oduncler[0].id
Odunc.uye yazılmış .oduncler[0].uye
Odunc.id varsayılan .oduncler[1].id
Odunc.uye yazılmış .oduncler[1].uye
Odunc.id varsayılan .oduncler[2].id
Odunc.uye yazılmış .oduncler[2].uye
Uye.ad varsayılan .oduncler[0].uye.ad
Uye.ad varsayılan .oduncler[1].uye.ad
Uye.ad varsayılan .oduncler[2].uye.ad
toplam çözücü çağrısı: 10 (yazılmış 4, varsayılan 6)
veri kaynağı turu: kitap=0 üye=3 ödünç=1 toplam=4
=== büyük sorgu ===
derinlik çağrı alanlar
1 1 Sorgu.oduncler
2 12 Odunc.id, Odunc.durum, Odunc.uye, Odunc.kalemler
3 11 Uye.ad, Kalem.sube, Kalem.kitap
4 8 Kitap.baslik, Kitap.yazar
toplam çözücü çağrısı: 32 (yazılmış 8, varsayılan 24)
veri kaynağı turu: kitap=4 üye=3 ödünç=1 toplam=8
ilk kayıt: {"id":"O-1","durum":"ACIK","uye":{"ad":"Ayse Yilmaz"},"kalemler":[{"sube":"merkez","kitap":{"baslik":"Algoritmalara Giris","yazar":"Cormen"}},{"sube":"sahil","kitap":{"baslik":"Programlama Sanati","yazar":"Knuth"}}]}
Küçük sorgunun izi yürütmenin biçimini doğrudan gösteriyor. Önce kök alan çözülür. Sonra üç
ödünç kaydının hepsinin id ve uye alanları çözülür. Ancak bunlar bittikten sonra
Uye.ad çağrıları başlar.
Yürütme derinlik değil düzey sırasıyla ilerler. Nedeni secimCalistir içindeki
Promise.all çağrısıdır: bir düzeydeki bütün kardeş alanlar birlikte başlatılır ve hepsi
bitmeden bir alt düzeye geçilmez. Bu, sonraki dersteki toplu getirmenin dayanacağı
özelliktir: aynı düzeydeki çağrılar zaten aynı anda yapılıyorsa, birleştirilebilirler.
İki kısıt bu sırayı zorunlu kılar. Bir çözücünün kaynağı üst düzeyin döndürdüğü değerdir; üst çözülmeden alt çözülemez. Ve kardeş alanlar arasında bir sıra güvencesi yoktur; sorgu alanları bağımsız kabul edildiği için birbirlerinin sonucuna dayanamazlar.
İki Ayrı Sayı
Büyük sorgu 32 çözücü çağrısı üretti ama veri kaynağına yalnız 8 kez gidildi. İki sayı ayrıdır ve ayrı şeyleri ölçer.
Çözücü çağrısı sayısı sorgunun biçiminden gelir: seçilen alan sayısı ile listelerin uzunluğunun çarpımı. Yirmi dördü varsayılan çözücüdür; bunlar bellekteki bir nesneden alan okur, maliyetleri yok sayılabilir.
Veri kaynağı turu sayısı ise yazılmış çözücülerden gelir ve asıl maliyet buradadır. Sekiz turun dağılımı dikkat çekicidir: bir ödünç listesi çağrısı, üç üye çağrısı, dört kitap çağrısı. Oysa veritabanında yalnız iki ayrı üye ve üç ayrı kitap vardır.
Aynı üye iki ödünç kaydında geçtiği için iki kez getirilmiş, aynı kitap iki kalemde geçtiği için iki kez getirilmiştir. Liste uzadıkça bu sayı listeyle birlikte büyür: yüz ödünç kaydı yüz üye çağrısı demektir, üyelerin çoğu aynı kişi olsa bile.
Özet
- Çözücü dört girdi alır: üst düzeyin döndürdüğü kaynak, çözülmüş argümanlar, istek boyunca ortak bağlam ve çağrının yerini bildiren bilgi.
- Varsayılan çözücü kaynaktan aynı adlı alanı okur; çözücü yalnız veri şekli ile şema şekli ayrıştığında yazılır.
- Yürütme derinlik değil düzey sırasıyla ilerler: bir düzeydeki bütün kardeş alanlar birlikte başlatılır, hepsi bitmeden alt düzeye geçilmez.
- Sıra iki kısıttan doğar: alt düzey üstün döndürdüğü değere muhtaçtır ve kardeş alanlar birbirinin sonucuna dayanamaz.
- Çözücü çağrısı sayısı sorgunun biçiminden, veri kaynağı turu sayısı yazılmış çözücülerden gelir; ikisi ayrı ölçülerdir.
- Aynı üye ve aynı kitap birden çok kez getirilir; bu sayı liste uzunluğuyla birlikte büyür.
Sonraki Adım
Son paragraftaki sayı bir sorunun adıdır. Üç ödünç kaydı için üç üye çağrısı yapıldı, oysa iki ayrı üye vardı; dört kitap çağrısı yapıldı, oysa üç ayrı kitap vardı. Liste yüz kayda çıktığında yüz bir tur olacak: bir liste sorgusu artı her kayıt için bir tane. Sonraki ders bu sorunu adıyla koyar, aynı düzeyde yapılan çağrıların birlikte başlatıldığı gerçeğine dayanan bir toplu getirme katmanı yazar ve tur sayısının kaça düştüğünü ölçer.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.