İçeriğe geç
academia.sh

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ü $id diye 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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat