Ders 29 / 34
Parçalar ve Değişkenler
Parçanın yinelenen seçim kümesini tek yere toplaması, çakışan parçalardaki alanların birleştirilmesiyle düşen çözücü çağrısı ve değişkenlerin ayrı belge sayısını sabitlemesi.
İçindekiler
Çalıştırıcı parçaları ve değişkenleri baştan beri destekliyordu ama ikisinin ne işe yaradığı gösterilmedi. Bu ders iki soruyu ölçerek yanıtlar: parça yazmanın karşılığı nedir ve değeri sorgu metnine gömmek yerine değişken kullanmak neyi değiştirir?
İkisinin de ortak bir yanı vardır: yanıtı değiştirmezler. Parçalı sorgu ile parçasız sorgu aynı veriyi döndürür, değişkenli sorgu ile değeri gömülü sorgu aynı veriyi döndürür. Kazanç başka yerdedir.
Bu dersteki ayrıştırıcı, şema, veri kaynağı ve çalıştırıcı önceki üç derste yazıldıkları hâliyle kullanılır; yalnız çalıştırıcının seçim kümesini düzleyen bölümü değişecek.
Parçanın Yaptığı İş
Sorgu parçası (fragment), bir tipe bağlanmış adlandırılmış seçim kümesidir; bu kurs içinde kısaca parça denir. Aynı alanları birden çok yerde okuyan bir sorgu, o kümeyi bir kez yazıp adıyla çağırır.
// kosu.mjs — parcali ve parcasiz sorgulari karsilastirir import { ayristir } from "./ayristirici.mjs"; import { calistir } from "./calistirici.mjs"; import { SEMA } from "./sema.mjs"; import { COZUCULER, sifirla } from "./veri.mjs"; // Ayni secim kumesi uc yerde tekrarlanmis hali const PARCASIZ = `{ acik: odunc(id: "O-1") { id durum uye { ad } kalemler { sube kitap { baslik } } } kapali: odunc(id: "O-3") { id durum uye { ad } kalemler { sube kitap { baslik } } } digeri: odunc(id: "O-2") { id durum uye { ad } kalemler { sube kitap { baslik } } } }`; // Ayni sorgu, secim kumesi tek yerde const PARCALI = `{ acik: odunc(id: "O-1") { ...OduncOzeti } kapali: odunc(id: "O-3") { ...OduncOzeti } digeri: odunc(id: "O-2") { ...OduncOzeti } } fragment OduncOzeti on Odunc { id durum uye { ad } kalemler { sube kitap { baslik } } }`; // "durum" gibi bir alan degistirilecek olsa kac satira dokunulurdu? const duzenlemeYeri = (metin, alan) => metin.split("\n").filter((s) => s.includes(alan)).length; async function calistirVeSay(metin, degiskenler = {}) { const iz = []; sifirla(); const s = await calistir(SEMA, COZUCULER, ayristir(metin), { degiskenler, izleyici: (b) => iz.push(b) }); return { veri: JSON.stringify(s.data), cagri: iz.length }; } const a = await calistirVeSay(PARCASIZ); const b = await calistirVeSay(PARCALI); console.log("belge satır 'durum' geçen satır çözücü çağrısı"); console.log(`parçasız ${String(PARCASIZ.split("\n").length).padStart(5)} ${String(duzenlemeYeri(PARCASIZ, "durum")).padStart(19)} ${String(a.cagri).padStart(14)}`); console.log(`parçalı ${String(PARCALI.split("\n").length).padStart(5)} ${String(duzenlemeYeri(PARCALI, "durum")).padStart(19)} ${String(b.cagri).padStart(14)}`); console.log(`\niki belgenin sonucu birebir aynı mı: ${a.veri === b.veri}`); console.log(`sonuç: ${a.veri.slice(0, 96)}...`);
belge satır 'durum' geçen satır çözücü çağrısı
parçasız 5 3 30
parçalı 6 1 30
iki belgenin sonucu birebir aynı mı: true
sonuç: {"acik":{"id":"O-1","durum":"ACIK","uye":{"ad":"Ayse Yilmaz"},"kalemler":[{"sube":"merkez","kita...
Parçalı belge bir satır daha uzun ama seçim kümesi tek yerde duruyor. Alan eklemek ya da çıkarmak gerektiğinde parçasız belgede üç satıra, parçalı belgede bir satıra dokunulur. Fark üçe birdir ve kök alan sayısıyla birlikte büyür.
Sonuçların birebir aynı olması ve çözücü çağrısı sayısının değişmemesi, parçanın çalışma anında yok olduğunu gösterir. Çalıştırıcı seçim kümesini düzlerken parçaları açar; o noktadan sonra parçalı belgeyle parçasız belge arasında hiçbir fark kalmaz. Parça bir yürütme kavramı değil, bir kaynak metin kavramıdır.
Bunun karşılığı, parçanın istemci tarafında bileşenle eşleşebilmesidir. Bileşen Tabanlı Arayüz Geliştirme kursundaki bileşen ağacında her bileşen kendi veri gereksinimini bir parça olarak bildirebilir ve üst bileşen bu parçaları birleştirerek tek bir sorgu kurar; alt bileşen bir alan eklediğinde üst sorgu kendiliğinden büyür.
Çakışan Alanların Birleştirilmesi
Parçalar birleştirilirken aynı alan birden çok kez seçilebilir. İki parça da id istiyorsa
ne olur?
Çalıştırıcının seçim kümesini düzleyen bölümü ayrı bir modüle alınır ve bu soruyu yanıtlar: aynı yanıt anahtarına düşen alanlar tek çağrıya indirilir, alt seçimleri birleşir.
// duzle.mjs — secim kumesini duzler; ayni yanit anahtarina dusen alanlari birlestirir import { olasiTipler } from "./sema.mjs"; // Parcalari acar, somut tipe uymayan dallari atar, kalanlari sirayla dizer. function ac(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)) ac(sema, belge, tipAdi, parca.secim, birikim); } return birikim; } // Ayni yanit anahtarina dusen alanlar tek cagriya indirilir, alt secimleri birlesir. export function duzle(sema, belge, tipAdi, secim, birlestir = true) { const acik = ac(sema, belge, tipAdi, secim); if (!birlestir) return acik; const kume = new Map(); for (const a of acik) { const onceki = kume.get(a.takma); if (!onceki) { kume.set(a.takma, { ...a, secim: a.secim ? [...a.secim] : null }); continue; } if (a.secim) onceki.secim = [...(onceki.secim ?? []), ...a.secim]; } return [...kume.values()]; }
Çalıştırıcı bu modülün üzerine yeniden kurulur. Çözücü imzası, izleyici ve düzey sıralı yürütme Çözücüler dersindeki gibidir; değişen iki şey vardır: düzleme işi dışarı alınmıştır ve birleştirme kapatılabilir. Ölçüm bu anahtara dayanacak.
// calistirici.mjs — duzleyicisi ayri modulden gelen calistirici // Cozucu imzasi ve izleyici Cozuculer dersindeki gibidir; secim kumesini duzleyen bolum // artik duzle.mjs'den gelir ve "birlestir" secenegiyle kapatilabilir. import { tipCoz, alanTanimi } from "./sema.mjs"; import { duzle } from "./duzle.mjs"; const YAPRAK = new Set(["skaler", "sirali"]); 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])); async function secimCalistir(o, tipAdi, secim, kaynak, yol, sirali = false) { const alanlar = duzle(o.sema, o.belge, tipAdi, secim, o.birlestir); 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; 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, birlestir = true } = {}) { const islem = islemAdi ? belge.islemler.find((i) => i.ad === islemAdi) : belge.islemler[0]; const o = { sema, cozucular, belge, dv: degiskenleriCoz(islem, degiskenler), baglam, izleyici, birlestir }; const kok = islem.tur === "mutation" ? sema.mutasyon : sema.sorgu; return { data: await secimCalistir(o, kok, islem.secim, null, "", islem.tur === "mutation") }; }
// cakisma.mjs — cakisan parcalarin ayni alani iki kez cozdurmesini olcer import { ayristir } from "./ayristirici.mjs"; import { calistir } from "./calistirici.mjs"; import { SEMA } from "./sema.mjs"; import { COZUCULER } from "./veri.mjs"; const SORGU = `{ odunc(id: "O-1") { id ...Kim ...Ne } } fragment Kim on Odunc { id durum uye { ad } } fragment Ne on Odunc { id durum kalemler { sube } }`; for (const birlestir of [false, true]) { const iz = []; const s = await calistir(SEMA, COZUCULER, ayristir(SORGU), { izleyici: (b) => iz.push(b), birlestir }); const kez = {}; for (const b of iz) kez[`${b.ustTip}.${b.alan}`] = (kez[`${b.ustTip}.${b.alan}`] ?? 0) + 1; console.log(`birleştirme ${birlestir ? "açık " : "kapalı"} çağrı=${String(iz.length).padStart(2)} ` + Object.entries(kez).map(([a, n]) => `${a}×${n}`).join(" ")); console.log(` sonuç: ${JSON.stringify(s.data)}`); }
birleştirme kapalı çağrı=11 Sorgu.odunc×1 Odunc.id×3 Odunc.durum×2 Odunc.uye×1 Odunc.kalemler×1 Uye.ad×1 Kalem.sube×2
sonuç: {"odunc":{"id":"O-1","durum":"ACIK","uye":{"ad":"Ayse Yilmaz"},"kalemler":[{"sube":"merkez"},{"sube":"sahil"}]}}
birleştirme açık çağrı= 8 Sorgu.odunc×1 Odunc.id×1 Odunc.durum×1 Odunc.uye×1 Odunc.kalemler×1 Uye.ad×1 Kalem.sube×2
sonuç: {"odunc":{"id":"O-1","durum":"ACIK","uye":{"ad":"Ayse Yilmaz"},"kalemler":[{"sube":"merkez"},{"sube":"sahil"}]}}
İki sonuç aynı, çağrı sayısı on birden sekize düştü. Birleştirme olmasaydı id alanı üç
kez, durum alanı iki kez çözülürdü; yanıtta yine tek kez görünürlerdi, çünkü aynı anahtara
yazılıyorlar. Yani birleştirmesiz yürütme yanlış sonuç vermez, gereksiz iş yapar.
Bu, parçaların bileşen başına yazılmasını güvenli kılan kuraldır. Beş bileşen kendi
parçasında id alanını isteyebilir; birleştirme sayesinde alan bir kez çözülür. Kural
olmasaydı parça yazmanın maliyeti kullanım sayısıyla çarpılırdı.
Birleştirmenin bir koşulu vardır: aynı anahtara düşen alanların çakışabilir olması gerekir. Aynı ada iki farklı argümanla iki kez sorulan alan birleştirilemez; bu durumda takma ad zorunludur ve iki ayrı anahtar oluşur.
Değişkenler
Değişken, sorgu metnini değerden ayırır. Ayrımın sonucu, aynı ekranın her açılışında sunucuya aynı belgenin gitmesidir.
// degiskenler.mjs — degeri metne gomen sorgu ile degisken kullanan sorguyu karsilastirir import { ayristir } from "./ayristirici.mjs"; import { calistir } from "./calistirici.mjs"; import { SEMA } from "./sema.mjs"; import { COZUCULER } from "./veri.mjs"; const KUCUK = ["O-1", "O-2", "O-3", "O-1", "O-2"]; // Buyuk yuk: 200 istek, 50 ayri kimlige dagilmis const BUYUK = Array.from({ length: 200 }, (_, i) => `O-${(i % 50) + 1}`); const GOMULU = { metin: (id) => `{ odunc(id: "${id}") { id durum uye { ad } } }`, degisken: () => ({}) }; const DEGISKENLI = { metin: () => `query($id: ID!) { odunc(id: $id) { id durum uye { ad } } }`, degisken: (id) => ({ id }) }; async function kosu(baslik, kimlikler, bicim, calistirilsin) { const onbellek = new Map(); // sunucu ayristirilmis belgeyi metnine gore saklar let iska = 0, bayt = 0; for (const id of kimlikler) { const metin = bicim.metin(id); if (!onbellek.has(metin)) { iska++; onbellek.set(metin, ayristir(metin)); } bayt += Buffer.byteLength(metin) + Buffer.byteLength(JSON.stringify(bicim.degisken(id))); if (calistirilsin) await calistir(SEMA, COZUCULER, onbellek.get(metin), { degiskenler: bicim.degisken(id) }); } console.log(`${baslik.padEnd(30)} istek=${String(kimlikler.length).padStart(3)} ayrı belge=${String(onbellek.size).padStart(3)} ` + `ayrıştırma=${String(iska).padStart(3)} gönderilen bayt=${bayt}`); } await kosu("5 istek, değer metne gömülü", KUCUK, GOMULU, true); await kosu("5 istek, değişkenli", KUCUK, DEGISKENLI, true); await kosu("200 istek, değer metne gömülü", BUYUK, GOMULU, false); await kosu("200 istek, değişkenli", BUYUK, DEGISKENLI, false);
5 istek, değer metne gömülü istek= 5 ayrı belge= 3 ayrıştırma= 3 gönderilen bayt=230 5 istek, değişkenli istek= 5 ayrı belge= 1 ayrıştırma= 1 gönderilen bayt=350 200 istek, değer metne gömülü istek=200 ayrı belge= 50 ayrıştırma= 50 gönderilen bayt=9364 200 istek, değişkenli istek=200 ayrı belge= 1 ayrıştırma= 1 gönderilen bayt=14164
Bayt sütunu değişkenli biçemin aleyhinedir ve öyle kalır: değişken bildirimi sorgu metnini uzatır, değişkenler ayrıca gönderilir. Kazanç bayt sayısında değil, ayrı belge sayısında görülür.
Beş istekte fark üçe birdir, iki yüz istekte elliye bir. Değeri metne gömen biçemde ayrı belge sayısı ayrı değer sayısıyla birlikte büyür; değişkenli biçemde bir tanedir ve öyle kalır. Bu sayı üç yerde işe yarar: sunucu ayrıştırma ve denetleme sonucunu belge başına saklayabilir, kütüklerde hangi sorgunun ne sıklıkla çalıştığı gruplanabilir, ve belgeler önceden kaydedilip istemci yalnız bir kimlik gönderebilir.
Bir de doğruluk gerekçesi vardır. Değeri metne gömmek, değeri sorgu diline enjekte etmek demektir; tırnak içeren bir değerin kaçırılmadan gömülmesi sorgunun yapısını değiştirebilir. Değişken kullanıldığında değer hiçbir zaman ayrıştırılmaz, ayrı bir JSON gövdesinde taşınır ve tipi şemaya karşı denetlenir. SQL’de hazır deyimlerin çözdüğü sorunun aynısı, aynı biçimde çözülür.
Özet
- Parça, bir tipe bağlanmış adlandırılmış seçim kümesidir; yinelenen alanları tek yere toplayarak düzenleme yeri sayısını üçten bire indirir.
- Parça çalışma anında yok olur: parçalı ve parçasız belge aynı sonucu ve aynı çözücü çağrı sayısını üretir; parça bir kaynak metin kavramıdır.
- Aynı yanıt anahtarına düşen alanlar birleştirilir; birleştirmesiz yürütme yanlış sonuç vermez ama aynı alanı birden çok kez çözer.
- Birleştirme, parçaların bileşen başına yazılmasını güvenli kılar; olmasaydı parça yazmanın maliyeti kullanım sayısıyla çarpılırdı.
- Değişken kullanmak gönderilen baytı azaltmaz, ayrı belge sayısını sabitler: iki yüz istekte elli ayrı belge yerine bir.
- Sabit belge sayısı ayrıştırma sonucunun saklanmasını, kütüklerin gruplanmasını ve belgelerin önceden kaydedilmesini mümkün kılar; ayrıca değer sorgu diline hiç girmediği için yapı bozulamaz.
Sonraki Adım
Çözücüler dersi bir sayıyı açıkta bırakmıştı: üç ödünç kaydı için üç üye çağrısı yapılmıştı, oysa iki ayrı üye vardı. Parçalar ve değişkenler bu sayıya dokunmadı; birleştirme yalnız aynı yanıt anahtarındaki çakışmaları giderir, farklı kayıtların aynı üyeyi istemesini gidermez. Liste yüz kayda çıktığında yüz üye çağrısı yapılacak. Sonraki ders bu sorunu adıyla koyar, aynı düzeydeki ç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.