---
title: 'Sayfalama Kalıpları'
source: 'https://academia.sh/tr/kurslar/api-tasarimi/sayfalama-kaliplari'
course: 'Web API Tasarımı'
language: tr
updated: '2026-08-17T18:06:45+00:00'
license: 'CC BY-SA 4.0'
---

# 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ı.

Ö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.

```js
// 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.

```js
// 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.

```js
// 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.

```js
// 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) },
};
```

```js
// 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.
