İçeriğe geç
academia.sh

Ders 31 / 34

Sayfalama Kalıpları

İmleç temelli bağlantı modelinin kurulması, kenar ile düğümün ayrılması, liste değişirken ofset ve imleç sayfalamanın ölçülen davranış farkı ve iç içe listelerde sayfalamanın çoğalması.

İçindekiler

Önceki dersin sorgusu elli ödünç kaydının tamamını tek yanıtta döndürdü. Kütüphanenin gerçek ödünç sayısı yüz binlerdir ve hiçbir istemci hepsini istemez.

Kaynak tasarımı konusunda sayfalama üç biçimde ele alınmıştı ama oradaki tasarım adres ve sorgu parametreleri üzerine kuruluydu: sayfalanacak şey bir uç noktaydı. Sorgu temelli yaklaşımda sayfalanacak şey bir alandır ve iç içe her listede yeniden ortaya çıkar. Bu ders alan düzeyinde sayfalamanın standartlaşmış biçimini kurar.

Ayrıştırıcı, düzleyici ve çalıştırıcı önceki derslerde yazıldıkları hâliyle kullanılır; şemaya bu derste bağlantı tipleri eklenir.

Bağlantı Modeli

Sayfalanan bir alan liste değil, bir bağlantı döndürür. Bağlantının üç parçası vardır.

  • Kenarlar, sayfadaki her öğe için bir kayıt tutar ve iki şey taşır: öğenin kendisi (düğüm) ve o öğenin listedeki konumunu gösteren imleç.
  • Sayfa bilgisi, gezinme durumunu bildirir: ileride ve geride sayfa olup olmadığı, sayfanın baş ve son imleci.
  • Toplam sayı gibi bağlantıya özgü alanlar isteğe bağlıdır.

Kenar ile düğümün ayrılması ilk bakışta gereksiz bir katman gibi görünür. Gerekçesi şudur: imleç öğenin değil, öğenin bu listedeki konumunun özelliğidir. Aynı ödünç kaydı iki ayrı listede farklı imleçler alır; imleç düğümün içine konsaydı bu olanaksızlaşırdı. Aynı yer ilişkiye özgü başka verileri de taşıyabilir — kaydın listeye eklenme anı, sıralamadaki puanı — ve bunların hiçbiri ödünç kaydının kendi alanı değildir.

// sayfalama.mjs — imlec temelli sayfalama; imlec siralama anahtarini tasir
// Kayitlar (olusturma, id) ikilisine gore siralidir; imlec bu ikilinin kodlanmis halidir.
export const imlecYaz = (k) => Buffer.from(`${k.olusturma}|${k.id}`).toString("base64url");
export const imlecOku = (i) => { const [olusturma, id] = Buffer.from(i, "base64url").toString().split("|"); return { olusturma, id }; };

const sirala = (a, b) => (a.olusturma === b.olusturma ? a.id.localeCompare(b.id) : a.olusturma.localeCompare(b.olusturma));

// Imlec temelli: "sonra" imlecinden buyuk ilk N kayit
export function imlecSayfasi(kayitlar, { ilk = 10, sonra = null }) {
  const sirali = [...kayitlar].sort(sirala);
  const baslangic = sonra ? sirali.findIndex((k) => sirala(k, imlecOku(sonra)) > 0) : 0;
  const dilim = baslangic < 0 ? [] : sirali.slice(baslangic, baslangic + ilk);
  return {
    kenarlar: dilim.map((k) => ({ imlec: imlecYaz(k), dugum: k })),
    sayfaBilgisi: {
      sonrakiSayfaVar: baslangic >= 0 && baslangic + ilk < sirali.length,
      oncekiSayfaVar: baslangic > 0,
      basImlec: dilim.length ? imlecYaz(dilim[0]) : null,
      sonImlec: dilim.length ? imlecYaz(dilim.at(-1)) : null,
    },
    toplamSayi: sirali.length,
  };
}

// Ofset temelli: siralamadaki N. kayittan basla
export function ofsetSayfasi(kayitlar, { ilk = 10, atla = 0 }) {
  const sirali = [...kayitlar].sort(sirala);
  return { kayitlar: sirali.slice(atla, atla + ilk), toplamSayi: sirali.length };
}

İmlecin kodlanmış olması bir gizleme değil, bir sözleşme kararıdır. İstemci imleci çözmemeli, yalnız geri göndermelidir; kodlama, içeriğin sözleşmenin parçası olmadığını görünür kılar. Sıralama anahtarı değiştiğinde imlecin yapısı da değişir ve bu, imleci ayrıştırmaya kalkmamış istemcileri etkilemez.

Liste Değişirken

Ofset ile imleç arasındaki fark, liste durağanken görünmez. Sayfalar arasında kayıt eklenip silindiğinde ortaya çıkar.

