İçeriğe geç
academia.sh

Ders 13 / 22

GraphQL İstemcisi

Alan seçiminin istemciye geçmesi, tekrar eden alan kümelerinin parçalarla adlandırılması, iç içe yanıtın kimlik tabanlı bir depoya normalleştirilmesi ve kısmi başarının hata sözleşmesine katılması.

İçindekiler

Önceki ders bir gerilimle bitti: kaynağın döndürdüğü alanlarla ekranın istediği alanlar çakışmıyor. Kuzey Yamaç istasyon kartı istasyonun adını, son ölçümü ve son iki ölçüm satırını birlikte gösteriyor; REST kaynak modelinde bu üç ayrı adres demek. Alternatif — ekrana özel bir uç nokta — sunucuyu arayüzün biçimine bağlar.

Bu ders, kararı yer değiştiren bir yaklaşımı ele alır. GraphQL’de sunucu bir şema yayımlar, istemci o şemadan hangi alanları istediğini yazar. Tek uç nokta, tek istek, tam olarak istenen alanlar. Kazanç açık; bedel, istemcinin şimdi kendi başına çözmesi gereken iki yeni problem: aynı alan kümesinin ekranlar arasında paylaşılması ve gelen iç içe yanıtın saklanması.

Alan Seçiminin İstemciye Geçmesi

Bir sorgu belgesi, şemadaki alanların bir alt ağacını seçer. istasyon(kod: "KY-01") alanının altında ad ve yukseklik istenirse yanıt yalnız o ikisini taşır; sonOlcum istenirse aynı istekte o da gelir. İç içe alanlar tek gidiş dönüşte toplanır, bu yüzden gecikmenin ekran başına çarpanı ortadan kalkar.

Üç yapısal fark istek katmanını etkiler. Birincisi, adres artık ayırt edici değildir: tüm sorgular aynı uç noktaya gider ve genellikle POST ile gönderilir. Önceki dersin “adres bir önbellek anahtarıdır” kuralı burada geçersizdir; anahtar, sorgu belgesi ile değişkenlerinin birleşiminden üretilir.

İkincisi, başarı ve başarısızlık aynı yanıtta bulunabilir. Sorgudaki bir alan çözümlenemezse HTTP durumu 200 kalır, gövde hem kısmi data hem de errors listesi taşır. Bu, önceki dersteki dört sınıflı hata sözleşmesinin kapsamadığı bir durumdur ve ders sonunda ele alınacaktır.

Üçüncüsü, sorgunun maliyeti istemcinin elindedir. Derin bir iç içe seçim, sunucuda kayıt başına ek okuma üretebilir; şemada bir liste alanının altında yine bir liste alanı seçmek maliyeti çarpar. Sunucu bunu derinlik ve karmaşıklık sınırlarıyla dizginler; istemci tarafında bunun karşılığı, ekranın gerçekten gösterdiği alanları istemektir.

Parça: Alan Kümesinin Adı

Aynı alan kümesi birden çok ekranda görünür. Ölçüm satırı hem istasyon kartında hem ölçüm geçmişi listesinde aynı beş alanı ister. Bu kümeyi her sorguya elle yazmak, bir alan eklendiğinde bazı ekranların geride kalması demektir.

Sorgu parçası (fragment), bir tür üzerinde adlandırılmış bir alan kümesidir. Sorgu parçayı adıyla çağırır, belge kurulurken tanımı yanına eklenir. Aşağıdaki dosya bu birleştirmeyi saf dizgi işlemi olarak yapar: parça çağrılarını bulur, bağımlılıkları özyinelemeli izler ve her tanımı belgeye bir kez koyar.

// parca-birlestirme.mjs — sorgu parcalarindan belge kurma
const PARCALAR = {
  IstasyonOzeti: {
    tur: "Istasyon",
    govde: ["__typename", "kod", "ad", "yukseklik"],
  },
  OlcumSatiri: {
    tur: "Olcum",
    govde: ["__typename", "kimlik", "zaman", "deger", "birim"],
  },
  IstasyonKarti: {
    tur: "Istasyon",
    govde: ["...IstasyonOzeti", "sonOlcum { ...OlcumSatiri }"],
  },
};

