Ders 27 / 34
Sorgu, Mutasyon ve Abonelik
Üç işlem türünün yürütme kuralları, mutasyon alanlarının sıralı yürütülmesinin tek kopyalı kitap üzerinde ölçülen sonucu ve aboneliğin aynı seçim kümesini zaman içinde birden çok kez uygulaması.
İçindekiler
Şema neyin sorulabileceğini söylüyor ama yanıtın nereden geleceğini söylemiyor. Bu ders çalıştırıcıyı yazar ve GraphQL’in üç işlem türünü ayıran şeyin ne olduğunu gösterir.
Fark sözdiziminde değildir; üçü de aynı seçim kümesi dilini kullanır. Fark yürütme kurallarındadır. Sorgu alanları birbirinden bağımsız kabul edilir ve birlikte yürütülebilir. Mutasyon alanları birbirini etkileyebilir ve sırayla yürütülmek zorundadır. Abonelik tek bir yanıt değil, zaman içinde birden çok yanıt üretir.
Çalıştırıcı
Çalıştırıcı seçim kümesini gezer, her alan için bir çözücü çağırır ve dönen değeri alanın tipine göre tamamlar. Yaprak tipte değer olduğu gibi döner; bileşik tipte alt seçim kümesi aynı yordamla yürütülür.
// calistirici.mjs — denetlenmis bir belgeyi calistirir // Sorgu alanlari birlikte, mutasyon alanlari sirayla yurutulur. 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, 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); }; 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) { const tanim = alanTanimi(o.sema.tipler[tipAdi], alan.ad); const cozucu = o.cozucular[tipAdi]?.[alan.ad] ?? ((k) => k?.[alan.ad]); const deger = await cozucu(kaynak, argCoz(alan.argumanlar, o.dv), o.baglam); return tamamla(o, tanim.tip, deger, alan.secim); } async function tamamla(o, tipDizgi, deger, secim) { if (deger === null || deger === undefined) return null; const t = tipCoz(tipDizgi); if (t.liste) return Promise.all(deger.map((d) => tamamla(o, t.ad, d, secim))); 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); } export async function calistir(sema, cozucular, belge, { islemAdi, degiskenler = {}, baglam = {} } = {}) { const islem = islemAdi ? belge.islemler.find((i) => i.ad === islemAdi) : belge.islemler[0]; const o = { sema, cozucular, belge, dv: degiskenleriCoz(islem, degiskenler), baglam }; const kok = islem.tur === "mutation" ? sema.mutasyon : sema.sorgu; return { data: await secimCalistir(o, kok, islem.secim, null, islem.tur === "mutation") }; } // Abonelik: cozucu bir olay akisi dondurur, her olay ayni secim kumesiyle bicimlenir. export async function* abone(sema, cozucular, belge, { degiskenler = {}, baglam = {} } = {}) { const islem = belge.islemler[0]; const o = { sema, cozucular, belge, dv: degiskenleriCoz(islem, degiskenler), baglam }; const alan = duzle(sema, belge, sema.abonelik, islem.secim)[0]; const akis = cozucular[sema.abonelik][alan.ad](null, argCoz(alan.argumanlar, o.dv), baglam); const tanim = alanTanimi(sema.tipler[sema.abonelik], alan.ad); for await (const olay of akis) yield { data: { [alan.takma]: await tamamla(o, tanim.tip, olay, alan.secim) } }; }
calistir işlevindeki tek dallanma, işlem türünün mutasyon olup olmadığıdır ve bu dallanma
secimCalistir çağrısına sirali bayrağı olarak geçer. Kural budur: bütün fark tek bir
for döngüsü ile Promise.all arasındaki seçimdir.
Çalıştırıcının veriye ulaşması için çözücülere ihtiyacı vardır. Aşağıdaki veri kaynağı bellek içidir ve ödünç verme işlemi bilerek gerçekçi yazılmıştır: kalan kopya sayısı okunur, bir veritabanı turu kadar beklenir, sonra kayıt yazılır.
// veri.mjs — bellek ici odunc verisi ve cozuculer export 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 }], ]); export 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" }], ]); export const ODUNCLER = new Map(); let sira = 0; export const bekle = (ms) => new Promise((c) => setTimeout(c, ms)); export const olaylar = []; // yurutme sirasini gormek icin export const sifirla = () => { ODUNCLER.clear(); sira = 0; olaylar.length = 0; }; const eldeki = (isbn) => [...ODUNCLER.values()].filter((o) => o.durum === "ACIK" && o.kalemler.some((k) => k.isbn === isbn)).length; export function oduncVer(uye, isbn, gecikme = 30) { return (async () => { olaylar.push(`oduncVer(${isbn.slice(-4)}) başladı`); const kalan = KITAPLAR.get(isbn).kopya - eldeki(isbn); // OKUMA await bekle(gecikme); // veritabani turu if (kalan <= 0) { olaylar.push(`oduncVer(${isbn.slice(-4)}) bitti: kopya yok`); return null; } const kayit = { id: `O-${++sira}`, olusturma: "2026-03-01", uyeId: uye, // YAZMA kalemler: [{ isbn, sube: "merkez" }], durum: "ACIK", iadeTarihi: null }; ODUNCLER.set(kayit.id, kayit); olaylar.push(`oduncVer(${isbn.slice(-4)}) bitti: ${kayit.id}`); return kayit; })(); } export const COZUCULER = { __tip: { Kayit: (d) => (d.kalemler ? "Odunc" : "Uye"), AramaSonucu: (d) => (d.isbn ? "Kitap" : "Uye") }, Sorgu: { odunc: (_, a) => ODUNCLER.get(a.id) ?? null, uye: (_, a) => UYELER.get(a.kimlik) ?? null, kayit: (_, a) => ODUNCLER.get(a.id) ?? UYELER.get(a.id) ?? null, arama: (_, a) => [...KITAPLAR.values()].filter((k) => k.baslik.toLowerCase().includes(a.terim.toLowerCase())), }, Mutasyon: { oduncVer: (_, a) => oduncVer(a.uye, a.isbn), iadeAl: (_, a) => { const o = ODUNCLER.get(a.id); if (o) o.durum = "KAPALI"; return o ?? null; }, }, Odunc: { uye: (o) => UYELER.get(o.uyeId), kalemler: (o) => o.kalemler, }, Kalem: { kitap: (k) => KITAPLAR.get(k.isbn) }, Uye: { oduncler: (u, a) => [...ODUNCLER.values()].filter((o) => o.uyeId === u.id && (!a.durum || o.durum === a.durum)) }, };
Üç İşlem Türü Yan Yana
Şema ve Tip Sistemi dersinde kurulan şemada iki kök vardı: sorgu ve mutasyon. Abonelik üçüncü köktür ve şemaya bir tip olarak eklenir; alanları, istemcinin izleyebileceği olayları adlandırır.
// sema-abonelik.mjs — 01. dersteki semaya abonelik koku eklenmis hali import { SEMA } from "./sema.mjs"; export const SEMA_A = { ...SEMA, abonelik: "Abonelik", tipler: { ...SEMA.tipler, Abonelik: { tur: "nesne", alanlar: { oduncDurumu: { tip: "Odunc!", argumanlar: { id: "ID!" } } }, }, }, };
// kosu.mjs — uc islem turunu calistirir; sorgu ile mutasyonun yurutme sirasini karsilastirir import { ayristir } from "./ayristirici.mjs"; import { calistir, abone } from "./calistirici.mjs"; import { SEMA_A as SEMA } from "./sema-abonelik.mjs"; import { COZUCULER, ODUNCLER, olaylar, bekle, sifirla } from "./veri.mjs"; const yaz = (b, d) => console.log(b, JSON.stringify(d)); // 1) SORGU — alanlar birlikte yurutulur ODUNCLER.set("O-9", { id: "O-9", olusturma: "2026-02-01", uyeId: "U-1001", kalemler: [{ isbn: "978-0201896831", sube: "sahil" }], durum: "ACIK", iadeTarihi: "2026-03-20" }); const sorgu = ayristir(`query($id: ID!) { odunc(id: $id) { id durum uye { ad } kalemler { kitap { baslik } sube } } kayit(id: "U-1002") { id olusturma ... on Uye { ad } } }`); yaz("sorgu ->", (await calistir(SEMA, COZUCULER, sorgu, { degiskenler: { id: "O-9" } })).data); // 2) MUTASYON — tek kopyali kitabi iki kez odunc vermeyi dene const mutasyon = ayristir(`mutation { birinci: oduncVer(uye: "U-1001", isbn: "978-0262033848") { id durum } ikinci: oduncVer(uye: "U-1002", isbn: "978-0262033848") { id durum } }`); sifirla(); yaz("mutasyon ->", (await calistir(SEMA, COZUCULER, mutasyon)).data); console.log(" yürütme sırası:", olaylar.join(" | ")); // Ayni iki alan birlikte yurutulseydi: sifirla(); const paralel = await Promise.all([ COZUCULER.Mutasyon.oduncVer(null, { uye: "U-1001", isbn: "978-0262033848" }), COZUCULER.Mutasyon.oduncVer(null, { uye: "U-1002", isbn: "978-0262033848" }), ]); console.log(" birlikte yürütülseydi:", olaylar.join(" | ")); console.log(" birlikte yürütmede verilen kopya sayısı:", paralel.filter(Boolean).length, "(kitapta 1 kopya var)"); // 3) ABONELIK — ayni secim kumesi zaman icinde birden cok kez uygulanir COZUCULER.Abonelik = { oduncDurumu: async function* (_, a) { for (const d of ["ACIK", "GECIKMIS", "KAPALI"]) { await bekle(5); const o = ODUNCLER.get(a.id); yield { ...o, durum: d }; } }, }; ODUNCLER.set("O-9", { id: "O-9", olusturma: "2026-02-01", uyeId: "U-1001", kalemler: [{ isbn: "978-0201896831", sube: "sahil" }], durum: "ACIK", iadeTarihi: "2026-03-20" }); const abonelik = ayristir(`subscription { oduncDurumu(id: "O-9") { id durum uye { ad } } }`); for await (const olay of abone(SEMA, COZUCULER, abonelik)) yaz("abonelik ->", olay.data);
sorgu -> {"odunc":{"id":"O-9","durum":"ACIK","uye":{"ad":"Ayse Yilmaz"},"kalemler":[{"kitap":{"baslik":"Programlama Sanati"},"sube":"sahil"}]},"kayit":{"id":"U-1002","olusturma":"2025-06-03","ad":"Deniz Kaya"}}
mutasyon -> {"birinci":{"id":"O-1","durum":"ACIK"},"ikinci":null}
yürütme sırası: oduncVer(3848) başladı | oduncVer(3848) bitti: O-1 | oduncVer(3848) başladı | oduncVer(3848) bitti: kopya yok
birlikte yürütülseydi: oduncVer(3848) başladı | oduncVer(3848) başladı | oduncVer(3848) bitti: O-1 | oduncVer(3848) bitti: O-2
birlikte yürütmede verilen kopya sayısı: 2 (kitapta 1 kopya var)
abonelik -> {"oduncDurumu":{"id":"O-9","durum":"ACIK","uye":{"ad":"Ayse Yilmaz"}}}
abonelik -> {"oduncDurumu":{"id":"O-9","durum":"GECIKMIS","uye":{"ad":"Ayse Yilmaz"}}}
abonelik -> {"oduncDurumu":{"id":"O-9","durum":"KAPALI","uye":{"ad":"Ayse Yilmaz"}}}
Sorgu: Alanlar Bağımsızdır
Sorgu yanıtındaki iki kök alan birbirini görmez. odunc ve kayit alanlarının hangi
sırayla çözüldüğü sonucu değiştirmez, çünkü ikisi de yalnız okur. Bu bağımsızlık varsayımı
GraphQL’in kök sorgu alanlarını birlikte yürütmesini meşru kılar ve pratikte belirgin bir
kazançtır: dört bağımsız kök alanı olan bir ekran, dört ayrı isteğin toplam süresini değil
en yavaşının süresini bekler.
Yanıttaki anahtar sırası ise sorgudaki sıradır, çözülme sırası değil. Çalıştırıcı bunu
alanları önceden null ile yerleştirerek sağlar. İstemcinin yanıtı sorgudan okuyabilmesi
buna bağlıdır.
kayit alanı arayüz döndürüyor ve ... on Uye { ad } dalı çalışmış. Somut tipin ne olduğu
__tip çözücüsünden sorulur; veri kaynağındaki bir kaydın hangi tip olduğunu şema
bilemez, bunu bildiren kod yazılmak zorundadır.
Mutasyon: Sıra Bir Güvencedir
Mutasyon çıktısı işin özünü gösteriyor. Tek kopyalı bir kitap iki kez ödünç istendi;
birinci başarılı oldu, ikinci null döndü. Yürütme sırası satırı nedenini söylüyor:
ikinci çözücü, birincisi bittikten sonra başladı ve kalan kopyayı sıfır gördü.
Aynı iki çözücü birlikte yürütüldüğünde sıra değişiyor: ikisi de başlıyor, ikisi de başlangıçtaki tek kopyayı okuyor ve ikisi de kayıt yazıyor. Tek kopyalı kitap iki kez ödünç verilmiş oluyor. Bu, veritabanı derslerindeki kayıp güncelleme sorununun aynısıdır ve GraphQL’in kök mutasyon alanlarını sırayla yürütmesinin nedeni budur.
Güvencenin sınırı da belirtilmelidir: sıralılık yalnız kök mutasyon alanları içindir ve yalnız tek bir işlem içinde geçerlidir. Aynı anda gelen iki ayrı isteğin mutasyonları yine çakışabilir; onu önleyen şey çözücünün kendi eşzamanlılık denetimidir. İşlem içi sıra, istemcinin tek bir belgede yazdığı adımların bildiği sırayla çalışacağını güvence altına alır; başka bir şeyi değil.
Abonelik: Aynı Seçim, Birden Çok Yanıt
Abonelik çıktısındaki üç satır aynı seçim kümesinden üretildi. Çözücü tek bir değer değil
bir olay akışı döndürür; çalıştırıcı her olayı aynı seçim kümesiyle biçimler. uye { ad }
dalı üç kez çözüldü.
Yapısal fark budur: sorgu ve mutasyonda kök alan bir değere çözülür, abonelikte bir akışa. Bunun iki sonucu vardır. Bir abonelik işleminde yalnız bir kök alan bulunabilir — akışların nasıl birleştirileceğinin tanımlı bir yanıtı yoktur. Ve aktarım katmanı istek–yanıt olamaz; tek bir istek üzerinden birden çok yanıt taşıyabilen bir taşıyıcı gerekir.
Özet
- Üç işlem türü aynı seçim kümesi dilini kullanır; ayrıldıkları yer yürütme kurallarıdır.
- Sorgu kök alanları bağımsız kabul edilir ve birlikte yürütülür; yanıttaki anahtar sırası yine de sorgudaki sıradır.
- Mutasyon kök alanları sırayla yürütülür; tek kopyalı kitap sıralı yürütmede bir kez, birlikte yürütmede iki kez ödünç verilir.
- Sıralılık güvencesi yalnız kök mutasyon alanları ve tek bir işlem içindir; ayrı isteklerin çakışmasını önlemek çözücünün işidir.
- Abonelik çözücüsü değer değil olay akışı döndürür; aynı seçim kümesi her olaya yeniden uygulanır.
- Abonelikte tek kök alan bulunabilir ve aktarım katmanı istek–yanıt olamaz.
Sonraki Adım
Çalıştırıcı her alan için bir çözücü çağırdı ama çözücülerin nasıl yazılacağı konuşulmadı.
Ödünç kaydının uye alanı için ayrı bir çözücü var, id alanı için yok; ikisi arasındaki
fark nedir? Bir çözücü hangi bilgilere erişir, hangi sırayla çağrılır, döndürdüğü değer bir
sonraki düzeyin çözücüsüne nasıl geçer? Sonraki ders çözücü imzasını dört parçasıyla
tanımlar, varsayılan çözücünün ne yaptığını gösterir ve bir sorgunun kaç çözücü çağrısı
ürettiğini sayarak yürütmenin biçimini ortaya koyar.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.