// kayma.mjs — sayfalar arasinda kayit eklenir/silinirse ofset ile imlecin davranisi
import { imlecSayfasi, ofsetSayfasi } from "./sayfalama.mjs";

const KAYITLAR = Array.from({ length: 9 }, (_, i) =>
  ({ id: `O-${String(i + 1).padStart(2, "0")}`, olusturma: `2026-01-${String(i + 1).padStart(2, "0")}` }));
const SAYFA = 3;

const DEGISIKLIKLER = {
  "başa ekleme": (v) => v.push({ id: "O-00", olusturma: "2026-01-00" }),
  "baştan silme": (v) => v.splice(v.findIndex((k) => k.id === "O-01"), 1),
};

const BICEMLER = {
  ofset: { baslangic: { atla: 0 }, al: (v, d) => ofsetSayfasi(v, { ilk: SAYFA, atla: d.atla }),
           idler: (s) => s.kayitlar.map((k) => k.id), sonraki: (s, d) => ({ atla: d.atla + SAYFA }) },
  imleç: { baslangic: { sonra: null }, al: (v, d) => imlecSayfasi(v, { ilk: SAYFA, sonra: d.sonra }),
           idler: (s) => s.kenarlar.map((k) => k.dugum.id), sonraki: (s) => ({ sonra: s.sayfaBilgisi.sonImlec }) },
};

for (const [degisiklikAdi, degistir] of Object.entries(DEGISIKLIKLER)) {
  console.log(`ilk sayfa okunduktan sonra: ${degisiklikAdi} (sayfa boyu ${SAYFA}, başlangıçta 9 kayıt)`);
  for (const [bicemAdi, b] of Object.entries(BICEMLER)) {
    const veri = [...KAYITLAR];
    const gorulen = [];
    let durum = b.baslangic;
    for (let sayfa = 0; sayfa < 4; sayfa++) {
      const s = b.al(veri, durum);
      const idler = b.idler(s);
      if (!idler.length) break;
      gorulen.push(...idler);
      if (sayfa === 0) degistir(veri);
      durum = b.sonraki(s, durum);
    }
    const tekil = new Set(gorulen);
    const atlanan = KAYITLAR.map((k) => k.id).filter((id) => !tekil.has(id) && veri.some((k) => k.id === id));
    console.log(`  ${bicemAdi}: ${gorulen.join(" ")}`);
    console.log(`  ${bicemAdi}: görülen=${gorulen.length} tekil=${tekil.size} yinelenen=${gorulen.length - tekil.size} atlanan=${atlanan.join(",") || "yok"}`);
  }
  console.log();
}
ilk sayfa okunduktan sonra: başa ekleme (sayfa boyu 3, başlangıçta 9 kayıt)
  ofset: O-01 O-02 O-03 O-03 O-04 O-05 O-06 O-07 O-08 O-09
  ofset: görülen=10 tekil=9 yinelenen=1 atlanan=yok
  imleç: O-01 O-02 O-03 O-04 O-05 O-06 O-07 O-08 O-09
  imleç: görülen=9 tekil=9 yinelenen=0 atlanan=yok

ilk sayfa okunduktan sonra: baştan silme (sayfa boyu 3, başlangıçta 9 kayıt)
  ofset: O-01 O-02 O-03 O-05 O-06 O-07 O-08 O-09
  ofset: görülen=8 tekil=8 yinelenen=0 atlanan=O-04
  imleç: O-01 O-02 O-03 O-04 O-05 O-06 O-07 O-08 O-09
  imleç: görülen=9 tekil=9 yinelenen=0 atlanan=yok

Ofset iki ayrı biçimde yanılıyor. Listeye kayıt eklendiğinde her şey bir konum kayar ve ikinci sayfa birinci sayfanın son kaydını yeniden gösterir; O-03 iki kez görülmüştür. Kayıt silindiğinde ters yönde kayma olur ve bir kayıt hiç görülmez; O-04 atlanmıştır.

İmleç iki durumda da doğru gezindi. Nedeni, sorulan sorunun farklı olmasıdır: ofset “baştan altıncı kayıttan itibaren ver” diye sorar ve baş hareket ettiğinde yanlış yeri gösterir; imleç “şu kayıttan sonrasını ver” diye sorar ve o kayıt yerinde durduğu sürece doğru yanıtı alır.

Atlanan kaydın maliyeti yinelenen kayıttan yüksektir. Yinelenme kullanıcıya görünür ve düzeltilebilir; atlama sessizdir. Bir dışa aktarma işi ofsetle sayfalanıyorsa, eksik kayıtla biten bir dosya hiçbir hata üretmez.

Bağlantının Çalıştırılması