// Bir govdedeki parca cagrilarini bulur: "...Ad" bicimindeki her sozcuk.
const cagrilar = (satirlar) =>
  satirlar.flatMap((s) => [...s.matchAll(/\.\.\.(\w+)/g)].map((e) => e[1]));

function gerekliParcalar(kokler, toplanan = new Set()) {
  for (const ad of kokler) {
    if (toplanan.has(ad)) continue;          // ayni parca iki kez yazilmaz
    const parca = PARCALAR[ad];
    if (!parca) throw new Error(`tanimsiz parça: ${ad}`);
    toplanan.add(ad);
    gerekliParcalar(cagrilar(parca.govde), toplanan); // ic ice bagimliliklar
  }
  return toplanan;
}

function belgeKur(ad, govde) {
  const adlar = gerekliParcalar(cagrilar(govde));
  const tanimlar = [...adlar].map((a) => {
    const p = PARCALAR[a];
    return `fragment ${a} on ${p.tur} {\n  ${p.govde.join("\n  ")}\n}`;
  });
  return [`query ${ad} {\n  ${govde.join("\n  ")}\n}`, ...tanimlar].join("\n\n");
}

const belge = belgeKur("IstasyonSayfasi", [
  "istasyon(kod: \"KY-01\") {",
  "  ...IstasyonKarti",
  "  olcumler(son: 2) { ...OlcumSatiri }",
  "}",
]);

console.log(belge);
console.log("---");
console.log("belgeye giren parça sayısı:", (belge.match(/^fragment /gm) ?? []).length);
query IstasyonSayfasi {
  istasyon(kod: "KY-01") {
    ...IstasyonKarti
    olcumler(son: 2) { ...OlcumSatiri }
  }
}

fragment IstasyonKarti on Istasyon {
  ...IstasyonOzeti
  sonOlcum { ...OlcumSatiri }
}

fragment IstasyonOzeti on Istasyon {
  __typename
  kod
  ad
  yukseklik
}

fragment OlcumSatiri on Olcum {
  __typename
  kimlik
  zaman
  deger
  birim
}
---
belgeye giren parça sayısı: 3

OlcumSatiri belgede iki yerden çağrılıyor — biri doğrudan sorgudan, biri IstasyonKarti içinden — ama tanımı bir kez yazılıyor. Bu, tekilleştirmenin gerçek faydasıdır: alan kümesi tek yerde durur.

Parçanın asıl tasarım değeri, alan ihtiyacının onu kullanan parçanın yanında bildirilmesidir. Ölçüm satırını çizen kod hangi alanlara ihtiyaç duyduğunu kendi parçasında yazar; sayfa sorgusu bunu bilmez, yalnız çağırır. Ekrana yeni bir alan eklendiğinde tek bir yer değişir ve o parçayı kullanan her sorgu alanı almaya başlar.

Her parçaya __typename alanının konması rastgele değildir. Bir sonraki bölümdeki depo, kayıtları türle kimliğin birleşiminden anahtarlar; tür bilgisi yanıtta yoksa anahtar üretilemez.

Önbellek Normalizasyonu

Yanıt iç içe bir ağaçtır. Aynı ölçüm kaydı hem sonOlcum altında hem olcumler listesinde görünür; iki kopya olarak saklanırsa biri güncellenip diğeri eskiyebilir. Normalizasyon, ağacı kimlik anahtarlı düz bir depoya çevirir: her kayıt bir kez durur, ağaçtaki yerinde ise ona bir başvuru kalır.

// normalizasyon.mjs — ic ice yaniti duz depoya cevirme, birlestirme ve geri okuma
const KIMLIK_ALANI = { Istasyon: "kod", Olcum: "kimlik" };

const anahtar = (nesne) => {
  const alan = KIMLIK_ALANI[nesne?.__typename];
  return alan && nesne[alan] != null ? `${nesne.__typename}:${nesne[alan]}` : null;
};

