Ders 05 / 34
GraphQL
Tek uç nokta üzerinden istemci güdümlü sorgu: sunucunun yayımladığı tip sistemi, sorgunun yürütülmeden önce şemaya göre doğrulanması, alan başına çözümleme ve bir alanın hata vermesiyle oluşan kısmen başarılı yanıt; değişkenlerle aynı belgenin yeniden kullanımı.
İçindekiler
Önceki ders sözleşmeyi şemada sabitleyen yaklaşımı kurdu ve bir eksiğini bıraktı: yanıtın biçimi sunucunun kararı olduğu için, istemcinin gereksinimi değiştiğinde sunucu değişmek zorundadır. İkinci derste ölçülen sorun buydu.
GraphQL bunun tersini yapar. Yanıtın biçimini istemci belirler, ama bunu serbest bir metinle değil, sunucunun yayımladığı bir tip sistemi (type system) içinde yapar. Ortada tek bir uç nokta vardır; ne istendiği adreste değil, gövdedeki sorgu belgesinde (query document) yazar. Bu ders o düzeneği küçük bir işleyici yazarak kurar.
Tip Sistemi Sunucunun Yayımladığı Yüzeydir
Tip sistemi, hangi alanların bulunduğunu, her alanın hangi tipte olduğunu ve hangi alanların alt seçim gerektirdiğini söyler. Her alanın verisini nasıl bulacağını bilen işleve çözücü (resolver) denir; bir alanın çözücüsü yoksa değer kaynak nesneden okunur.
// sema.mjs — tip sistemi ve cozuculer. Sunucunun yayimladigi yuzeyin tamami buradadir. export const TIPLER = { Sorgu: { uye: { tip: "Uye", arg: "id" }, kitap: { tip: "Kitap", arg: "isbn" } }, Uye: { id: "ID", ad: "Dizgi", eposta: "Dizgi", ceza: "Sayi", oduncler: { tip: "Odunc", liste: true } }, Odunc: { id: "ID", sonTarih: "Dizgi", gecikmeGunu: "Sayi", kitap: { tip: "Kitap" } }, Kitap: { isbn: "ID", ad: "Dizgi", yazar: "Dizgi", sayfa: "Sayi" }, }; const UYELER = [{ id: "u1", ad: "Ayse Kaya", eposta: "[email protected]", ceza: 0 }]; const KITAPLAR = { "978-0262033848": { isbn: "978-0262033848", ad: "Algoritmalara Giris", yazar: "Cormen", sayfa: 1312 }, "978-0201896831": { isbn: "978-0201896831", ad: "Programlama Sanati", yazar: "Knuth", sayfa: 650 }, }; const ODUNCLER = [{ id: "o11", uye: "u1", isbn: "978-0262033848", sonTarih: "2024-05-15" }, { id: "o12", uye: "u1", isbn: "978-0201896831", sonTarih: "2024-05-17" }]; // Cozucu: her alan kendi verisini kendi getirir. Bir alanin hata vermesi digerlerini durdurmaz. export const COZUCULER = { "Sorgu.uye": (_, arg) => UYELER.find((u) => u.id === arg), "Sorgu.kitap": (_, arg) => KITAPLAR[arg], "Uye.oduncler": (uye) => ODUNCLER.filter((o) => o.uye === uye.id), "Odunc.kitap": (odunc) => KITAPLAR[odunc.isbn], "Odunc.gecikmeGunu": () => { throw new Error("gecikme servisi yanit vermiyor"); }, };
Odunc.gecikmeGunu çözücüsünün hata atması bilinçlidir: gecikme tutarı gerçek bir kurulumda
ayrı bir servisten gelir ve o servis yanıt vermeyebilir. Bu alan, kısmen başarılı yanıtı
göstermek için oradadır.
İşleyicinin Üç Adımı
Sorgu belgesi üç adımdan geçer: çözümleme (metin bir seçim ağacına dönüşür), doğrulama (ağaç tip sistemiyle karşılaştırılır) ve yürütme (her alan kendi çözücüsüyle hesaplanır). Doğrulamanın yürütmeden önce gelmesi belirleyicidir: geçersiz bir sorgu için hiçbir çözücü çalışmaz, dolayısıyla hiçbir veri erişimi yapılmaz.
// yurut.mjs — kucuk sorgu isleyicisi: cozumleme, tip sistemine gore dogrulama, alan basina yurutme import { TIPLER, COZUCULER } from "./sema.mjs"; export const cozumle = (metin) => { // sorgu belgesini secim agacina cevirir const s = metin.match(/[A-Za-z_][A-Za-z0-9_]*|[{}():]|"[^"]*"|\d+/g) ?? []; let i = 0; const kume = () => { const alanlar = []; i++; while (s[i] !== "}") { const ad = s[i++]; let arg = null; if (s[i] === "(") { arg = s[i + 3].replace(/^"|"$/g, ""); i += 5; } // ad ( anahtar : deger ) alanlar.push({ ad, arg, alt: s[i] === "{" ? kume() : null }); } i++; return alanlar; }; s.unshift("{"); s.push("}"); return kume(); }; export const dogrula = (alanlar, tipAdi = "Sorgu", yol = []) => { // tip sistemine gore denetim const kusur = []; for (const alan of alanlar) { const tanim = TIPLER[tipAdi]?.[alan.ad]; const su = [...yol, alan.ad]; if (!tanim) { kusur.push(`${su.join(".")}: ${tipAdi} tipinde boyle bir alan yok`); continue; } const nesneMi = typeof tanim === "object"; if (nesneMi && !alan.alt) kusur.push(`${su.join(".")}: ${tanim.tip} bir nesne, alt alan secilmeli`); else if (!nesneMi && alan.alt) kusur.push(`${su.join(".")}: ${tanim} bir skaler, alt alan secilemez`); else if (nesneMi) kusur.push(...dogrula(alan.alt, tanim.tip, su)); } return kusur; }; export const yurut = (alanlar, kok) => { const hatalar = []; const adim = (alanlar, kaynak, tipAdi, yol) => { const cikti = {}; for (const alan of alanlar) { const tanim = TIPLER[tipAdi][alan.ad]; const su = [...yol, alan.ad]; const cozucu = COZUCULER[`${tipAdi}.${alan.ad}`] ?? ((k) => k[alan.ad]); let deger; // Alan basina cozumleme: bir alanin hatasi digerlerini durdurmaz, o alan null olur. try { deger = cozucu(kaynak, alan.arg); } catch (e) { hatalar.push({ yol: su.join("."), ileti: e.message }); cikti[alan.ad] = null; continue; } if (typeof tanim !== "object") { cikti[alan.ad] = deger ?? null; continue; } cikti[alan.ad] = tanim.liste ? deger.map((d, k) => adim(alan.alt, d, tanim.tip, [...su, k])) : adim(alan.alt, deger, tanim.tip, su); } return cikti; }; return { veri: adim(alanlar, kok, "Sorgu", []), hatalar }; }; export const degiskenleriYerlestir = (belge, degiskenler) => belge.replace(/\$(\w+)/g, (_, ad) => JSON.stringify(degiskenler[ad]));
// sunucu.mjs — tek uc nokta. Yol her sorgu icin aynidir; ne istendigi govdede yazar. import { createServer } from "node:http"; import { cozumle, dogrula, yurut, degiskenleriYerlestir } from "./yurut.mjs"; createServer(async (istek, yanit) => { yanit.sendDate = false; let ham = ""; for await (const parca of istek) ham += parca; const { belge, degiskenler = {} } = JSON.parse(ham); const alanlar = cozumle(degiskenleriYerlestir(belge, degiskenler)); const kusur = dogrula(alanlar); // Dogrulama yurutmeden once yapilir: gecersiz sorgu icin hicbir cozucu calismaz. const govde = kusur.length ? { hatalar: kusur } : yurut(alanlar, null); const metin = JSON.stringify(govde); yanit.setHeader("Content-Type", "application/json; charset=utf-8"); yanit.setHeader("Content-Length", Buffer.byteLength(metin)); yanit.writeHead(kusur.length ? 400 : 200).end(metin); }).listen(8471, "127.0.0.1", () => console.log("tek uc nokta 127.0.0.1:8471/graf"));
Ölçüm
// olc.mjs — tek uc noktanin dort ozelligini olcer const sor = async (belge, degiskenler) => { const govde = JSON.stringify({ belge, degiskenler }); const y = await fetch("http://127.0.0.1:8471/graf", { method: "POST", body: govde }); const metin = await y.text(); return { kod: y.status, bayt: Buffer.byteLength(metin), gonderilen: Buffer.byteLength(govde), govde: JSON.parse(metin) }; }; const yaz = (baslik, y) => console.log(` ${baslik.padEnd(22)} HTTP ${y.kod} ` + `gonderilen=${String(y.gonderilen).padStart(3)} B alinan=${String(y.bayt).padStart(3)} B\n` + ` ${JSON.stringify(y.govde)}`); console.log("-- 1) ayni uc nokta, iki farkli istemci gereksinimi --"); yaz("raf ekrani", await sor('uye(id: "u1") { ad }')); yaz("gecikme raporu", await sor('uye(id: "u1") { ad oduncler { sonTarih kitap { ad yazar } } }')); console.log("\n-- 2) tip sistemi: sorgu yurutulmeden once dogrulanir --"); yaz("olmayan alan", await sor('uye(id: "u1") { ad telefon }')); yaz("skalerde alt secim", await sor('uye(id: "u1") { ad { uzunluk } }')); yaz("nesnede secim yok", await sor('uye(id: "u1") { ad oduncler }')); console.log("\n-- 3) kismi basari: bir alanin cozucusu hata veriyor --"); yaz("gecikme alani", await sor('uye(id: "u1") { ad oduncler { sonTarih gecikmeGunu } }')); console.log("\n-- 4) degiskenler: ayni belge, iki farkli deger --"); const BELGE = "kitap(isbn: $kimlik) { ad yazar sayfa }"; for (const kimlik of ["978-0262033848", "978-0201896831"]) yaz(`kimlik=${kimlik.slice(-4)}`, await sor(BELGE, { kimlik }));
node sunucu.mjs & p=$! curl -s --retry 20 --retry-connrefused --retry-delay 0 -o /dev/null \ -X POST -d '{"belge":"kitap(isbn: \"978-0262033848\") { ad }"}' http://127.0.0.1:8471/graf node olc.mjs kill $p
tek uc nokta 127.0.0.1:8471/graf
-- 1) ayni uc nokta, iki farkli istemci gereksinimi --
raf ekrani HTTP 200 gonderilen= 34 B alinan= 48 B
{"veri":{"uye":{"ad":"Ayse Kaya"}},"hatalar":[]}
gecikme raporu HTTP 200 gonderilen= 75 B alinan=219 B
{"veri":{"uye":{"ad":"Ayse Kaya","oduncler":[{"sonTarih":"2024-05-15","kitap":{"ad":"Algoritmalara Giris","yazar":"Cormen"}},{"sonTarih":"2024-05-17","kitap":{"ad":"Programlama Sanati","yazar":"Knuth"}}]}},"hatalar":[]}
-- 2) tip sistemi: sorgu yurutulmeden once dogrulanir --
olmayan alan HTTP 400 gonderilen= 42 B alinan= 59 B
{"hatalar":["uye.telefon: Uye tipinde boyle bir alan yok"]}
skalerde alt secim HTTP 400 gonderilen= 46 B alinan= 60 B
{"hatalar":["uye.ad: Dizgi bir skaler, alt alan secilemez"]}
nesnede secim yok HTTP 400 gonderilen= 43 B alinan= 65 B
{"hatalar":["uye.oduncler: Odunc bir nesne, alt alan secilmeli"]}
-- 3) kismi basari: bir alanin cozucusu hata veriyor --
gecikme alani HTTP 200 gonderilen= 68 B alinan=306 B
{"veri":{"uye":{"ad":"Ayse Kaya","oduncler":[{"sonTarih":"2024-05-15","gecikmeGunu":null},{"sonTarih":"2024-05-17","gecikmeGunu":null}]}},"hatalar":[{"yol":"uye.oduncler.0.gecikmeGunu","ileti":"gecikme servisi yanit vermiyor"},{"yol":"uye.oduncler.1.gecikmeGunu","ileti":"gecikme servisi yanit vermiyor"}]}
-- 4) degiskenler: ayni belge, iki farkli deger --
kimlik=3848 HTTP 200 gonderilen= 93 B alinan= 90 B
{"veri":{"kitap":{"ad":"Algoritmalara Giris","yazar":"Cormen","sayfa":1312}},"hatalar":[]}
kimlik=6831 HTTP 200 gonderilen= 93 B alinan= 87 B
{"veri":{"kitap":{"ad":"Programlama Sanati","yazar":"Knuth","sayfa":650}},"hatalar":[]}
Ölçümün Söyledikleri
Tek uç nokta, iki farklı yanıt biçimi. Raf ekranı 48 bayt, gecikme raporu 219 bayt almıştır. İkisi de aynı adrese gitmiştir ve sunucuda hiçbir şey değişmemiştir. İkinci derste sorgu temelli biçemin gösterdiği özellik budur; buradaki fark, seçimin serbest bir metin değil, yayımlanmış bir tip sistemi içinde yapılmasıdır.
Tip sistemi üç ayrı hatayı yürütmeden önce yakalar. Var olmayan bir alan, bir skalerin
altında alt seçim yapılması ve bir nesnenin alt seçimsiz istenmesi — üçü de 400 ile
reddedilir ve hiçbir çözücü çalışmaz. Bu, sorgu temelli bir arayüzün gevşek olmadığını
gösterir: istemci alan bileşimini seçebilir, ama şemanın dışına çıkamaz.
Kısmen başarılı yanıt bir HTTP hatası değildir. Gecikme alanının çözücüsü hata atmıştır,
ama yanıt 200 gelmiştir. Veri kısmında istenen alanların çoğu doludur, hata veren alan
null olmuştur ve hata listesi her başarısızlığı yoluyla birlikte bildirir:
uye.oduncler.0.gecikmeGunu. Bu davranış alan başına çözümlemenin doğrudan sonucudur —
her alan kendi verisini kendi getirdiği için bir alanın başarısızlığı komşusunu etkilemez.
Bunun tüketici tarafındaki karşılığı, HTTP durum kodunun tek başına yeterli bir gösterge
olmamasıdır. 200 alan bir istemci gövdeyi de incelemek zorundadır. Uygulama Mimarisi
kursunda kısmi başarı kavramı istemci tarafından anlatılmıştı; burada aynı davranışın
sunucudaki kaynağı görünür.
Değişkenler sorgu belgesini yeniden kullanılabilir kılar. Aynı belge iki farklı kitap için çalışmıştır. Değişkenlerin ayrı gönderilmesinin ikinci bir yararı da vardır: belge sabit kaldığı için sunucu tarafında tanınabilir, kaydedilebilir ve maliyeti önceden hesaplanabilir.
Bedeller
Bu esnekliğin dört bedeli vardır ve üçü ölçümde görünür.
Önbellekleme zorlaşır. Bütün sorgular tek adrese, POST yöntemiyle gider. Önceki dersteki
varlık etiketi düzeneği burada doğrudan çalışmaz, çünkü adres kaydı adlandırmaz. Önbellekleme
ya istemci tarafında, sorgu belgesi anahtar alınarak yapılır ya da alan düzeyinde kurulur.
Yanıtın maliyeti önceden bilinmez. Sunucu, gelen sorgunun kaç veri erişimi doğuracağını sorgu gelmeden hesaplayamaz. Derinliği artan bir sorgu ya da uzun bir liste içinde iç içe seçim, tek bir istekle çok iş yaptırabilir. Bu, sorgu maliyeti ve derinlik sınırı gibi korumaları gerektirir.
Alan başına çözümleme veri erişimini çoğaltır. Ölçümdeki iki ödünç için Odunc.kitap
çözücüsü iki kez çalıştı; ödünç sayısı arttıkça bu sayı da artar. İkinci derste kaynak
temelli biçemde görülen N+1 örüntüsü burada sunucunun içine taşınmıştır. Toplu yükleme ile
çözümü bu kursun GraphQL Ayrıntısı konusunda ele alınır.
Hata sözleşmesi ikiye bölünür. Bir istekte hem taşıma düzeyinde durum kodu hem de gövde içinde hata listesi bulunur. İkisinin ne zaman hangisini kullanacağı açıkça kararlaştırılmazsa, tüketiciler hataları iki ayrı yerde aramak zorunda kalır.
Özet
- GraphQL tek uç nokta üzerinden çalışır; ne istendiği adreste değil, gövdedeki sorgu belgesinde yazar ve yanıtın biçimini istemcinin seçim kümesi belirler.
- Sunucunun yayımladığı tip sistemi seçimi sınırlar: ölçümde var olmayan alan, skalerde alt
seçim ve alt seçimsiz nesne yürütülmeden önce
400ile reddedildi. - Alan başına çözümleme, bir alanın çözücüsü hata verdiğinde diğerlerinin çalışmaya devam
etmesini sağlar; sonuç
200durumlu, veri ile hata listesini birlikte taşıyan kısmen başarılı bir yanıttır. - Değişkenler belgeyi sabit tutar; aynı belge farklı değerlerle yeniden kullanılır ve sunucu tarafında tanınabilir hâle gelir.
- Bedeller ölçüme yansır: tek adres ve
POSTönbelleklemeyi zorlaştırır, yanıtın doğuracağı iş önceden bilinmez, alan başına çözümleme veri erişimini çoğaltır ve hata sözleşmesi iki yere bölünür.
Sonraki Adım
Tek uç nokta üzerinden, gövdede taşınan bir işlem adıyla çalışmak yeni bir fikir değildir. Aynı düzeneği kuran ve arkasında geniş bir miras bırakan bir protokol ailesi vardır: her iletinin bir zarfa sarıldığı, işlem adının ve hatanın da o zarfın içinde taşındığı, sözleşmenin ayrı bir tanım dosyasında yayımlandığı bir aile. Sonraki ders bu zarf tabanlı yaklaşımı kendi küçük ayrıştırıcısıyla kurar, zarfın bayt maliyetini ölçer ve bugünkü arayüz kararlarının hangilerinin oradan devralındığını gösterir.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.