Bağlantı ancak şemada tanımlıysa sorulabilir. Kenar, sayfa bilgisi ve bağlantının kendisi sıradan nesne tipleridir; ayrıcalıklı bir yanları yoktur. Şema ve Tip Sistemi dersindeki tip kaydı olduğu gibi kalır, üzerine bu üç tipi ve ikisi de bağlantı döndüren iki alanı ekleyen bir uzantı konur.

// sema-baglanti.mjs — 01. dersteki semaya baglanti tipleri eklenmis hali
import { SEMA } from "./sema.mjs";

const BAGLANTI_ALANI = { tip: "OduncBaglantisi!", argumanlar: { ilk: "Int", sonra: "String" } };

export const SEMA_B = {
  ...SEMA,
  tipler: {
    ...SEMA.tipler,
    Boolean:        { tur: "skaler" },
    SayfaBilgisi:   { tur: "nesne", alanlar: { sonrakiSayfaVar: "Boolean!", oncekiSayfaVar: "Boolean!",
                                               basImlec: "String", sonImlec: "String" } },
    OduncKenari:    { tur: "nesne", alanlar: { imlec: "String!", dugum: "Odunc!" } },
    OduncBaglantisi:{ tur: "nesne", alanlar: { kenarlar: "[OduncKenari!]!",
                                               sayfaBilgisi: "SayfaBilgisi!", toplamSayi: "Int!" } },

    Uye:   { ...SEMA.tipler.Uye,
             alanlar: { ...SEMA.tipler.Uye.alanlar, oduncBaglantisi: BAGLANTI_ALANI } },
    Sorgu: { ...SEMA.tipler.Sorgu,
             alanlar: { ...SEMA.tipler.Sorgu.alanlar, oduncBaglantisi: BAGLANTI_ALANI } },
  },
};

Veri kaynağı dokuz ödünç kaydı ve üç üye içerir. Bağlantı çözücüleri kendileri sayfalama yapmaz; kayıtları sayfalama.mjs içindeki imleç sayfasına verir ve dönen yapıyı olduğu gibi döndürürler. Kenar, sayfa bilgisi ve toplam sayı alanları için çözücü yazılmamıştır, çünkü dönen yapı bu adları zaten taşır.

// veri.mjs — dokuz odunc kaydi, uc uye ve baglanti cozuculeri
import { imlecSayfasi } from "./sayfalama.mjs";

export const UYELER = new Map(Array.from({ length: 3 }, (_, i) =>
  [`U-${1001 + i}`, { id: `U-${1001 + i}`, olusturma: "2025-01-01", ad: `Üye ${i}` }]));

export const ODUNCLER = Array.from({ length: 9 }, (_, i) => ({
  id: `O-${String(i + 1).padStart(2, "0")}`, olusturma: `2026-01-${String(i + 1).padStart(2, "0")}`,
  uyeId: `U-${1001 + (i % 3)}`, durum: i % 3 === 0 ? "KAPALI" : "ACIK", iadeTarihi: "2026-04-01",
  kalemler: [{ isbn: "978-0262033848", sube: "merkez" }],
}));

const sayfala = (kayitlar, a) => imlecSayfasi(kayitlar, { ilk: a.ilk ?? 10, sonra: a.sonra ?? null });

export const COZUCULER = {
  __tip: { Kayit: (d) => (d.kalemler ? "Odunc" : "Uye") },
  Sorgu: {
    uye: (_, a) => UYELER.get(a.kimlik) ?? null,
    oduncBaglantisi: (_, a) => sayfala(ODUNCLER, a),
  },
  Uye:   { oduncBaglantisi: (u, a) => sayfala(ODUNCLER.filter((o) => o.uyeId === u.id), a) },
  Odunc: { uye: (o) => UYELER.get(o.uyeId) },
};
// baglanti.mjs — baglanti modelini calistirir; ic ice sayfalamayi olcer
import { ayristir } from "./ayristirici.mjs";
import { calistir } from "./calistirici.mjs";
import { SEMA_B as SEMA } from "./sema-baglanti.mjs";
import { COZUCULER } from "./veri.mjs";

const SAYFA = `query($ilk: Int!, $sonra: String) {
  oduncBaglantisi(ilk: $ilk, sonra: $sonra) {
    toplamSayi
    sayfaBilgisi { sonrakiSayfaVar oncekiSayfaVar sonImlec }
    kenarlar { imlec dugum { id durum } }
  }
}`;

// Imleci izleyerek butun sayfalari gez
let sonra = null, sayfa = 0, toplamKenar = 0;
while (true) {
  const s = await calistir(SEMA, COZUCULER, ayristir(SAYFA), { degiskenler: { ilk: 4, sonra } });
  const b = s.data.oduncBaglantisi;
  toplamKenar += b.kenarlar.length;
  console.log(`sayfa ${++sayfa}: ${b.kenarlar.map((k) => k.dugum.id).join(" ")}   sonraki=${b.sayfaBilgisi.sonrakiSayfaVar}  önceki=${b.sayfaBilgisi.oncekiSayfaVar}`);
  if (!b.sayfaBilgisi.sonrakiSayfaVar) { console.log(`toplam kayıt=${b.toplamSayi}  gezilen kenar=${toplamKenar}`); break; }
  sonra = b.sayfaBilgisi.sonImlec;
}