function normalize(deger, depo) {
  if (Array.isArray(deger)) return deger.map((e) => normalize(e, depo));
  if (deger === null || typeof deger !== "object") return deger;

  const alanlar = {};
  for (const [ad, alt] of Object.entries(deger)) alanlar[ad] = normalize(alt, depo);

  const kimlik = anahtar(deger);
  if (!kimlik) return alanlar;                            // kimliksiz nesne yerinde kalir
  depo[kimlik] = { ...(depo[kimlik] ?? {}), ...alanlar };  // alan bazinda birlestirme
  return { __ref: kimlik };
}

// Okuma her zaman bir secim kumesiyle yapilir; depo cevrimli oldugu icin
// secimsiz dolasma biter bir sey degildir.
function oku(secim, deger, depo) {
  if (Array.isArray(deger)) return deger.map((e) => oku(secim, e, depo));
  if (deger === null || typeof deger !== "object") return deger;
  const kaynak = deger.__ref ? depo[deger.__ref] : deger;
  if (!kaynak) return { __eksik: deger.__ref };            // depoda yok
  const cikti = {};
  for (const [ad, alt] of Object.entries(secim)) {
    if (!(ad in kaynak)) { cikti[ad] = { __eksik: ad }; continue; }
    cikti[ad] = alt === true ? kaynak[ad] : oku(alt, kaynak[ad], depo);
  }
  return cikti;
}

const depo = {};
const kokler = {};

// --- Birinci sorgu: istasyon karti + son iki olcum --------------------------
const yanit1 = {
  istasyon: {
    __typename: "Istasyon", kod: "KY-01", ad: "Kuzey Yamaç", yukseklik: 1840,
    sonOlcum: {
      __typename: "Olcum", kimlik: "o-114", zaman: "2026-01-14T06:00Z",
      deger: -4.2, birim: "C",
      istasyon: { __typename: "Istasyon", kod: "KY-01", ad: "Kuzey Yamaç" },
    },
    olcumler: [
      { __typename: "Olcum", kimlik: "o-114", zaman: "2026-01-14T06:00Z", deger: -4.2, birim: "C" },
      { __typename: "Olcum", kimlik: "o-113", zaman: "2026-01-13T06:00Z", deger: -1.8, birim: "C" },
    ],
  },
};
kokler['istasyon(kod:"KY-01")'] = normalize(yanit1.istasyon, depo);

console.log("depo anahtarları:", Object.keys(depo).join("  "));
console.log("KY-01           :", JSON.stringify(depo["Istasyon:KY-01"]));
console.log("o-114           :", JSON.stringify(depo["Olcum:o-114"]));

// --- Ikinci sorgu: baska bir ekran, ayni olcumu farkli alanlarla ister ------
const yanit2 = { olcum: { __typename: "Olcum", kimlik: "o-114", deger: -4.4, kaydeden: "gözlemci-3" } };
kokler['olcum(kimlik:"o-114")'] = normalize(yanit2.olcum, depo);

console.log("--- ikinci yanıttan sonra ---");
console.log("o-114           :", JSON.stringify(depo["Olcum:o-114"]));
console.log("kayıt sayısı    :", Object.keys(depo).length);

// --- Birinci ekran, kendi secim kumesiyle depodan yeniden okunur -----------
const KART = {
  kod: true, ad: true,
  sonOlcum: { kimlik: true, deger: true, birim: true },
  olcumler: { kimlik: true, deger: true },
};
console.log("--- birinci ekranın yeni görüntüsü ---");
console.log(JSON.stringify(oku(KART, kokler['istasyon(kod:"KY-01")'], depo)));