console.log(`\nilk sayfanın ilk kenarı: ${JSON.stringify((await calistir(SEMA, COZUCULER, ayristir(SAYFA), { degiskenler: { ilk: 1, sonra: null } })).data.oduncBaglantisi.kenarlar[0])}`);

// Ic ice sayfalama: her uyenin kendi odunc baglantisi ayri sayfalanir
const ICICE = `{
  u1: uye(kimlik: "U-1001") { ad oduncBaglantisi(ilk: 2) { toplamSayi kenarlar { dugum { id } } } }
  u2: uye(kimlik: "U-1002") { ad oduncBaglantisi(ilk: 2) { toplamSayi kenarlar { dugum { id } } } }
}`;
const ic = (await calistir(SEMA, COZUCULER, ayristir(ICICE))).data;
for (const [anahtar, u] of Object.entries(ic)) {
  console.log(`${anahtar} ${u.ad}: ${u.oduncBaglantisi.kenarlar.map((k) => k.dugum.id).join(" ")}  (toplam ${u.oduncBaglantisi.toplamSayi})`);
}
sayfa 1: O-01 O-02 O-03 O-04   sonraki=true  önceki=false
sayfa 2: O-05 O-06 O-07 O-08   sonraki=true  önceki=true
sayfa 3: O-09   sonraki=false  önceki=true
toplam kayıt=9  gezilen kenar=9

ilk sayfanın ilk kenarı: {"imlec":"MjAyNi0wMS0wMXxPLTAx","dugum":{"id":"O-01","durum":"KAPALI"}}
u1 Üye 0: O-01 O-04  (toplam 3)
u2 Üye 1: O-02 O-05  (toplam 3)

Üç sayfada dokuz kenar gezildi; yinelenme yok, atlama yok. İmleç değeri kodlanmış hâliyle görünüyor ve içeriğinin okunmasının istenmediği buradan anlaşılıyor.

Son iki satır sorgu temelli sayfalamanın kaynak temelli sayfalamadan ayrıldığı yeri gösteriyor. İki üyenin ödünç bağlantısı ayrı ayrı sayfalandı; her birinin kendi toplamSayi değeri, kendi kenarları var. Bir uç nokta tasarımında bu, iki ayrı istek demektir; burada tek sorgunun iki dalıdır.

Bunun sonucu, sayfalamanın artık tek bir sınır olmamasıdır. Kök alandan ilk: 20 üye istenip her üyenin ilk: 20 ödüncü istenirse yanıt dört yüz ödünç kaydı taşır. Her alan kendi sınırına uyar ve hepsi uyduğu hâlde toplam beklenenden büyük çıkar. Sayfalama tek başına yanıtın boyutunu sınırlamaz.

Özet

  • Sayfalanan alan liste değil bağlantı döndürür; bağlantı kenarlardan, sayfa bilgisinden ve bağlantıya özgü alanlardan oluşur.
  • İmleç öğenin değil öğenin o listedeki konumunun özelliğidir; kenar katmanı bu yüzden vardır ve ilişkiye özgü başka verileri de taşıyabilir.
  • İmleç kodlanmış tutulur; bu bir gizleme değil, içeriğinin sözleşmeye dahil olmadığını bildiren bir karardır.
  • Liste değişirken ofset sayfalama kayıt eklenmesinde yinelenme, silinmesinde atlama üretir; imleç sayfalama iki durumda da doğru gezinir.
  • Atlanan kaydın maliyeti yinelenenden yüksektir, çünkü atlama sessizdir ve hiçbir hata üretmez.
  • Sayfalama alan başına uygulanır; iç içe listelerde her alan kendi sınırına uyar ama toplam yanıt boyutu sınırlardan bağımsız olarak büyüyebilir.

Sonraki Adım

Bağlantı modeli yanıtın hangi kayıtları taşıyacağını düzenledi ama bir varsayımı korudu: sorulan her alan okunabilir ve her alan bir değer döndürür. Kütüphanede bu doğru değildir. Bir üyenin ceza bilgisini yalnız o üye ve görevliler görebilir; ödünç kaydının notu yalnız kaydı açan şubede okunabilir. Bir alanın yetkisiz olması, bütün sorgunun reddedilmesini gerektirmez — yanıtın geri kalanı hâlâ geçerlidir. Sonraki ders alan düzeyinde yetkilendirmeyi kurar, tek bir alanın çökmesinin yanıtın nesine dokunduğunu gösterir ve kısmi veriyle hatanın aynı yanıtta nasıl birlikte döndüğü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