// --- Deponun tasimadigi alan istenirse ------------------------------------
const RAPOR = { kod: true, sonOlcum: { kimlik: true, sapma: true } };
console.log("--- eksik alan isteyen ekran ---");
console.log(JSON.stringify(oku(RAPOR, kokler['istasyon(kod:"KY-01")'], depo)));
depo anahtarları: Istasyon:KY-01  Olcum:o-114  Olcum:o-113
KY-01           : {"__typename":"Istasyon","kod":"KY-01","ad":"Kuzey Yamaç","yukseklik":1840,"sonOlcum":{"__ref":"Olcum:o-114"},"olcumler":[{"__ref":"Olcum:o-114"},{"__ref":"Olcum:o-113"}]}
o-114           : {"__typename":"Olcum","kimlik":"o-114","zaman":"2026-01-14T06:00Z","deger":-4.2,"birim":"C","istasyon":{"__ref":"Istasyon:KY-01"}}
--- ikinci yanıttan sonra ---
o-114           : {"__typename":"Olcum","kimlik":"o-114","zaman":"2026-01-14T06:00Z","deger":-4.4,"birim":"C","istasyon":{"__ref":"Istasyon:KY-01"},"kaydeden":"gözlemci-3"}
kayıt sayısı    : 3
--- birinci ekranın yeni görüntüsü ---
{"kod":"KY-01","ad":"Kuzey Yamaç","sonOlcum":{"kimlik":"o-114","deger":-4.4,"birim":"C"},"olcumler":[{"kimlik":"o-114","deger":-4.4},{"kimlik":"o-113","deger":-1.8}]}
--- eksik alan isteyen ekran ---
{"kod":"KY-01","sonOlcum":{"kimlik":"o-114","sapma":{"__eksik":"sapma"}}}

Çıktı dört şey söylüyor.

Depo düzdür ve tekrar barındırmaz. İlk yanıtta ölçüm o-114 üç yerde görünüyordu; depoda tek kayıt var, diğer yerlerde __ref başvurusu duruyor. İstasyon da aynı biçimde tekilleşti: ölçümün içindeki gömülü istasyon nesnesi ayrı bir kayıt üretmedi, var olanla birleşti.

Birleştirme alan bazındadır. İkinci sorgu ölçümün yalnız iki alanını istedi ama depo kaydı beş alanını korudu ve yeni gelen kaydeden alanını ekledi. Yeni değer -4.4, eskisinin üzerine yazıldı. Kısmi bir yanıt, saklanan kaydı budamaz.

Bir ekranın verisi başka bir ekranın isteğiyle tazelenir. İstasyon kartı yeniden istek atmadı; depo değiştiği için kartın okuduğu değer -4.4 oldu. Normalize edilmemiş bir önbellekte bu güncelleme yalnız ikinci ekrana yansırdı.

Okuma bir seçim kümesi gerektirir. Depo çevrimlidir: istasyon ölçüme, ölçüm istasyona başvurur. Seçimsiz bir dolaşma sonlanmaz. Okuyucu her düzeyde hangi alanların istendiğini bilmek zorundadır — yani sorgu belgesi yalnız istek anında değil, okuma anında da gerekir. Son satır bunun devamını gösterir: depoda olmayan bir alan istendiğinde okuma eksik olduğunu bildirir; bu durumda sunucuya gitmek gerekir.

Normalizasyonun Sınırları

Depo yalnız kimliği olan kayıtları tekilleştirir. Kimliksiz nesneler — bir toplam, bir sayfa bilgisi, hesaplanmış bir özet — ait oldukları kaydın içinde kalır. Şemada bir tür için kimlik alanı yoksa ya da alan adı türden türe değişiyorsa, eşleme tablosunun (burada KIMLIK_ALANI) elle bakımı gerekir.

İkinci sınır listelerdir. olcumler(son: 2) ile olcumler(son: 20) aynı alanın iki farklı sonucudur; ikisi tek bir alan değeri altında saklanamaz. Bu yüzden liste alanları argümanlarıyla birlikte anahtarlanır. Sayfalama söz konusuysa bir de birleştirme kuralı gerekir: yeni sayfa öncekinin üstüne mi yazılacak, sonuna mı eklenecek. Bu karar alan bazında verilir ve depo bunu bilemez, uygulamanın söylemesi gerekir.

Üçüncü sınır silmedir. Bir ölçüm silindiğinde depo kaydını atmak yetmez; o kayda başvuran listelerin de temizlenmesi gerekir. Aksi hâlde okuma __eksik döndürür ve ekranda boşluk belirir. Değişiklik işlemlerinden sonra hangi kök alanların geçersizleştirileceği, Durum Yönetimi konusundaki geçersizleştirme kuralının bu bağlamdaki karşılığıdır.

Kısmi Başarı ve Hata Sözleşmesi

Önceki dersin sözleşmesi dört sınıf tanıyordu: ağ, HTTP, gövde, iptal. Sorgu tabanlı erişim beşinci bir durum getirir: taşıma başarılı, alan başarısız. Yanıt 200 döner, data içinde istenen alanların bir kısmı null olur ve errors listesi hangi yolda ne olduğunu söyler.

Bu yanıtı tek bir “hata” ya da tek bir “başarı” saymak ikisi de yanlıştır. İstek katmanı üç bilgiyi ayrı taşımalıdır: elde edilen kısmi veri, başarısız alan yolları ve bu başarısızlığın kullanıcıya gösterilip gösterilmeyeceği. İstasyon adı geldi ama son ölçüm alanı boş döndüyse kart çizilebilir; ölçüm bölümünün yerine hata bildirimi konur. Tüm ekranı hata durumuna düşürmek, elde olan veriyi çöpe atar.

Ayrım şu soruyla yapılır: ekranın anlamlı olması için hangi alanlar zorunlu? Bu soru sunucu tarafından yanıtlanamaz; hangi alanın vazgeçilebilir olduğunu ekranı çizen kod bilir. Bu yüzden kısmi başarı, istek katmanının otomatik olarak karara bağlayabileceği bir şey değildir — taşınır, karar çağrı noktasında verilir.

Alan düzeyindeki hataların yanında protokol düzeyindeki hatalar da vardır: geçersiz sorgu belgesi, şemada olmayan alan, tip uyumsuzluğu. Bunlar sorgu hiç çalıştırılmadan bildirilir ve bir kullanıcı hatası değil, bir program hatasıdır; kullanıcıya gösterilecek iletisi yoktur, geliştirme sırasında yakalanması gerekir.

Özet

  • Sorgu tabanlı erişimde alan seçimi istemciye geçer; tek uç nokta ve tek istek, ekran başına gidiş dönüş sayısını ekranın karmaşıklığından bağımsızlaştırır.
  • Adres artık ayırt edici değildir; önbellek anahtarı sorgu belgesi ile değişkenlerin birleşiminden üretilir.
  • Sorgu parçası, bir tür üzerinde adlandırılmış alan kümesidir; alan ihtiyacı onu kullanan kodun yanında bildirilir ve belgeye bir kez yazılır.
  • Normalizasyon iç içe yanıtı tür ile kimliğin birleştiği anahtarlarla düz bir depoya çevirir; birleştirme alan bazındadır ve bir ekranın isteği diğerinin verisini tazeler.
  • Normalleştirilmiş depo çevrimlidir; okuma her zaman bir seçim kümesiyle yapılır ve depoda bulunmayan alan eksik olarak bildirilir.
  • Kısmi başarı beşinci bir hata durumudur: kısmi veri, başarısız alan yolları ve zorunluluk kararı ayrı taşınır; hangi alanın vazgeçilebilir olduğunu ekran bilir.

Sonraki Adım

Buraya kadar veri her zaman istemcinin sorduğu anda geldi: sayfa açıldı, istek atıldı, yanıt işlendi. Kuzey Yamaç istasyonu ise ölçümü kimse sormadan üretir. Ölçüm geçmişi ekranı açıkken yeni bir kayıt oluşursa, kullanıcının bunu görmesi için sayfayı yenilemesi gerekmemeli. Bu, veri akışının yönünü tersine çevirir: bağlantıyı istemci kurar ama zamanlamayı sunucu belirler. Sonraki ders bu tersine akışın iki taşıyıcısını — tek yönlü olay akışını ve çift yönlü soketi — ve gelen olayların depoya nasıl işleneceğini ele alır.

İ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