---
title: 'Veri Erişim Katmanı ve İş Mantığı'
source: 'https://academia.sh/tr/kurslar/veri-erisim-katmani'
language: tr
updated: '2026-08-17T04:38:51+00:00'
license: 'CC BY-SA 4.0'
lessons: 21
outcomes:
    - 'Veri erişimini iş mantığından ayıran bir katman kurar'
    - 'İşlem sınırlarını doğru yerde tanımlar'
    - 'Yaygın veri erişim başarım sorunlarını tanır ve giderir'
---

# Veri Erişim Katmanı ve İş Mantığı

Bir isteğin veriye inen yolunun kurulması: doğrudan sorgu ile eşleyici arasındaki ödünleşim, depo kalıbı, bağlantı havuzu ve şema göçleri, işlem sınırları ve kilitleme, N+1 ile sayfalama gibi başarım sorunları, iş mantığının katmanlara yerleşimi.

## Veri Erişim Yaklaşımları

Doğrudan sorgu ile eşleyici arasındaki ödünleşim, nesne modeliyle ilişkisel modelin uyuşmayan noktaları, kalıcılık ayrıntısını gizleyen depo kalıbı, bileşik sorgu kurma, bağlantı havuzunun doygunluk davranışı ve şema göçlerinin ileri–geri yürütülmesi.

### Doğrudan Sorgu ve Eşleyici

Source: https://academia.sh/tr/kurslar/veri-erisim-katmani/dogrudan-sorgu-ve-esleyici

Aynı listenin iki yoldan kurulması: elle yazılan SQL ile bildirilen eşlemeden SQL üreten küçük bir eşleyici, üretilen sorguların sayılması ve iki yaklaşımın denetim–tekrar ödünleşimi.

Kimlik Doğrulama ve Yetkilendirme kursu bir soruyu kapattı: bir isteği kimin yapabileceği,
hangi kaynağa hangi rolle dokunabileceği artık karara bağlanmış durumda. Denetimden geçen
istek uygulamaya girer ve orada bekleyen ikinci soruyla karşılaşır. Çağrı veriye nasıl
inecek?

Bu kurs o inişi kurar. İlk adım en alttaki katmandır: uygulamadaki nesnelerle
veritabanındaki satırlar arasındaki çeviri. Çeviriyi elle yazmak da, bir eşleyiciye
(mapper) bırakmak da mümkündür. İkisi de aynı listeyi üretir; ürettikleri SQL ve
çalıştırdıkları sorgu sayısı aynı değildir. Bu ders farkı ölçer.

#### Ortak Görev ve Şema

Kurs boyunca kullanılan şema, SQL Temelleri kursundaki kütüphane şemasıdır: şube, kitap,
üye ve ödünç bağıntıları. Aşağıdaki blok veritabanını sıfırdan kurar; bu dersin bütün
örnekleri bu dosya üzerinde çalışır.

```sh
rm -f kutuphane.db
sqlite3 kutuphane.db <<'SQL'
CREATE TABLE sube (sube_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, sehir TEXT NOT NULL);
CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL, yazar TEXT NOT NULL,
                    basim_yili INTEGER, sube_id INTEGER REFERENCES sube(sube_id));
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, soyad TEXT NOT NULL,
                  eposta TEXT, kayit_tarihi TEXT NOT NULL);
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL REFERENCES kitap(kitap_id),
                    uye_id INTEGER NOT NULL REFERENCES uye(uye_id), alis_tarihi TEXT NOT NULL,
                    iade_tarihi TEXT);
INSERT INTO sube VALUES (1,'Merkez','Ankara'),(2,'Bahçelievler','Ankara'),
  (3,'Kadıköy','İstanbul'),(4,'Konak','İzmir');
INSERT INTO kitap VALUES (1,'Körlük','José Saramago',1995,1),(2,'Tutunamayanlar','Oğuz Atay',1972,1),
  (3,'Kum Kitabı','Jorge Luis Borges',1975,2),(4,'Yaban','Yakup Kadri',1932,2),
  (5,'Sessiz Ev','Orhan Pamuk',1983,3),(6,'Anayurt Oteli','Yusuf Atılgan',NULL,3),
  (7,'Tehlikeli Oyunlar','Oğuz Atay',1973,NULL);
INSERT INTO uye VALUES (1,'Ayşe','Demir','ayse@ornek.test','2023-02-14'),
  (2,'Mehmet','Kaya','mehmet@ornek.test','2023-05-30'),(3,'Zeynep','Arslan',NULL,'2024-01-09'),
  (4,'Emre','Yıldız','emre@ornek.test','2024-03-22'),(5,'Selin','Aydın',NULL,'2024-11-05'),
  (6,'Burak','Şahin','burak@ornek.test','2025-01-18');
INSERT INTO odunc VALUES (1,1,1,'2025-01-10','2025-01-24'),(2,2,1,'2025-02-02','2025-02-20'),
  (3,1,2,'2025-02-11',NULL),(4,3,3,'2025-03-01','2025-03-15'),(5,4,3,'2025-03-18','2025-04-02'),
  (6,1,4,'2025-04-05','2025-04-19'),(7,5,4,'2025-04-21',NULL),(8,2,5,'2025-05-02','2025-05-30'),
  (9,7,1,'2025-05-14','2025-05-28'),(10,3,5,'2025-06-03',NULL),(11,6,2,'2025-06-11','2025-06-25'),
  (12,4,4,'2025-06-20','2025-07-04');
SQL
```

Görev şudur: iade edilmemiş ödünç kayıtlarını, kitabın başlığı ve üyenin adıyla birlikte
listele. Bu, ödünç servisinin en sık çağrılan okuma işidir; kurs boyunca aynı görev
farklı katmanlardan geçerek yeniden ele alınacak.

#### Doğrudan Sorgu

İlk yaklaşımda sorgu elle yazılır. Veritabanı üç bağıntıyı tek adımda birleştirir, uygulama
gelen satırları kendi nesnesine çevirir.

```js
// dogrudan.mjs — acik odunc listesini tek sorguyla kurar
import { DatabaseSync } from "node:sqlite";

const db = new DatabaseSync("kutuphane.db");
let sorguSayisi = 0;

const sql = `SELECT o.odunc_id, o.alis_tarihi, k.baslik, u.ad, u.soyad
             FROM odunc o
             JOIN kitap k ON k.kitap_id = o.kitap_id
             JOIN uye  u ON u.uye_id  = o.uye_id
             WHERE o.iade_tarihi IS NULL
             ORDER BY o.odunc_id`;

sorguSayisi += 1;
const satirlar = db.prepare(sql).all();

// Satir -> nesne cevrimi elle yazilir.
const acikOdunc = satirlar.map((s) => ({
  id: s.odunc_id,
  alisTarihi: s.alis_tarihi,
  kitapBasligi: s.baslik,
  uyeAdi: `${s.ad} ${s.soyad}`,
}));

for (const o of acikOdunc) console.log(`${o.id}  ${o.alisTarihi}  ${o.kitapBasligi}  ${o.uyeAdi}`);
console.log("calistirilan sorgu sayisi:", sorguSayisi);
```

```sh
node dogrudan.mjs
```

```
3  2025-02-11  Körlük  Mehmet Kaya
7  2025-04-21  Sessiz Ev  Emre Yıldız
10  2025-06-03  Kum Kitabı  Selin Aydın
calistirilan sorgu sayisi: 1
```

Tek sorgu. Veritabanına giden istek sayısı bir, dönen satır sayısı üç, aktarılan sütun
sayısı beş. Bu yaklaşımın kazancı doğrudan görünür: sorgu metni gözle okunabilir, plan
üzerinde denetim tamdır, sonuç kümesinin biçimi yazan kişinin elindedir.

Bedeli de aynı yerdedir. `satirlar.map` çağrısındaki çevrim elle yazılmıştır. Ödünç
kaydına yeni bir sütun eklendiğinde hem SQL hem çevrim güncellenir. Uygulamada bu görevin
on beş varyantı varsa, çevrim on beş kez yazılır.

#### Bildirilen Eşleme

İkinci yaklaşım çevrimi bir kez bildirir. Hangi tipin hangi tabloya, hangi alanın hangi
sütuna karşılık geldiği bir eşleme tablosunda durur; SQL o tablodan üretilir.

```js
// esleyici.mjs — bildirilen esleme tablosundan SQL ureten kucuk esleyici
import { DatabaseSync } from "node:sqlite";

const db = new DatabaseSync("kutuphane.db");
export const uretilen = [];

const esleme = {
  Odunc: {
    tablo: "odunc", anahtar: "odunc_id",
    alanlar: { id: "odunc_id", alisTarihi: "alis_tarihi", iadeTarihi: "iade_tarihi" },
    iliskiler: { kitap: { hedef: "Kitap", yerel: "kitap_id" },
                 uye:   { hedef: "Uye",   yerel: "uye_id" } },
  },
  Kitap: { tablo: "kitap", anahtar: "kitap_id",
           alanlar: { id: "kitap_id", baslik: "baslik", yazar: "yazar" }, iliskiler: {} },
  Uye:   { tablo: "uye", anahtar: "uye_id",
           alanlar: { id: "uye_id", ad: "ad", soyad: "soyad" }, iliskiler: {} },
};

const secimListesi = (tip) => [
  ...Object.values(esleme[tip].alanlar),
  ...Object.values(esleme[tip].iliskiler).map((i) => i.yerel),
].join(", ");

function calistir(sql, ...bagli) {
  uretilen.push(sql);
  return db.prepare(sql).all(...bagli);
}

function nesneyeCevir(tip, satir) {
  const m = esleme[tip];
  const nesne = {};
  for (const [ad, sutun] of Object.entries(m.alanlar)) nesne[ad] = satir[sutun];
  // Iliski, okundugu anda yuklenir: getter cagrildikca sorgu calisir.
  for (const [ad, iliski] of Object.entries(m.iliskiler)) {
    const kimlik = satir[iliski.yerel];
    Object.defineProperty(nesne, ad, { get: () => bul(iliski.hedef, kimlik) });
  }
  return nesne;
}

export function bul(tip, kimlik) {
  const m = esleme[tip];
  const satir = calistir(`SELECT ${secimListesi(tip)} FROM ${m.tablo} WHERE ${m.anahtar} = ?`, kimlik)[0];
  return satir ? nesneyeCevir(tip, satir) : null;
}

export function ara(tip, kosul) {
  const m = esleme[tip];
  const sql = `SELECT ${secimListesi(tip)} FROM ${m.tablo} WHERE ${kosul} ORDER BY ${m.anahtar}`;
  return calistir(sql).map((satir) => nesneyeCevir(tip, satir));
}
```

Eşleyicinin yaptığı iş üç maddede toplanır: eşlemeden seçim listesi kurar, sonucu nesneye
çevirir, ilişkileri okunduğu anda yükler. Son madde kritik olandır. `kitap` özelliği
gerçek bir değer değil, çağrıldığında sorgu çalıştıran bir okuyucudur. Bu davranışa
**tembel yükleme** (lazy loading) denir.

Çağıran taraf artık SQL görmez.

```js
// esleyici-kullan.mjs — ayni listeyi esleyiciyle kurar ve uretilen SQL'i sayar
import { ara, uretilen } from "./esleyici.mjs";

for (const o of ara("Odunc", "iade_tarihi IS NULL")) {
  console.log(`${o.id}  ${o.alisTarihi}  ${o.kitap.baslik}  ${o.uye.ad} ${o.uye.soyad}`);
}

console.log("calistirilan sorgu sayisi:", uretilen.length);
for (const sql of uretilen) console.log("  " + sql.replace(/\s+/g, " "));
```

```sh
node esleyici-kullan.mjs
```

```
3  2025-02-11  Körlük  Mehmet Kaya
7  2025-04-21  Sessiz Ev  Emre Yıldız
10  2025-06-03  Kum Kitabı  Selin Aydın
calistirilan sorgu sayisi: 10
  SELECT odunc_id, alis_tarihi, iade_tarihi, kitap_id, uye_id FROM odunc WHERE iade_tarihi IS NULL ORDER BY odunc_id
  SELECT kitap_id, baslik, yazar FROM kitap WHERE kitap_id = ?
  SELECT uye_id, ad, soyad FROM uye WHERE uye_id = ?
  SELECT uye_id, ad, soyad FROM uye WHERE uye_id = ?
  SELECT kitap_id, baslik, yazar FROM kitap WHERE kitap_id = ?
  SELECT uye_id, ad, soyad FROM uye WHERE uye_id = ?
  SELECT uye_id, ad, soyad FROM uye WHERE uye_id = ?
  SELECT kitap_id, baslik, yazar FROM kitap WHERE kitap_id = ?
  SELECT uye_id, ad, soyad FROM uye WHERE uye_id = ?
  SELECT uye_id, ad, soyad FROM uye WHERE uye_id = ?
```

Çıktı aynı, sorgu sayısı bir yerine on. Sayı iki kaynaktan gelir. Birincisi tembel
yüklemenin doğası: üç ödünç kaydının her biri için kitap ve üye ayrı ayrı okunur. İkincisi
daha sinsidir. Yazdırma satırında `o.uye.ad` ve `o.uye.soyad` iki ayrı okuma yapar; her
okuma okuyucuyu yeniden çalıştırır, dolayısıyla üye sorgusu ödünç başına iki kez döner.
Üçü birlikte 1 + 3 + 6 = 10 eder.

Bu, eşleyici kullanmanın merkezî riskidir. **Sorgu, kod okunurken görünmez.** Bir özellik
erişimi ile bir veritabanı gidiş-dönüşü arasındaki fark, yazan kişinin gözünden kalkmıştır.

#### Kimlik Haritası

Sayının bir kısmı eşleyicinin kendi içinde kapatılabilir. Aynı tipten aynı kimliğe sahip
satır bir kez okunup saklanırsa, ikinci erişim sorgu üretmez. Yüklenmiş nesneleri kimliğine
göre tutan bu yapıya **kimlik haritası** (identity map) denir.

```js
// esleyici-kimlik.mjs — yuklenen nesneleri kimligine gore saklayan surum
import { DatabaseSync } from "node:sqlite";

const db = new DatabaseSync("kutuphane.db");
export const uretilen = [];
const kimlikHaritasi = new Map(); // tip:kimlik -> nesne

const esleme = {
  Odunc: {
    tablo: "odunc", anahtar: "odunc_id",
    alanlar: { id: "odunc_id", alisTarihi: "alis_tarihi", iadeTarihi: "iade_tarihi" },
    iliskiler: { kitap: { hedef: "Kitap", yerel: "kitap_id" },
                 uye:   { hedef: "Uye",   yerel: "uye_id" } },
  },
  Kitap: { tablo: "kitap", anahtar: "kitap_id",
           alanlar: { id: "kitap_id", baslik: "baslik", yazar: "yazar" }, iliskiler: {} },
  Uye:   { tablo: "uye", anahtar: "uye_id",
           alanlar: { id: "uye_id", ad: "ad", soyad: "soyad" }, iliskiler: {} },
};

const secimListesi = (tip) => [
  ...Object.values(esleme[tip].alanlar),
  ...Object.values(esleme[tip].iliskiler).map((i) => i.yerel),
].join(", ");

function calistir(sql, ...bagli) {
  uretilen.push(sql);
  return db.prepare(sql).all(...bagli);
}

function nesneyeCevir(tip, satir) {
  const m = esleme[tip];
  const nesne = {};
  for (const [ad, sutun] of Object.entries(m.alanlar)) nesne[ad] = satir[sutun];
  for (const [ad, iliski] of Object.entries(m.iliskiler)) {
    const kimlik = satir[iliski.yerel];
    Object.defineProperty(nesne, ad, { get: () => bul(iliski.hedef, kimlik) });
  }
  return nesne;
}

export function bul(tip, kimlik) {
  const kutu = `${tip}:${kimlik}`;
  if (kimlikHaritasi.has(kutu)) return kimlikHaritasi.get(kutu);
  const m = esleme[tip];
  const satir = calistir(`SELECT ${secimListesi(tip)} FROM ${m.tablo} WHERE ${m.anahtar} = ?`, kimlik)[0];
  const nesne = satir ? nesneyeCevir(tip, satir) : null;
  kimlikHaritasi.set(kutu, nesne);
  return nesne;
}

export function ara(tip, kosul) {
  const m = esleme[tip];
  const sql = `SELECT ${secimListesi(tip)} FROM ${m.tablo} WHERE ${kosul} ORDER BY ${m.anahtar}`;
  return calistir(sql).map((satir) => nesneyeCevir(tip, satir));
}
```

```js
// kimlik-kullan.mjs — kimlik haritali surumle ayni listeyi kurar
import { ara, uretilen } from "./esleyici-kimlik.mjs";

for (const o of ara("Odunc", "iade_tarihi IS NULL")) {
  console.log(`${o.id}  ${o.alisTarihi}  ${o.kitap.baslik}  ${o.uye.ad} ${o.uye.soyad}`);
}

console.log("calistirilan sorgu sayisi:", uretilen.length);
```

```sh
node kimlik-kullan.mjs
```

```
3  2025-02-11  Körlük  Mehmet Kaya
7  2025-04-21  Sessiz Ev  Emre Yıldız
10  2025-06-03  Kum Kitabı  Selin Aydın
calistirilan sorgu sayisi: 7
```

On sorgu yediye indi: aynı üyeye iki kez dokunmak artık bir kez okuyor. Kimlik haritası
ikinci bir güvence daha verir. Aynı satır için tek bir nesne döndüğünden, bir yerde
yapılan değişiklik başka bir yerde okunan kopyada da görünür; iki farklı kopyanın
birbirinin üzerine yazması olasılığı ortadan kalkar.

Yedi sayısı hâlâ birden büyüktür ve bu sayı listedeki kayıt sayısıyla birlikte artar.
Üç kayıtta yedi, üç yüz kayıtta yüzlerce sorgu demektir. Bu tabloya **N+1 sorgu sorunu**
denir; Başarım Sorunları konusunda ölçülüp giderilecek.

#### Ödünleşim Nerede Duruyor

İki yaklaşım arasındaki seçim "hangisi daha iyi" sorusuna indirgenmez; ne tür maliyetin
düşürüldüğüne bakılır.

Doğrudan sorgu **denetimi** maksimize eder. Çalışacak SQL yazıldığı gibidir; hangi
sütunların döndüğü, birleştirmenin nasıl kurulduğu, planın neye benzeyeceği görünür.
Karmaşık raporlar, toplama işlevleri ve elle ayarlanmış birleştirmeler bu tarafta kalır.
Bedeli tekrardır: her okuma işi için çevrim kodu yeniden yazılır.

Eşleyici **tekrarı** düşürür. Eşleme bir kez bildirilir, onlarca okuma işi ondan üretilir;
şemaya sütun eklemek tek bir yerde değişiklik ister. Bedeli görünürlüğün kaybıdır: kaç
sorgu çalıştığı ancak sayılarak öğrenilir.

Pratikte iki yaklaşım aynı uygulamada yan yana durur. Ayrım işin türüne göre yapılır:
kayıt yaşam döngüsü ve tekil nesne okumaları eşleyiciye, çok tablolu raporlar ve toplu
işlemler doğrudan sorguya bırakılır. Kararın koşulu ölçülebilirliktir. Hangi yaklaşım
seçilirse seçilsin, üretilen SQL'i ve sorgu sayısını görebildiğin bir kanca bulunmalıdır;
bu dersteki `uretilen` dizisi o kancanın en küçük biçimidir.

#### Özet

- Aynı okuma işi doğrudan yazılan SQL ile de, bildirilen eşlemeden SQL üreten bir
  eşleyiciyle de kurulabilir; iki yol aynı çıktıyı verir.
- Ölçüm farkı gösterdi: doğrudan sorgu 1, tembel yüklemeli eşleyici 10, kimlik haritası
  eklenmiş eşleyici 7 sorgu çalıştırdı.
- Tembel yükleme, özellik erişimini görünmez bir veritabanı gidiş-dönüşüne çevirir; aynı
  ilişkiye iki kez dokunmak iki sorgu üretir.
- Kimlik haritası aynı kimliğin tek nesneyle temsil edilmesini sağlar, yinelenen okumaları
  keser ve kopyaların birbirini ezmesini önler.
- Doğrudan sorgu denetimi, eşleyici tekrarı azaltır; seçim işin türüne göre yapılır ve her
  iki durumda da üretilen SQL sayılabilir olmalıdır.

#### Sonraki Adım

Eşleyicinin ürettiği SQL beklenenden fazla sorgu çalıştırdı, ama asıl sorun sayıda değil.
Eşleme tablosunda `Odunc` tipi ile `odunc` bağıntısı bire bir örtüşüyordu; her alan bir
sütuna, her ilişki bir yabancı anahtara denk düştü. Gerçek alan modellerinde bu örtüşme
kurulmaz: kalıtım hiyerarşisinin tek bir tablo karşılığı yoktur, nesne kimliği ile birincil
anahtar aynı şey değildir, çift yönlü bir ilişkinin ilişkisel modelde tek bir sahibi
bulunur. Sonraki ders bu uyuşmazlıkların nereden çıktığını ve her birinin hangi eşleme
kararıyla kapatıldığını inceler.

### Nesne–İlişki Uyumsuzluğu

Source: https://academia.sh/tr/kurslar/veri-erisim-katmani/nesne-iliski-uyumsuzlugu

Nesne modeli ile ilişkisel modelin örtüşmediği dört nokta: kimlik, kalıtım, ilişki yönü ve tanecik. Her uyumsuzluğun eşleme kararıyla nasıl kapatıldığı ölçülerek gösterilir.

Önceki ders küçük bir eşleyici kurdu ve orada `Odunc` tipi ile `odunc` bağıntısı bire bir
örtüştü: her alan bir sütun, her ilişki bir yabancı anahtardı. Bu örtüşme kolay bir örnekten
gelir, kuraldan değil.

Nesne modeli ile ilişkisel model iki ayrı kuramın ürünüdür. Nesne modelinde kimlik
başvurudur, kalıtım vardır, bağlar iki yönlüdür ve bir nesnenin içine başka nesneler
gömülebilir. İlişkisel modelde kimlik anahtar değeridir, kalıtım yoktur, bağ yabancı
anahtarla tek yönlü kurulur ve tablo düz bir demet listesidir. Bu iki modelin örtüşmeyen
noktalarına **nesne–ilişki uyumsuzluğu** (object–relational impedance mismatch) denir.
Uyumsuzluk bir hata değil, kapatılması gereken bir açıklıktır. Ders dört açıklığı sırayla
ölçer.

Aşağıdaki blok bu dersin ilk bölümünde kullanılan veritabanını kurar.

```sh
rm -f kutuphane.db
sqlite3 kutuphane.db <<'SQL'
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, soyad TEXT NOT NULL,
                  eposta TEXT, kayit_tarihi TEXT NOT NULL);
CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL, yazar TEXT NOT NULL,
                    basim_yili INTEGER);
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL REFERENCES kitap(kitap_id),
                    uye_id INTEGER NOT NULL REFERENCES uye(uye_id), alis_tarihi TEXT NOT NULL,
                    iade_tarihi TEXT);
INSERT INTO uye VALUES (1,'Ayşe','Demir','ayse@ornek.test','2023-02-14'),
  (2,'Mehmet','Kaya','mehmet@ornek.test','2023-05-30'),(3,'Zeynep','Arslan',NULL,'2024-01-09');
INSERT INTO kitap VALUES (1,'Körlük','José Saramago',1995),(6,'Anayurt Oteli','Yusuf Atılgan',NULL);
INSERT INTO odunc VALUES (3,1,2,'2025-02-11',NULL),(11,6,2,'2025-06-11','2025-06-25');
SQL
```

#### Kimlik Uyumsuzluğu

Bir bağıntıda iki satırın "aynı" olması birincil anahtarlarının eşit olması demektir.
Nesne dünyasında ise iki tür eşitlik vardır: aynı başvuru olmak ve aynı değeri taşımak.
Eşleyici satırı nesneye çevirirken bu iki eşitlik ayrışır.

```js
// kimlik-farki.mjs — ayni satirin iki kez okunmasi iki ayri nesne uretir
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("kutuphane.db");

const oku = (id) => db.prepare("SELECT uye_id, ad, soyad FROM uye WHERE uye_id = ?").get(id);

const a = oku(2);
const b = oku(2);

console.log("ayni satir mi (birincil anahtar):", a.uye_id === b.uye_id);
console.log("ayni nesne mi (basvuru):        ", a === b);

a.ad = "Mehmet Ali";
console.log("a.ad:", a.ad, "| b.ad:", b.ad);

const harita = new Map();
const okuHaritali = (id) => {
  if (!harita.has(id)) harita.set(id, oku(id));
  return harita.get(id);
};
const c = okuHaritali(2), d = okuHaritali(2);
c.ad = "Mehmet Ali";
console.log("kimlik haritasiyla ayni nesne mi:", c === d, "| d.ad:", d.ad);
```

```sh
node kimlik-farki.mjs
```

```
ayni satir mi (birincil anahtar): true
ayni nesne mi (basvuru):         false
a.ad: Mehmet Ali | b.ad: Mehmet
kimlik haritasiyla ayni nesne mi: true | d.ad: Mehmet Ali
```

İki okuma aynı satırı getirdi ama iki ayrı nesne üretti. Birinde yapılan değişiklik
ötekinde görünmüyor; iki nesne de aynı satıra yazılırsa biri diğerini eziyor. Çıktının son
satırı çözümü gösteriyor: önceki derste kurulan **kimlik haritası**, bir isteğin ömrü
boyunca her satırı tek nesneyle temsil eder ve iki eşitliği yeniden çakıştırır.

Bunun ikinci bir sonucu vardır. Veritabanına henüz yazılmamış bir nesnenin birincil
anahtarı yoktur. Anahtarı kimliğin tek ölçütü yaparsan, kaydedilmemiş iki nesne birbirine
eşit görünür. Bu yüzden eşleme katmanında yeni nesneler ya uygulama tarafından üretilen bir
kimlikle doğar ya da anahtar atanana kadar yalnız başvuru eşitliğiyle karşılaştırılır.

#### Kalıtım Uyumsuzluğu

Kütüphane hem basılı hem sesli eserler tutmaya başlarsa nesne modelinde bir üst tip ve iki
alt tip belirir: ortak alanlar üstte, özel alanlar altta. İlişkisel modelde alt tip diye bir
şey yoktur. Hiyerarşi üç yoldan tabloya indirilebilir.

```sh
rm -f kalitim.db
sqlite3 kalitim.db <<'SQL'
-- 1. Tek tablo: butun alt turler tek baginti, ayirt edici sutunla
CREATE TABLE tek_tablo_eser (
  eser_id INTEGER PRIMARY KEY, tur TEXT NOT NULL, baslik TEXT NOT NULL, yazar TEXT NOT NULL,
  sayfa_sayisi INTEGER, sure_dakika INTEGER, seslendiren TEXT);
INSERT INTO tek_tablo_eser VALUES
  (1,'basili','Körlük','José Saramago',352,NULL,NULL),
  (2,'basili','Tutunamayanlar','Oğuz Atay',724,NULL,NULL),
  (3,'sesli','Kum Kitabı','Jorge Luis Borges',NULL,214,'Deniz Ak'),
  (4,'sesli','Yaban','Yakup Kadri',NULL,398,'Ece Gür');

-- 2. Somut tur basina tablo: ortak sutunlar her tabloda tekrarlanir
CREATE TABLE somut_basili (eser_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL,
  yazar TEXT NOT NULL, sayfa_sayisi INTEGER NOT NULL);
CREATE TABLE somut_sesli (eser_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL,
  yazar TEXT NOT NULL, sure_dakika INTEGER NOT NULL, seslendiren TEXT NOT NULL);
INSERT INTO somut_basili VALUES (1,'Körlük','José Saramago',352),(2,'Tutunamayanlar','Oğuz Atay',724);
INSERT INTO somut_sesli VALUES (3,'Kum Kitabı','Jorge Luis Borges',214,'Deniz Ak'),
  (4,'Yaban','Yakup Kadri',398,'Ece Gür');

-- 3. Tur basina tablo: ortak alanlar ustte, ozel alanlar alt bagintida
CREATE TABLE eser (eser_id INTEGER PRIMARY KEY, tur TEXT NOT NULL,
  baslik TEXT NOT NULL, yazar TEXT NOT NULL);
CREATE TABLE eser_basili (eser_id INTEGER PRIMARY KEY REFERENCES eser(eser_id),
  sayfa_sayisi INTEGER NOT NULL);
CREATE TABLE eser_sesli (eser_id INTEGER PRIMARY KEY REFERENCES eser(eser_id),
  sure_dakika INTEGER NOT NULL, seslendiren TEXT NOT NULL);
INSERT INTO eser VALUES (1,'basili','Körlük','José Saramago'),(2,'basili','Tutunamayanlar','Oğuz Atay'),
  (3,'sesli','Kum Kitabı','Jorge Luis Borges'),(4,'sesli','Yaban','Yakup Kadri');
INSERT INTO eser_basili VALUES (1,352),(2,724);
INSERT INTO eser_sesli VALUES (3,214,'Deniz Ak'),(4,398,'Ece Gür');
SQL
```

Üçü de aynı dört eseri saklıyor. Fark, "bütün eserleri getir" isteğinin maliyetinde
görünür.

```js
// kalitim-olc.mjs — uc esleme stratejisinde sorgu ve bos deger sayisi
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("kalitim.db");

function olc(ad, sorgular) {
  let bosDeger = 0, satir = 0;
  for (const s of sorgular) {
    for (const r of db.prepare(s).all()) {
      satir += 1;
      bosDeger += Object.values(r).filter((v) => v === null).length;
    }
  }
  console.log(`${ad.padEnd(22)} sorgu=${sorgular.length}  satir=${satir}  bos_deger=${bosDeger}`);
}

olc("tek tablo", ["SELECT * FROM tek_tablo_eser"]);
olc("somut tur basina", ["SELECT * FROM somut_basili", "SELECT * FROM somut_sesli"]);
olc("tur basina (birlesik)", [
  `SELECT e.eser_id, e.tur, e.baslik, e.yazar, b.sayfa_sayisi, s.sure_dakika, s.seslendiren
   FROM eser e LEFT JOIN eser_basili b ON b.eser_id = e.eser_id
               LEFT JOIN eser_sesli  s ON s.eser_id = e.eser_id`,
]);
```

```sh
node kalitim-olc.mjs
```

```
tek tablo              sorgu=1  satir=4  bos_deger=6
somut tur basina       sorgu=2  satir=4  bos_deger=0
tur basina (birlesik)  sorgu=1  satir=4  bos_deger=6
```

Sayılar üç stratejinin karakterini veriyor. **Tek tablo** tek sorguyla okur ama boş değer
üretir; alt tipe özgü her sütun, öteki alt tipin satırlarında `NULL` kalır. Alt tipe özgü
alanlar `NOT NULL` yapılamaz, çünkü kısıt bütün satırlara uygular. **Somut tür başına
tablo** hiç boş değer üretmez ve `NOT NULL` kısıtına izin verir; bedeli sorgu sayısıdır ve
üst tip üzerinden sorulan her soru birleşim gerektirir. **Tür başına tablo** hiyerarşiyi
şemaya birebir yansıtır; okumada dış birleştirme, yazmada iki tabloya ekleme ister.

Somut tür başına tablo bir kısıt daha kaybettirir: hiyerarşinin tamamına başvuran bir
yabancı anahtar kurulamaz.

```sh
sqlite3 kalitim.db <<'SQL'
PRAGMA foreign_keys = ON;
CREATE TABLE rezervasyon (rez_id INTEGER PRIMARY KEY,
  eser_id INTEGER NOT NULL REFERENCES somut_basili(eser_id), uye_id INTEGER NOT NULL);
INSERT INTO rezervasyon VALUES (1, 1, 3);
INSERT INTO rezervasyon VALUES (2, 3, 3);
SQL
```

```
Runtime error near line 5: FOREIGN KEY constraint failed (19)
```

Basılı esere yapılan rezervasyon geçti, sesli esere yapılan reddedildi. Yabancı anahtar tek
bir bağıntıyı gösterebilir; hiyerarşi iki bağıntıya bölündüğünde başvuru bütünlüğü
uygulamaya devredilir. Üst tipe başvuru gerekiyorsa tür başına tablo stratejisi seçilir,
çünkü orada `eser` bağıntısı bütün hiyerarşiyi temsil eder.

#### İlişki Yönü Uyumsuzluğu

Nesne grafiğinde bir bağ tek yönlüdür; iki yön isteniyorsa iki ayrı başvuru tutulur ve
ikisi elle tutarlı kılınır. İlişkisel modelde tek bir yabancı anahtar sütunu her iki yönü
de yanıtlar.

```js
// yon-farki.mjs — nesne tarafinda iki yon, iliskisel tarafta tek yabanci anahtar
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("kutuphane.db");

// Nesne grafigi: iki yonlu bag. Iki taraf da elle tutulur.
const uye = { id: 2, ad: "Mehmet", oduncler: [] };
const odunc = { id: 99, kitapId: 6, uye: null };

odunc.uye = uye;                       // yalniz bir yon kuruldu
console.log("odunc.uye.id:", odunc.uye.id);
console.log("uye.oduncler.length:", uye.oduncler.length);

uye.oduncler.push(odunc);              // ikinci yon elle eklenir
console.log("ikinci yon kurulduktan sonra:", uye.oduncler.length);

// Iliskisel taraf: tek sutun iki yonu de tasir.
db.exec("CREATE TEMP TABLE gecici AS SELECT * FROM odunc");
db.prepare("INSERT INTO gecici VALUES (?,?,?,?,NULL)").run(99, 6, 2, "2025-07-01");
const ileri = db.prepare("SELECT uye_id FROM gecici WHERE odunc_id = 99").get();
const geri = db.prepare("SELECT odunc_id FROM gecici WHERE uye_id = 2 ORDER BY odunc_id").all();
console.log("ileri yon (odunc -> uye):", ileri.uye_id);
console.log("geri yon (uye -> odunc):", geri.map((r) => r.odunc_id).join(", "));
```

```sh
node yon-farki.mjs
```

```
odunc.uye.id: 2
uye.oduncler.length: 0
ikinci yon kurulduktan sonra: 1
ileri yon (odunc -> uye): 2
geri yon (uye -> odunc): 3, 11, 99
```

Nesne tarafında bir yön kurulduğunda öteki yön boş kaldı; ilişkisel tarafta tek satır her
iki soruyu da yanıtladı. Bu farkın eşleme kararı **ilişkinin sahibini seçmektir**: hangi
tarafın yazması yabancı anahtarı değiştirir, öteki taraf yalnız okur. Sahip seçilmezse iki
taraftan gelen iki güncelleme birbiriyle çelişir ve hangisinin diske indiği çağrı sırasına
bağlı kalır.

Çoktan çoğa ilişkilerde uyumsuzluk daha da belirgindir. Nesne tarafında iki listeyle
kurulan bağın ilişkisel karşılığı yoktur; SQL Temelleri kursunda tanıtılan bağlantı
bağıntısı zorunludur. Bağlantı bağıntısının kendi öznitelikleri varsa (ödünç tarihi gibi)
bu bağıntı artık bir bağ değil, kendi başına bir varlıktır ve nesne modeline de varlık
olarak yansımalıdır.

#### Tanecik Uyumsuzluğu

Nesne modelinde her kavram ayrı bir tip olabilir. Üyenin adresi `Adres` tipiyle temsil
edilirse, bu tipin kimliği yoktur; iki adres aynı alanları taşıyorsa aynıdır. Kimliği
olmayan, yalnız değeriyle tanımlanan bu tiplere **değer nesnesi** (value object) denir.

İlişkisel modelde her tablonun bir birincil anahtarı vardır. Değer nesnesine tablo verirsen
ona olmayan bir kimlik uydurmuş olursun; vermezsen alanlarını sahip bağıntının içine
gömersin. İkinci karar **gömme** (embedding) adını alır ve genellikle doğru olandır: `uye`
bağıntısı `adres_il`, `adres_ilce`, `adres_posta_kodu` sütunlarını taşır, `Adres` tipi
eşleyici tarafından bu üç sütundan kurulur.

Karar ölçütü paylaşımdır. Değer nesnesi birden çok sahip tarafından paylaşılıyor ve
tek noktadan güncellenmesi gerekiyorsa gömme yetmez; o zaman kavram aslında bir varlıktır
ve kimliği hak eder. Kütüphane örneğinde adres üyeye özeldir, gömülür; şube ise kitap ve
personel tarafından paylaşılır, kendi bağıntısında durur.

#### Özet

- Nesne–ilişki uyumsuzluğu kimlik, kalıtım, ilişki yönü ve tanecik olmak üzere dört
  noktada ortaya çıkar; her biri ayrı bir eşleme kararıyla kapatılır.
- Aynı satırın iki kez okunması iki ayrı nesne üretti; kimlik haritası satır ile nesne
  arasındaki tekliği geri kurdu.
- Kalıtımın üç stratejisi ölçüldü: tek tablo 1 sorgu ve 6 boş değer, somut tür başına tablo
  2 sorgu ve 0 boş değer, tür başına tablo 1 sorgu ve 6 boş değer üretti.
- Somut tür başına tablo stratejisinde hiyerarşinin tamamına başvuran yabancı anahtar
  kurulamadı; başvuru bütünlüğü uygulamaya devredildi.
- İlişki yönü uyumsuzluğu sahibin seçilmesiyle, tanecik uyumsuzluğu değer nesnesinin
  sahip bağıntıya gömülmesiyle kapatılır.

#### Sonraki Adım

Uyumsuzluğu kapatan kararlar şimdiye kadar çağıran kodun içine dağıldı: hangi stratejinin
seçildiği, hangi ilişkinin sahibi olduğu, hangi alanın gömüldüğü bilgisi sorguyu yazan her
yerde tekrarlanıyor. Bu bilgiyi tek bir yerde toplayan, çağıran tarafa yalnız alan
kavramlarıyla konuşan bir arayüz bırakan yapıya depo kalıbı denir. Sonraki ders bu arayüzü
kurar ve kalıcılık ayrıntısının gerçekten gizlendiğini, deponun sahte bir uygulamasıyla iş
mantığını veritabanı olmadan sınayarak gösterir.

### Depo Kalıbı

Source: https://academia.sh/tr/kurslar/veri-erisim-katmani/depo-kalibi

Kalıcılık ayrıntısını tek bir arayüzün arkasına toplayan depo kalıbı: iş kurallarının sahte depoyla veritabanı olmadan sınanması, aynı sözleşmenin iki uygulamaya uygulanması ve kalıcılık sızıntısının modül başına ölçülmesi.

Önceki ders nesne modeliyle ilişkisel modelin dört noktada örtüşmediğini gösterdi ve her
uyumsuzluğu kapatan kararı adlandırdı. Bu kararlar bir yere yazılmadıkça sorguyu yazan her
çağrı noktasında yeniden verilir: kalıtımın hangi stratejiyle indirildiği, ilişkinin
sahibinin hangi taraf olduğu, hangi alanın gömüldüğü bilgisi kodun içine dağılır.

Bu dersin konusu o bilgiyi tek bir yerde toplayan arayüzdür. **Depo** (repository), çağıran
tarafa yalnız alan kavramlarıyla konuşan, kalıcılığın nasıl gerçekleştiğini gizleyen bir
sınırdır. Sınırın gerçekten kapalı olup olmadığı iddia edilerek değil, sınanarak
gösterilir: iş kuralları veritabanı olmadan çalışıyorsa sınır kapalıdır.

#### Deponun Vaadi

Depo, sorgu yazma aracı değildir. Çağıran taraf ondan tablo, sütun veya birleştirme
istemez; ödünç kaydı ister. Aşağıdaki beş işlem ödünç servisinin veriye ihtiyaç duyduğu
noktaların tamamıdır.

| İşlem | Anlamı |
|---|---|
| `bul(oduncId)` | Kimliği verilen ödünç kaydını döndürür, yoksa `null` |
| `kitabinAcikKaydi(kitapId)` | Kitabın iade edilmemiş kaydı varsa döndürür |
| `uyeninAcikKayitlari(uyeId)` | Üyenin iade edilmemiş kayıtlarının listesi |
| `ekle(kayit)` | Yeni kaydı saklar ve kimliğini döndürür |
| `iadeIsaretle(oduncId, tarih)` | Kaydı kapalı duruma geçirir |

Listede `SELECT` yok, bağlantı yok, satır yok. Bu, deponun tanımıdır: **arayüz alan
diliyle yazılır, uygulama kalıcılık diliyle.**

#### Kurallar Depoya Bağlı Değildir

Ödünç verme iki kural taşır: ödünçte olan kitap yeniden verilemez ve bir üye aynı anda
üçten çok kitap tutamaz. Bu kurallar hiçbir kalıcılık ayrıntısı içermez.

```js
// odunc-servisi.mjs — is kurallari; hicbir SQL, hicbir surucu adi icermez
export const UYE_SINIRI = 3;

export class OduncServisi {
  constructor(depo) { this.depo = depo; }

  ver(uyeId, kitapId, tarih) {
    if (this.depo.kitabinAcikKaydi(kitapId) !== null) {
      return { durum: "reddedildi", sebep: "kitap zaten odunc verilmis" };
    }
    const acik = this.depo.uyeninAcikKayitlari(uyeId);
    if (acik.length >= UYE_SINIRI) {
      return { durum: "reddedildi", sebep: `uye siniri asildi (${UYE_SINIRI})` };
    }
    const id = this.depo.ekle({ kitapId, uyeId, alisTarihi: tarih, iadeTarihi: null });
    return { durum: "verildi", oduncId: id };
  }

  iadeAl(oduncId, tarih) {
    const kayit = this.depo.bul(oduncId);
    if (kayit === null) return { durum: "bulunamadi" };
    if (kayit.iadeTarihi !== null) return { durum: "zaten iade edilmis" };
    this.depo.iadeIsaretle(oduncId, tarih);
    return { durum: "iade alindi" };
  }
}
```

Servis, deposunu yapıcı üzerinden alır. Kendisi bir depo kurmaz, hangi deponun geleceğini
bilmez. Bu ayrıntı bütün dersi taşır: depoyu dışarıdan almak, sınama sırasında başka bir
depo vermeyi mümkün kılar.

#### Sahte Depo

Sözleşmeyi karşılayan en küçük uygulama bir dizidir.

```js
// bellek-deposu.mjs — deponun sahte uygulamasi; veritabani yok
export class BellekOduncDeposu {
  constructor(baslangic = []) {
    this.kayitlar = baslangic.map((k) => ({ ...k }));
    this.sonrakiId = this.kayitlar.length + 1;
  }
  bul(oduncId) {
    return this.kayitlar.find((k) => k.oduncId === oduncId) ?? null;
  }
  kitabinAcikKaydi(kitapId) {
    return this.kayitlar.find((k) => k.kitapId === kitapId && k.iadeTarihi === null) ?? null;
  }
  uyeninAcikKayitlari(uyeId) {
    return this.kayitlar.filter((k) => k.uyeId === uyeId && k.iadeTarihi === null);
  }
  ekle(kayit) {
    const oduncId = this.sonrakiId++;
    this.kayitlar.push({ oduncId, ...kayit });
    return oduncId;
  }
  iadeIsaretle(oduncId, tarih) {
    const k = this.bul(oduncId);
    if (k !== null) k.iadeTarihi = tarih;
  }
}
```

Sahte depoyla iş kuralları doğrudan sınanabilir. Sınama dosyası önceki iki bloğun
tanımladığı `odunc-servisi.mjs` ve `bellek-deposu.mjs` modüllerini kullanır.

```js
// servis.test.mjs — is kurallari sahte depoyla sinanir
import { test } from "node:test";
import assert from "node:assert/strict";
import { OduncServisi } from "./odunc-servisi.mjs";
import { BellekOduncDeposu } from "./bellek-deposu.mjs";

test("odunte olmayan kitap verilir", () => {
  const servis = new OduncServisi(new BellekOduncDeposu());
  assert.deepEqual(servis.ver(1, 5, "2025-07-01"), { durum: "verildi", oduncId: 1 });
});

test("odunte olan kitap reddedilir", () => {
  const depo = new BellekOduncDeposu([
    { oduncId: 1, kitapId: 5, uyeId: 2, alisTarihi: "2025-06-01", iadeTarihi: null },
  ]);
  const sonuc = new OduncServisi(depo).ver(1, 5, "2025-07-01");
  assert.equal(sonuc.durum, "reddedildi");
  assert.match(sonuc.sebep, /zaten odunc/);
});

test("uye siniri asilinca reddedilir", () => {
  const depo = new BellekOduncDeposu([
    { oduncId: 1, kitapId: 1, uyeId: 4, alisTarihi: "2025-06-01", iadeTarihi: null },
    { oduncId: 2, kitapId: 2, uyeId: 4, alisTarihi: "2025-06-02", iadeTarihi: null },
    { oduncId: 3, kitapId: 3, uyeId: 4, alisTarihi: "2025-06-03", iadeTarihi: null },
  ]);
  const sonuc = new OduncServisi(depo).ver(4, 7, "2025-07-01");
  assert.equal(sonuc.durum, "reddedildi");
  assert.equal(sonuc.sebep, "uye siniri asildi (3)");
});

test("iade edilen kitap yeniden verilebilir", () => {
  const depo = new BellekOduncDeposu([
    { oduncId: 1, kitapId: 5, uyeId: 2, alisTarihi: "2025-06-01", iadeTarihi: null },
  ]);
  const servis = new OduncServisi(depo);
  assert.equal(servis.iadeAl(1, "2025-06-20").durum, "iade alindi");
  assert.equal(servis.ver(1, 5, "2025-07-01").durum, "verildi");
});
```

```sh
node --test servis.test.mjs
```

```
✔ odunte olmayan kitap verilir (0.593333ms)
✔ odunte olan kitap reddedilir (0.103167ms)
✔ uye siniri asilinca reddedilir (0.056542ms)
✔ iade edilen kitap yeniden verilebilir (0.063459ms)
ℹ tests 4
ℹ suites 0
ℹ pass 4
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 35.385792
```

Parantez içindeki süreler ve `duration_ms` alanı her çalıştırmada değişir; anlamlı olan
`pass 4` ve `fail 0` satırlarıdır. Dört kural, veritabanı dosyası olmadan, şema kurulmadan
ve tek bir bağlantı açılmadan sınandı. Sınamanın kurulum adımı bir dizi kayıt yazmaktan
ibaret; kurallara giren başlangıç durumu doğrudan okunabiliyor.

#### Sahtenin Sadakati

Sahte depo bir risk taşır: gerçek uygulamadan farklı davranırsa, sınamalar geçer ama
uygulama çalışmaz. Bu riski kapatan yöntem, **aynı sınama kümesini iki uygulamaya da
uygulamaktır**. Önce gerçek uygulama.

```js
// sqlite-deposu.mjs — deponun gercek uygulamasi; butun SQL burada toplanir
const NESNE = (s) => (s === undefined ? null : {
  oduncId: s.odunc_id, kitapId: s.kitap_id, uyeId: s.uye_id,
  alisTarihi: s.alis_tarihi, iadeTarihi: s.iade_tarihi,
});

export class SqliteOduncDeposu {
  constructor(db) { this.db = db; }
  bul(oduncId) {
    return NESNE(this.db.prepare("SELECT * FROM odunc WHERE odunc_id = ?").get(oduncId));
  }
  kitabinAcikKaydi(kitapId) {
    return NESNE(this.db.prepare(
      "SELECT * FROM odunc WHERE kitap_id = ? AND iade_tarihi IS NULL").get(kitapId));
  }
  uyeninAcikKayitlari(uyeId) {
    return this.db.prepare(
      "SELECT * FROM odunc WHERE uye_id = ? AND iade_tarihi IS NULL ORDER BY odunc_id")
      .all(uyeId).map(NESNE);
  }
  ekle(kayit) {
    const sonuc = this.db.prepare(
      "INSERT INTO odunc (kitap_id, uye_id, alis_tarihi, iade_tarihi) VALUES (?,?,?,?)")
      .run(kayit.kitapId, kayit.uyeId, kayit.alisTarihi, kayit.iadeTarihi);
    return Number(sonuc.lastInsertRowid);
  }
  iadeIsaretle(oduncId, tarih) {
    this.db.prepare("UPDATE odunc SET iade_tarihi = ? WHERE odunc_id = ?").run(tarih, oduncId);
  }
}
```

Uygulama bağlantıyı da dışarıdan alıyor. Dosya adını kendi içinde tutsaydı, sınama sırasında
bellek içi bir veritabanına yönlendirilemezdi.

```js
// sozlesme.test.mjs — ayni sinama kumesi iki uygulamaya da uygulanir
import { test, describe } from "node:test";
import assert from "node:assert/strict";
import { DatabaseSync } from "node:sqlite";
import { BellekOduncDeposu } from "./bellek-deposu.mjs";
import { SqliteOduncDeposu } from "./sqlite-deposu.mjs";

const BASLANGIC = [
  { oduncId: 1, kitapId: 5, uyeId: 2, alisTarihi: "2025-06-01", iadeTarihi: null },
  { oduncId: 2, kitapId: 6, uyeId: 2, alisTarihi: "2025-06-02", iadeTarihi: "2025-06-25" },
];

const bellekKur = () => new BellekOduncDeposu(BASLANGIC);

function sqliteKur() {
  const db = new DatabaseSync(":memory:");
  db.exec(`CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL,
           uye_id INTEGER NOT NULL, alis_tarihi TEXT NOT NULL, iade_tarihi TEXT)`);
  for (const k of BASLANGIC) {
    db.prepare("INSERT INTO odunc VALUES (?,?,?,?,?)")
      .run(k.oduncId, k.kitapId, k.uyeId, k.alisTarihi, k.iadeTarihi);
  }
  return new SqliteOduncDeposu(db);
}

for (const [ad, kur] of [["bellek", bellekKur], ["sqlite", sqliteKur]]) {
  describe(`depo sozlesmesi: ${ad}`, () => {
    test("acik kayit bulunur, kapali kayit bulunmaz", () => {
      const d = kur();
      assert.equal(d.kitabinAcikKaydi(5).oduncId, 1);
      assert.equal(d.kitabinAcikKaydi(6), null);
    });
    test("uyenin acik kayitlari yalniz acik olanlardir", () => {
      assert.deepEqual(kur().uyeninAcikKayitlari(2).map((k) => k.oduncId), [1]);
    });
    test("eklenen kayit yeni kimlik alir ve bulunur", () => {
      const d = kur();
      const id = d.ekle({ kitapId: 7, uyeId: 3, alisTarihi: "2025-07-01", iadeTarihi: null });
      assert.equal(d.bul(id).kitapId, 7);
    });
    test("iade isaretlemesi acik kaydi kapatir", () => {
      const d = kur();
      d.iadeIsaretle(1, "2025-07-02");
      assert.equal(d.kitabinAcikKaydi(5), null);
    });
    test("olmayan kimlik null doner", () => {
      assert.equal(kur().bul(999), null);
    });
  });
}
```

```sh
node --test sozlesme.test.mjs
```

```
▶ depo sozlesmesi: bellek
  ✔ acik kayit bulunur, kapali kayit bulunmaz (0.537917ms)
  ✔ uyenin acik kayitlari yalniz acik olanlardir (0.262666ms)
  ✔ eklenen kayit yeni kimlik alir ve bulunur (0.05475ms)
  ✔ iade isaretlemesi acik kaydi kapatir (0.046084ms)
  ✔ olmayan kimlik null doner (0.039042ms)
✔ depo sozlesmesi: bellek (1.312042ms)
▶ depo sozlesmesi: sqlite
  ✔ acik kayit bulunur, kapali kayit bulunmaz (0.342959ms)
  ✔ uyenin acik kayitlari yalniz acik olanlardir (0.113333ms)
  ✔ eklenen kayit yeni kimlik alir ve bulunur (0.107ms)
  ✔ iade isaretlemesi acik kaydi kapatir (0.082458ms)
  ✔ olmayan kimlik null doner (0.083541ms)
✔ depo sozlesmesi: sqlite (0.824958ms)
ℹ tests 10
ℹ suites 2
ℹ pass 10
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 34.280709
```

On sınama, iki uygulama, sıfır başarısızlık. Bu sınama kümesi **deponun sözleşmesidir**;
sahtenin gerçeğe sadık kaldığını her çalıştırmada yeniden doğrular. Yeni bir uygulama
eklendiğinde (başka bir veri deposu, önbellek katmanı, kayıtları dosyada tutan bir sürüm)
aynı küme ona da uygulanır.

Sözleşme sınamasının bir sınırı vardır. Yalıtım düzeyi, kilitlenme davranışı ve eşzamanlı
güncelleme çatışması bellek içi bir dizide üretilemez; bunlar yalnız gerçek uygulama
üzerinde sınanır. Sahte deponun kapsamı **iş kurallarıdır**, kalıcılık davranışı değil.

#### Sızıntının Ölçülmesi

Sınırın kapalılığı sayılabilir. Kalıcılığın izleri (SQL anahtar sözcükleri, sürücü tipi,
deyim hazırlama çağrısı) hangi modülde kaç kez geçiyor? Karşılaştırma için, aynı işi depo
olmadan yapan bir sürüm de yazalım.

```js
// sizdiran-servis.mjs — deponun yerine dogrudan baglanti kullanan surum
export class SizdiranOduncServisi {
  constructor(db) { this.db = db; }
  ver(uyeId, kitapId, tarih) {
    const acikKitap = this.db.prepare(
      "SELECT 1 FROM odunc WHERE kitap_id = ? AND iade_tarihi IS NULL").get(kitapId);
    if (acikKitap !== undefined) return { durum: "reddedildi", sebep: "kitap zaten odunc verilmis" };
    const sayi = this.db.prepare(
      "SELECT count(*) AS n FROM odunc WHERE uye_id = ? AND iade_tarihi IS NULL").get(uyeId).n;
    if (sayi >= 3) return { durum: "reddedildi", sebep: "uye siniri asildi (3)" };
    const sonuc = this.db.prepare(
      "INSERT INTO odunc (kitap_id, uye_id, alis_tarihi, iade_tarihi) VALUES (?,?,?,NULL)")
      .run(kitapId, uyeId, tarih);
    return { durum: "verildi", oduncId: Number(sonuc.lastInsertRowid) };
  }
}
```

```js
// sizinti-olc.mjs — hangi modulde kac tane kalicilik izi var
import { readFileSync } from "node:fs";

const IZLER = /\b(SELECT|INSERT|UPDATE|DELETE|prepare|DatabaseSync)\b/g;

for (const dosya of ["odunc-servisi.mjs", "sizdiran-servis.mjs", "sqlite-deposu.mjs", "bellek-deposu.mjs"]) {
  const iz = readFileSync(dosya, "utf8").match(IZLER) ?? [];
  console.log(`${dosya.padEnd(22)} kalicilik izi = ${iz.length}`);
}
```

```sh
node sizinti-olc.mjs
```

```
odunc-servisi.mjs      kalicilik izi = 0
sizdiran-servis.mjs    kalicilik izi = 6
sqlite-deposu.mjs      kalicilik izi = 10
bellek-deposu.mjs      kalicilik izi = 0
```

Sayılar dağılımı gösteriyor. Depo kullanan serviste iz sıfır; kalıcılığın tamamı tek bir
modülde, on iz olarak toplanmış. Sızdıran serviste ise altı iz iş kurallarının arasına
karışmış durumda. Bu altı iz, o servisin sahte bir depoyla sınanamamasının nedenidir:
`prepare` çağrısı bir dizide karşılığı olmayan bir yeteneği ister.

Ölçüm kaba bir araçtır, kesin bir kanıt değil. Bir modül SQL yazmadan da kalıcılığa
bağlanabilir; örneğin `bul` çağrısının dönüş sırasının veritabanının varsayılan
sıralamasına güvenmesi de bir bağımlılıktır. Sayının işlevi, sınırın nereye çizildiğini
gözle görünür kılmaktır.

#### Deponun Sınırı ve Sorgu Çoğalması

Aynı servisin gerçek depoyla çalıştırılması ek bir kod değişikliği istemez.

```js
// gercekle-calistir.mjs — ayni servis, bu kez veritabanina bagli depoyla
import { DatabaseSync } from "node:sqlite";
import { OduncServisi } from "./odunc-servisi.mjs";
import { SqliteOduncDeposu } from "./sqlite-deposu.mjs";

const depo = new SqliteOduncDeposu(new DatabaseSync("kutuphane.db"));
const servis = new OduncServisi(depo);

console.log("kitap 1 (acik kaydi var):", JSON.stringify(servis.ver(6, 1, "2025-07-15")));
console.log("kitap 7 (bos):           ", JSON.stringify(servis.ver(6, 7, "2025-07-15")));
console.log("uye 4 acik kayit sayisi: ", depo.uyeninAcikKayitlari(4).length);
```

Bu blok, SQL Temelleri kursundaki kütüphane şemasını taşıyan `kutuphane.db` dosyasını
bekler. Aşağıdaki komut şemayı ve dersin çalıştığı satırları kurar.

```sh
rm -f kutuphane.db
sqlite3 kutuphane.db <<'SQL'
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL,
                    uye_id INTEGER NOT NULL, alis_tarihi TEXT NOT NULL, iade_tarihi TEXT);
INSERT INTO odunc VALUES (1,1,1,'2025-01-10','2025-01-24'),(2,2,1,'2025-02-02','2025-02-20'),
  (3,1,2,'2025-02-11',NULL),(4,3,3,'2025-03-01','2025-03-15'),(5,4,3,'2025-03-18','2025-04-02'),
  (6,1,4,'2025-04-05','2025-04-19'),(7,5,4,'2025-04-21',NULL),(8,2,5,'2025-05-02','2025-05-30'),
  (9,7,1,'2025-05-14','2025-05-28'),(10,3,5,'2025-06-03',NULL),(11,6,2,'2025-06-11','2025-06-25'),
  (12,4,4,'2025-06-20','2025-07-04');
SQL
node gercekle-calistir.mjs
```

```
kitap 1 (acik kaydi var): {"durum":"reddedildi","sebep":"kitap zaten odunc verilmis"}
kitap 7 (bos):            {"durum":"verildi","oduncId":13}
uye 4 acik kayit sayisi:  1
```

Kitap 1'in açık kaydı bulunduğu için istek reddedildi, boştaki kitap 7 verildi ve on
üçüncü kimliği aldı. Servis kodunda tek satır değişmedi.

Depo kalıbının bedeli burada belirir. Arayüz alan diliyle yazıldığı için, her yeni soru
yeni bir yöntem ister: gecikmiş kayıtlar, belirli şubedeki açık kayıtlar, belirli tarih
aralığında verilenler, hem gecikmiş hem belirli şubede olanlar. Ölçütler birleştirilebilir
olduğundan yöntem sayısı ölçüt sayısının kombinasyonuyla artar. Bir noktadan sonra depo
arayüzü, gizlemeye çalıştığı sorgu diline benzemeye başlar.

#### Özet

- Depo, çağıran tarafa alan kavramlarıyla konuşan bir arayüzdür; sorguyu, bağlantıyı ve
  satır–nesne çevrimini kendi içinde tutar.
- Servisin deposunu dışarıdan alması, sınama sırasında bellek içi bir sahte depo
  verilmesini mümkün kılar; dört iş kuralı veritabanı olmadan sınandı.
- Aynı sözleşme sınaması iki uygulamaya da uygulandı ve on sınamanın tamamı geçti; bu,
  sahtenin gerçeğe sadık kaldığının sürekli doğrulanmasıdır.
- Kalıcılık izleri sayıldığında depo kullanan serviste sıfır, sızdıran serviste altı iz
  çıktı; sınırın kapalılığı böyle görünür kılınır.
- Yalıtım ve eşzamanlılık davranışı sahte depoda üretilemez; sahtenin kapsamı iş
  kurallarıdır.

#### Sonraki Adım

Deponun arayüzü kapandıkça sorulacak soru sayısı arttı ve her soru yeni bir yöntem istedi.
Gecikmiş kayıtlar, şubeye göre süzülmüş kayıtlar, tarih aralığıyla sınırlanmış kayıtlar ve
bunların birleşimleri ayrı ayrı yazıldığında depo şişer. Sorun ölçütlerin kendisinde değil,
ölçütlerin arayüzde sabit yöntem adlarına gömülmesindedir. Sonraki ders ölçütü bir nesne
hâline getirir: her ölçüt kendi SQL parçasını ve bağlı değişkenlerini üretir, ölçütler
"ve", "veya", "değil" ile birleştirilir ve depo tek bir yöntemle bütün birleşimleri
karşılar.

### Sorgu Nesneleri ve Belirtimler

Source: https://academia.sh/tr/kurslar/veri-erisim-katmani/sorgu-nesneleri-ve-belirtimler

Ölçütün nesneye dönüşmesi: her belirtimin kendi SQL parçasını ve bağlı değerlerini üretmesi, ve–veya–değil ile birleştirilmesi, aynı ölçütün bellekte yüklem olarak çalışması ve tanıtıcıların izin listesiyle sınırlanması.

Depo kalıbı sınırı kapattı, ama arayüzü şişirdi. Her yeni soru yeni bir yöntem istedi:
açık kayıtlar, üyenin açık kayıtları, tarih aralığındaki kayıtlar, üyenin belirli tarih
aralığındaki açık kayıtları. Ölçütler birleşebildiği için yöntem sayısı ölçüt sayısının
kombinasyonuyla artıyor.

Sorun ölçütlerde değil, ölçütlerin yöntem adına gömülmesindedir. `uyeninAcikKayitlari`
adının içinde iki ölçüt ve bir birleştirici saklıdır; hiçbiri ayrı ayrı kullanılamaz. Bu
ders ölçütü adından çıkarıp nesne yapar. Nesne olan ölçüt birleştirilebilir; birleştirilen
ölçütten tek bir sorgu üretilir.

#### Ölçütün Nesneleşmesi

Bir **belirtim** (specification) üç şey taşır: SQL parçası, o parçadaki soru
işaretlerine karşılık gelen değerler ve aynı ölçütün bellek içi karşılığı olan bir yüklem.

```js
// belirtim.mjs — her belirtim hem SQL parcasi hem bellek ici yuklem tasir
const belirtim = (sql, degerler, yuklem) => ({ sql, degerler, yuklem });

export const acik = () =>
  belirtim("iade_tarihi IS NULL", [], (k) => k.iadeTarihi === null);

export const uyenin = (uyeId) =>
  belirtim("uye_id = ?", [uyeId], (k) => k.uyeId === uyeId);

export const kitabin = (kitapId) =>
  belirtim("kitap_id = ?", [kitapId], (k) => k.kitapId === kitapId);

export const alisAraliginda = (bas, son) =>
  belirtim("alis_tarihi BETWEEN ? AND ?", [bas, son],
           (k) => k.alisTarihi >= bas && k.alisTarihi <= son);

export const ve = (...b) =>
  belirtim(`(${b.map((x) => x.sql).join(" AND ")})`,
           b.flatMap((x) => x.degerler),
           (k) => b.every((x) => x.yuklem(k)));

export const veya = (...b) =>
  belirtim(`(${b.map((x) => x.sql).join(" OR ")})`,
           b.flatMap((x) => x.degerler),
           (k) => b.some((x) => x.yuklem(k)));

export const degil = (b) =>
  belirtim(`NOT ${b.sql}`, b.degerler, (k) => !b.yuklem(k));
```

Birleştiricilerin yaptığı iş üç adımdadır: alt belirtimlerin SQL parçalarını parantez
içinde birleştirir, değer dizilerini **aynı sırada** uç uca ekler, yüklemleri mantıksal
olarak bağlar. İkinci adım kritiktir. Soru işaretleri metinde hangi sırayla geçiyorsa,
değerler dizisinde de o sırada bulunmalıdır; `flatMap` bu sırayı, `map` çağrısındaki sırayla
aynı tuttuğu için korur.

#### Üretilen Sorgunun Görünür Olması

Belirtimlerin doğruluğu ürettikleri metne bakılarak denetlenir.

```js
// belirtim-yazdir.mjs — bilesik belirtimlerin urettigi SQL ve bagli degerler
import { acik, uyenin, kitabin, alisAraliginda, ve, veya, degil } from "./belirtim.mjs";

const ornekler = {
  "acik": acik(),
  "uyenin acik kayitlari": ve(uyenin(4), acik()),
  "acik degil, 2025 ilk yarisi": ve(degil(acik()), alisAraliginda("2025-01-01", "2025-06-30")),
  "uye 1 veya uye 2, kitap 1": ve(veya(uyenin(1), uyenin(2)), kitabin(1)),
};

for (const [ad, b] of Object.entries(ornekler)) {
  console.log(ad);
  console.log("  sql   :", b.sql);
  console.log("  degerler:", JSON.stringify(b.degerler));
}
```

```sh
node belirtim-yazdir.mjs
```

```
acik
  sql   : iade_tarihi IS NULL
  degerler: []
uyenin acik kayitlari
  sql   : (uye_id = ? AND iade_tarihi IS NULL)
  degerler: [4]
acik degil, 2025 ilk yarisi
  sql   : (NOT iade_tarihi IS NULL AND alis_tarihi BETWEEN ? AND ?)
  degerler: ["2025-01-01","2025-06-30"]
uye 1 veya uye 2, kitap 1
  sql   : ((uye_id = ? OR uye_id = ?) AND kitap_id = ?)
  degerler: [1,2,1]
```

Son örnek sıranın nasıl korunduğunu gösteriyor: üç soru işareti, üç değer, ilk ikisi iç
`veya` belirtiminden, üçüncüsü dıştaki `kitabin` belirtiminden geliyor. Üçüncü örnekte
`degil` birleştiricisi hiç değer taşımıyor, çünkü sardığı belirtim de taşımıyordu; değer
dizisi bu yüzden yalnız iki tarih içeriyor.

Parantezleme ihmal edilirse hata sessiz kalır. `veya` çıktısı parantezsiz olsaydı,
`(A OR B) AND C` yerine `A OR (B AND C)` okunurdu; SQL yine geçerli, sonuç yine dolu, ama
yanlış olurdu.

#### Tek Yöntemli Depo

Ölçüt nesneleşince deponun arayüzü küçülür. Süzme dışındaki isteklerle birlikte —
sıralama, sınır, atlama — ölçüt tek bir kayıtta toplanır. Bu kayda **sorgu nesnesi** (query
object) denir.

```js
// sorgu-deposu.mjs — tek arama yontemi; olcut disaridan gelir
const NESNE = (s) => ({ oduncId: s.odunc_id, kitapId: s.kitap_id, uyeId: s.uye_id,
                        alisTarihi: s.alis_tarihi, iadeTarihi: s.iade_tarihi });

export class SorguDeposu {
  constructor(db) { this.db = db; this.uretilen = []; }

  ara(sorgu) {
    const { belirtim, siralama = "odunc_id", yon = "ASC", sinir = null, atlama = 0 } = sorgu;
    let sql = `SELECT * FROM odunc WHERE ${belirtim.sql} ORDER BY ${siralama} ${yon}`;
    const degerler = [...belirtim.degerler];
    if (sinir !== null) { sql += " LIMIT ? OFFSET ?"; degerler.push(sinir, atlama); }
    this.uretilen.push({ sql, degerler });
    return this.db.prepare(sql).all(...degerler).map(NESNE);
  }
}
```

Dört ayrı istek, tek bir yöntemden geçiyor.

```js
// depo-calistir.mjs — ayni depo yontemi uc farkli olcutu karsilar
import { DatabaseSync } from "node:sqlite";
import { SorguDeposu } from "./sorgu-deposu.mjs";
import { acik, uyenin, kitabin, alisAraliginda, ve, veya, degil } from "./belirtim.mjs";

const depo = new SorguDeposu(new DatabaseSync("kutuphane.db"));

const istekler = [
  ["butun acik kayitlar", { belirtim: acik() }],
  ["uye 4'un acik kayitlari", { belirtim: ve(uyenin(4), acik()) }],
  ["kapali, 2025 ilk yarisi, en yenisi",
   { belirtim: ve(degil(acik()), alisAraliginda("2025-01-01", "2025-06-30")),
     siralama: "alis_tarihi", yon: "DESC", sinir: 2 }],
  ["kitap 1'i alan uye 1 veya 2", { belirtim: ve(veya(uyenin(1), uyenin(2)), kitabin(1)) }],
];

for (const [ad, sorgu] of istekler) {
  const sonuc = depo.ara(sorgu);
  console.log(`${ad}: ${sonuc.map((k) => k.oduncId).join(", ") || "(bos)"}`);
}

console.log("\nuretilen sorgular:");
for (const u of depo.uretilen) console.log(`  ${u.sql}\n    <- ${JSON.stringify(u.degerler)}`);
```

Aşağıdaki komut önce kütüphane şemasının ödünç bağıntısını kurar, sonra betiği çalıştırır.

```sh
rm -f kutuphane.db
sqlite3 kutuphane.db <<'SQL'
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL,
                    uye_id INTEGER NOT NULL, alis_tarihi TEXT NOT NULL, iade_tarihi TEXT);
INSERT INTO odunc VALUES (1,1,1,'2025-01-10','2025-01-24'),(2,2,1,'2025-02-02','2025-02-20'),
  (3,1,2,'2025-02-11',NULL),(4,3,3,'2025-03-01','2025-03-15'),(5,4,3,'2025-03-18','2025-04-02'),
  (6,1,4,'2025-04-05','2025-04-19'),(7,5,4,'2025-04-21',NULL),(8,2,5,'2025-05-02','2025-05-30'),
  (9,7,1,'2025-05-14','2025-05-28'),(10,3,5,'2025-06-03',NULL),(11,6,2,'2025-06-11','2025-06-25'),
  (12,4,4,'2025-06-20','2025-07-04');
SQL
node depo-calistir.mjs
```

```
butun acik kayitlar: 3, 7, 10
uye 4'un acik kayitlari: 7
kapali, 2025 ilk yarisi, en yenisi: 12, 11
kitap 1'i alan uye 1 veya 2: 1, 3

uretilen sorgular:
  SELECT * FROM odunc WHERE iade_tarihi IS NULL ORDER BY odunc_id ASC
    <- []
  SELECT * FROM odunc WHERE (uye_id = ? AND iade_tarihi IS NULL) ORDER BY odunc_id ASC
    <- [4]
  SELECT * FROM odunc WHERE (NOT iade_tarihi IS NULL AND alis_tarihi BETWEEN ? AND ?) ORDER BY alis_tarihi DESC LIMIT ? OFFSET ?
    <- ["2025-01-01","2025-06-30",2,0]
  SELECT * FROM odunc WHERE ((uye_id = ? OR uye_id = ?) AND kitap_id = ?) ORDER BY odunc_id ASC
    <- [1,2,1]
```

Üçüncü sorgu, sınırın ve atlamanın da bağlı değer olarak eklendiğini gösteriyor: ölçütün
iki tarihinden sonra `2` ve `0` geliyor. `uretilen` dizisi, ilk derste kurulan sayma
kancasının bu katmandaki karşılığıdır; hangi ölçütün hangi metne dönüştüğü çalışma anında
görülebiliyor.

#### Aynı Ölçüt, İki Yer

Belirtimin ikinci alanı — yüklem — henüz kullanılmadı. Onun değeri, ölçütün veritabanına
gitmeden de sorulabilmesidir. Kaydedilmemiş bir aday kaydın kurala uyup uymadığı bellekte
sınanır.

```js
// bellekte-dogrula.mjs — ayni belirtim, veritabanina gitmeden aday kayda uygulanir
import { acik, uyenin, alisAraliginda, ve } from "./belirtim.mjs";

const kural = ve(uyenin(4), acik(), alisAraliginda("2025-01-01", "2025-12-31"));

const adaylar = [
  { oduncId: null, uyeId: 4, kitapId: 7, alisTarihi: "2025-07-15", iadeTarihi: null },
  { oduncId: null, uyeId: 5, kitapId: 7, alisTarihi: "2025-07-15", iadeTarihi: null },
  { oduncId: null, uyeId: 4, kitapId: 7, alisTarihi: "2024-12-30", iadeTarihi: null },
];

for (const a of adaylar) {
  console.log(`uye=${a.uyeId} alis=${a.alisTarihi} -> ${kural.yuklem(a)}`);
}
console.log("ayni belirtimin SQL karsiligi:", kural.sql);
```

```sh
node bellekte-dogrula.mjs
```

```
uye=4 alis=2025-07-15 -> true
uye=5 alis=2025-07-15 -> false
uye=4 alis=2024-12-30 -> false
ayni belirtimin SQL karsiligi: (uye_id = ? AND iade_tarihi IS NULL AND alis_tarihi BETWEEN ? AND ?)
```

Üç adayın hiçbiri veritabanında değil; kimliklerinin `null` olması bunu gösteriyor. Kural
yine de uygulandı. Kuralın tek bir yerde tanımlanması, sorgu ile denetimin ayrışmasını
önler: iki tanım yazılırsa, biri değiştiğinde öteki sessizce eskir.

Bu ikiliğin bedeli, iki tanımın uyumlu kalma zorunluluğudur. `alisAraliginda` belirtiminde
SQL tarafı `BETWEEN` kullanıyor, yüklem tarafı iki ayrı karşılaştırma yapıyor; ikisinin de
uç değerleri kapsaması gerekir. Böyle bir çift her yazıldığında, iki tarafın aynı sonucu
verdiği sınanmalıdır.

#### Bağlı Değer ve Tanıtıcı

Belirtimlerin değerleri soru işaretiyle taşıması bir seçim değil, zorunluluktur. Aynı
girdinin iki yoldan sorguya girmesi arasındaki fark ölçülebilir.

```js
// baglama.mjs — bagli deger ile metin birlestirmenin ayni girdideki farki
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("kutuphane.db");

const girdi = "2 OR 1=1";

const bagli = db.prepare("SELECT count(*) AS n FROM odunc WHERE uye_id = ?").get(girdi).n;
const birlestirilmis = db.prepare(`SELECT count(*) AS n FROM odunc WHERE uye_id = ${girdi}`).get().n;

console.log("bagli degerle donen satir sayisi      :", bagli);
console.log("metin birlestirmeyle donen satir sayisi:", birlestirilmis);
console.log("tablodaki toplam satir sayisi          :",
  db.prepare("SELECT count(*) AS n FROM odunc").get().n);
```

```sh
node baglama.mjs
```

```
bagli degerle donen satir sayisi      : 0
metin birlestirmeyle donen satir sayisi: 12
tablodaki toplam satir sayisi          : 12
```

Bağlı değer olarak gönderilen girdi, sorgunun yapısına değil bir karşılaştırmanın sağ
tarafına yerleşti; hiçbir satır eşleşmedi. Metin birleştirmede aynı girdi `OR 1=1` koşulu
hâline geldi ve bütün tabloyu döndürdü. İleri SQL kursunda tanıtılan SQL enjeksiyonunun
uygulama katmanındaki görünümü budur.

Bağlama her yerde çalışmaz. Tablo ve sütun adları — yani **tanıtıcılar** — bağlı değer
olamaz; sorgu planı bunlar bilinmeden kurulamaz. `SorguDeposu` sınıfındaki `siralama` ve
`yon` alanları metne doğrudan giriyordu. Dışarıdan gelen bir değer bu alanlara ulaşıyorsa
denetim listeyle yapılır.

```js
// tanitici-denetimi.mjs — siralama sutunu baglanamaz, listeyle sinirlanir
import { DatabaseSync } from "node:sqlite";

const SIRALANABILIR = new Set(["odunc_id", "alis_tarihi", "iade_tarihi"]);
const YONLER = new Set(["ASC", "DESC"]);

function guvenliSirala(db, siralama, yon) {
  if (!SIRALANABILIR.has(siralama)) throw new Error(`siralanamaz sutun: ${siralama}`);
  if (!YONLER.has(yon)) throw new Error(`gecersiz yon: ${yon}`);
  return db.prepare(`SELECT odunc_id FROM odunc ORDER BY ${siralama} ${yon} LIMIT 3`).all()
           .map((r) => r.odunc_id);
}

const db = new DatabaseSync("kutuphane.db");
console.log("alis_tarihi DESC:", guvenliSirala(db, "alis_tarihi", "DESC").join(", "));
for (const kotu of ["uye_id", "odunc_id; DROP TABLE odunc"]) {
  try { guvenliSirala(db, kotu, "ASC"); }
  catch (h) { console.log("reddedildi:", h.message); }
}
console.log("odunc tablosu hala yerinde, satir sayisi:",
  db.prepare("SELECT count(*) AS n FROM odunc").get().n);
```

```sh
node tanitici-denetimi.mjs
```

```
alis_tarihi DESC: 12, 11, 10
reddedildi: siralanamaz sutun: uye_id
reddedildi: siralanamaz sutun: odunc_id; DROP TABLE odunc
odunc tablosu hala yerinde, satir sayisi: 12
```

Beyaz liste yaklaşımı kaçış karakteri aramaktan güvenlidir, çünkü izin verilenleri
sayar; yasaklıları tahmin etmeye çalışmaz. Listede olmayan `uye_id` de reddedildi: bu
sütun geçerli olsa bile listede yoksa geçmez.

#### Kalıbın Sınırı

Belirtim, ölçütü taşınabilir kılar ama şemayı gizlemez. `uye_id` sütun adı belirtimin
içinde geçiyor; şemada bu ad değişirse belirtim de değişir. Bu, deponun vaadinin bir
adım geriye çekilmesidir: çağıran taraf artık sorgu yazmıyor, ama alan adlarını biliyor.
Ödünleşim bilinçli kabul edilir; alternatifi, her ölçüt bileşimi için ayrı bir depo
yöntemi yazmaktır.

İkinci sınır kapsamdır. Bu belirtimler tek bir bağıntı üzerinde çalışıyor. Birleştirme
gerektiren ölçütler (şubeye göre süzme gibi) belirtimin yalnız `WHERE` parçasını değil
`FROM` parçasını da etkilemesini gerektirir; bu, kalıbı hızla karmaşıklaştırır. Çok tablolu
raporlar ilk derste konulan ayrımın doğrudan sorgu tarafında kalır.

#### Özet

- Ölçütü yöntem adına gömmek yerine nesne yapmak, ölçütlerin ve–veya–değil ile
  birleştirilmesini ve tek bir depo yönteminden geçmesini sağlar.
- Her belirtim kendi SQL parçasını ve bağlı değerlerini üretir; birleştiriciler değer
  sırasını SQL metnindeki soru işareti sırasıyla aynı tutar.
- Sorgu nesnesi ölçütün yanında sıralama, sınır ve atlamayı da taşır; üretilen metin ve
  değerler yazdırılarak doğrulanabilir.
- Aynı belirtimin yüklem alanı, kaydedilmemiş bir adayı veritabanına gitmeden denetler;
  kural tek yerde tanımlı kalır.
- Değerler bağlanır, tanıtıcılar bağlanamaz: sıralama sütunu ve yön izin listesiyle
  sınırlanır.

#### Sonraki Adım

Sorgular artık tek bir noktadan üretiliyor, ama hepsi aynı şeye ihtiyaç duyuyor: bir
bağlantı. Şimdiye kadar her betik kendi bağlantısını açtı ve iş bitince süreç sona erdi.
Sürekli çalışan bir servis böyle davranamaz; bağlantı açmak pahalıdır ve veritabanının
kabul edebileceği eşzamanlı bağlantı sayısı sınırlıdır. Sonraki ders bağlantıları önceden
açıp ödünç veren havuzu kurar, havuz boyutunu değiştirerek isteklerin kuyrukta bekleme
süresini paralel çağrılarla ölçer ve doygunluk noktasının nerede belirdiğini gösterir.

### Bağlantı Havuzu

Source: https://academia.sh/tr/kurslar/veri-erisim-katmani/baglanti-havuzu

Bağlantının pahalı kaynak olarak yönetilmesi: açma maliyetinin ölçülmesi, sabit boyutlu havuz ve kuyruk, havuz boyutu ile bekleme süresi ilişkisinin paralel isteklerle ölçülmesi, doygunlukta kuyruk zaman aşımı ve oturum durumunun taşınması.

Önceki iki ders sorguyu tek bir noktadan üretilir hâle getirdi. Sorgunun çalışabilmesi için
bir şey daha gerekir: bağlantı. Şimdiye kadar her betik kendi bağlantısını açtı, işini
bitirdi ve süreç sona erdi.

Sürekli çalışan bir servis böyle davranamaz. Bağlantı açmak bedava değildir ve
veritabanının kabul edeceği eşzamanlı bağlantı sayısı sınırlıdır. **Bağlantı havuzu**
(connection pool), önceden açılmış bağlantıları saklayan ve isteklere ödünç veren yapıdır.
Bu ders havuzu kurar, havuz boyutunu değiştirerek isteklerin kuyrukta ne kadar beklediğini
ölçer ve havuzun getirdiği iki tuzağı gösterir.

#### Açma Maliyeti

İlk soru maliyetin gerçekten var olup olmadığıdır.

```js
// acma-maliyeti.mjs — her istekte baglanti acmak ile tek baglantiyi yeniden kullanmak
import { DatabaseSync } from "node:sqlite";

const N = 2000;
const SORGU = "SELECT count(*) AS n FROM odunc WHERE iade_tarihi IS NULL";

let t = performance.now();
for (let i = 0; i < N; i++) {
  const db = new DatabaseSync("kutuphane.db");
  db.prepare(SORGU).get();
  db.close();
}
const acKapa = performance.now() - t;

const db = new DatabaseSync("kutuphane.db");
t = performance.now();
for (let i = 0; i < N; i++) db.prepare(SORGU).get();
const yenidenKullan = performance.now() - t;
db.close();

console.log(`her istekte ac-kapa : ${acKapa.toFixed(0)} ms  (${(acKapa / N).toFixed(3)} ms/istek)`);
console.log(`yeniden kullan      : ${yenidenKullan.toFixed(0)} ms  (${(yenidenKullan / N).toFixed(3)} ms/istek)`);
console.log(`oran                : ${(acKapa / yenidenKullan).toFixed(1)}x`);
```

Aşağıdaki komut şemayı kurar ve ölçümü çalıştırır.

```sh
rm -f kutuphane.db
sqlite3 kutuphane.db <<'SQL'
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL,
                    uye_id INTEGER NOT NULL, alis_tarihi TEXT NOT NULL, iade_tarihi TEXT);
INSERT INTO odunc VALUES (1,1,1,'2025-01-10','2025-01-24'),(3,1,2,'2025-02-11',NULL),
  (7,5,4,'2025-04-21',NULL),(10,3,5,'2025-06-03',NULL),(12,4,4,'2025-06-20','2025-07-04');
SQL
node acma-maliyeti.mjs
```

```
her istekte ac-kapa : 113 ms  (0.057 ms/istek)
yeniden kullan      : 10 ms  (0.005 ms/istek)
oran                : 11.3x
```

Süreler donanıma ve dosya sistemine bağlıdır; başka bir makinede sayılar değişir. Anlamlı
olan orandır: aynı işi yapmak için bağlantıyı her seferinde açmak, açık bir bağlantıyı
yeniden kullanmaktan on bir kat pahalıya geldi. Bu ölçüm yerel bir dosya üzerindedir. Ağ
üzerinden konuşulan bir veritabanında açma maliyeti ayrıca oturum kurulumu, kimlik
doğrulama ve şifreleme el sıkışması içerir; oran büyür.

#### Havuz ve Kuyruk

Havuz iki durumu yönetir: boştaki bağlantıların listesi ve boş bağlantı bekleyenlerin
kuyruğu. Boş bağlantı varsa istek anında karşılanır; yoksa kuyruğa girer.

```js
// havuz.mjs — sabit boyutlu kaynak havuzu ve kuyrugu
export class Havuz {
  constructor(uret, boyut, kuyrukZamanAsimiMs) {
    this.bos = Array.from({ length: boyut }, (_, i) => uret(i));
    this.kuyruk = [];
    this.kuyrukZamanAsimiMs = kuyrukZamanAsimiMs;
    this.boyut = boyut;
  }
  al() {
    if (this.bos.length > 0) return Promise.resolve(this.bos.pop());
    return new Promise((coz, hata) => {
      const kayit = { coz, hata };
      kayit.sayac = setTimeout(() => {
        this.kuyruk.splice(this.kuyruk.indexOf(kayit), 1);
        hata(new Error("kuyruk zaman asimi"));
      }, this.kuyrukZamanAsimiMs);
      this.kuyruk.push(kayit);
    });
  }
  birak(baglanti) {
    const kayit = this.kuyruk.shift();
    if (kayit === undefined) { this.bos.push(baglanti); return; }
    clearTimeout(kayit.sayac);
    kayit.coz(baglanti);
  }
}
```

`birak` yönteminin davranışı dikkat ister: bırakılan bağlantı boş listeye geri konmadan
önce kuyruğa bakılır. Kuyrukta bekleyen varsa bağlantı doğrudan ona devredilir. Bu, boş
listeye koyup sonra yeniden almaktan hem daha hızlıdır hem de bekleyenlerin sırasını korur.

Havuzun bir bağlantıyı sonsuza kadar bekletmemesi gerekir. `kuyrukZamanAsimiMs` süresi
dolduğunda bekleyen istek kuyruktan çıkarılır ve hata alır. Bu **kuyruk zaman aşımı**dır
(queue timeout); yokluğunda yavaşlayan bir veritabanı, bütün isteklerin süresiz beklemesine
ve uygulamanın tamamen durmasına yol açar.

#### Havuzlu Servis

Servis her istekte havuzdan bir bağlantı alır, sorguyu çalıştırır ve bağlantıyı `finally`
bloğunda geri bırakır. `finally` zorunludur: hata durumunda bırakılmayan bağlantı havuzdan
kalıcı olarak eksilir.

```js
// sunucu.mjs — her istek havuzdan bir baglanti alir, sorguyu calistirir, geri birakir
import { createServer } from "node:http";
import { DatabaseSync } from "node:sqlite";
import { Havuz } from "./havuz.mjs";

const BOYUT = Number(process.env.HAVUZ_BOYUTU ?? 4);
const GIDIS_DONUS_MS = Number(process.env.GIDIS_DONUS_MS ?? 40);
const ZAMAN_ASIMI_MS = Number(process.env.ZAMAN_ASIMI_MS ?? 2000);

const havuz = new Havuz(() => new DatabaseSync("kutuphane.db"), BOYUT, ZAMAN_ASIMI_MS);
const olcumler = [];
const bekle = (ms) => new Promise((c) => setTimeout(c, ms));

const sunucu = createServer(async (istek, yanit) => {
  const yol = new URL(istek.url, "http://yerel").pathname;

  if (yol === "/kapat") { yanit.end("kapaniyor\n"); sunucu.close(); return; }

  if (yol === "/rapor") {
    const b = olcumler.map((o) => o.bekleme).sort((x, y) => x - y);
    const yuzdelik = (p) => (b.length === 0 ? 0 : +b[Math.min(b.length - 1, Math.floor(b.length * p))].toFixed(1));
    yanit.writeHead(200, { "content-type": "application/json" });
    yanit.end(JSON.stringify({
      havuz_boyutu: BOYUT, istek: olcumler.length,
      reddedilen: olcumler.filter((o) => o.reddedildi).length,
      bekleme_ortanca_ms: yuzdelik(0.5), bekleme_p95_ms: yuzdelik(0.95),
      bekleme_en_yuksek_ms: b.length === 0 ? 0 : +b[b.length - 1].toFixed(1),
    }));
    return;
  }

  const kuyrugaGiris = performance.now();
  let baglanti;
  try {
    baglanti = await havuz.al();
  } catch {
    olcumler.push({ bekleme: performance.now() - kuyrugaGiris, reddedildi: true });
    yanit.writeHead(503, { "content-type": "text/plain" });
    yanit.end("havuz dolu\n");
    return;
  }
  const bekleme = performance.now() - kuyrugaGiris;
  try {
    const satir = baglanti.prepare(
      "SELECT count(*) AS n FROM odunc WHERE iade_tarihi IS NULL").get();
    await bekle(GIDIS_DONUS_MS);          // veritabani gidis-donusunun modeli
    olcumler.push({ bekleme, reddedildi: false });
    yanit.writeHead(200, { "content-type": "text/plain" });
    yanit.end(`acik kayit: ${satir.n}  bekleme_ms: ${bekleme.toFixed(1)}\n`);
  } finally {
    havuz.birak(baglanti);
  }
});
sunucu.listen(8080, () => console.log(`havuz boyutu ${BOYUT}, port 8080`));
```

`GIDIS_DONUS_MS` süresi bir modeldir ve bu ders boyunca model olarak kalır. Buradaki
veritabanı süreç içinde çalıştığı için gerçek bir ağ gidiş-dönüşü yoktur; ayrı bir sunucuda
duran veritabanının cevap süresi bu bekleme ile temsil edilir. Ölçülen şey gidiş-dönüşün
kendisi değil, **bağlantının o süre boyunca meşgul olmasının kuyruğa etkisidir**.

#### Havuz Boyutu ve Bekleme Süresi

Ölçüm düzeneği basittir: aynı anda yirmi dört istek gönder, havuz boyutunu değiştir,
isteklerin kuyrukta ne kadar beklediğine bak.

```sh
# olc.sh — havuz boyutunu degistirerek 24 paralel istegin bekleme suresini olcer
for boyut in 1 2 4 8 24; do
  HAVUZ_BOYUTU=$boyut GIDIS_DONUS_MS=40 node sunucu.mjs >/dev/null &
  sunucu=$!
  sleep 0.6
  istekler=""
  baslangic=$(date +%s%N)
  for i in $(seq 24); do
    curl -s -o /dev/null http://localhost:8080/odunc &
    istekler="$istekler $!"
  done
  wait $istekler
  bitis=$(date +%s%N)
  printf 'toplam_ms=%s  ' $(( (bitis - baslangic) / 1000000 ))
  curl -s http://localhost:8080/rapor
  echo
  curl -s -o /dev/null http://localhost:8080/kapat
  wait $sunucu
done
```

```sh
sh olc.sh
```

```
toplam_ms=1035  {"havuz_boyutu":1,"istek":24,"reddedilen":0,"bekleme_ortanca_ms":493.8,"bekleme_p95_ms":904.2,"bekleme_en_yuksek_ms":945.1}
toplam_ms=533  {"havuz_boyutu":2,"istek":24,"reddedilen":0,"bekleme_ortanca_ms":241.7,"bekleme_p95_ms":444.5,"bekleme_en_yuksek_ms":445.1}
toplam_ms=284  {"havuz_boyutu":4,"istek":24,"reddedilen":0,"bekleme_ortanca_ms":115.3,"bekleme_p95_ms":196.3,"bekleme_en_yuksek_ms":196.7}
toplam_ms=161  {"havuz_boyutu":8,"istek":24,"reddedilen":0,"bekleme_ortanca_ms":37,"bekleme_p95_ms":76.5,"bekleme_en_yuksek_ms":76.5}
toplam_ms=83  {"havuz_boyutu":24,"istek":24,"reddedilen":0,"bekleme_ortanca_ms":0,"bekleme_p95_ms":0.1,"bekleme_en_yuksek_ms":0.3}
```

Mutlak süreler makineye bağlıdır; ilişki değildir. Havuz boyutu her ikiye katlandığında
ortanca bekleme yarıya iniyor: 494, 242, 115, 37 milisaniye. Bunun nedeni doğrudan
görülebilir. Yirmi dört istek, boyut $b$ olan bir havuzda $\lceil 24/b \rceil$ turda
işlenir; her tur bir hizmet süresi kadar sürer, dolayısıyla kuyruktaki ortalama bekleme
tur sayısıyla orantılıdır.

Havuz istek sayısına eşitlendiğinde bekleme sıfırlanıyor. Bu, "havuzu büyütmek her zaman
iyidir" sonucunu vermez. Beklemenin sıfırlanması, işin daha hızlı yapılması demek değildir;
kuyruğun uygulamadan veritabanına taşınması demektir. Veritabanının kabul edebileceği
eşzamanlı bağlantı sayısı ve işleyebileceği eşzamanlı sorgu sayısı sınırlıdır; havuz o
sınırın üstüne çıkarsa bekleme uygulamada değil veritabanında birikir ve orada ölçmek
zorlaşır.

İkinci nokta çarpımdır. Uygulama birden çok kopya hâlinde çalışıyorsa toplam bağlantı
sayısı kopya sayısı ile havuz boyutunun çarpımıdır. Sekiz kopya ve yirmi bağlantılık havuz,
veritabanından yüz altmış bağlantı ister.

#### Doygunluk

Havuzdaki bütün bağlantılar meşgulken ve kuyruk beklemesi zaman aşımını geçtiğinde havuz
**doygunluğa** ulaşmıştır. Bu durumda istek reddedilir.

```sh
# doygunluk.sh — havuz kucuk, kuyruk zaman asimi kisa: reddedilen istekler
HAVUZ_BOYUTU=2 GIDIS_DONUS_MS=40 ZAMAN_ASIMI_MS=200 node sunucu.mjs >/dev/null &
sunucu=$!
sleep 0.6
istekler=""
for i in $(seq 24); do
  curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8080/odunc >> kodlar.txt &
  istekler="$istekler $!"
done
wait $istekler
echo "yanit kodu dagilimi:"
sort kodlar.txt | uniq -c
rm -f kodlar.txt
curl -s http://localhost:8080/rapor; echo
curl -s -o /dev/null http://localhost:8080/kapat
wait $sunucu
```

```sh
sh doygunluk.sh
```

```
yanit kodu dagilimi:
  12 200
  12 503
{"havuz_boyutu":2,"istek":24,"reddedilen":12,"bekleme_ortanca_ms":200.4,"bekleme_p95_ms":201.6,"bekleme_en_yuksek_ms":201.7}
```

Yirmi dört isteğin yarısı yanıtlandı, yarısı iki yüz milisaniyeyi bekledikten sonra 503 ile
reddedildi. Kabul edilen ve reddedilen isteklerin oranı makinenin o andaki yüküne göre
çalıştırmadan çalıştırmaya değişir; değişmeyen, reddedilen isteklerin beklemesinin zaman
aşımı değerinin hemen üstünde toplanmasıdır. Bu, sınırın çalıştığının işaretidir.

Reddetmek başarısızlık gibi görünür, ama alternatifi daha kötüdür. Zaman aşımı olmasaydı bu
on iki istek beklemeye devam eder, istemci tarafındaki zaman aşımları devreye girer,
istemciler yeniden dener ve kuyruk daha da uzardı. Sınırlı bir kuyruk, hızlı ve okunabilir
bir hata üretir.

#### Oturum Durumunun Taşınması

Havuzun ikinci tuzağı bağlantının kendisiyle ilgilidir. Bir bağlantı yalnız bir borudan
ibaret değildir; üzerinde geçici tablolar, ayarlar ve açık bir işlem bulunabilir. Bu
birikime **oturum durumu** (session state) denir ve bırakılan bağlantıyla birlikte sonraki
alıcıya geçer.

```js
// oturum-durumu.mjs — birakilan baglantidaki artik durum sonraki alana gecer
import { DatabaseSync } from "node:sqlite";
import { Havuz } from "./havuz.mjs";

const havuz = new Havuz(() => new DatabaseSync("kutuphane.db"), 1, 1000);

const birinci = await havuz.al();
birinci.exec("CREATE TEMP TABLE gecici_rapor (n INTEGER)");
birinci.exec("INSERT INTO gecici_rapor VALUES (42)");
havuz.birak(birinci);                       // temizlik yapilmadi

const ikinci = await havuz.al();
const kalinti = ikinci.prepare(
  "SELECT count(*) AS n FROM temp.sqlite_master WHERE name = 'gecici_rapor'").get().n;
console.log("ikinci alicinin gordugu artik tablo sayisi:", kalinti);
if (kalinti > 0) console.log("icindeki deger:", ikinci.prepare("SELECT n FROM gecici_rapor").get().n);

ikinci.exec("DROP TABLE IF EXISTS gecici_rapor");   // birakmadan once temizlik
havuz.birak(ikinci);
const ucuncu = await havuz.al();
console.log("temizlikten sonra artik tablo sayisi     :", ucuncu.prepare(
  "SELECT count(*) AS n FROM temp.sqlite_master WHERE name = 'gecici_rapor'").get().n);
```

```sh
node oturum-durumu.mjs
```

```
ikinci alicinin gordugu artik tablo sayisi: 1
icindeki deger: 42
temizlikten sonra artik tablo sayisi     : 0
```

İkinci alıcı, hiç oluşturmadığı bir tabloyu ve içindeki değeri gördü. Bu, iki isteğin
birbirinin durumunu okuması demektir. Havuzun bırakma yolunda bir temizlik adımı olmak
zorundadır: açık işlem geri alınır, geçici nesneler düşürülür, oturum ayarları
varsayılana döner.

İlişkisel Veritabanı Yönetimi kursunda tanıtılan havuzlama düzeyleri bu soruna verilen
farklı yanıtlardır. Oturum düzeyinde havuzlamada bağlantı istemciye oturum boyunca aittir
ve oturum durumu korunur. İşlem düzeyinde havuzlamada bağlantı her işlem sonunda geri
alınır; bu, hazır deyimler ve geçici tablolar gibi oturuma bağlı özelliklerin
kullanılamaması pahasına havuz kullanımını yükseltir.

#### Özet

- Bağlantıyı her istekte açmak, açık bir bağlantıyı yeniden kullanmaktan bu ölçümde on bir
  kat pahalıya geldi; havuzun varlık nedeni bu farktır.
- Havuz iki yapı tutar: boştaki bağlantılar ve bekleyenlerin kuyruğu. Bırakılan bağlantı
  boş listeye dönmeden önce kuyruğa devredilir.
- Yirmi dört paralel istekte havuz boyutu ikiye katlandıkça ortanca bekleme yarıya indi
  (494 → 242 → 115 → 37 ms); havuz istek sayısına eşitlendiğinde bekleme sıfırlandı.
- Havuzu büyütmek beklemeyi yok etmez, kuyruğu veritabanına taşır; toplam bağlantı sayısı
  uygulama kopyası sayısıyla çarpılır.
- Doygunlukta kuyruk zaman aşımı isteği hızla reddeder; sınırsız kuyruk yerine hızlı hata
  tercih edilir.
- Bırakılan bağlantıdaki oturum durumu sonraki alıcıya geçer; bırakma yolunda temizlik
  zorunludur.

#### Sonraki Adım

Havuz kurulduğunda bütün bağlantılar aynı şemayı görür. Bu varsayım, şema değişene kadar
doğrudur. Yeni bir sütun eklendiğinde, bir sütun kaldırıldığında ya da bir alan adı
değiştiğinde uygulamanın çalışan kopyaları ile veritabanının durumu birbirinden ayrılır;
eski kopya yeni şemayı, yeni kopya eski şemayı görebilir. Sonraki ders şema değişikliğini
sürümlenmiş göç adımlarına böler, ileri ve geri yönde çalıştırır ve bir sütunun hizmet
kesilmeden nasıl yeniden adlandırıldığını üç adımlı bir düzenle uygulayarak gösterir.

### Şema Göçleri

Source: https://academia.sh/tr/kurslar/veri-erisim-katmani/sema-gocleri

Şema değişikliğinin sürümlenmiş adımlara bölünmesi: ileri ve geri göç betikleri, sürüm tablosu, başarısız adımın atomik geri alınması ve bir sütunun genişlet–yaz–daralt düzeniyle hizmet kesilmeden değiştirilmesi.

Bağlantı havuzu, bütün bağlantıların aynı şemayı gördüğü varsayımıyla çalışır. Varsayım
şema değişene kadar doğrudur. Sütun eklendiğinde, kaldırıldığında ya da adı değiştiğinde
uygulamanın çalışan kopyalarıyla veritabanının durumu ayrışır: hangi kopyanın hangi şemayı
gördüğü, kopyaların ne zaman yenilendiğine bağlı kalır.

SQL Temelleri kursunda şema değiştirme, tek başına çalıştırılan bir komut olarak tanıtıldı.
Uygulama katmanında aynı komut bir **göç** (migration) adımına dönüşür: sürümlenmiş,
sırayla uygulanan, geri alınabilir ve hangi ortamda hangisinin uygulandığı kayıtlı bir
birim. Bu ders göç yürütücüsünü kurar, iki yönde çalıştırır ve bir sütunu hizmet kesilmeden
değiştirir.

#### Adımlar ve Sürüm Tablosu

Göç adımları dosyadır. Her adımın ileri ve geri yönü ayrı dosyada durur; ad, sıra numarası
ile başlar.

```sh
mkdir -p goc
cat > goc/001-odunc-tablosu.up.sql <<'SQL'
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, soyad TEXT NOT NULL,
                  eposta TEXT, kayit_tarihi TEXT NOT NULL);
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL,
                    uye_id INTEGER NOT NULL REFERENCES uye(uye_id),
                    alis_tarihi TEXT NOT NULL, iade_tarihi TEXT);
SQL
cat > goc/001-odunc-tablosu.down.sql <<'SQL'
DROP TABLE odunc;
DROP TABLE uye;
SQL
cat > goc/002-gecikme-ucreti.up.sql <<'SQL'
ALTER TABLE odunc ADD COLUMN gecikme_ucreti REAL NOT NULL DEFAULT 0;
SQL
cat > goc/002-gecikme-ucreti.down.sql <<'SQL'
ALTER TABLE odunc DROP COLUMN gecikme_ucreti;
SQL
cat > goc/003-acik-odunc-dizini.up.sql <<'SQL'
CREATE INDEX odunc_acik_dizini ON odunc (uye_id) WHERE iade_tarihi IS NULL;
SQL
cat > goc/003-acik-odunc-dizini.down.sql <<'SQL'
DROP INDEX odunc_acik_dizini;
SQL
```

Hangi adımın uygulandığı bilgisi veritabanının kendisinde durur. Dosya sisteminde ya da
yapılandırma dosyasında tutulsaydı, veritabanı kopyalandığında bilgi kopyayla birlikte
gitmezdi.

```js
// goc.mjs — surum tablosuna bakarak goc adimlarini ileri ve geri calistirir
import { DatabaseSync } from "node:sqlite";
import { readdirSync, readFileSync } from "node:fs";

const db = new DatabaseSync(process.env.VT ?? "kutuphane.db");
db.exec(`CREATE TABLE IF NOT EXISTS sema_surumu (
           surum INTEGER PRIMARY KEY, ad TEXT NOT NULL, uygulanma TEXT NOT NULL)`);

const adimlar = [...new Set(readdirSync("goc").map((d) => d.replace(/\.(up|down)\.sql$/, "")))]
  .sort()
  .map((ad) => ({ surum: Number(ad.slice(0, 3)), ad }));

const uygulanan = () => db.prepare("SELECT surum FROM sema_surumu ORDER BY surum")
                          .all().map((r) => r.surum);

function calistir(adim, yon) {
  const sql = readFileSync(`goc/${adim.ad}.${yon}.sql`, "utf8");
  db.exec("BEGIN");
  try {
    db.exec(sql);
    if (yon === "up") {
      db.prepare("INSERT INTO sema_surumu VALUES (?,?,datetime('now'))").run(adim.surum, adim.ad);
    } else {
      db.prepare("DELETE FROM sema_surumu WHERE surum = ?").run(adim.surum);
    }
    db.exec("COMMIT");
    console.log(`${yon === "up" ? "ileri" : "geri "}  ${adim.ad}`);
  } catch (h) {
    db.exec("ROLLBACK");
    console.log(`HATA  ${adim.ad}: ${h.message}`);
    process.exit(1);
  }
}

const [komut, hedefArg] = process.argv.slice(2);
const hedef = hedefArg === undefined ? null : Number(hedefArg);

if (komut === "durum") {
  const u = new Set(uygulanan());
  for (const a of adimlar) console.log(`${u.has(a.surum) ? "[x]" : "[ ]"} ${a.ad}`);
  console.log("gecerli surum:", Math.max(0, ...u));
} else if (komut === "yukselt") {
  const u = new Set(uygulanan());
  for (const a of adimlar) {
    if (!u.has(a.surum) && (hedef === null || a.surum <= hedef)) calistir(a, "up");
  }
} else if (komut === "alcalt") {
  const u = uygulanan().sort((x, y) => y - x);
  for (const s of u) {
    if (s > hedef) calistir(adimlar.find((a) => a.surum === s), "down");
  }
} else {
  console.log("kullanim: node goc.mjs durum | yukselt [surum] | alcalt <surum>");
}
```

Yürütücünün üç kararı vardır. Adım ile sürüm kaydı **aynı işlem içinde** yazılır; adımlar
dosya adına göre sıralanır, dolayısıyla numaralandırma sıralamayı belirler; geri yönde
sıra tersine döner.

#### İleri Yürütme

```sh
node goc.mjs durum
node goc.mjs yukselt 3
node goc.mjs durum
```

```
[ ] 001-odunc-tablosu
[ ] 002-gecikme-ucreti
[ ] 003-acik-odunc-dizini
gecerli surum: 0
ileri  001-odunc-tablosu
ileri  002-gecikme-ucreti
ileri  003-acik-odunc-dizini
[x] 001-odunc-tablosu
[x] 002-gecikme-ucreti
[x] 003-acik-odunc-dizini
gecerli surum: 3
```

Üç adım sırayla uygulandı ve sürüm tablosuna yazıldı. İkinci `durum` çağrısı bilgiyi
dosyalardan değil veritabanından okuyor; aynı göç dizini başka bir veritabanına karşı
çalıştırılsaydı orada bütün kutular boş görünürdü.

#### Başarısız Adımın Atomikliği

Bir adımın ortasında hata çıkarsa, o adımın yarısı uygulanmış olarak kalmamalıdır.
Aşağıdaki adım ikinci satırında kasıtlı olarak hata üretir.

```sh
cat > goc/004-bozuk-ornek.up.sql <<'SQL'
ALTER TABLE odunc ADD COLUMN sube_id INTEGER;
ALTER TABLE odunc ADD COLUMN sube_id INTEGER;
SQL
cat > goc/004-bozuk-ornek.down.sql <<'SQL'
ALTER TABLE odunc DROP COLUMN sube_id;
SQL
node goc.mjs yukselt 4
echo "cikis kodu: $?"
node goc.mjs durum
sqlite3 kutuphane.db "PRAGMA table_info(odunc);" | grep -c sube_id
```

```
HATA  004-bozuk-ornek: duplicate column name: sube_id
cikis kodu: 1
[x] 001-odunc-tablosu
[x] 002-gecikme-ucreti
[x] 003-acik-odunc-dizini
[ ] 004-bozuk-ornek
gecerli surum: 3
0
```

Üç sonuç birden görünüyor: yürütücü sıfırdan farklı bir çıkış kodu verdi, sürüm tablosunda
adım işaretlenmedi ve son satırdaki sayı `sube_id` sütununun tabloda bulunmadığını
gösteriyor. İlk `ALTER TABLE` komutu başarılı olmuştu; işlem geri alındığında o da geri
alındı.

Bu davranış motora bağlıdır. Şema değiştirme komutlarının işlem içinde geri alınabilmesi
her ilişkisel veritabanında yoktur; geri alınamayan motorlarda her adım tek bir komut
içerecek şekilde küçültülür, çünkü adımın yarısında kalmak elle onarım gerektirir.

#### Geri Yürütme

Geri yön, uygulanmış adımları tersten çalıştırır.

```sh
node goc.mjs alcalt 1
node goc.mjs durum
```

```
geri   003-acik-odunc-dizini
geri   002-gecikme-ucreti
[x] 001-odunc-tablosu
[ ] 002-gecikme-ucreti
[ ] 003-acik-odunc-dizini
[ ] 004-bozuk-ornek
gecerli surum: 1
```

Önce dizin düştü, sonra sütun kaldırıldı. Sıra ters olsaydı sütunu kullanan bir dizin
ortada kalırdı. Geri yönün yazılması ihmal edilirse göç tek yönlü olur ve hatalı bir
sürümden dönüş yolu kalmaz.

#### Sıfır Kesintili Sütun Değişimi

Asıl zorluk, uygulamanın çalışmaya devam ettiği bir değişikliktir. `uye` bağıntısındaki
`eposta` sütununun adı `e_posta` olacak. Tek adımda yeniden adlandırma **kırıcı
değişikliktir**: adı değiştiren komut çalıştığı anda, henüz yenilenmemiş uygulama kopyaları
hata almaya başlar.

Çözüm değişikliği üç adıma bölmektir. İlişkisel Veritabanı Yönetimi kursunda bu düzen
**genişlet–daralt göçü** adıyla geçti; arada bir yazma evresi bulunur.

1. **Genişlet:** yeni sütun eklenir ve var olan değerler kopyalanır. Eski sütun durur.
2. **Yaz:** uygulamanın yeni sürümü iki sütuna birden yazar, yeni sütundan okur. Bu
   evredeki yazma davranışına **çift yazma** denir. Eski sürüm de çalışmaya devam eder.
3. **Daralt:** bütün kopyalar yenilendikten sonra eski sütun kaldırılır.

Adımlar üç göç dosyasıdır.

```sh
cat > goc/005-eposta-genislet.up.sql <<'SQL'
ALTER TABLE uye ADD COLUMN e_posta TEXT;
UPDATE uye SET e_posta = eposta WHERE e_posta IS NULL;
SQL
cat > goc/005-eposta-genislet.down.sql <<'SQL'
ALTER TABLE uye DROP COLUMN e_posta;
SQL
cat > goc/006-eposta-dolgu.up.sql <<'SQL'
UPDATE uye SET e_posta = eposta WHERE e_posta IS NULL AND eposta IS NOT NULL;
SQL
cat > goc/006-eposta-dolgu.down.sql <<'SQL'
SELECT 1;
SQL
cat > goc/007-eposta-daralt.up.sql <<'SQL'
ALTER TABLE uye DROP COLUMN eposta;
SQL
cat > goc/007-eposta-daralt.down.sql <<'SQL'
ALTER TABLE uye ADD COLUMN eposta TEXT;
UPDATE uye SET eposta = e_posta;
SQL
```

İki kod sürümü, uygulamanın eski ve yeni kopyalarını temsil eder.

```js
// eski-kod.mjs — calisan eski surum: yalniz eposta sutununu bilir
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("kutuphane.db");
try {
  db.prepare("INSERT INTO uye (ad, soyad, eposta, kayit_tarihi) VALUES (?,?,?,?)")
    .run("Eski", "Kayit", "eski@ornek.test", "2025-07-20");
  const n = db.prepare("SELECT count(*) AS n FROM uye WHERE eposta IS NOT NULL").get().n;
  console.log(`eski kod: calisti, eposta dolu satir = ${n}`);
} catch (h) {
  console.log(`eski kod: HATA -> ${h.message}`);
}
```

```js
// yeni-kod.mjs — yeni surum: e_posta sutununu okur; CIFT_YAZMA=1 iken eskiye de yazar
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("kutuphane.db");
const cift = process.env.CIFT_YAZMA === "1";
try {
  if (cift) {
    db.prepare("INSERT INTO uye (ad, soyad, e_posta, eposta, kayit_tarihi) VALUES (?,?,?,?,?)")
      .run("Yeni", "Kayit", "yeni@ornek.test", "yeni@ornek.test", "2025-07-20");
  } else {
    db.prepare("INSERT INTO uye (ad, soyad, e_posta, kayit_tarihi) VALUES (?,?,?,?)")
      .run("Yeni", "Kayit", "yeni@ornek.test", "2025-07-20");
  }
  const n = db.prepare("SELECT count(*) AS n FROM uye WHERE e_posta IS NOT NULL").get().n;
  console.log(`yeni kod: calisti (cift yazma=${cift ? "acik" : "kapali"}), e_posta dolu satir = ${n}`);
} catch (h) {
  console.log(`yeni kod: HATA -> ${h.message}`);
}
```

Aşağıdaki betik veritabanını sıfırdan kurar, her adımdan sonra iki kod sürümünü de
çalıştırır. Bozuk örnek adımı, sonraki adımların önünü tıkamaması için siliniyor.

```sh
# sifir-kesinti.sh — uc adimli sutun degisiminde iki kod surumunun durumu
rm -f kutuphane.db goc/004-bozuk-ornek.up.sql goc/004-bozuk-ornek.down.sql
node goc.mjs yukselt 3 >/dev/null
sqlite3 kutuphane.db "INSERT INTO uye (ad,soyad,eposta,kayit_tarihi)
  VALUES ('Ayşe','Demir','ayse@ornek.test','2023-02-14'),
         ('Zeynep','Arslan',NULL,'2024-01-09');"

echo "== surum 3: yalniz eski sutun var =="
node eski-kod.mjs
node yeni-kod.mjs

echo "== surum 5 (genislet): yeni sutun eklendi ve dolduruldu =="
node goc.mjs yukselt 5
node eski-kod.mjs
CIFT_YAZMA=1 node yeni-kod.mjs

echo "== surum 6 (dolgu): eski kodun yazdiklari yeni sutuna tasindi =="
node goc.mjs yukselt 6
node eski-kod.mjs
CIFT_YAZMA=1 node yeni-kod.mjs

echo "== surum 7 (daralt): eski sutun kaldirildi =="
node goc.mjs yukselt 7
node eski-kod.mjs
node yeni-kod.mjs
echo "-- adresi bos kalan satirlar --"
sqlite3 -header -column kutuphane.db "SELECT uye_id, ad, soyad FROM uye WHERE e_posta IS NULL;"
```

```sh
sh sifir-kesinti.sh
```

```
== surum 3: yalniz eski sutun var ==
eski kod: calisti, eposta dolu satir = 2
yeni kod: HATA -> table uye has no column named e_posta
== surum 5 (genislet): yeni sutun eklendi ve dolduruldu ==
ileri  005-eposta-genislet
eski kod: calisti, eposta dolu satir = 3
yeni kod: calisti (cift yazma=acik), e_posta dolu satir = 3
== surum 6 (dolgu): eski kodun yazdiklari yeni sutuna tasindi ==
ileri  006-eposta-dolgu
eski kod: calisti, eposta dolu satir = 5
yeni kod: calisti (cift yazma=acik), e_posta dolu satir = 5
== surum 7 (daralt): eski sutun kaldirildi ==
ileri  007-eposta-daralt
eski kod: HATA -> table uye has no column named eposta
yeni kod: calisti (cift yazma=kapali), e_posta dolu satir = 6
-- adresi bos kalan satirlar --
uye_id  ad      soyad 
------  ------  ------
2       Zeynep  Arslan
6       Eski    Kayit 
```

Ortadaki iki evre kesintisizliğin kanıtıdır: sürüm 5 ve 6'da hem eski hem yeni kod hatasız
çalıştı. Değişikliğin kırıcı olduğu tek an sürüm 7'dir ve o an, bütün kopyalar yenilendikten
sonraya bırakılabilir.

#### Daralt Adımının Koşulu

Çıktının son bölümü bir uyarı taşıyor. `e_posta` sütunu boş kalan iki satır var. Bunlardan
`Zeynep` başlangıçtan beri adressizdi; `Eski Kayit` ise sorunludur. O satırı eski kod, dolgu
adımından **sonra** yazdı: eski sütuna yazdı, yeni sütun boş kaldı ve daralt adımı eski
sütunu kaldırdığında değer kayboldu.

Daralt adımının koşulu buradan çıkar: **eski sütuna yazan hiçbir kopya kalmamalıdır.**
Koşul, dolgu adımını tekrarlayarak değil, eski kopyaların tamamen çekildiğini doğrulayarak
sağlanır. Uygulamada bu doğrulama iki yolla yapılır: yenileme sürecinin bittiğinin
onaylanması ve daralt adımından hemen önce boş kalan satır sayısının sıfır olduğunun
sınanması.

#### Geri Göçün Sınırı

Daralt adımından dönmek mümkündür, ama dönüş şemayı geri getirir; veriyi getirmez.

```sh
node goc.mjs alcalt 6
sqlite3 -header -column kutuphane.db "SELECT uye_id, ad, eposta, e_posta FROM uye WHERE uye_id IN (1,2,6);"
```

```
geri   007-eposta-daralt
uye_id  ad      eposta           e_posta        
------  ------  ---------------  ---------------
1       Ayşe    ayse@ornek.test  ayse@ornek.test
2       Zeynep                                  
6       Eski                                    
```

`eposta` sütunu geri geldi ve `e_posta` değerlerinden dolduruldu. Altıncı satırın adresi
yine boş: daralt adımı onu sildiğinde bilgi tamamen kayboldu. Geri göç bir **geri dönüş
planıdır**, bir yedek değildir. Veri kaybettiren adımların geri yönü ancak silinen veriyi
başka bir yerde saklamışsa anlamlıdır; aksi hâlde geri dönüş yalnız şemayı onarır.

#### Özet

- Göç adımı sürümlenmiş, iki yönlü ve sırayla uygulanan bir birimdir; hangi adımın
  uygulandığı bilgisi veritabanının kendi içindeki sürüm tablosunda durur.
- Adımın kendisi ile sürüm kaydı aynı işlemde yazılır; hata çıkan adımda ikisi birlikte
  geri alındı ve sürüm tablosu değişmedi.
- Bir sütunun kesintisiz değiştirilmesi üç evre ister: genişlet, çift yazmayla yaz, daralt.
  Ortadaki iki evrede eski ve yeni kod sürümleri birlikte çalıştı.
- Daralt adımı, eski sütuna yazan kopya kalmadan uygulanamaz; kurala uyulmadığında dolgudan
  sonra yazılan satırın değeri kayboldu.
- Geri göç şemayı geri alır, silinen veriyi geri getirmez; geri dönüş planı yedeğin yerini
  tutmaz.

#### Sonraki Adım

Göç yürütücüsü her adımı `BEGIN` ile açıp `COMMIT` ile kapattı ve hata durumunda
`ROLLBACK` çağırdı; adım ile sürüm kaydının birlikte yazılmasını sağlayan da buydu. Aynı
soru uygulamanın normal çalışması için de geçerlidir: bir ödünç verme isteği hangi
yazmaları kapsar, bu yazmaların hepsi birlikte mi kesinleşmelidir, işlemi açan ve kapatan
kod hangi katmanda durur? Sonraki konu bu soruyla açılır ve ilk ders işlem sınırını iş
biriminin tanımı üzerinden kurar: sınır fazla dar çizilirse yarım kalan durumlar, fazla
geniş çizilirse uzun süre tutulan kilitler üretir.

## İşlemler ve Tutarlılık

İş biriminin sınırının nereye çizileceği, yalıtım düzeylerinin uygulamadan görünen etkisi, iyimser ile kötümser kilitlemenin çatışma çözümü, servisler arası atomikliğin maliyeti ve yeniden denemeyi güvenli kılan değişmez işlem tasarımı.

### İşlem Sınırları

Source: https://academia.sh/tr/kurslar/veri-erisim-katmani/islem-sinirlari

İş biriminin sınırının nereye çizileceği: sınırsız yazmaların bıraktığı yarım durum, depo başına işlem açmanın bileşememesi, işlem sınırını tek yerde tutan iş birimi, iç içe sınırlarda kaydetme noktası ve sınır genişliğinin bekleme süresine etkisi.

Göç yürütücüsü her adımı `BEGIN` ile açtı, `COMMIT` ile kapattı ve hata durumunda
`ROLLBACK` çağırdı. Adımın kendisiyle sürüm kaydının birlikte yazılmasını sağlayan buydu.

Aynı soru uygulamanın normal çalışması için de geçerlidir. Bir ödünç verme isteği tek bir
yazma değildir: ödünç kaydı eklenir, kitabın durumu değişir, hareket kütüğüne satır yazılır.
Bunların hepsi birlikte kesinleşmeli midir, sınır nereye çizilir ve sınırı kim açar? İşlem
kavramı İlişkisel Kuram kursunda tanımlandı; bu ders sınırın uygulama katmanındaki yerini
kurar.

#### Üç Yazmalı Bir İş

Aşağıdaki şema kitabın durumunu ve hareket kütüğünü ekler. Hareket türü bir denetim koşuluyla
sınırlıdır; ders boyunca hatayı bu kısıt üretecek.

```sh
# kur.sh — dersin veritabanini sifirdan kurar
rm -f kutuphane.db
sqlite3 kutuphane.db >/dev/null <<'SQL'
PRAGMA journal_mode = WAL;
CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL,
                    durumu TEXT NOT NULL DEFAULT 'rafta' CHECK (durumu IN ('rafta','odunc')));
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL REFERENCES kitap(kitap_id),
                    uye_id INTEGER NOT NULL, alis_tarihi TEXT NOT NULL, iade_tarihi TEXT);
CREATE TABLE hareket (hareket_id INTEGER PRIMARY KEY, odunc_id INTEGER NOT NULL,
                      tur TEXT NOT NULL CHECK (tur IN ('verildi','iade')), tarih TEXT NOT NULL);
INSERT INTO kitap VALUES (1,'Körlük','rafta'),(2,'Tutunamayanlar','rafta'),(3,'Kum Kitabı','rafta');
SQL
```

Durumu her adımda görebilmek için küçük bir okuyucu gerekir.

```js
// durum.mjs — uc bagintideki satir sayilarini ve kitap durumunu yazdirir
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("kutuphane.db");
const say = (t) => db.prepare(`SELECT count(*) AS n FROM ${t}`).get().n;
console.log(`odunc=${say("odunc")}  hareket=${say("hareket")}  kitap1_durumu=${
  db.prepare("SELECT durumu FROM kitap WHERE kitap_id = 1").get().durumu}`);
```

#### Sınır Olmadan

İlk sürüm üç yazmayı arka arkaya çalıştırır; aralarında hiçbir sınır yoktur.

```js
// islem-yok.mjs — uc yazma, islem siniri yok
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("kutuphane.db");
const tur = process.argv[2] ?? "verildi";

const sonuc = db.prepare(
  "INSERT INTO odunc (kitap_id, uye_id, alis_tarihi) VALUES (?,?,?)").run(1, 4, "2025-07-20");
const oduncId = Number(sonuc.lastInsertRowid);
db.prepare("UPDATE kitap SET durumu = 'odunc' WHERE kitap_id = ?").run(1);
try {
  db.prepare("INSERT INTO hareket (odunc_id, tur, tarih) VALUES (?,?,?)")
    .run(oduncId, tur, "2025-07-20");
  console.log("uc yazma da tamamlandi");
} catch (h) {
  console.log("ucuncu yazma basarisiz:", h.message);
}
```

Üçüncü yazmaya geçersiz bir tür verilerek hata üretiliyor.

```sh
sh kur.sh
node islem-yok.mjs "gecikme"
node durum.mjs
```

```
ucuncu yazma basarisiz: CHECK constraint failed: tur IN ('verildi','iade')
odunc=1  hareket=0  kitap1_durumu=odunc
```

Ortaya çıkan durum tutarsızdır. Ödünç kaydı var, kitap ödünçte görünüyor, ama hareket
kütüğünde karşılığı yok. Uygulama açısından bu, "verilmiş ama verildiği kaydedilmemiş" bir
kitaptır; hiçbir raporda görünmez, iade akışında hangi kuralın uygulanacağı belirsizdir.

#### Sınırın İçinde

Aynı üç yazma bir işlem sınırının içine alındığında sonuç değişir.

```js
// islemli.mjs — ayni uc yazma, tek islem sinirinin icinde
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("kutuphane.db");
const tur = process.argv[2] ?? "verildi";

db.exec("BEGIN");
try {
  const sonuc = db.prepare(
    "INSERT INTO odunc (kitap_id, uye_id, alis_tarihi) VALUES (?,?,?)").run(1, 4, "2025-07-20");
  const oduncId = Number(sonuc.lastInsertRowid);
  db.prepare("UPDATE kitap SET durumu = 'odunc' WHERE kitap_id = ?").run(1);
  db.prepare("INSERT INTO hareket (odunc_id, tur, tarih) VALUES (?,?,?)")
    .run(oduncId, tur, "2025-07-20");
  db.exec("COMMIT");
  console.log("islem kesinlestirildi");
} catch (h) {
  db.exec("ROLLBACK");
  console.log("islem geri alindi:", h.message);
}
```

```sh
sh kur.sh
node islemli.mjs "gecikme"
node durum.mjs
node islemli.mjs verildi
node durum.mjs
```

```
islem geri alindi: CHECK constraint failed: tur IN ('verildi','iade')
odunc=0  hareket=0  kitap1_durumu=rafta
islem kesinlestirildi
odunc=1  hareket=1  kitap1_durumu=odunc
```

İlk çağrıda hiçbir yazma kalmadı, ikinci çağrıda üçü birden kaldı. Aradaki durum yok. Bu,
**iş birimi**nin (unit of work) tanımıdır: dışarıdan tek bir değişiklik gibi görünen, ya
tamamı ya hiçbiri uygulanan yazma kümesi.

İş biriminin sınırı iş kuralının sınırıdır, teknik bir sınır değil. "Ödünç verme" bir iş
birimidir çünkü yarısı anlamsızdır. "Gün sonu raporu hazırlama" ile "ödünç verme" ayrı iş
birimleridir, çünkü birinin başarısızlığı ötekini geçersiz kılmaz.

#### Sınır Depoya Çizilirse

Sınırın nereye çizilmeyeceği, çizildiğinde ne olduğuna bakılarak görülür. Her depo yöntemi
kendi işlemini açıp kapatırsa iki yöntem birlikte atomik olamaz.

```js
// kendi-islemi.mjs — her depo yontemi kendi islemini acip kapatirsa ne olur
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("kutuphane.db");

const oduncDeposu = {
  ekle(kitapId, uyeId, tarih) {
    db.exec("BEGIN");
    const s = db.prepare("INSERT INTO odunc (kitap_id, uye_id, alis_tarihi) VALUES (?,?,?)")
                .run(kitapId, uyeId, tarih);
    db.exec("COMMIT");
    return Number(s.lastInsertRowid);
  },
};
const hareketDeposu = {
  ekle(oduncId, tur, tarih) {
    db.exec("BEGIN");
    try {
      db.prepare("INSERT INTO hareket (odunc_id, tur, tarih) VALUES (?,?,?)")
        .run(oduncId, tur, tarih);
      db.exec("COMMIT");
    } catch (h) { db.exec("ROLLBACK"); throw h; }
  },
};

try {
  const id = oduncDeposu.ekle(1, 4, "2025-07-20");
  hareketDeposu.ekle(id, "gecikme", "2025-07-20");
} catch (h) {
  console.log("servis hata aldi:", h.message);
}

console.log("dis islem acmayi dene:");
db.exec("BEGIN");
try {
  oduncDeposu.ekle(2, 5, "2025-07-21");
} catch (h) {
  console.log("  ->", h.message);
  db.exec("ROLLBACK");
}
```

```sh
sh kur.sh
node kendi-islemi.mjs
node durum.mjs
```

```
servis hata aldi: CHECK constraint failed: tur IN ('verildi','iade')
dis islem acmayi dene:
  -> cannot start a transaction within a transaction
odunc=1  hareket=0  kitap1_durumu=rafta
```

İki ayrı sorun aynı çıktıda görünüyor. Birincisi: ödünç kaydı kesinleşmiş, hareket
yazılamamış; servis hata almış olsa bile veritabanında yarım bir durum kalmış. İkincisi:
servis kendisi bir işlem açarak sorunu çözmeye çalıştığında depo yöntemi içeriden ikinci bir
işlem açamamış ve hata vermiş.

Sonuç kuraldır: **işlemi depo açmaz.** Depo, verilen bağlantı üzerinde çalışır; sınırın
nerede başlayıp bittiğine bir üst katman karar verir.

#### İş Birimi

Sınırı taşıyan yapı, depoların kullanacağı bağlantıyı da veren yapıdır.

```js
// is-birimi.mjs — islem sinirini tek yerde tutan is birimi; depolar baglantiyi disaridan alir
export class IsBirimi {
  constructor(db) { this.db = db; this.derinlik = 0; }
  calistir(is) {
    const icIse = this.derinlik > 0;
    const nokta = `nokta_${this.derinlik}`;
    this.db.exec(icIse ? `SAVEPOINT ${nokta}` : "BEGIN");
    this.derinlik += 1;
    try {
      const sonuc = is(this.db);
      this.db.exec(icIse ? `RELEASE ${nokta}` : "COMMIT");
      return sonuc;
    } catch (h) {
      this.db.exec(icIse ? `ROLLBACK TO ${nokta}` : "ROLLBACK");
      throw h;
    } finally {
      this.derinlik -= 1;
    }
  }
}

export const oduncDeposu = {
  ekle(db, kitapId, uyeId, tarih) {
    return Number(db.prepare("INSERT INTO odunc (kitap_id, uye_id, alis_tarihi) VALUES (?,?,?)")
                    .run(kitapId, uyeId, tarih).lastInsertRowid);
  },
};
export const kitapDeposu = {
  oduncIsaretle(db, kitapId) {
    db.prepare("UPDATE kitap SET durumu = 'odunc' WHERE kitap_id = ?").run(kitapId);
  },
};
export const hareketDeposu = {
  ekle(db, oduncId, tur, tarih) {
    db.prepare("INSERT INTO hareket (odunc_id, tur, tarih) VALUES (?,?,?)")
      .run(oduncId, tur, tarih);
  },
};
```

Derinlik sayacı iç içe çağrıları ayırır. En dıştaki çağrı gerçek işlemi açar; içerideki
çağrılar İleri SQL kursunda tanıtılan **kaydetme noktası** kurar. Böylece iç bir birimin
başarısızlığı yalnız kendi yazmalarını geri alır, dış birimi düşürmez.

```js
// servis.mjs — is birimi uc depo cagrisini tek sinirda toplar
import { DatabaseSync } from "node:sqlite";
import { IsBirimi, oduncDeposu, kitapDeposu, hareketDeposu } from "./is-birimi.mjs";

const isBirimi = new IsBirimi(new DatabaseSync("kutuphane.db"));

function oduncVer(kitapId, uyeId, tarih, tur) {
  return isBirimi.calistir((db) => {
    const oduncId = oduncDeposu.ekle(db, kitapId, uyeId, tarih);
    kitapDeposu.oduncIsaretle(db, kitapId);
    hareketDeposu.ekle(db, oduncId, tur, tarih);
    return oduncId;
  });
}

for (const [kitapId, tur] of [[1, "gecikme"], [2, "verildi"]]) {
  try {
    console.log(`kitap ${kitapId} verildi, odunc_id =`, oduncVer(kitapId, 4, "2025-07-20", tur));
  } catch (h) {
    console.log(`kitap ${kitapId} basarisiz:`, h.message);
  }
}
```

```sh
sh kur.sh
node servis.mjs
node durum.mjs
sqlite3 -header -column kutuphane.db "SELECT kitap_id, durumu FROM kitap;"
```

```
kitap 1 basarisiz: CHECK constraint failed: tur IN ('verildi','iade')
kitap 2 verildi, odunc_id = 1
odunc=1  hareket=1  kitap1_durumu=rafta
kitap_id  durumu
--------  ------
1         rafta 
2         odunc 
3         rafta 
```

Birinci istek tamamen geri alındı: kitap 1 rafta kaldı. İkinci istek üç yazmayı birlikte
kesinleştirdi. Servis kodunda `BEGIN` geçmiyor; sınır `calistir` çağrısının kendisidir.

İç içe sınırın davranışı ayrıca görülebilir.

```js
// ic-ice.mjs — ic is birimi basarisiz olsa da dis is birimi kesinlesir
import { DatabaseSync } from "node:sqlite";
import { IsBirimi, oduncDeposu, kitapDeposu, hareketDeposu } from "./is-birimi.mjs";

const isBirimi = new IsBirimi(new DatabaseSync("kutuphane.db"));

isBirimi.calistir((db) => {
  const oduncId = oduncDeposu.ekle(db, 3, 6, "2025-07-22");
  kitapDeposu.oduncIsaretle(db, 3);
  hareketDeposu.ekle(db, oduncId, "verildi", "2025-07-22");
  try {
    isBirimi.calistir((ic) => hareketDeposu.ekle(ic, oduncId, "bildirim", "2025-07-22"));
  } catch (h) {
    console.log("ic is birimi geri alindi:", h.message);
  }
  console.log("dis is birimi devam ediyor, odunc_id =", oduncId);
});
```

```sh
sh kur.sh
node ic-ice.mjs
node durum.mjs
```

```
ic is birimi geri alindi: CHECK constraint failed: tur IN ('verildi','iade')
dis is birimi devam ediyor, odunc_id = 1
odunc=1  hareket=1  kitap1_durumu=rafta
```

İç birimin yazması geri alındı, dıştaki üç yazma kesinleşti. Hareket sayısı bir; iki
olsaydı iç birim geri alınmamış olurdu.

#### Sınırın Genişliği

Sınırı geniş çizmek atomikliği artırır, ama bedeli vardır: işlem açık kaldığı sürece
tuttuğu kilitler serbest kalmaz. Bedel ölçülebilir.

```js
// uzun-islem.mjs — yazma islemini verilen sure boyunca acik tutar
import { DatabaseSync } from "node:sqlite";
import { writeFileSync, rmSync } from "node:fs";
const db = new DatabaseSync("kutuphane.db");
const sureMs = Number(process.argv[2]);
db.exec("BEGIN IMMEDIATE");
db.prepare("UPDATE kitap SET durumu = 'odunc' WHERE kitap_id = 1").run();
writeFileSync("hazir", "");                       // B artik deneyebilir
const bitis = Date.now() + sureMs;
while (Date.now() < bitis) { /* dis cagri beklemesinin modeli */ }
db.exec("COMMIT");
rmSync("hazir");
console.log(`A: islem ${sureMs} ms acik kaldi ve kesinlestirildi`);
```

```js
// bekleyen-yazma.mjs — ayni satira yazmak isteyen ikinci baglantinin bekleme suresi
import { DatabaseSync } from "node:sqlite";
import { existsSync } from "node:fs";
while (!existsSync("hazir")) { /* A'nin islemi acmasini bekle */ }
const db = new DatabaseSync("kutuphane.db");
db.exec("PRAGMA busy_timeout = 10000");
const t = performance.now();
db.exec("BEGIN IMMEDIATE");
db.prepare("UPDATE kitap SET durumu = 'rafta' WHERE kitap_id = 1").run();
db.exec("COMMIT");
console.log(`B: yazma icin ${(performance.now() - t).toFixed(0)} ms bekledi`);
```

```sh
# sinir-genisligi.sh — islem ne kadar acik kalirsa ikinci yazan o kadar bekler
for sure in 100 400 800; do
  sh kur.sh >/dev/null
  rm -f hazir
  node bekleyen-yazma.mjs &
  bekleyen=$!
  node uzun-islem.mjs $sure
  wait $bekleyen
done
```

```sh
sh sinir-genisligi.sh
```

```
A: islem 100 ms acik kaldi ve kesinlestirildi
B: yazma icin 114 ms bekledi
A: islem 400 ms acik kaldi ve kesinlestirildi
B: yazma icin 403 ms bekledi
A: islem 800 ms acik kaldi ve kesinlestirildi
B: yazma icin 844 ms bekledi
```

İkinci yazanın beklemesi, birinci işlemin açık kalma süresini izliyor. Süreler makineye
bağlıdır; değişmeyen, ilişkinin doğrusal olmasıdır. `uzun-islem.mjs` içindeki döngü bir
modeldir ve dışarıya yapılan bir çağrının — bildirim gönderme, ödeme servisini arama, dosya
yazma — işlem içinde beklenmesini temsil eder.

Buradan iki kural çıkar. İşlem sınırı içinde dış çağrı yapılmaz; dış çağrı ya sınırdan
önce ya sonra yürütülür. Ve sınır, iş kuralının gerektirdiğinden bir yazma bile geniş
tutulmaz.

#### Özet

- Üç yazmadan biri başarısız olduğunda sınırsız sürüm yarım bir durum bıraktı: ödünç kaydı
  ve kitap durumu değişti, hareket kütüğü boş kaldı.
- Aynı yazmalar tek bir işlem sınırına alındığında sonuç ya tamamı ya hiçbiri oldu; iş
  birimi bu kümenin adıdır ve sınırı iş kuralı belirler.
- Depo başına işlem açmak iki sorun üretti: çağrılar bileşemedi ve dıştan açılan işlemle
  çakıştı. İşlemi depo açmaz, üst katman açar.
- İş birimi en dışta gerçek işlemi, içeride kaydetme noktası kurar; iç birimin
  başarısızlığı dış birimi düşürmedi.
- İşlem açık kaldığı sürece ikinci yazan bekler: 100, 400 ve 800 ms açık kalan işlemler
  sırasıyla 114, 403 ve 844 ms bekleme üretti. Dış çağrı işlem sınırının içine alınmaz.

#### Sonraki Adım

Bu derste iki bağlantı aynı satıra yazmak istedi ve biri diğerini bekledi. Yazma–yazma
çakışması en görünür durumdur; okuma tarafında olanlar daha sessizdir. Bir işlem sürerken
başka bir işlem aynı satırları okursa ne görür: değişikliğin öncesini mi, sonrasını mı,
yoksa duruma göre ikisini birden mi? Yanıt yalıtım düzeyine bağlıdır ve seçilen düzey
uygulamanın gördüğü veriyi değiştirir. Sonraki ders aynı sorguyu iki farklı düzeyde
çalıştırarak tekrarlanamayan okumanın ve hayalet satırın uygulamadan nasıl göründüğünü
ölçer.

### Yalıtım Düzeylerinin Uygulama Etkisi

Source: https://academia.sh/tr/kurslar/veri-erisim-katmani/yalitim-duzeylerinin-uygulama-etkisi

Okuma anomalilerinin uygulamadan görünümü: aynı sorgunun iki kez çalıştırılmasıyla tekrarlanamayan okuma ve hayalet satırın ölçülmesi, iki toplama sorgusu arasındaki değişmezin binlerce örnekte ihlal edilmesi ve yalıtımın bedeli.

Önceki ders iki bağlantının aynı satıra yazmak istemesini ve birinin ötekini beklemesini
ölçtü. Yazma–yazma çakışması görünür bir olaydır: biri bekler, sonra yazar. Okuma tarafında
olanlar sessizdir.

Bir işlem sürerken başka bir bağlantı aynı satırları okursa ne görür? Değişikliğin öncesini
mi, sonrasını mı, yoksa sorgudan sorguya değişen bir karışımı mı? Yanıt yalıtım düzeyine
bağlıdır. İleri SQL kursunda düzeyler ve izin verdikleri anomaliler tanımlandı; bu ders aynı
anomalileri uygulama kodundan ölçer ve düzeyin seçilmesinin uygulamada ne anlama geldiğini
gösterir.

#### Düzenek

Şema küçüktür: bir üye, beş kitap, ödünç kayıtları.

```sh
# kur.sh — dersin veritabanini ve isaret dosyalarini sifirlar
rm -f kutuphane.db kutuphane.db-wal kutuphane.db-shm okundu yazildi
sqlite3 kutuphane.db >/dev/null <<'SQL'
PRAGMA journal_mode = WAL;
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, odunc_siniri INTEGER NOT NULL);
CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL,
                    durumu TEXT NOT NULL CHECK (durumu IN ('rafta','odunc')));
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL,
                    uye_id INTEGER NOT NULL, iade_tarihi TEXT);
INSERT INTO uye VALUES (4,'Emre',3);
INSERT INTO kitap VALUES (1,'Körlük','rafta'),(2,'Tutunamayanlar','rafta'),
  (3,'Kum Kitabı','rafta'),(4,'Yaban','rafta'),(5,'Sessiz Ev','rafta');
SQL
```

İki süreç birbirini işaret dosyalarıyla bekler. Bu, zamanlamayı rastlantıya bırakmadan
belirli bir sıralamayı üretmenin en küçük yoludur.

#### İki Anomali, Tek Senaryo

Okuyucu aynı iki sorguyu iki kez çalıştırır: bir satırın değerini ve bir bağıntının satır
sayısını. Arada yazıcı devreye girer.

```js
// okuyucu.mjs — ayni iki sorguyu iki kez calistirir; kip: otomatik | islem
import { DatabaseSync } from "node:sqlite";
import { writeFileSync, existsSync } from "node:fs";

const kip = process.argv[2];
const db = new DatabaseSync("kutuphane.db");
const oku = () => ({
  sinir: db.prepare("SELECT odunc_siniri AS s FROM uye WHERE uye_id = 4").get().s,
  kitap: db.prepare("SELECT count(*) AS n FROM kitap").get().n,
});

if (kip === "islem") db.exec("BEGIN");
const ilk = oku();
writeFileSync("okundu", "");
while (!existsSync("yazildi")) { /* yazicinin kesinlestirmesini bekle */ }
const ikinci = oku();
if (kip === "islem") db.exec("COMMIT");

console.log(`kip=${kip}`);
console.log(`  ilk okuma   : sinir=${ilk.sinir}  kitap=${ilk.kitap}`);
console.log(`  ikinci okuma: sinir=${ikinci.sinir}  kitap=${ikinci.kitap}`);
console.log(`  tekrarlanamayan okuma: ${ilk.sinir !== ikinci.sinir}`);
console.log(`  hayalet satir        : ${ilk.kitap !== ikinci.kitap}`);
```

```js
// yazici.mjs — okuyucunun ilk okumasindan sonra guncelleyip yeni satir ekler
import { DatabaseSync } from "node:sqlite";
import { writeFileSync, existsSync } from "node:fs";

while (!existsSync("okundu")) { /* okuyucunun ilk okumasini bekle */ }
const db = new DatabaseSync("kutuphane.db");
db.exec("BEGIN IMMEDIATE");
db.prepare("UPDATE uye SET odunc_siniri = 5 WHERE uye_id = 4").run();
db.prepare("INSERT INTO kitap VALUES (6,'Anayurt Oteli','rafta')").run();
db.exec("COMMIT");
writeFileSync("yazildi", "");
console.log("yazici: sinir 5 yapildi, yeni kitap eklendi");
```

Aynı senaryo iki kipte çalıştırılır. `otomatik` kipinde okuyucu hiçbir işlem açmaz; her
sorgu kendi başına yürür. `islem` kipinde iki okuma tek bir işlem sınırının içindedir.

```sh
# deney.sh — ayni senaryo iki kipte
for kip in otomatik islem; do
  sh kur.sh
  node yazici.mjs >/dev/null &
  yazici=$!
  node okuyucu.mjs $kip
  wait $yazici
  echo
done
```

```sh
sh deney.sh
```

```
kip=otomatik
  ilk okuma   : sinir=3  kitap=5
  ikinci okuma: sinir=5  kitap=6
  tekrarlanamayan okuma: true
  hayalet satir        : true

kip=islem
  ilk okuma   : sinir=3  kitap=5
  ikinci okuma: sinir=3  kitap=5
  tekrarlanamayan okuma: false
  hayalet satir        : false
```

İki anomali de ilk kipte göründü. `odunc_siniri` değeri 3'ten 5'e döndü: aynı satır, aynı
sorgu, farklı sonuç. Buna **tekrarlanamayan okuma** denir. Kitap sayısı 5'ten 6'ya çıktı:
sorgu aynı koşulu sordu, sonuç kümesine yeni bir satır girdi. Buna **hayalet satır** denir.
İkisinin farkı, birincisinin var olan bir satırın değerinde, ikincisinin sonuç kümesinin
üyeliğinde olmasıdır.

İkinci kipte iki okuma da ilk anın görüntüsünü verdi. İşlem açıldığında okuyucu bir **anlık
görüntü** üzerinde çalışır ve o görüntü işlem boyunca sabit kalır.

Buradaki asıl gözlem uygulamayla ilgilidir. Kod iki kipte de aynı sorguları çalıştırdı;
değişen tek şey bir `BEGIN` çağrısı oldu. **Uygulamanın gördüğü yalıtım düzeyi, okumaların
işlem sınırına alınıp alınmamasıyla belirlenir.** Sorguları sınırsız çalıştıran bir servis,
motorun sunduğu düzey ne olursa olsun, ardışık iki okuma arasında tutarlılık göremez.

#### Değişmezin İhlal Oranı

Yukarıdaki senaryo zamanlamayı elle kurdu. Gerçek yükte zamanlama kurulmaz, rastlantıya
kalır. Bu durumda anomali "olur mu" değil "ne sıklıkla olur" sorusuna dönüşür.

Kütüphanede tutulması gereken bir **değişmez** var: ödünçte görünen kitap sayısı, iade
edilmemiş ödünç kaydı sayısına eşit olmalı. Yazıcı bu değişmezi hiç bozmuyor; her adımı tek
bir işlemde yapıyor.

```js
// dolasan-yazici.mjs — kitaplari surekli odunc verip iade eder; her adim tek islem
import { DatabaseSync } from "node:sqlite";
import { writeFileSync, rmSync } from "node:fs";

const db = new DatabaseSync("kutuphane.db");
db.exec("PRAGMA busy_timeout = 5000");
const bitis = Date.now() + Number(process.argv[2] ?? 1500);
writeFileSync("yaziyor", "");
let adim = 0;
while (Date.now() < bitis) {
  const kitapId = (adim % 5) + 1;
  db.exec("BEGIN IMMEDIATE");
  const durum = db.prepare("SELECT durumu FROM kitap WHERE kitap_id = ?").get(kitapId).durumu;
  if (durum === "rafta") {
    db.prepare("UPDATE kitap SET durumu = 'odunc' WHERE kitap_id = ?").run(kitapId);
    db.prepare("INSERT INTO odunc (kitap_id, uye_id, iade_tarihi) VALUES (?,4,NULL)").run(kitapId);
  } else {
    db.prepare("UPDATE kitap SET durumu = 'rafta' WHERE kitap_id = ?").run(kitapId);
    db.prepare(`UPDATE odunc SET iade_tarihi = '2025-07-25'
                WHERE odunc_id = (SELECT max(odunc_id) FROM odunc
                                  WHERE kitap_id = ? AND iade_tarihi IS NULL)`).run(kitapId);
  }
  db.exec("COMMIT");
  adim += 1;
}
rmSync("yaziyor");
console.log(`yazici: ${adim} islem kesinlestirdi`);
```

Okuyucu değişmezi sürekli örnekler ve kaç örnekte bozuk gördüğünü sayar.

```js
// tutarsizlik-say.mjs — iki toplama sorgusu arasindaki degismezi orneklerle sinar
import { DatabaseSync } from "node:sqlite";
import { existsSync } from "node:fs";

const kip = process.argv[2];
const db = new DatabaseSync("kutuphane.db");
db.exec("PRAGMA busy_timeout = 5000");
while (!existsSync("yaziyor")) { /* yazicinin baslamasini bekle */ }

let ornek = 0, ihlal = 0;
while (existsSync("yaziyor")) {
  if (kip === "islem") db.exec("BEGIN");
  const oduncteKitap = db.prepare("SELECT count(*) AS n FROM kitap WHERE durumu = 'odunc'").get().n;
  const acikKayit = db.prepare("SELECT count(*) AS n FROM odunc WHERE iade_tarihi IS NULL").get().n;
  if (kip === "islem") db.exec("COMMIT");
  ornek += 1;
  if (oduncteKitap !== acikKayit) ihlal += 1;
}
console.log(`kip=${kip}  ornek=${ornek}  degismez ihlali=${ihlal}`);
```

```sh
# degismez.sh — ayni degismez iki okuma kipinde ornekleniyor
for kip in otomatik islem; do
  sh kur.sh
  rm -f yaziyor
  node tutarsizlik-say.mjs $kip &
  okuyucu=$!
  node dolasan-yazici.mjs 1500
  wait $okuyucu
done
```

```sh
sh degismez.sh
```

```
yazici: 29188 islem kesinlestirdi
kip=otomatik  ornek=17277  degismez ihlali=3327
yazici: 23632 islem kesinlestirdi
kip=islem  ornek=17446  degismez ihlali=0
```

Sayılar makineye ve o andaki yüke göre değişir; bir buçuk saniyede kaç işlem
kesinleştirileceği sabit değildir. Değişmeyen ayrımdır: sınırsız okumada on yedi bin
örneğin üç binden çoğu değişmezi bozuk gördü, işlem içinde okumada hiçbiri bozuk görmedi.

Oran yaklaşık beşte bir. Bu, ilgili raporun beş çağrıdan birinde tutarsız bir çift sayı
döndürmesi demektir. Hata kütüğüne hiçbir şey düşmez; sorgular başarılıdır, kısıt ihlali
yoktur, yalnız iki sayı birbirini tutmaz.

#### Düzeylerin Uygulamadaki Karşılığı

İleri SQL kursunda dört anomali sıralanmıştı: kirli okuma, tekrarlanamayan okuma, hayalet
satır ve seri hâle getirilebilirlik ihlalleri. Uygulama tarafında bunların karşılığı üç
soruya iner.

**Hangi sorgular birlikte anlamlıdır?** Birlikte anlamlı olan sorgular aynı işlem sınırının
içine alınır. Yukarıdaki iki toplama sorgusu birlikte anlamlıydı; ayrı çalıştıklarında
ürettikleri çift anlamsız oldu.

**Okunan değer üzerine karar veriliyor mu?** Okunan değere bakıp bir yazma yapılıyorsa,
okuma ile yazma arasındaki aralıkta o değerin değişmemesi gerekir. Ödünç sınırını okuyup
"yer var" diyerek yazan bir servis, aradaki değişikliğe karşı korunmalıdır. Bu, yalıtım
düzeyinin tek başına çözmediği bir sorundur ve sonraki dersin konusudur.

**Bayat veriye tahammül var mı?** Anlık görüntü tutarlıdır ama günceldir demek değildir.
İşlem uzun sürerse, okuduğu veri kesinleşme anında eskimiş olabilir. Uzun raporlar için bu
kabul edilir; para hareketi gibi işler için edilmez.

Yalıtımın bedeli de bu üçüncü noktada durur. Yüksek düzey, ya kilit tutarak ötekileri
bekletir ya da eski sürümleri saklayarak depolama ve temizlik maliyeti üretir. İlişkisel
Veritabanı Yönetimi kursunda çok sürümlü eşzamanlılık denetimi olarak geçen mekanizma bu
ikinci yolu izler: eski sürümler bir okuyucu onlara ihtiyaç duyduğu sürece silinemez, uzun
açık kalan okuma işlemleri temizliği geciktirir.

#### Özet

- Aynı iki sorgu, sınırsız çalıştığında değeri değişmiş bir satır (tekrarlanamayan okuma)
  ve sonuç kümesine giren yeni bir satır (hayalet satır) gördü; işlem sınırı içinde ikisi de
  görünmedi.
- Uygulamanın gördüğü yalıtım düzeyi, okumaların işlem sınırına alınıp alınmamasıyla
  belirlenir; kod aynı kalırken tek bir `BEGIN` çağrısı sonucu değiştirdi.
- İki toplama sorgusu arasındaki değişmez, sınırsız okumada on yedi bin örneğin yaklaşık
  beşte birinde bozuk göründü; işlem içinde okumada hiç bozulmadı.
- Bu tür tutarsızlık hata üretmez: sorgular başarılıdır, yalnız birlikte anlamsızdırlar.
- Yüksek yalıtımın bedeli bekleme ya da eski sürümlerin saklanmasıdır; uzun açık okuma
  işlemleri temizliği geciktirir.

#### Sonraki Adım

Yalıtım, okunan verinin tutarlı olmasını sağlar; okunan değere dayanarak yapılan yazmanın
doğruluğunu garanti etmez. İki üye aynı anda son kalan kopyayı ödünç almak isterse, ikisi de
"kitap rafta" görür, ikisi de yazar ve biri ötekinin sonucunu geçersiz kılar. Bu çatışmayı
çözmenin iki yolu vardır: değişikliği yazarken kaydın hâlâ okunduğu hâlde olduğunu sınamak
ya da okuma anında kaydı kilitleyip ötekini beklemek. Sonraki ders iki yolu da kurar, çatışma
üretir, kilit hatasını birebir gösterir ve hangi yükte hangisinin daha ucuz olduğunu ölçer.

### İyimser ve Kötümser Kilitleme

Source: https://academia.sh/tr/kurslar/veri-erisim-katmani/iyimser-ve-kotumser-kilitleme

Eşzamanlı güncelleme çatışmasının iki çözümü: sürüm sütunuyla çatışmayı yazma anında saptayan iyimser kilitleme, kararı kilit altında veren kötümser kilitleme, yükseltme çakışmasının beklemeyle çözülememesi ve iki yaklaşımın deneme sayısı ile süre karşılaştırması.

Yalıtım, okunan verinin kendi içinde tutarlı olmasını sağlar. Okunan değere dayanarak
yapılan yazmanın doğruluğunu garanti etmez.

İki üye aynı anda son kalan kopyayı ödünç almak isterse ikisi de "kitap rafta" görür.
İkisinin gördüğü doğrudur; okudukları anda kitap gerçekten raftaydı. Sorun karar ile yazma
arasındaki aralıkta ortaya çıkar. Bu ders o aralığı kapatmanın iki yolunu kurar, çatışmayı
gerçekten üretir ve ikisinin maliyetini ölçer.

#### Kayıp Güncelleme

Şema tek bir kitap ve bir sürüm sütunu içeriyor. Sürüm sütunu ilk bölümde kullanılmayacak;
ikinci bölümde devreye girecek.

```sh
# kur.sh — tek kitapli yaris duzenegi
rm -f kutuphane.db kutuphane.db-wal kutuphane.db-shm
sqlite3 kutuphane.db >/dev/null <<'SQL'
PRAGMA journal_mode = WAL;
CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL,
                    durumu TEXT NOT NULL CHECK (durumu IN ('rafta','odunc')),
                    surum INTEGER NOT NULL DEFAULT 1);
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL,
                    uye_id INTEGER NOT NULL, iade_tarihi TEXT);
INSERT INTO kitap VALUES (1,'Körlük','rafta',1);
SQL
```

```js
// say.mjs — bir kitap icin acik odunc kaydi sayisini ve surumu yazdirir
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("kutuphane.db");
const k = db.prepare("SELECT durumu, surum FROM kitap WHERE kitap_id = 1").get();
const n = db.prepare("SELECT count(*) AS n FROM odunc WHERE kitap_id = 1 AND iade_tarihi IS NULL").get().n;
console.log(`kitap durumu=${k.durumu} surum=${k.surum}  acik odunc kaydi=${n}`);
```

İlk sürüm okur, karar verir, yazar. Yazma işlemi bir sınırın içindedir; önceki dersin
kuralına uyuyor. Yine de yetmez.

```js
// korumasiz.mjs — oku, karar ver, yaz; arada koruma yok
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("kutuphane.db");
db.exec("PRAGMA busy_timeout = 5000");
const uyeId = Number(process.argv[2]);
const gecikmeMs = Number(process.argv[3] ?? 120);

const kitap = db.prepare("SELECT durumu FROM kitap WHERE kitap_id = 1").get();
const bitis = Date.now() + gecikmeMs;
while (Date.now() < bitis) { /* karar verme suresinin modeli */ }

if (kitap.durumu === "rafta") {
  db.exec("BEGIN IMMEDIATE");
  db.prepare("UPDATE kitap SET durumu = 'odunc' WHERE kitap_id = 1").run();
  db.prepare("INSERT INTO odunc (kitap_id, uye_id, iade_tarihi) VALUES (1,?,NULL)").run(uyeId);
  db.exec("COMMIT");
  console.log(`uye ${uyeId}: kitabi aldi`);
} else {
  console.log(`uye ${uyeId}: kitap oduncte, reddedildi`);
}
```

```sh
sh kur.sh
node korumasiz.mjs 4 & node korumasiz.mjs 5 & wait
node say.mjs
```

```
uye 4: kitabi aldi
uye 5: kitabi aldi
kitap durumu=odunc surum=1  acik odunc kaydi=2
```

Tek nüshalık kitap iki üyeye birden verildi. Bu, İlişkisel Kuram kursunda tanımlanan **kayıp
güncelleme**nin uygulamadaki hâlidir: ikinci yazma, birinci yazmanın dayandığı gerçeği
geçersiz kıldığı hâlde geçti. Okuma ile yazmanın ayrı ayrı doğru olması yetmiyor; ikisinin
arasındaki varsayımın hâlâ geçerli olduğunun sınanması gerekiyor.

`gecikmeMs` döngüsü bir modeldir; kararın anlık olmadığını, kural denetimi ve dış
doğrulamalar için zaman geçtiğini temsil eder. Aralık daralınca çatışma seyrekleşir, ama
yok olmaz.

#### İyimser Kilitleme

Birinci çözüm çatışmayı önlemeye çalışmaz; olduğunda saptar. Kayıt, okunduğu andaki
sürümüyle birlikte alınır ve yazma koşuluna o sürüm konur. Sürüm değişmişse yazma hiçbir
satırı etkilemez.

```js
// iyimser.mjs — okunan surum yazma kosuluna konur; etkilenen satir sayisi cakismayi soyler
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("kutuphane.db");
db.exec("PRAGMA busy_timeout = 5000");
const uyeId = Number(process.argv[2]);
const gecikmeMs = Number(process.argv[3] ?? 120);

const kitap = db.prepare("SELECT durumu, surum FROM kitap WHERE kitap_id = 1").get();
const bitis = Date.now() + gecikmeMs;
while (Date.now() < bitis) { /* karar verme suresinin modeli */ }

if (kitap.durumu !== "rafta") {
  console.log(`uye ${uyeId}: kitap oduncte, reddedildi`);
} else {
  db.exec("BEGIN IMMEDIATE");
  const sonuc = db.prepare(
    "UPDATE kitap SET durumu = 'odunc', surum = surum + 1 WHERE kitap_id = 1 AND surum = ?")
    .run(kitap.surum);
  if (sonuc.changes === 0) {
    db.exec("ROLLBACK");
    console.log(`uye ${uyeId}: cakisma (okunan surum ${kitap.surum}, etkilenen satir 0)`);
  } else {
    db.prepare("INSERT INTO odunc (kitap_id, uye_id, iade_tarihi) VALUES (1,?,NULL)").run(uyeId);
    db.exec("COMMIT");
    console.log(`uye ${uyeId}: kitabi aldi (surum ${kitap.surum} -> ${kitap.surum + 1})`);
  }
}
```

```sh
sh kur.sh
node iyimser.mjs 4 & node iyimser.mjs 5 & wait
node say.mjs
```

```
uye 5: kitabi aldi (surum 1 -> 2)
uye 4: cakisma (okunan surum 1, etkilenen satir 0)
kitap durumu=odunc surum=2  acik odunc kaydi=1
```

Hangi üyenin kazandığı çalıştırmadan çalıştırmaya değişir; değişmeyen, birinin kazanıp
ötekinin çatışma bildirmesidir. Açık ödünç kaydı bire indi.

Yöntemin çalışma ilkesi tek bir sayıdadır: **etkilenen satır sayısı**. Bir tane olması
kaydın okunduğu hâlde bulunduğunu, sıfır olması araya birinin girdiğini söyler. Bu sayı
sürücüden okunabildiği için hiçbir ek altyapı gerekmez.

Sürüm sütununun yerine son değişiklik zaman damgası da kullanılabilir, ama zaman damgasının
çözünürlüğü yetmezse iki değişiklik aynı değeri taşır ve çatışma kaçar. Tek yönde artan bir
tam sayı bu riski taşımaz.

Çatışma saptandığında ne yapılacağı ayrı bir karardır. Kullanıcıya "kayıt değişti" demek,
işlemi baştan okuyup yeniden denemek ya da iki değişikliği birleştirmek seçenekleri vardır.
Ödünç örneğinde doğru davranış yeniden denemektir; ikinci denemede kitap ödünçte görüneceği
için istek düzgün biçimde reddedilir.

#### Kötümser Kilitleme

İkinci çözüm çatışmayı hiç oluşturmaz. Karar verilmeden önce kayıt kilitlenir; ikinci
istemci ya bekler ya reddedilir.

```js
// kotumser.mjs — karar suresi boyunca yazma kilidi tutulur
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("kutuphane.db");
db.exec(`PRAGMA busy_timeout = ${process.env.BEKLEME_MS ?? 0}`);
const uyeId = Number(process.argv[2]);
const gecikmeMs = Number(process.argv[3] ?? 120);

try {
  db.exec("BEGIN IMMEDIATE");
} catch (h) {
  console.log(`uye ${uyeId}: kilit alinamadi -> ${h.message}`);
  process.exit(0);
}
const kitap = db.prepare("SELECT durumu FROM kitap WHERE kitap_id = 1").get();
const bitis = Date.now() + gecikmeMs;
while (Date.now() < bitis) { /* karar verme suresinin modeli */ }

if (kitap.durumu === "rafta") {
  db.prepare("UPDATE kitap SET durumu = 'odunc', surum = surum + 1 WHERE kitap_id = 1").run();
  db.prepare("INSERT INTO odunc (kitap_id, uye_id, iade_tarihi) VALUES (1,?,NULL)").run(uyeId);
  db.exec("COMMIT");
  console.log(`uye ${uyeId}: kitabi aldi`);
} else {
  db.exec("COMMIT");
  console.log(`uye ${uyeId}: kitap oduncte, reddedildi`);
}
```

Bekleme süresi sıfırken ikinci istemci kilidi hiç beklemez.

```sh
sh kur.sh
node kotumser.mjs 4 & sleep 0.05; node kotumser.mjs 5 & wait
node say.mjs
```

```
uye 5: kilit alinamadi -> database is locked
uye 4: kitabi aldi
kitap durumu=odunc surum=2  acik odunc kaydi=1
```

Hata metni sürücünün verdiği metindir ve motora göre değişir; ortak olan, kilidin
alınamadığını söyleyen ayrı bir hata sınıfının bulunmasıdır. Uygulama bu hatayı öteki
hatalardan ayırmak zorundadır: kilit hatası yeniden denenebilir, kısıt ihlali denenemez.

Bekleme süresi verildiğinde ikinci istemci kilidi bekler ve sırası geldiğinde **güncel**
veriyi okur.

```sh
sh kur.sh
BEKLEME_MS=5000 node kotumser.mjs 4 & sleep 0.05; BEKLEME_MS=5000 node kotumser.mjs 5 & wait
node say.mjs
```

```
uye 4: kitabi aldi
uye 5: kitap oduncte, reddedildi
kitap durumu=odunc surum=2  acik odunc kaydi=1
```

İkinci üye "çatışma" değil "kitap ödünçte" yanıtı aldı. Fark önemlidir: iyimser yaklaşımda
uygulama bir çatışmayı ele almak zorundaydı, kötümserde iş kuralının normal reddi devreye
girdi. Bedeli, ikinci üyenin karar süresi boyunca beklemesidir.

#### Yükseltme Çakışması

Kötümser kilitlemenin doğru kurulması, kilidin **okumadan önce** alınmasına bağlıdır.
Önce okuyup sonra yazmaya çalışan iki işlem, bekleyerek çözülemeyen bir çakışma üretir.

```js
// yukseltme.mjs — once okuyup sonra yazmaya calisan iki islem
import { DatabaseSync } from "node:sqlite";
import { writeFileSync, existsSync } from "node:fs";
const rol = process.argv[2];
const db = new DatabaseSync("kutuphane.db");
db.exec("PRAGMA busy_timeout = 3000");

db.exec("BEGIN");                                  // ertelenmis: okumada anlik goruntu alinir
db.prepare("SELECT durumu FROM kitap WHERE kitap_id = 1").get();
if (rol === "a") { writeFileSync("a_okudu", ""); while (!existsSync("b_okudu")) { /* bekle */ } }
else { writeFileSync("b_okudu", ""); while (!existsSync("a_okudu")) { /* bekle */ } }

if (rol === "b") { while (!existsSync("a_yazdi")) { /* A once yazsin */ } }
try {
  db.prepare("UPDATE kitap SET surum = surum + 1 WHERE kitap_id = 1").run();
  db.exec("COMMIT");
  if (rol === "a") writeFileSync("a_yazdi", "");
  console.log(`${rol}: yazdi ve kesinlestirdi`);
} catch (h) {
  db.exec("ROLLBACK");
  console.log(`${rol}: yukseltme basarisiz -> ${h.message}`);
}
```

```sh
sh kur.sh
rm -f a_okudu b_okudu a_yazdi
node yukseltme.mjs a & node yukseltme.mjs b & wait
node say.mjs
```

```
b: yukseltme basarisiz -> database is locked
a: yazdi ve kesinlestirdi
kitap durumu=rafta surum=2  acik odunc kaydi=0
```

B'nin bekleme süresi üç saniyeydi; buna rağmen beklemeden hata aldı. Nedeni şudur: B'nin
işlemi bir anlık görüntü üzerinde açıldı ve o görüntü artık eskimiş durumda. Beklemek
görüntüyü tazelemez, çünkü işlem boyunca sabit kalması gerekir. Tek çözüm işlemi geri alıp
baştan başlamaktır.

İleri SQL kursunda **yükseltme çakışması** olarak geçen durum budur. Uygulama tarafındaki
kuralı verir: yazma niyeti varsa kilit okumadan önce alınır (`BEGIN IMMEDIATE` gibi bir
yazma niyeti bildirimiyle), yoksa çatışma iyimser yöntemle ele alınır. İkisinin arasında
kalan üçüncü bir seçenek yoktur.

#### Maliyetin Ölçülmesi

İki yaklaşımın farkı çekişme altında görünür. Aşağıdaki düzenek dört işçiyle tek bir sayacı
artırır; her artırma bir okuma, üç milisaniyelik bir karar süresi ve bir yazmadır.

```sh
# sayac-kur.sh — tek satirli sayac
rm -f sayac.db sayac.db-wal sayac.db-shm
sqlite3 sayac.db >/dev/null <<'SQL'
PRAGMA journal_mode = WAL;
CREATE TABLE sayac (ad TEXT PRIMARY KEY, deger INTEGER NOT NULL, surum INTEGER NOT NULL);
INSERT INTO sayac VALUES ('odunc_sayisi', 0, 1);
SQL
```

```js
// sayac-iyimser.mjs — oku, karar ver, yaz; cakismada yeniden dene
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("sayac.db");
db.exec("PRAGMA busy_timeout = 10000");
const adet = Number(process.argv[2]);
const dusunmeMs = Number(process.argv[3]);
const dusun = () => { const b = Date.now() + dusunmeMs; while (Date.now() < b) { /* karar suresi */ } };

let deneme = 0;
for (let i = 0; i < adet; i++) {
  for (;;) {
    deneme += 1;
    const s = db.prepare("SELECT deger, surum FROM sayac WHERE ad = 'odunc_sayisi'").get();
    dusun();
    db.exec("BEGIN IMMEDIATE");
    const sonuc = db.prepare(
      "UPDATE sayac SET deger = ?, surum = surum + 1 WHERE ad = 'odunc_sayisi' AND surum = ?")
      .run(s.deger + 1, s.surum);
    db.exec(sonuc.changes === 0 ? "ROLLBACK" : "COMMIT");
    if (sonuc.changes === 1) break;
  }
}
console.log(`iyimser isci: ${adet} artirma, ${deneme} deneme`);
```

```js
// sayac-kotumser.mjs — kilit once alinir, karar kilit altinda verilir
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("sayac.db");
db.exec("PRAGMA busy_timeout = 10000");
const adet = Number(process.argv[2]);
const dusunmeMs = Number(process.argv[3]);
const dusun = () => { const b = Date.now() + dusunmeMs; while (Date.now() < b) { /* karar suresi */ } };

for (let i = 0; i < adet; i++) {
  db.exec("BEGIN IMMEDIATE");
  const s = db.prepare("SELECT deger FROM sayac WHERE ad = 'odunc_sayisi'").get();
  dusun();
  db.prepare("UPDATE sayac SET deger = ?, surum = surum + 1 WHERE ad = 'odunc_sayisi'")
    .run(s.deger + 1);
  db.exec("COMMIT");
}
console.log(`kotumser isci: ${adet} artirma, ${adet} deneme`);
```

```sh
# sayac-olc.sh — dort isci, isci basina 40 artirma, 3 ms karar suresi
for yaklasim in iyimser kotumser; do
  sh sayac-kur.sh
  baslangic=$(date +%s%N)
  isciler=""
  for i in 1 2 3 4; do node sayac-$yaklasim.mjs 40 3 & isciler="$isciler $!"; done
  wait $isciler
  bitis=$(date +%s%N)
  printf '%s: toplam %s ms, son deger = %s\n\n' "$yaklasim" $(( (bitis-baslangic)/1000000 )) \
    "$(sqlite3 sayac.db 'SELECT deger FROM sayac;')"
done
```

```sh
sh sayac-olc.sh
```

```
iyimser isci: 40 artirma, 127 deneme
iyimser isci: 40 artirma, 143 deneme
iyimser isci: 40 artirma, 145 deneme
iyimser isci: 40 artirma, 150 deneme
iyimser: toplam 508 ms, son deger = 160

kotumser isci: 40 artirma, 40 deneme
kotumser isci: 40 artirma, 40 deneme
kotumser isci: 40 artirma, 40 deneme
kotumser isci: 40 artirma, 40 deneme
kotumser: toplam 663 ms, son deger = 160
```

Son değer iki yaklaşımda da 160; ikisi de doğru. Süreler ve deneme sayıları makineye ve
zamanlamaya göre değişir, ilişki değişmez. İyimser yaklaşım her kırk artırma için yüz
yirmiden çok deneme harcadı; işin dörtte üçü boşa gitti. Buna karşılık işçiler karar
sürelerini paralel geçirdiği için toplam süre daha kısa çıktı. Kötümser yaklaşımda hiç boşa
deneme yok, ama karar süreleri sıraya girdi.

Seçim kuralı buradan çıkar. Çatışma seyrekse iyimser yaklaşım ucuzdur; boşa giden deneme
sayısı düşük kalır ve hiç kimse beklemez. Çatışma sıksa iyimser yaklaşım aynı işi defalarca
yapar; kötümser yaklaşım sırayı baştan kurarak bu israfı keser. Karar süresi uzunsa kötümser
yaklaşımın bedeli hızla büyür, çünkü kilit o süre boyunca tutulur.

#### Özet

- Korumasız oku–karar ver–yaz akışı tek nüshalık kitabı iki üyeye verdi; kayıp güncelleme
  yazmanın işlem içinde olmasına rağmen oluştu.
- İyimser kilitleme okunan sürümü yazma koşuluna koyar; etkilenen satır sayısının sıfır
  olması çatışmanın işaretidir ve çatışan taraf yeniden dener.
- Kötümser kilitleme kararı kilit altında verir; bekleme süresi sıfırken kilit hatası,
  bekleme verildiğinde güncel veriyle normal ret üretti.
- Önce okuyup sonra yazmaya çalışan işlem yükseltme çakışması aldı; bu hata beklemekle
  çözülmez, işlem baştan başlatılır.
- Ölçümde iyimser yaklaşım kırk artırma için yüz yirmiden çok deneme harcadı ama daha kısa
  sürdü; seçim çatışma sıklığına ve karar süresine bağlıdır.

#### Sonraki Adım

Bu dersteki iki çözüm de tek bir veritabanının içinde çalıştı; kilidi de sürüm sayacını da
aynı motor yönetti. Ödünç servisi tek başına değilse durum değişir. Kitap kataloğu bir
serviste, üye kayıtları başka bir serviste, ceza tahsilatı üçüncü bir serviste duruyorsa,
"ödünç ver" işlemi üç ayrı veritabanına dokunur ve hiçbiri ötekinin işlemini göremez.
Sonraki ders atomikliğin servis sınırını geçemediğini gösterir, kaybın ne anlama geldiğini
modelleyerek ölçer ve telafi ile giden kutusu düzenlerini kurar.

### Dağıtık İşlem Sorunu

Source: https://academia.sh/tr/kurslar/veri-erisim-katmani/dagitik-islem-sorunu

Atomikliğin servis sınırını geçememesi: iki ayrı veritabanına yapılan yazmaların yarım kalması, telafi adımlarıyla geri sarma, telafinin de başarısız olabilmesi ve durum değişikliğiyle iletinin aynı işlemde yazıldığı giden kutusu düzeni.

Önceki dersteki iki çözüm de tek bir veritabanının içinde çalıştı. Kilidi de sürüm sayacını
da aynı motor yönetti; `ROLLBACK` çağrısı bütün yazmaları birlikte geri aldı.

Ödünç servisi tek başına değilse bu dayanak kalkar. Kitap kataloğu bir serviste, üye
kayıtları başka bir serviste duruyorsa "ödünç ver" isteği iki ayrı veritabanına dokunur ve
hiçbiri ötekinin işlemini göremez. Bu ders o durumu kurar, kaybı ölçer ve iki düzeni
gösterir.

#### Modelin Sınırı

Aşağıdaki düzenek gerçek bir dağıtık sistem değildir. İki servis aynı makinede, iki ayrı
SQLite dosyası üzerinde çalışan iki modüldür; aralarında ağ yoktur, çağrılar süreç içinde
yapılır. **Modelin taşıdığı tek özellik, iki veritabanının ortak bir işlem sınırı
paylaşmamasıdır.** Atomikliğin kaybı bu özellikten doğar; ağ gecikmesi, kısmi hata ve zaman
aşımı gibi gerçek dağıtık sistem sorunları modelde yoktur ve modelden çıkarılamaz.

```sh
# kur.sh — iki servisin ayri veritabanlari
rm -f katalog.db uyelik.db
sqlite3 katalog.db >/dev/null <<'SQL'
CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL,
                    durumu TEXT NOT NULL CHECK (durumu IN ('rafta','odunc')));
CREATE TABLE giden_kutusu (ileti_id INTEGER PRIMARY KEY, tur TEXT NOT NULL,
                           kitap_id INTEGER NOT NULL, uye_id INTEGER NOT NULL,
                           durum TEXT NOT NULL DEFAULT 'bekliyor');
INSERT INTO kitap VALUES (1,'Körlük','rafta'),(2,'Tutunamayanlar','rafta'),(3,'Kum Kitabı','rafta');
SQL
sqlite3 uyelik.db >/dev/null <<'SQL'
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL,
                  acik_odunc INTEGER NOT NULL, sinir INTEGER NOT NULL);
INSERT INTO uye VALUES (4,'Emre',2,3),(5,'Selin',3,3),(6,'Burak',0,9);
SQL
```

Her servis kendi işlem sınırını kendi içinde kurar. Dışarıya yalnız iş anlamı taşıyan
çağrılar verir.

```js
// servisler.mjs — iki ayri veritabani, iki ayri servis; ortak islem yok
import { DatabaseSync } from "node:sqlite";

export const katalog = {
  db: new DatabaseSync("katalog.db"),
  oduncIsaretle(kitapId) {
    this.db.exec("BEGIN IMMEDIATE");
    const s = this.db.prepare(
      "UPDATE kitap SET durumu = 'odunc' WHERE kitap_id = ? AND durumu = 'rafta'").run(kitapId);
    this.db.exec(s.changes === 1 ? "COMMIT" : "ROLLBACK");
    if (s.changes !== 1) throw new Error("katalog: kitap rafta degil");
  },
  raftaIsaretle(kitapId) {
    if (process.env.KATALOG_ERISILEMEZ === "1") throw new Error("katalog: servise ulasilamiyor");
    this.db.exec("BEGIN IMMEDIATE");
    this.db.prepare("UPDATE kitap SET durumu = 'rafta' WHERE kitap_id = ?").run(kitapId);
    this.db.exec("COMMIT");
  },
  durum(kitapId) {
    return this.db.prepare("SELECT durumu FROM kitap WHERE kitap_id = ?").get(kitapId).durumu;
  },
};

export const uyelik = {
  db: new DatabaseSync("uyelik.db"),
  oduncEkle(uyeId) {
    this.db.exec("BEGIN IMMEDIATE");
    const s = this.db.prepare(
      "UPDATE uye SET acik_odunc = acik_odunc + 1 WHERE uye_id = ? AND acik_odunc < sinir")
      .run(uyeId);
    this.db.exec(s.changes === 1 ? "COMMIT" : "ROLLBACK");
    if (s.changes !== 1) throw new Error("uyelik: odunc siniri asildi");
  },
  sayi(uyeId) {
    return this.db.prepare("SELECT acik_odunc FROM uye WHERE uye_id = ?").get(uyeId).acik_odunc;
  },
};
```

#### Atomikliğin Kaybı

En doğrudan yazım iki çağrıyı sırayla yapar.

```js
// telafisiz.mjs — iki servisi sirayla cagirir; ikincisi basarisiz olursa geri donus yok
import { katalog, uyelik } from "./servisler.mjs";
const [kitapId, uyeId] = process.argv.slice(2).map(Number);
try {
  katalog.oduncIsaretle(kitapId);
  uyelik.oduncEkle(uyeId);
  console.log(`odunc verildi: kitap ${kitapId} -> uye ${uyeId}`);
} catch (h) {
  console.log("basarisiz:", h.message);
}
console.log(`  katalog: kitap ${kitapId} durumu = ${katalog.durum(kitapId)}`);
console.log(`  uyelik : uye ${uyeId} acik odunc = ${uyelik.sayi(uyeId)}`);
```

Üye 4'ün iki açık kaydı ve üç sınırı var; isteği geçmeli. Üye 5 sınırında; isteği
reddedilmeli.

```sh
sh kur.sh
node telafisiz.mjs 1 4
echo "---"
node telafisiz.mjs 2 5
```

```
odunc verildi: kitap 1 -> uye 4
  katalog: kitap 1 durumu = odunc
  uyelik : uye 4 acik odunc = 3
---
basarisiz: uyelik: odunc siniri asildi
  katalog: kitap 2 durumu = odunc
  uyelik : uye 5 acik odunc = 3
```

İkinci istek reddedildi, ama katalogdaki kitap ödünçte kaldı. Ortada kimsenin almadığı,
hiçbir üyenin hesabında görünmeyen bir ödünç kitap var. Kimse onu ödünç alamaz; iade de
edilemez, çünkü iade edecek bir kayıt yok.

`try` bloğu bunu düzeltmez. Hata yakalandı, kod hatayı bildirdi; katalogdaki değişiklik
zaten kesinleşmişti. **Atomiklik işlem sınırının içinde geçerlidir; iki ayrı sınır arasında
geçerli değildir.**

Klasik çözüm iki evreli kesinleştirmedir: bir eşgüdümcü bütün katılımcılara "hazır mısın"
diye sorar, hepsi olumlu yanıt verirse "kesinleştir" der. Bedeli, katılımcıların oy ile
karar arasında kilitlerini tutmak zorunda kalmasıdır; eşgüdümcü bu aralıkta erişilemez
duruma gelirse katılımcılar belirsiz durumda bekler. Bu maliyet, servis sınırlarının
ayrıldığı sistemlerde çoğunlukla kabul edilmez ve aşağıdaki iki düzen tercih edilir.

#### Telafi Düzeni

Birinci düzen atomikliği taklit eder. Her adımın etkisini geri alan bir **telafi adımı**
tanımlanır; bir adım başarısız olunca daha önce tamamlananlar tersten telafi edilir. Bu
düzene **saga** denir.

```js
// saga.mjs — her adimin bir telafi adimi vardir; hata cikinca tersten yurutulur
import { katalog, uyelik } from "./servisler.mjs";
const [kitapId, uyeId] = process.argv.slice(2).map(Number);

const adimlar = [
  { ad: "katalog.oduncIsaretle", yap: () => katalog.oduncIsaretle(kitapId),
    telafi: () => katalog.raftaIsaretle(kitapId) },
  { ad: "uyelik.oduncEkle", yap: () => uyelik.oduncEkle(uyeId), telafi: () => {} },
];

const yapilan = [];
try {
  for (const a of adimlar) { a.yap(); yapilan.push(a); console.log(`  adim tamam: ${a.ad}`); }
  console.log(`odunc verildi: kitap ${kitapId} -> uye ${uyeId}`);
} catch (h) {
  console.log("  hata:", h.message);
  let eksik = 0;
  for (const a of yapilan.reverse()) {
    try { a.telafi(); console.log(`  telafi edildi: ${a.ad}`); }
    catch (t) { eksik += 1; console.log(`  TELAFI BASARISIZ: ${a.ad} -> ${t.message}`); }
  }
  console.log(eksik === 0 ? "istek geri alindi" : `istek yarim kaldi (${eksik} telafi bekliyor)`);
}
console.log(`  katalog: kitap ${kitapId} durumu = ${katalog.durum(kitapId)}`);
console.log(`  uyelik : uye ${uyeId} acik odunc = ${uyelik.sayi(uyeId)}`);
```

```sh
sh kur.sh
node saga.mjs 2 5
```

```
  adim tamam: katalog.oduncIsaretle
  hata: uyelik: odunc siniri asildi
  telafi edildi: katalog.oduncIsaretle
istek geri alindi
  katalog: kitap 2 durumu = rafta
  uyelik : uye 5 acik odunc = 3
```

Kitap rafa döndü. Dışarıdan bakınca sonuç geri almaya benziyor, ama aynı şey değil. Geri
almada değişiklik hiç görünmez; telafide değişiklik bir süre görünür ve sonra ters bir
değişiklikle kapatılır. Bu aralıkta kitabı sorgulayan biri onu ödünçte görür. Sistem bu
aralıkta tutarsızdır ve tutarlılığa **sonradan** ulaşır.

Telafi adımının kendisi de bir iş kararıdır. "Ödünç işaretini kaldır" telafi edilebilir bir
adımdır. "Üyeye bildirim gönder" edilemez; gönderilmiş bir bildirim geri alınamaz, ancak
ikinci bir bildirimle düzeltilebilir. Bu yüzden telafi edilemeyen adımlar sıralamanın
sonuna konur.

#### Telafinin Sınırı

Telafi çağrısı da bir uzak çağrıdır ve başarısız olabilir.

```sh
sh kur.sh
KATALOG_ERISILEMEZ=1 node saga.mjs 2 5
```

```
  adim tamam: katalog.oduncIsaretle
  hata: uyelik: odunc siniri asildi
  TELAFI BASARISIZ: katalog.oduncIsaretle -> katalog: servise ulasilamiyor
istek yarim kaldi (1 telafi bekliyor)
  katalog: kitap 2 durumu = odunc
  uyelik : uye 5 acik odunc = 3
```

Baştaki tutarsız durumun aynısına geri dönüldü. Saga düzeni sorunu kaldırmadı, sorumluluğu
taşıdı: artık sistemde "telafi bekliyor" durumundaki bir istek var ve birinin onu yeniden
denemesi gerekiyor. Bu yüzden telafi adımları kalıcı olarak kaydedilir; süreç çöktüğünde
bile bekleyen telafinin izi kalmalıdır.

#### Giden Kutusu

İkinci düzen sorunu başka bir yerden yakalar. Katalog servisi durumu değiştirirken, "bu
oldu" bilgisini de **aynı işlemde** kendi veritabanına yazar. İki yazma tek bir sınırın
içinde olduğu için ya ikisi de olur ya hiçbiri.

```js
// kutuya-yaz.mjs — durum degisikligi ile iletiyi ayni islemde yazar
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("katalog.db");
const [kitapId, uyeId] = process.argv.slice(2).map(Number);

db.exec("BEGIN IMMEDIATE");
const s = db.prepare(
  "UPDATE kitap SET durumu = 'odunc' WHERE kitap_id = ? AND durumu = 'rafta'").run(kitapId);
if (s.changes !== 1) { db.exec("ROLLBACK"); console.log("kitap rafta degil"); process.exit(1); }
db.prepare("INSERT INTO giden_kutusu (tur, kitap_id, uye_id) VALUES ('odunc_verildi',?,?)")
  .run(kitapId, uyeId);
db.exec("COMMIT");
console.log(`katalog: kitap ${kitapId} oduncte, ileti kutuya yazildi`);
```

İletiyi hedefe götüren iş ayrı bir süreçtir. Bu düzene **giden kutusu** (outbox) denir.

```js
// aktarici.mjs — kutudaki iletileri hedef servise gonderir; --cokme isaretleme oncesi durur
import { DatabaseSync } from "node:sqlite";
import { uyelik } from "./servisler.mjs";
const cokme = process.argv.includes("--cokme");
const db = new DatabaseSync("katalog.db");

const bekleyenler = db.prepare(
  "SELECT ileti_id, kitap_id, uye_id FROM giden_kutusu WHERE durum = 'bekliyor' ORDER BY ileti_id").all();

for (const i of bekleyenler) {
  uyelik.oduncEkle(i.uye_id);
  console.log(`aktarici: ileti ${i.ileti_id} gonderildi (uye ${i.uye_id})`);
  if (cokme) { console.log("aktarici: isaretleme oncesi coktu"); process.exit(0); }
  db.prepare("UPDATE giden_kutusu SET durum = 'gonderildi' WHERE ileti_id = ?").run(i.ileti_id);
}
console.log(`aktarici: ${bekleyenler.length} ileti islendi`);
```

Kazanç şudur: durum değiştiyse ileti mutlaka vardır. Aktarıcı çökerse ileti kutuda bekler,
aktarıcı yeniden başladığında gönderilir. Bilgi kaybolmaz.

#### En Az Bir Kez Teslim

Kazancın bir bedeli var. Aktarıcı iletiyi gönderdikten sonra, kutuyu işaretlemeden önce
çökebilir.

```sh
sh kur.sh
node kutuya-yaz.mjs 3 6
node aktarici.mjs --cokme
echo "-- yeniden calistir --"
node aktarici.mjs
sqlite3 uyelik.db "SELECT uye_id, acik_odunc FROM uye WHERE uye_id = 6;"
sqlite3 katalog.db "SELECT ileti_id, durum FROM giden_kutusu;"
```

```
katalog: kitap 3 oduncte, ileti kutuya yazildi
aktarici: ileti 1 gonderildi (uye 6)
aktarici: isaretleme oncesi coktu
-- yeniden calistir --
aktarici: ileti 1 gonderildi (uye 6)
aktarici: 1 ileti islendi
6|2
1|gonderildi
```

Tek bir ödünç verildi, üyenin açık ödünç sayısı ikiye çıktı. İleti iki kez teslim edildi ve
etkisi iki kez uygulandı.

Bu kaçınılmazdır. Aktarıcının "gönderdim" bilgisini yazması ile hedefin iletiyi işlemesi
iki ayrı veritabanında olur; yani ilk bölümdeki sorunun aynısı, bir katman aşağıda yeniden
karşımızdadır. Sonsuz gerilemeyi kesen tek yol, garantiyi düşürmek ve sonucu düzeltmektir:
teslim **en az bir kez** yapılır, alıcı da aynı iletiyi ikinci kez aldığında ek bir etki
üretmez. Alıcının bu özelliği taşıması, giden kutusu düzeninin karşılanması gereken
koşuludur.

#### Özet

- İki ayrı veritabanına yapılan yazmalar ortak bir işlem sınırı paylaşmaz; ikinci çağrı
  başarısız olduğunda birinci kesinleşmiş kaldı ve kimsenin almadığı bir ödünç kitap oluştu.
- İki evreli kesinleştirme atomikliği korur, bedeli katılımcıların karar beklerken kilit
  tutmasıdır.
- Telafi düzeni tamamlanan adımları tersten geri sarar; geri almadan farkı, tutarsız
  durumun bir süre görünür olmasıdır.
- Telafi çağrısı da başarısız olabilir; bu durumda düzen sorunu kaldırmaz, bekleyen bir
  telafi kaydı bırakır ve yeniden denenmesini gerektirir.
- Giden kutusu, durum değişikliğiyle iletiyi aynı işlemde yazarak bilgi kaybını önler;
  bedeli, iletinin en az bir kez — bazen birden çok kez — teslim edilmesidir.

#### Sonraki Adım

Giden kutusu düzeninin karşılanmamış tek koşulu kaldı: aynı iletinin ikinci kez işlenmesi
ek bir etki üretmemeli. Bu ölçümde üyenin açık ödünç sayısı ikiye çıktı, yani koşul
sağlanmadı. Aynı sorun ağ üzerinden gelen her yeniden denemede belirir; istemci zaman aşımı
alıp isteği tekrarladığında sunucunun ilk isteği işlemiş olup olmadığını bilmesi gerekir.
Sonraki ders bir işlemin tekrarının neden ek etki üretmediğini tanımlar, doğal olarak bu
özelliği taşıyan yazmalarla taşımayanları ayırır ve taşımayanları tekillik anahtarıyla
güvenli hâle getirir.

### Etkisiz İşlemler

Source: https://academia.sh/tr/kurslar/veri-erisim-katmani/etkisiz-islemler

Yeniden denemenin güvenli kılınması: üç yazma biçiminin tekrar altındaki davranışının ölçülmesi, atama–türetme–çatışma davranışıyla tekrarı etkisiz kılma, tekillik anahtarının yanıtla birlikte aynı işlemde saklanması ve giden kutusu tüketicisinin düzeltilmesi.

Önceki ders giden kutusu düzenini kurdu ve karşılanmamış bir koşul bıraktı: aynı iletinin
ikinci kez işlenmesi ek etki üretmemeliydi. Ölçümde üyenin açık ödünç sayısı bir yerine ikiye
çıktı; koşul sağlanmadı.

Aynı sorun her yeniden denemede belirir. İstemci zaman aşımı alıp isteği tekrarladığında,
sunucunun ilk isteği işleyip işlemediği belli değildir. Yanıt kaybolmuş olabilir. Bu ders
tekrarın ne zaman zararsız olduğunu tanımlar ve zararlı olduğu durumları zararsız hâle
getirir.

Bir işlem, ikinci ve sonraki çalıştırmaları ilkinin ötesinde bir etki üretmiyorsa **etkisiz
işlem**dir (idempotent operation). API Tasarımı kursunda aynı özellik HTTP yöntemleri için
"değişmez yöntem" adıyla geçti; burada ilgilendiğimiz, o özelliğin veri erişim katmanında
nasıl kurulduğudur.

#### Üç Yazma Biçimi

Ödünç verme üç yazma içerir: kitabın durumunun değişmesi, üyenin açık ödünç sayısının
artması, ödünç satırının eklenmesi. Üçü aynı tekrar altında farklı davranır.

```sh
# kur.sh — olcum icin bos baslangic durumu
rm -f kutuphane.db
sqlite3 kutuphane.db >/dev/null <<'SQL'
CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL,
                    durumu TEXT NOT NULL CHECK (durumu IN ('rafta','odunc')));
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, acik_odunc INTEGER NOT NULL);
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL,
                    uye_id INTEGER NOT NULL, alis_tarihi TEXT NOT NULL, iade_tarihi TEXT);
INSERT INTO kitap VALUES (1,'Körlük','rafta');
INSERT INTO uye VALUES (4,'Emre',0);
SQL
```

```js
// uc-yazma.mjs — uc yazma bicimi, her biri ardarda uc kez calistirilir
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("kutuphane.db");

const yazmalar = {
  "durum atama   ": () => db.prepare("UPDATE kitap SET durumu = 'odunc' WHERE kitap_id = 1").run(),
  "sayac artirma ": () => db.prepare("UPDATE uye SET acik_odunc = acik_odunc + 1 WHERE uye_id = 4").run(),
  "kayit ekleme  ": () => db.prepare(
    "INSERT INTO odunc (kitap_id, uye_id, alis_tarihi) VALUES (1,4,'2025-07-20')").run(),
};

for (const [ad, yaz] of Object.entries(yazmalar)) {
  for (let i = 0; i < 3; i++) yaz();
}
const k = db.prepare("SELECT durumu FROM kitap WHERE kitap_id = 1").get().durumu;
const u = db.prepare("SELECT acik_odunc AS n FROM uye WHERE uye_id = 4").get().n;
const o = db.prepare("SELECT count(*) AS n FROM odunc").get().n;
console.log(`uc kez calistiktan sonra:  kitap.durumu=${k}  uye.acik_odunc=${u}  odunc satiri=${o}`);
```

```sh
sh kur.sh
node uc-yazma.mjs
```

```
uc kez calistiktan sonra:  kitap.durumu=odunc  uye.acik_odunc=3  odunc satiri=3
```

Ayrım net. **Atama** etkisizdir: `durumu = 'odunc'` yazması sonucu hedef değere getirir,
kaçıncı kez çalıştığı önemli değildir. **Artırma** etkisiz değildir, çünkü sonuç önceki
değere bağlıdır. **Ekleme** etkisiz değildir, çünkü her çağrı yeni bir satır üretir.

Kural buradan çıkarılabilir: bir yazma, sonucu yalnız hedef duruma bağlıysa etkisizdir;
sonucu kendi çalıştırılma sayısına bağlıysa değildir.

#### Etkisiz Biçime Çevirme

İki sorunlu yazma da biçim değiştirerek düzeltilebilir. Kayıt eklemede iş anlamı taşıyan bir
benzersizlik kısıtı tanımlanır ve çatışma davranışı belirtilir. Sayaç artırma ise atamayla
değiştirilir: sayaç bir birikim değil, mevcut satırlardan türetilen bir değer olur.

```sh
# kur2.sh — kayit ekleme icin is anlamli benzersizlik kisiti
rm -f kutuphane.db
sqlite3 kutuphane.db >/dev/null <<'SQL'
CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL,
                    durumu TEXT NOT NULL CHECK (durumu IN ('rafta','odunc')));
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, acik_odunc INTEGER NOT NULL);
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL,
                    uye_id INTEGER NOT NULL, alis_tarihi TEXT NOT NULL, iade_tarihi TEXT);
CREATE UNIQUE INDEX odunc_tekil ON odunc (kitap_id, uye_id, alis_tarihi);
INSERT INTO kitap VALUES (1,'Körlük','rafta');
INSERT INTO uye VALUES (4,'Emre',0);
SQL
```

```js
// uc-yazma-etkisiz.mjs — ayni uc is, tekrari etki uretmeyen bicimde
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("kutuphane.db");

const yazmalar = {
  "durum atama": () => db.prepare("UPDATE kitap SET durumu = 'odunc' WHERE kitap_id = 1").run(),
  "kayit ekleme": () => db.prepare(
    `INSERT INTO odunc (kitap_id, uye_id, alis_tarihi) VALUES (1,4,'2025-07-20')
     ON CONFLICT (kitap_id, uye_id, alis_tarihi) DO NOTHING`).run(),
  "sayac tureme": () => db.prepare(
    `UPDATE uye SET acik_odunc =
       (SELECT count(*) FROM odunc WHERE uye_id = 4 AND iade_tarihi IS NULL)
     WHERE uye_id = 4`).run(),
};

for (let i = 0; i < 3; i++) {
  const etkilenen = Object.entries(yazmalar).map(([ad, yaz]) => `${ad}=${yaz().changes}`);
  console.log(`tur ${i + 1}: ${etkilenen.join("  ")}`);
}
const k = db.prepare("SELECT durumu FROM kitap WHERE kitap_id = 1").get().durumu;
const u = db.prepare("SELECT acik_odunc AS n FROM uye WHERE uye_id = 4").get().n;
const o = db.prepare("SELECT count(*) AS n FROM odunc").get().n;
console.log(`uc turdan sonra:  kitap.durumu=${k}  uye.acik_odunc=${u}  odunc satiri=${o}`);
```

```sh
sh kur2.sh
node uc-yazma-etkisiz.mjs
```

```
tur 1: durum atama=1  kayit ekleme=1  sayac tureme=1
tur 2: durum atama=1  kayit ekleme=0  sayac tureme=1
tur 3: durum atama=1  kayit ekleme=0  sayac tureme=1
uc turdan sonra:  kitap.durumu=odunc  uye.acik_odunc=1  odunc satiri=1
```

Üç tur sonunda tek bir ödünç satırı ve doğru sayaç değeri kaldı. Etkilenen satır sayıları
mekanizmayı gösteriyor: ikinci turdan itibaren kayıt eklemenin etkisi sıfır. Sayaç türemesi
her turda bir satırı etkilemeye devam ediyor, ama yazdığı değer değişmiyor; etkisizlik,
etkilenen satır sayısının sıfır olması değil, **sonucun değişmemesi** demektir.

Benzersizlik kısıtının hangi sütunlardan kurulacağı bir iş kararıdır. Buradaki kısıt "aynı
üye aynı kitabı aynı gün iki kez ödünç alamaz" der. Kural yanlışsa etkisizlik de yanlış
kurulmuş olur; kısıt gerçekten aynı iş niyetini tanımlayan sütunlardan seçilmelidir.

#### Tekillik Anahtarı

İş anlamı taşıyan bir benzersizlik her zaman bulunamaz. Aynı üye aynı kitabı aynı gün iade
edip yeniden alabilir; bu durumda ikinci istek yinelenen istek değil, ayrı bir iştir.
Niyeti ayırt etmenin yolu, isteği yapanın ona bir ad vermesidir. API Tasarımı kursunda
tanıtılan **tekillik anahtarı**, veri erişim katmanında bir bağıntıya yazılır.

Kritik ayrıntı yazma anıdır: anahtar ile etkinin **aynı işlemde** yazılması gerekir. Ayrı
işlemlerde yazılırsa, ikisinin arasında çöken bir süreç ya etkisiz bir anahtar ya da
anahtarsız bir etki bırakır.

```sh
# kur3.sh — tekillik bagintili kurulum
rm -f kutuphane.db
sqlite3 kutuphane.db >/dev/null <<'SQL'
CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL,
                    durumu TEXT NOT NULL CHECK (durumu IN ('rafta','odunc')));
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL,
                    uye_id INTEGER NOT NULL, alis_tarihi TEXT NOT NULL, iade_tarihi TEXT);
CREATE TABLE tekillik (anahtar TEXT PRIMARY KEY, yanit TEXT NOT NULL);
INSERT INTO kitap VALUES (1,'Körlük','rafta'),(2,'Tutunamayanlar','rafta');
SQL
```

```js
// tekillik.mjs — anahtar ile yanit, etkiyle ayni islemde yazilir
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("kutuphane.db");

function oduncVer(anahtar, kitapId, uyeId, tarih) {
  db.exec("BEGIN IMMEDIATE");
  const onceki = db.prepare("SELECT yanit FROM tekillik WHERE anahtar = ?").get(anahtar);
  if (onceki !== undefined) {
    db.exec("ROLLBACK");
    return { ...JSON.parse(onceki.yanit), yeniden: true };
  }
  const s = db.prepare(
    "UPDATE kitap SET durumu = 'odunc' WHERE kitap_id = ? AND durumu = 'rafta'").run(kitapId);
  let yanit;
  if (s.changes !== 1) {
    yanit = { durum: "reddedildi", sebep: "kitap rafta degil" };
  } else {
    const id = Number(db.prepare(
      "INSERT INTO odunc (kitap_id, uye_id, alis_tarihi) VALUES (?,?,?)")
      .run(kitapId, uyeId, tarih).lastInsertRowid);
    yanit = { durum: "verildi", oduncId: id };
  }
  db.prepare("INSERT INTO tekillik (anahtar, yanit) VALUES (?,?)").run(anahtar, JSON.stringify(yanit));
  db.exec("COMMIT");
  return { ...yanit, yeniden: false };
}

for (const [anahtar, kitapId] of [["OD-7f21", 1], ["OD-7f21", 1], ["OD-7f21", 1], ["OD-9c04", 1]]) {
  console.log(`${anahtar} kitap=${kitapId} ->`, JSON.stringify(oduncVer(anahtar, kitapId, 4, "2025-07-20")));
}
console.log("odunc satiri:", db.prepare("SELECT count(*) AS n FROM odunc").get().n);
console.log("tekillik satiri:", db.prepare("SELECT count(*) AS n FROM tekillik").get().n);
```

```sh
sh kur3.sh
node tekillik.mjs
```

```
OD-7f21 kitap=1 -> {"durum":"verildi","oduncId":1,"yeniden":false}
OD-7f21 kitap=1 -> {"durum":"verildi","oduncId":1,"yeniden":true}
OD-7f21 kitap=1 -> {"durum":"verildi","oduncId":1,"yeniden":true}
OD-9c04 kitap=1 -> {"durum":"reddedildi","sebep":"kitap rafta degil","yeniden":false}
odunc satiri: 1
tekillik satiri: 2
```

Aynı anahtarla gelen üç çağrı tek bir ödünç kaydı üretti ve üçü de aynı kimliği döndürdü.
Yanıtın saklanması, yalnız etkiyi engellemekten fazlasını yapıyor: yinelenen istek de
başarılı bir yanıt alıyor. Yanıt saklanmasaydı, ikinci çağrının "zaten yapılmış" demesi
gerekirdi ve istemci bunu bir hata olarak yorumlayabilirdi.

Dördüncü satır önemli bir ayrımı gösteriyor. Farklı anahtarla gelen istek yinelenen istek
değildir; işlendi ve iş kuralı gereği reddedildi. Ret de bir sonuçtur ve o da saklandı —
tekillik bağıntısında iki satır var. Saklanmasaydı, aynı reddedilen isteğin yeniden
denenmesi kuralın yeniden çalıştırılmasına yol açardı.

Tekillik bağıntısı sınırsız büyür. Anahtarların bir saklama penceresi olmalıdır; pencere,
istemcilerin yeniden deneme süresinden uzun, depolama maliyetinin kabul edilebilir olduğu
kadar kısa seçilir.

#### Giden Kutusunun Tüketicisi

Önceki dersteki ölçüm artık düzeltilebilir. Tüketici, iletiyi işlerken ileti kimliğini de
aynı işlemde kaydeder.

```sh
# kur4.sh — tuketici tarafi
rm -f uyelik.db
sqlite3 uyelik.db >/dev/null <<'SQL'
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, acik_odunc INTEGER NOT NULL);
CREATE TABLE islenen_ileti (ileti_id INTEGER PRIMARY KEY, islenme TEXT NOT NULL);
INSERT INTO uye VALUES (6,'Burak',0);
SQL
```

```js
// tuket.mjs — ileti kimligi ile etki ayni islemde yazilir; ikinci teslim etkisiz kalir
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("uyelik.db");
const iletiId = Number(process.argv[2]);
const uyeId = Number(process.argv[3]);

db.exec("BEGIN IMMEDIATE");
const yeni = db.prepare(
  "INSERT INTO islenen_ileti (ileti_id, islenme) VALUES (?, datetime('now')) ON CONFLICT DO NOTHING")
  .run(iletiId);
if (yeni.changes === 0) {
  db.exec("ROLLBACK");
  console.log(`ileti ${iletiId}: daha once islenmis, etki uygulanmadi`);
} else {
  db.prepare("UPDATE uye SET acik_odunc = acik_odunc + 1 WHERE uye_id = ?").run(uyeId);
  db.exec("COMMIT");
  console.log(`ileti ${iletiId}: islendi`);
}
console.log(`  uye ${uyeId} acik odunc =`,
  db.prepare("SELECT acik_odunc AS n FROM uye WHERE uye_id = ?").get(uyeId).n);
```

```sh
sh kur4.sh
node tuket.mjs 1 6
node tuket.mjs 1 6
node tuket.mjs 2 6
```

```
ileti 1: islendi
  uye 6 acik odunc = 1
ileti 1: daha once islenmis, etki uygulanmadi
  uye 6 acik odunc = 1
ileti 2: islendi
  uye 6 acik odunc = 2
```

Bir numaralı iletinin iki kez teslimi tek bir etki üretti; iki numaralı ileti ayrı bir iş
olarak işlendi. Önceki derste ikiye çıkan sayaç artık bir. Sayaç artırma yazmasının kendisi
hâlâ etkisiz değil; etkisiz olan, tüketicinin tamamıdır. **Etkisizlik yazma biçiminden ya
da bir bekçi kaydından gelir; ikisinden biri yeterlidir.**

Bekçi kaydının etkiyle aynı işlemde yazılması burada da zorunludur. Kayıt önce yazılıp
işlem sonra çökerse ileti işlenmiş sayılır ama etkisi uygulanmamış olur; etki önce
uygulanıp kayıt yazılamazsa ileti yeniden işlenir. İkisini tek sınıra almak bu iki hatayı
da kapatır.

#### Özet

- Bir yazma, sonucu yalnız hedef duruma bağlıysa etkisizdir; ölçümde durum ataması üç
  çalıştırmadan sonra aynı sonucu verdi, sayaç artırma 3, kayıt ekleme 3 satır üretti.
- Kayıt ekleme iş anlamlı bir benzersizlik kısıtı ve çatışma davranışıyla, sayaç artırma
  türetilmiş bir atamayla etkisiz biçime çevrildi; üç turdan sonra tek satır kaldı.
- İş anlamlı benzersizlik bulunamadığında istek kendi adını taşır; tekillik anahtarı etkiyle
  aynı işlemde yazılır.
- Yanıtın saklanması yinelenen isteğin de başarılı sonuç almasını sağlar; reddedilen sonuç
  da saklanır.
- Giden kutusu tüketicisi ileti kimliğini etkiyle aynı işlemde kaydettiğinde iki kez teslim
  edilen ileti tek etki üretti.

#### Sonraki Adım

İşlemler konusu, yazmanın doğruluğunu kuran dört soruyu kapattı: sınır nereye çizilir,
okuma neyi görür, eşzamanlı güncelleme nasıl çözülür, yeniden deneme neden güvenlidir. Bu
sorular doğrulukla ilgiliydi; hiçbiri hızla ilgili değildi. Doğru çalışan bir veri erişim
katmanı yine de yavaş olabilir ve yavaşlığın kaynağı çoğunlukla sorgunun kendisi değil,
sorgu **sayısıdır**. İlk derste kurulan eşleyici üç kayıt için on sorgu çalıştırmıştı.
Sonraki konu o sayıyı ölçer, kayıt sayısıyla nasıl büyüdüğünü gösterir ve toplu getirmeyle
nasıl sabitlendiğini uygulayarak kanıtlar.

## Başarım Sorunları

Sorgu sayısını patlatan N+1 sorununun ölçülmesi ve giderilmesi, aşırı veri çekmenin aktarılan bayt cinsinden bedeli, toplu ekleme ve güncelleme kalıpları, uygulamadan görülen yavaş sorgunun plan okunarak tanılanması ve derin sayfalamanın maliyeti.

### N+1 Sorgu Sorunu

Source: https://academia.sh/tr/kurslar/veri-erisim-katmani/n-arti-1-sorgu-sorunu

Sorgu sayısının kayıt sayısıyla büyümesi: sayan bir sarmalayıcıyla ölçüm, tek tek yükleme ile toplu getirme ve birleştirmenin karşılaştırılması, ölçeklenme tablosu, gidiş-dönüş maliyetinin etkisi ve sorgu bütçesinin bir sınama olarak yazılması.

İşlemler konusu yazmanın doğruluğunu kurdu: sınır nereye çizilir, okuma neyi görür,
eşzamanlı güncelleme nasıl çözülür, yeniden deneme neden güvenlidir. Hiçbiri hızla ilgili
değildi.

Doğru çalışan bir veri erişim katmanı yavaş olabilir ve yavaşlığın kaynağı çoğunlukla
sorgunun kendisi değil, sorgu **sayısıdır**. Bu kursun ilk dersinde küçük bir eşleyici üç
ödünç kaydı için on sorgu çalıştırmıştı. Bu ders o sayıyı ölçer, kayıt sayısıyla nasıl
büyüdüğünü gösterir ve sabitler.

#### Ölçüm Verisi

Ölçüm için gerçek büyüklükte bir veri kümesi gerekir.

```js
// uret.mjs — olcum icin kutuphane verisini uretir: 200 kitap, 60 uye, 2000 odunc kaydi
import { DatabaseSync } from "node:sqlite";
import { rmSync } from "node:fs";

for (const d of ["kutuphane.db", "kutuphane.db-wal", "kutuphane.db-shm"]) {
  rmSync(d, { force: true });
}
const db = new DatabaseSync("kutuphane.db");
db.exec(`
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, soyad TEXT NOT NULL);
CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL, yazar TEXT NOT NULL);
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL,
                    uye_id INTEGER NOT NULL, alis_tarihi TEXT NOT NULL, iade_tarihi TEXT);`);

db.exec("BEGIN");
const uyeEkle = db.prepare("INSERT INTO uye VALUES (?,?,?)");
for (let i = 1; i <= 60; i++) uyeEkle.run(i, `Uye${i}`, `Soyad${i}`);
const kitapEkle = db.prepare("INSERT INTO kitap VALUES (?,?,?)");
for (let i = 1; i <= 200; i++) kitapEkle.run(i, `Kitap ${i}`, `Yazar ${(i % 40) + 1}`);
const oduncEkle = db.prepare("INSERT INTO odunc VALUES (?,?,?,?,?)");
for (let i = 1; i <= 2000; i++) {
  const gun = String((i % 28) + 1).padStart(2, "0");
  const ay = String((i % 12) + 1).padStart(2, "0");
  oduncEkle.run(i, (i % 200) + 1, (i % 60) + 1, `2025-${ay}-${gun}`, i % 4 === 0 ? null : "2025-12-31");
}
db.exec("COMMIT");
console.log("uye:", db.prepare("SELECT count(*) AS n FROM uye").get().n,
            " kitap:", db.prepare("SELECT count(*) AS n FROM kitap").get().n,
            " odunc:", db.prepare("SELECT count(*) AS n FROM odunc").get().n,
            " acik:", db.prepare("SELECT count(*) AS n FROM odunc WHERE iade_tarihi IS NULL").get().n);
```

```sh
node uret.mjs
```

```
uye: 60  kitap: 200  odunc: 2000  acik: 500
```

#### Sorguyu Görünür Kılmak

Ölçülmeyen sayı düşmez. Bağlantının önüne konan ince bir sarmalayıcı, çalışan sorguları ve
dönen satırları sayar.

```js
// sayan.mjs — calisan sorgulari sayan ince bir sarmalayici
import { DatabaseSync } from "node:sqlite";

export function sayanBaglanti(dosya) {
  const db = new DatabaseSync(dosya);
  const sayac = { sorgu: 0, satir: 0 };
  return {
    sayac,
    tek(sql, ...d) { sayac.sorgu += 1; const r = db.prepare(sql).get(...d);
                     if (r !== undefined) sayac.satir += 1; return r; },
    cok(sql, ...d) { sayac.sorgu += 1; const r = db.prepare(sql).all(...d);
                     sayac.satir += r.length; return r; },
    sifirla() { sayac.sorgu = 0; sayac.satir = 0; },
  };
}
```

#### Üç Yaklaşım

Görev aynı: iade edilmemiş ödünç kayıtlarını kitap başlığı ve üye adıyla listele. Üç yol
denenir. **Tek tek** yükleme her kayıt için ilişkileri ayrı ayrı okur; ilk derste kurulan
tembel yüklemenin doğrudan yazılmış hâlidir. **Toplu getirme** kimlikleri toplayıp tek
sorguda okur. **Birleştirme** işi veritabanına bırakır.

```js
// n-arti-1.mjs — uc yaklasim, ayni liste; sorgu ve satir sayilari olculur
import { sayanBaglanti } from "./sayan.mjs";
const db = sayanBaglanti("kutuphane.db");
const SINIR = Number(process.argv[2] ?? 100);

function tekTek() {
  db.sifirla();
  const t = performance.now();
  const kayitlar = db.cok(
    "SELECT odunc_id, kitap_id, uye_id FROM odunc WHERE iade_tarihi IS NULL ORDER BY odunc_id LIMIT ?", SINIR);
  const liste = kayitlar.map((o) => ({
    id: o.odunc_id,
    baslik: db.tek("SELECT baslik FROM kitap WHERE kitap_id = ?", o.kitap_id).baslik,
    uye: db.tek("SELECT ad FROM uye WHERE uye_id = ?", o.uye_id).ad,
  }));
  return { ad: "tek tek", liste, ...db.sayac, sure: performance.now() - t };
}

function topluGetir() {
  db.sifirla();
  const t = performance.now();
  const kayitlar = db.cok(
    "SELECT odunc_id, kitap_id, uye_id FROM odunc WHERE iade_tarihi IS NULL ORDER BY odunc_id LIMIT ?", SINIR);
  const kitapIds = [...new Set(kayitlar.map((o) => o.kitap_id))];
  const uyeIds = [...new Set(kayitlar.map((o) => o.uye_id))];
  const kitapYer = new Map(db.cok(
    `SELECT kitap_id, baslik FROM kitap WHERE kitap_id IN (${kitapIds.map(() => "?").join(",")})`,
    ...kitapIds).map((k) => [k.kitap_id, k.baslik]));
  const uyeYer = new Map(db.cok(
    `SELECT uye_id, ad FROM uye WHERE uye_id IN (${uyeIds.map(() => "?").join(",")})`,
    ...uyeIds).map((u) => [u.uye_id, u.ad]));
  const liste = kayitlar.map((o) => ({
    id: o.odunc_id, baslik: kitapYer.get(o.kitap_id), uye: uyeYer.get(o.uye_id),
  }));
  return { ad: "toplu getirme", liste, ...db.sayac, sure: performance.now() - t };
}

function birlestirme() {
  db.sifirla();
  const t = performance.now();
  const liste = db.cok(
    `SELECT o.odunc_id AS id, k.baslik, u.ad AS uye
     FROM odunc o JOIN kitap k ON k.kitap_id = o.kitap_id JOIN uye u ON u.uye_id = o.uye_id
     WHERE o.iade_tarihi IS NULL ORDER BY o.odunc_id LIMIT ?`, SINIR);
  return { ad: "birlestirme", liste, ...db.sayac, sure: performance.now() - t };
}

const sonuclar = [tekTek(), topluGetir(), birlestirme()];
const ilk = JSON.stringify(sonuclar[0].liste);
for (const s of sonuclar) {
  console.log(`${s.ad.padEnd(14)} sorgu=${String(s.sorgu).padStart(4)}  ` +
    `donen_satir=${String(s.satir).padStart(4)}  sure=${s.sure.toFixed(1).padStart(6)} ms  ` +
    `sonuc_ayni=${JSON.stringify(s.liste) === ilk}`);
}
```

```sh
node n-arti-1.mjs 100
```

```
tek tek        sorgu= 201  donen_satir= 300  sure=   1.2 ms  sonuc_ayni=true
toplu getirme  sorgu=   3  donen_satir= 165  sure=   0.2 ms  sonuc_ayni=true
birlestirme    sorgu=   1  donen_satir= 100  sure=   0.1 ms  sonuc_ayni=true
```

Üç yol da aynı listeyi üretti; son sütun bunu doğruluyor. Sorgu sayıları 201, 3 ve 1.

İlk sayının yapısı adı veriyor: bir sorgu listeyi getirir, sonra her kayıt için ek sorgular
çalışır. Bu örnekte kayıt başına iki ilişki okunduğu için sayı $1 + 2N$ oluyor. Genel
biçimiyle **N+1 sorgu sorunu** budur: liste için bir, her öğe için bir sorgu.

#### Sayının Büyümesi

Sorunun ciddiyeti tek bir ölçümde görünmez; kayıt sayısı arttıkça görünür.

```sh
for n in 10 50 100 250 500; do echo "--- kayit sayisi $n ---"; node n-arti-1.mjs $n; done
```

```
--- kayit sayisi 10 ---
tek tek        sorgu=  21  donen_satir=  30  sure=   0.3 ms  sonuc_ayni=true
toplu getirme  sorgu=   3  donen_satir=  30  sure=   0.1 ms  sonuc_ayni=true
birlestirme    sorgu=   1  donen_satir=  10  sure=   0.0 ms  sonuc_ayni=true
--- kayit sayisi 50 ---
tek tek        sorgu= 101  donen_satir= 150  sure=   0.8 ms  sonuc_ayni=true
toplu getirme  sorgu=   3  donen_satir= 115  sure=   0.2 ms  sonuc_ayni=true
birlestirme    sorgu=   1  donen_satir=  50  sure=   0.1 ms  sonuc_ayni=true
--- kayit sayisi 100 ---
tek tek        sorgu= 201  donen_satir= 300  sure=   1.2 ms  sonuc_ayni=true
toplu getirme  sorgu=   3  donen_satir= 165  sure=   0.2 ms  sonuc_ayni=true
birlestirme    sorgu=   1  donen_satir= 100  sure=   0.1 ms  sonuc_ayni=true
--- kayit sayisi 250 ---
tek tek        sorgu= 501  donen_satir= 750  sure=   2.6 ms  sonuc_ayni=true
toplu getirme  sorgu=   3  donen_satir= 315  sure=   0.2 ms  sonuc_ayni=true
birlestirme    sorgu=   1  donen_satir= 250  sure=   0.1 ms  sonuc_ayni=true
--- kayit sayisi 500 ---
tek tek        sorgu=1001  donen_satir=1500  sure=   5.1 ms  sonuc_ayni=true
toplu getirme  sorgu=   3  donen_satir= 565  sure=   0.4 ms  sonuc_ayni=true
birlestirme    sorgu=   1  donen_satir= 500  sure=   0.2 ms  sonuc_ayni=true
```

Süreler makineye bağlıdır; sorgu sayıları değildir. Tek tek yüklemede sayı kayıt sayısıyla
doğrusal artıyor. Toplu getirmede üçte, birleştirmede birde sabit kalıyor. **Sabitlik
aranan özelliktir**; listedeki kayıt sayısı iki katına çıktığında sorgu sayısının
değişmemesi gerekir.

Dönen satır sayıları ikinci bir ayrımı gösteriyor. Toplu getirme beş yüz kayıt için 565
satır döndürdü, birleştirme 500. İlkinde ödünç kayıtları bir kez, kitaplar ve üyeler
tekilleştirilmiş hâlde okundu; ikincisinde her satır kendi kitap ve üye sütunlarını
taşıdığı için aynı kitabın başlığı defalarca aktarıldı. Bu fark, bir sonraki dersin
konusudur.

#### Yerel Ölçümün Gizlediği

Yukarıdaki sürelerde tek tek yükleme beş yüz kayıt için beş milisaniye aldı; kabul
edilebilir görünüyor. Bu, ölçümün yerel bir dosya üzerinde yapılmasından kaynaklanıyor.

Bağlantı havuzu dersinde konu edilen gidiş-dönüş burada belirleyici olur. Veritabanı ayrı
bir sunucudaysa her sorgu bir ağ turu ister. Tur başına yalnız yarım milisaniye varsayalım:
1001 sorgu 500 milisaniyeden çok, tek sorgu yarım milisaniye eder. Bu bir ölçüm değil,
ölçülen sorgu sayısıyla yapılan bir hesaptır; hesabın söylediği, sorgu sayısının yerel
ortamda gizlenip gerçek ortamda ortaya çıkan bir maliyet olduğudur.

Bu yüzden başarım ölçütü süre değil **sorgu sayısı** olarak konur. Süre ortamdan ortama
değişir; sorgu sayısı kodun bir özelliğidir.

#### Toplu Getirme mi, Birleştirme mi

İki çözüm arasındaki seçim veri şekline bağlıdır.

Birleştirme tek sorgudur ve en az satırı döndürür; ilişkiler bire bir ya da bire az sayıda
olduğunda doğru seçimdir. Bire çok ilişkilerde ise sonuç kümesi çarpar: her ödünç kaydı için
üç etiket varsa satır sayısı üçe katlanır ve ödünç sütunları her satırda tekrarlanır.

Toplu getirme sabit sayıda ek sorgu ekler, ama çarpma üretmez. Her bağıntı bir kez okunur ve
birleştirme uygulamada yapılır. Çok sayıda bire çok ilişki varsa bu yol daha az veri
aktarır.

Toplu getirmenin bir uygulama ayrıntısı vardır: kimlik listesi sorgu metnine soru işareti
olarak girer. Liste büyüdükçe metin büyür ve motorların bağlı değişken sayısı sınırına
yaklaşılır. Bu yüzden kimlikler parçalara bölünerek sorulur; parça büyüklüğü sabit tutulduğu
sürece sorgu sayısı kayıt sayısıyla değil, sabit bir bölme oranıyla artar.

#### Bütçeyi Sınamaya Çevirmek

Sorgu sayısı ölçülebiliyorsa sınanabilir. Aşağıdaki sınama, listenin belirli bir bütçenin
üstüne çıkmasını hata sayar.

```js
// butce.test.mjs — sorgu sayisi butcesi bir sinama olarak yazilir
import { test } from "node:test";
import assert from "node:assert/strict";
import { sayanBaglanti } from "./sayan.mjs";

const BUTCE = 5;

function acikOduncListesi(db, sinir) {
  const kayitlar = db.cok(
    "SELECT odunc_id, kitap_id, uye_id FROM odunc WHERE iade_tarihi IS NULL ORDER BY odunc_id LIMIT ?", sinir);
  const kitapIds = [...new Set(kayitlar.map((o) => o.kitap_id))];
  const uyeIds = [...new Set(kayitlar.map((o) => o.uye_id))];
  const kitapYer = new Map(db.cok(
    `SELECT kitap_id, baslik FROM kitap WHERE kitap_id IN (${kitapIds.map(() => "?").join(",")})`,
    ...kitapIds).map((k) => [k.kitap_id, k.baslik]));
  const uyeYer = new Map(db.cok(
    `SELECT uye_id, ad FROM uye WHERE uye_id IN (${uyeIds.map(() => "?").join(",")})`,
    ...uyeIds).map((u) => [u.uye_id, u.ad]));
  return kayitlar.map((o) => ({ id: o.odunc_id, baslik: kitapYer.get(o.kitap_id), uye: uyeYer.get(o.uye_id) }));
}

for (const sinir of [10, 100, 500]) {
  test(`acik odunc listesi ${sinir} kayit icin butceyi asmiyor`, () => {
    const db = sayanBaglanti("kutuphane.db");
    const liste = acikOduncListesi(db, sinir);
    assert.equal(liste.length, sinir);
    assert.ok(db.sayac.sorgu <= BUTCE, `sorgu sayisi ${db.sayac.sorgu} > ${BUTCE}`);
  });
}
```

```sh
node --test butce.test.mjs
```

```
✔ acik odunc listesi 10 kayit icin butceyi asmiyor (0.722292ms)
✔ acik odunc listesi 100 kayit icin butceyi asmiyor (0.57875ms)
✔ acik odunc listesi 500 kayit icin butceyi asmiyor (0.475583ms)
ℹ tests 3
ℹ suites 0
ℹ pass 3
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 36.655375
```

Sınamanın değeri, üç farklı kayıt sayısında aynı bütçeyi istemesidir. Biri tembel yüklemeye
dönen bir değişiklik yaparsa, beş yüz kayıtlı sınama bütçeyi aşar ve hata sorgu sayısıyla
birlikte bildirilir. Bu, N+1 sorununun kod incelemesinde gözden kaçmasını engelleyen tek
otomatik yoldur.

#### Özet

- Sorgu sayısı, sarmalayıcıyla sayıldığında görünür oldu: aynı liste için tek tek yükleme
  201, toplu getirme 3, birleştirme 1 sorgu çalıştırdı.
- Tek tek yüklemede sorgu sayısı kayıt sayısıyla doğrusal artıyor; beş yüz kayıtta 1001'e
  çıktı. Öteki iki yolda sabit kaldı.
- Yerel ölçümde fark milisaniyelerle kalır; ağ üzerinden her sorgu bir gidiş-dönüş
  istediğinden aynı fark saniyelere çıkar. Bu yüzden ölçüt süre değil sorgu sayısıdır.
- Birleştirme en az satırı döndürür ama bire çok ilişkilerde sonucu çarpar; toplu getirme
  çarpma üretmez, karşılığında sabit sayıda ek sorgu ister.
- Sorgu bütçesi bir sınamaya çevrildiğinde, tembel yüklemeye dönüş otomatik olarak yakalanır.

#### Sonraki Adım

Bu derste bir sayı düştü, başka bir sayı gözden kaçtı. Toplu getirme beş yüz kayıt için 565
satır, birleştirme 500 satır döndürdü; ama satır sayısı aktarılan verinin ölçüsü değil.
Sorgular yalnız gereken sütunları istemişti. Uygulamalarda çoğu sorgu bütün sütunları ister,
çoğu liste gereğinden çok satır getirir ve fark bayt cinsinden ölçülene kadar görünmez.
Sonraki ders aynı listeyi farklı sütun kümeleriyle çekerek aktarılan veriyi bayt olarak
ölçer ve alan ile satır düzeyinde sınırlamanın karşılığını gösterir.

### Aşırı Veri Çekme

Source: https://academia.sh/tr/kurslar/veri-erisim-katmani/asiri-veri-cekme

Aktarılan verinin bayt cinsinden ölçülmesi: bütün sütunları çekmekle gereken sütunları çekmenin farkı, satır sınırlamasının katkısı, toplamanın uygulamada mı veritabanında mı yapılacağı ve yıldız seçiminin sütun çakışmasıyla sessizce veri düşürmesi.

Önceki ders sorgu sayısını düşürdü. Toplu getirme beş yüz kayıt için 565, birleştirme 500
satır döndürdü ve satır sayısı doğru ölçütmüş gibi göründü.

Satır sayısı aktarılan verinin ölçüsü değildir. Bir satır beş sütun da taşıyabilir, yirmi
sütun da; sütunlardan biri kısa bir tarih de olabilir, bin karakterlik bir özet de. Bu ders
ölçüyü bayta çevirir ve sınırlamanın iki düzeyini — hangi sütunlar ve kaç satır — ayrı ayrı
ölçer.

#### Ölçüm Verisi

Ölçümün anlamlı olması için bağıntıların gerçekçi genişlikte olması gerekir: kitapta özet ve
kapak metni, üyede adres ve notlar.

```js
// uret.mjs — ozet metni tasiyan kitap bagintisi ile olcum verisi
import { DatabaseSync } from "node:sqlite";
import { rmSync } from "node:fs";
rmSync("kutuphane.db", { force: true });

const db = new DatabaseSync("kutuphane.db");
db.exec(`
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, soyad TEXT NOT NULL,
                  eposta TEXT, adres TEXT, notlar TEXT);
CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL, yazar TEXT NOT NULL,
                    isbn TEXT NOT NULL, ozet TEXT NOT NULL, kapak_metni TEXT NOT NULL);
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL,
                    uye_id INTEGER NOT NULL, alis_tarihi TEXT NOT NULL, iade_tarihi TEXT);`);

const metin = (n, tohum) => `${tohum} `.repeat(n).slice(0, n * 6);
db.exec("BEGIN");
const uyeEkle = db.prepare("INSERT INTO uye VALUES (?,?,?,?,?,?)");
for (let i = 1; i <= 60; i++) {
  uyeEkle.run(i, `Uye${i}`, `Soyad${i}`, `uye${i}@ornek.test`,
              metin(20, `Sokak${i}`), metin(30, "not"));
}
const kitapEkle = db.prepare("INSERT INTO kitap VALUES (?,?,?,?,?,?)");
for (let i = 1; i <= 200; i++) {
  kitapEkle.run(i, `Kitap ${i}`, `Yazar ${(i % 40) + 1}`,
                `978-0-00-${String(100000 + i).slice(0, 6)}`, metin(120, "ozet"), metin(60, "kapak"));
}
const oduncEkle = db.prepare("INSERT INTO odunc VALUES (?,?,?,?,?)");
for (let i = 1; i <= 2000; i++) {
  const gun = String((i % 28) + 1).padStart(2, "0");
  const ay = String((i % 12) + 1).padStart(2, "0");
  oduncEkle.run(i, (i % 200) + 1, (i % 60) + 1, `2025-${ay}-${gun}`, i % 4 === 0 ? null : "2025-12-31");
}
db.exec("COMMIT");
console.log("kitap:", db.prepare("SELECT count(*) AS n FROM kitap").get().n,
            " ortalama ozet uzunlugu:", db.prepare("SELECT avg(length(ozet)) AS n FROM kitap").get().n,
            " acik odunc:", db.prepare("SELECT count(*) AS n FROM odunc WHERE iade_tarihi IS NULL").get().n);
```

```sh
node uret.mjs
```

```
kitap: 200  ortalama ozet uzunlugu: 600  acik odunc: 500
```

#### İki Düzeyde Sınırlama

Görev yine aynı: açık ödünç listesi. Ekranda gösterilen alanlar ödünç kimliği, alış tarihi,
kitap başlığı ve üyenin adı. Üç sorgu bu listeyi üç farklı genişlikte çeker.

```js
// alan-olcumu.mjs — ayni listeyi farkli sutun kumeleriyle ceker, aktarilan baytlari olcer
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("kutuphane.db");

const olc = (ad, sql) => {
  const t = performance.now();
  const satirlar = db.prepare(sql).all();
  const sure = performance.now() - t;
  const bayt = Buffer.byteLength(JSON.stringify(satirlar));
  console.log(`${ad.padEnd(26)} satir=${String(satirlar.length).padStart(4)}  ` +
    `bayt=${String(bayt).padStart(7)}  satir_basi=${String(Math.round(bayt / satirlar.length)).padStart(5)}  ` +
    `sure=${sure.toFixed(1)} ms`);
  return bayt;
};

const hepsi = olc("butun sutunlar", `
  SELECT * FROM odunc o JOIN kitap k ON k.kitap_id = o.kitap_id
                        JOIN uye u ON u.uye_id = o.uye_id
  WHERE o.iade_tarihi IS NULL ORDER BY o.odunc_id`);

const gerekli = olc("yalniz gereken sutunlar", `
  SELECT o.odunc_id, o.alis_tarihi, k.baslik, u.ad, u.soyad
  FROM odunc o JOIN kitap k ON k.kitap_id = o.kitap_id JOIN uye u ON u.uye_id = o.uye_id
  WHERE o.iade_tarihi IS NULL ORDER BY o.odunc_id`);

const sayfa = olc("gereken sutun + 20 satir", `
  SELECT o.odunc_id, o.alis_tarihi, k.baslik, u.ad, u.soyad
  FROM odunc o JOIN kitap k ON k.kitap_id = o.kitap_id JOIN uye u ON u.uye_id = o.uye_id
  WHERE o.iade_tarihi IS NULL ORDER BY o.odunc_id LIMIT 20`);

console.log(`\nsutun secimi orani: ${(hepsi / gerekli).toFixed(1)}x`);
console.log(`sutun + satir orani: ${(hepsi / sayfa).toFixed(1)}x`);
```

```sh
node alan-olcumu.mjs
```

```
butun sutunlar             satir= 500  bayt= 731162  satir_basi= 1462  sure=1.5 ms
yalniz gereken sutunlar    satir= 500  bayt=  47744  satir_basi=   95  sure=0.3 ms
gereken sutun + 20 satir   satir=  20  bayt=   1867  satir_basi=   93  sure=0.0 ms

sutun secimi orani: 15.3x
sutun + satir orani: 391.6x
```

Satır sayısı ilk iki ölçümde aynı: beş yüz. Aktarılan veri on beş kat farklı. Fark tamamen
sütun seçiminden geliyor; satır başına 1462 bayt yerine 95 bayt aktarılıyor. Ekranda
gösterilmeyen özet, kapak metni, adres ve notlar sütunları taşınan verinin on dörtte on
üçünü oluşturuyor.

Satır sınırlaması ikinci bir çarpan ekliyor. Sayfa başına yirmi satır gösteren bir arayüz
için beş yüz satırın tamamını çekmek gereksizdir; iki sınırlama birlikte oranı 391 kata
çıkarıyor.

Süreler burada yanıltıcıdır ve yanıltıcı olduğu için bırakıldı: veritabanı süreç içinde
olduğu için 731 kilobayt bir buçuk milisaniyede "aktarıldı". Ağ üzerinden aynı veri, bant
genişliği ve gecikmeyle çarpılarak taşınır. Ölçüt yine süre değil, **bayt** olarak konur.

#### Toplamayı Nereye Koymalı

Aşırı veri çekmenin en keskin biçimi, uygulamanın veritabanının işini üstlenmesidir. "Kaç
açık ödünç var" sorusu iki yoldan yanıtlanabilir.

```js
// toplama-nerede.mjs — sayimi uygulamada mi veritabaninda mi yapmali
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("kutuphane.db");

let t = performance.now();
const satirlar = db.prepare("SELECT * FROM odunc WHERE iade_tarihi IS NULL").all();
const uygulamada = satirlar.length;
const uygulamaSure = performance.now() - t;
const uygulamaBayt = Buffer.byteLength(JSON.stringify(satirlar));

t = performance.now();
const veritabaninda = db.prepare(
  "SELECT count(*) AS n FROM odunc WHERE iade_tarihi IS NULL").get().n;
const vtSure = performance.now() - t;
const vtBayt = Buffer.byteLength(JSON.stringify({ n: veritabaninda }));

console.log(`uygulamada sayma  : sonuc=${uygulamada}  bayt=${uygulamaBayt}  sure=${uygulamaSure.toFixed(2)} ms`);
console.log(`veritabaninda sayma: sonuc=${veritabaninda}  bayt=${vtBayt}  sure=${vtSure.toFixed(2)} ms`);
console.log(`bayt orani: ${Math.round(uygulamaBayt / vtBayt)}x`);
```

```sh
node toplama-nerede.mjs
```

```
uygulamada sayma  : sonuc=500  bayt=44845  sure=0.31 ms
veritabaninda sayma: sonuc=500  bayt=9  sure=0.05 ms
bayt orani: 4983x
```

Aynı sayı, beş bin kat farklı veriyle. Uygulamada sayma yöntemi kayıt sayısıyla büyür;
veritabanında sayma sabit dokuz bayt döndürür. Toplama, süzme ve sıralama gibi işler
verinin bulunduğu yerde yapılır; uygulamaya yalnız sonuç taşınır. Bu ilke gruplama ve uç
değer sorguları için de aynen geçerlidir.

#### Yıldız Seçiminin İkinci Bedeli

Bütün sütunları istemenin maliyeti yalnız bayt değildir. Birleştirmede aynı adı taşıyan
sütunlar çakışır ve sonuç nesnesinde biri ötekini örter.

```js
// yildiz-tuzagi.mjs — birlestirmede yildiz secimi ayni adli sutunlari sessizce birlestirir
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("kutuphane.db");

const BIRLESTIRME = `FROM odunc o JOIN kitap k ON k.kitap_id = o.kitap_id
                                  JOIN uye u ON u.uye_id = o.uye_id
                     WHERE o.odunc_id = 4`;

console.log("uc bagintinin sutun toplami: 5 + 6 + 6 = 17");
console.log("yildizla donen alan sayisi :", Object.keys(db.prepare(`SELECT * ${BIRLESTIRME}`).get()).length);

// Sema degisir: kitap bagintisina da 'notlar' sutunu eklenir.
db.exec("ALTER TABLE kitap ADD COLUMN notlar TEXT");
db.exec("UPDATE kitap SET notlar = 'cildi yipranmis'");

const yildiz = db.prepare(`SELECT * ${BIRLESTIRME}`).get();
const acik = db.prepare(
  `SELECT k.notlar AS kitap_notu, u.notlar AS uye_notu ${BIRLESTIRME}`).get();

console.log("sema degisiminden sonra yildizla donen alan sayisi:", Object.keys(yildiz).length);
console.log("  yildiz.notlar   :", JSON.stringify(yildiz.notlar.slice(0, 20)));
console.log("  acik.kitap_notu :", JSON.stringify(acik.kitap_notu));
console.log("  acik.uye_notu   :", JSON.stringify(acik.uye_notu.slice(0, 20)));
```

```sh
node uret.mjs
node yildiz-tuzagi.mjs
```

```
kitap: 200  ortalama ozet uzunlugu: 600  acik odunc: 500
uc bagintinin sutun toplami: 5 + 6 + 6 = 17
yildizla donen alan sayisi : 15
sema degisiminden sonra yildizla donen alan sayisi: 15
  yildiz.notlar   : "not not not not not "
  acik.kitap_notu : "cildi yipranmis"
  acik.uye_notu   : "not not not not not "
```

İlk satırda on yedi sütun istendi, on beş alan geldi: `kitap_id` ve `uye_id` adları iki
bağıntıda da bulunduğu için birleşti. Şemaya yeni bir sütun eklendikten sonra istenen sütun
sayısı on sekize çıktı, dönen alan sayısı yine on beş kaldı. `notlar` alanı üyenin notunu
taşıyor; kitabın notu sessizce düştü. Açık seçimle sorulduğunda iki değer de yerinde.

Buradaki tehlike hata vermemesidir. Sorgu başarılı, alan dolu, değer geçerli — ama yanlış
bağıntıdan geliyor. Şema göçleri dersinde eklenen bir sütun, yıldız seçimi kullanan bir
kod yolunu bu şekilde bozabilir ve göç sırasında hiçbir uyarı üretmez.

#### Ne Kadar Sınırlamalı

Sınırlamanın ters yönü de vardır. Gereğinden az sütun çeken bir sorgu, eksik kalan alan için
ikinci bir gidiş-dönüş gerektirir; önceki dersin N+1 sorunu bu yoldan geri gelebilir. Bu
yüzden ölçüt "en az sütun" değil, **gösterilen alanların tam kümesidir.**

Pratikte üç kural işe yarar. Liste görünümleri yalnız listede görünen alanları çeker;
ayrıntı görünümü ayrı bir sorgu ile çağrılır. Uzun metin ve ikili içerik taşıyan sütunlar
liste sorgularına hiç girmez. Sayfalama, sonuç kümesinin sınırını sorguya yazar; uygulamada
diliminin alınması aktarımı azaltmaz, çünkü veri zaten taşınmıştır.

Bu kuralların uygulanabilmesi, sorgunun hangi alanları döndürdüğünün kod okunarak
görülebilmesine bağlıdır. Yıldız seçimi bu görünürlüğü ortadan kaldırır: sorguya bakan biri
hangi alanların taşındığını göremez, şema değiştiğinde taşınan veri sessizce büyür.

#### Özet

- Aynı beş yüz satırlık liste, bütün sütunlarla 731162 bayt, yalnız gereken sütunlarla
  47744 bayt aktardı; satır sayısı aynıyken fark 15,3 kat.
- Satır sınırlaması ikinci bir çarpan ekledi; iki sınırlama birlikte oranı 391 kata çıkardı.
- Sayımı uygulamada yapmak 44845 bayt, veritabanında yapmak 9 bayt taşıdı; toplama, süzme ve
  sıralama verinin bulunduğu yerde yapılır.
- Yıldız seçimi birleştirmede aynı adlı sütunları birleştirdi; şemaya eklenen bir sütunun
  değeri sessizce düştü ve hiçbir hata üretilmedi.
- Ölçüt "en az sütun" değil, gösterilen alanların tam kümesidir; eksik seçim ikinci bir
  gidiş-dönüş üretir.

#### Sonraki Adım

Bu iki ders okuma yolunu ele aldı: kaç sorgu çalışıyor ve her sorgu ne kadar veri taşıyor.
Yazma yolunun kendi ölçeği vardır. Ödünç verilerinin başka bir sistemden aktarıldığını
düşünelim: iki bin kayıt tek tek mi yazılacak, tek bir işlemde mi, yoksa tek bir deyimde mi?
Üç yol da aynı satırları üretir; süreleri arasında büyüklük mertebesi farkı vardır. Sonraki
ders bu farkı ölçer, farkın nereden geldiğini gösterir ve parti büyüklüğünün nasıl
seçildiğini ele alır.

### Toplu İşlemler

Source: https://academia.sh/tr/kurslar/veri-erisim-katmani/toplu-islemler

Yazma yolunun ölçeklenmesi: aynı iki bin satırın dört yoldan yazılması ve sürelerin karşılaştırılması, işlem sınırının belirleyici payı, parti büyüklüğünün deyim sayısı ve sorgu uzunluğuyla ilişkisi, toplu güncellemede deyim sayısının ölçülmesi.

Önceki iki ders okuma yolunu ölçtü: kaç sorgu çalışıyor ve her sorgu ne kadar veri taşıyor.
Yazma yolunun kendi ölçeği vardır.

Ödünç verilerinin başka bir sistemden aktarıldığını düşünelim: iki bin kayıt yazılacak. Bu
iş tek tek de yapılabilir, tek bir işlemde de, tek bir deyimde de. Üç yol da aynı satırları
üretir. Bu ders aralarındaki farkı ölçer ve farkın nereden geldiğini gösterir.

#### Dört Yol

Ölçüm her yol için veritabanını sıfırdan kurar; böylece hiçbir yol öncekinin ısıttığı
önbellekten yararlanmaz.

```js
// toplu-ekleme.mjs — ayni 2000 satir dort yoldan yazilir, sureler olculur
import { DatabaseSync } from "node:sqlite";
import { rmSync } from "node:fs";

const ADET = 2000;
const veri = Array.from({ length: ADET }, (_, i) => [
  (i % 200) + 1, (i % 60) + 1, `2025-${String((i % 12) + 1).padStart(2, "0")}-01`,
]);

function yeniVeritabani() {
  rmSync("aktarim.db", { force: true });
  const db = new DatabaseSync("aktarim.db");
  db.exec(`CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL,
           uye_id INTEGER NOT NULL, alis_tarihi TEXT NOT NULL)`);
  return db;
}

function olc(ad, yaz) {
  const db = yeniVeritabani();
  const t = performance.now();
  yaz(db);
  const sure = performance.now() - t;
  const n = db.prepare("SELECT count(*) AS n FROM odunc").get().n;
  db.close();
  console.log(`${ad.padEnd(38)} satir=${n}  sure=${sure.toFixed(0).padStart(6)} ms  ` +
    `satir_basi=${(sure * 1000 / n).toFixed(1).padStart(6)} us`);
  return sure;
}

const a = olc("her satir ayri islem", (db) => {
  for (const s of veri) {
    db.prepare("INSERT INTO odunc (kitap_id, uye_id, alis_tarihi) VALUES (?,?,?)").run(...s);
  }
});

const b = olc("tek islem, her satir hazirlanir", (db) => {
  db.exec("BEGIN");
  for (const s of veri) {
    db.prepare("INSERT INTO odunc (kitap_id, uye_id, alis_tarihi) VALUES (?,?,?)").run(...s);
  }
  db.exec("COMMIT");
});

const c = olc("tek islem, deyim yeniden kullanilir", (db) => {
  db.exec("BEGIN");
  const ekle = db.prepare("INSERT INTO odunc (kitap_id, uye_id, alis_tarihi) VALUES (?,?,?)");
  for (const s of veri) ekle.run(...s);
  db.exec("COMMIT");
});

const d = olc("tek islem, 500'luk cok satirli deyim", (db) => {
  db.exec("BEGIN");
  for (let i = 0; i < veri.length; i += 500) {
    const parti = veri.slice(i, i + 500);
    const yerler = parti.map(() => "(?,?,?)").join(",");
    db.prepare(`INSERT INTO odunc (kitap_id, uye_id, alis_tarihi) VALUES ${yerler}`)
      .run(...parti.flat());
  }
  db.exec("COMMIT");
});

console.log(`\noranlar (ilk yaklasima gore): ${(a/b).toFixed(1)}x  ${(a/c).toFixed(1)}x  ${(a/d).toFixed(1)}x`);
```

```sh
node toplu-ekleme.mjs
```

```
her satir ayri islem                   satir=2000  sure=   376 ms  satir_basi= 188.2 us
tek islem, her satir hazirlanir        satir=2000  sure=     3 ms  satir_basi=   1.5 us
tek islem, deyim yeniden kullanilir    satir=2000  sure=     1 ms  satir_basi=   0.4 us
tek islem, 500'luk cok satirli deyim   satir=2000  sure=     1 ms  satir_basi=   0.7 us

oranlar (ilk yaklasima gore): 127.9x  470.4x  276.9x
```

Süreler donanıma ve dosya sistemine bağlıdır; oranlar yapıyı gösterir.

En büyük sıçrama ilk iki satır arasında. Aradaki tek fark işlem sınırıdır. İlk yaklaşımda
her `INSERT` kendi işlemini açıp kapatır; kapanış her seferinde kalıcılık gerektirir, yani
verinin diske indiğinin garanti edilmesi beklenir. İki bin kesinleştirme, iki bin
dayanıklılık noktası demektir. İkinci yaklaşımda tek bir kesinleştirme var.

**Toplu yazmada birinci karar deyim biçimi değil, işlem sınırıdır.** SQL Temelleri kursunda
toplu ekleme, çok satırlı `INSERT` deyimi olarak tanıtılmıştı; ölçüm, asıl kazancın orada
olmadığını gösteriyor.

İkinci ve üçüncü satır arasındaki fark deyim hazırlamadan geliyor. Her satır için deyimi
yeniden hazırlamak, aynı metni iki bin kez ayrıştırmak demektir. Hazır deyimi bir kez kurup
yeniden kullanmak bu işi bir kereye indiriyor.

Dördüncü satır, yerel ölçümde beklenenden yavaş. Çok satırlı deyim her parti için farklı
uzunlukta bir SQL metni üretir, dolayısıyla her parti yeniden ayrıştırılır; yerel bir
veritabanında bu, hazır deyimi yeniden kullanmaktan pahalıdır. Çok satırlı deyimin kazancı
başka yerdedir: **deyim sayısını düşürür**, dolayısıyla ağ üzerindeki gidiş-dönüş sayısını
düşürür. Yerel ölçüm bu kazancı göstermez.

#### Parti Büyüklüğü

Çok satırlı deyimde seçilecek tek sayı parti büyüklüğüdür.

```js
// parti-boyu.mjs — cok satirli deyimde parti buyuklugunun etkisi
import { DatabaseSync } from "node:sqlite";
import { rmSync } from "node:fs";

const ADET = 2000;
const veri = Array.from({ length: ADET }, (_, i) => [
  (i % 200) + 1, (i % 60) + 1, `2025-${String((i % 12) + 1).padStart(2, "0")}-01`,
]);

for (const parti of [1, 10, 50, 200, 1000]) {
  rmSync("aktarim.db", { force: true });
  const db = new DatabaseSync("aktarim.db");
  db.exec(`CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL,
           uye_id INTEGER NOT NULL, alis_tarihi TEXT NOT NULL)`);
  const t = performance.now();
  db.exec("BEGIN");
  let deyim = 0, enUzun = 0;
  for (let i = 0; i < veri.length; i += parti) {
    const dilim = veri.slice(i, i + parti);
    const sql = `INSERT INTO odunc (kitap_id, uye_id, alis_tarihi) VALUES ${
      dilim.map(() => "(?,?,?)").join(",")}`;
    enUzun = Math.max(enUzun, sql.length);
    db.prepare(sql).run(...dilim.flat());
    deyim += 1;
  }
  db.exec("COMMIT");
  const sure = performance.now() - t;
  console.log(`parti=${String(parti).padStart(4)}  deyim=${String(deyim).padStart(4)}  ` +
    `en_uzun_sql=${String(enUzun).padStart(6)} karakter  sure=${sure.toFixed(1).padStart(6)} ms`);
  db.close();
}
```

```sh
node parti-boyu.mjs
```

```
parti=   1  deyim=2000  en_uzun_sql=    64 karakter  sure=   4.1 ms
parti=  10  deyim= 200  en_uzun_sql=   136 karakter  sure=   1.4 ms
parti=  50  deyim=  40  en_uzun_sql=   456 karakter  sure=   1.1 ms
parti= 200  deyim=  10  en_uzun_sql=  1656 karakter  sure=   1.0 ms
parti=1000  deyim=   2  en_uzun_sql=  8056 karakter  sure=   0.9 ms
```

Kazancın büyük kısmı ilk adımlarda alınıyor: birden ona geçmek süreyi üçte bire indiriyor,
elliden bine geçmek neredeyse hiçbir şey kazandırmıyor. Buna karşılık sorgu metni
doğrusal büyüyor ve bin satırlık partide sekiz kilobayta yaklaşıyor.

Parti büyüklüğünü sınırlayan üç etken vardır. Bağlı değişken sayısının motor tarafından
konan bir üst sınırı bulunur; parti başına sütun sayısıyla çarpılan değişken sayısı bu
sınırı aşamaz. Uzun sorgu metni ayrıştırma maliyetini ve bellek kullanımını artırır. Ve
büyük parti, hata durumunda geri alınacak işin de büyümesi demektir.

Bu yüzden parti, "olabildiğince büyük" değil, kazancın düzleştiği noktada seçilir. Ölçümde
bu nokta elli ile iki yüz arasındadır; başka bir şemada başka bir yerde olur, ama eğrinin
biçimi aynıdır.

#### Toplu Güncelleme

Güncellemede her satıra farklı bir değer yazmak gerekir; bu, tek bir `UPDATE` deyimiyle
yapılabilir ama nasıl yapılacağı seçim ister.

```js
// toplu-guncelleme.mjs — 1000 satira ayri ayri deger yazmanin uc yolu
import { DatabaseSync } from "node:sqlite";
import { rmSync } from "node:fs";

const ADET = 1000;
const guncellemeler = Array.from({ length: ADET }, (_, i) => [
  i + 1, `2025-${String((i % 12) + 1).padStart(2, "0")}-15`,
]);

function hazirla() {
  rmSync("aktarim.db", { force: true });
  const db = new DatabaseSync("aktarim.db");
  db.exec(`CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL,
           uye_id INTEGER NOT NULL, alis_tarihi TEXT NOT NULL, iade_tarihi TEXT)`);
  db.exec("BEGIN");
  const ekle = db.prepare("INSERT INTO odunc VALUES (?,?,?,?,NULL)");
  for (let i = 1; i <= ADET; i++) ekle.run(i, (i % 200) + 1, (i % 60) + 1, "2025-01-01");
  db.exec("COMMIT");
  return db;
}

function olc(ad, guncelle) {
  const db = hazirla();
  const sayac = { deyim: 0 };
  const t = performance.now();
  guncelle(db, sayac);
  const sure = performance.now() - t;
  const n = db.prepare("SELECT count(*) AS n FROM odunc WHERE iade_tarihi IS NOT NULL").get().n;
  const ornek = db.prepare("SELECT iade_tarihi FROM odunc WHERE odunc_id = 7").get().iade_tarihi;
  db.close();
  console.log(`${ad.padEnd(30)} guncellenen=${n}  ornek(7)=${ornek}  ` +
    `deyim=${String(sayac.deyim).padStart(4)}  sure=${sure.toFixed(1).padStart(6)} ms`);
  return sure;
}

const a = olc("tek tek guncelleme", (db, sayac) => {
  db.exec("BEGIN");
  const g = db.prepare("UPDATE odunc SET iade_tarihi = ? WHERE odunc_id = ?");
  for (const [id, tarih] of guncellemeler) { g.run(tarih, id); sayac.deyim += 1; }
  db.exec("COMMIT");
});

const b = olc("deger listesinden guncelleme", (db, sayac) => {
  db.exec("BEGIN");
  const yerler = guncellemeler.map(() => "(?,?)").join(",");
  db.prepare(`UPDATE odunc SET iade_tarihi = v.tarih
              FROM (SELECT column1 AS id, column2 AS tarih FROM (VALUES ${yerler})) AS v
              WHERE odunc.odunc_id = v.id`).run(...guncellemeler.flat());
  sayac.deyim += 1;
  db.exec("COMMIT");
});

const c = olc("hazirlik tablosu uzerinden", (db, sayac) => {
  db.exec("BEGIN");
  db.exec("CREATE TEMP TABLE hazirlik (id INTEGER PRIMARY KEY, tarih TEXT NOT NULL)");
  const ekle = db.prepare("INSERT INTO hazirlik VALUES (?,?)");
  for (const [id, tarih] of guncellemeler) { ekle.run(id, tarih); sayac.deyim += 1; }
  db.exec(`UPDATE odunc SET iade_tarihi = (SELECT tarih FROM hazirlik WHERE id = odunc.odunc_id)
           WHERE odunc_id IN (SELECT id FROM hazirlik)`);
  sayac.deyim += 1;
  db.exec("DROP TABLE hazirlik");
  db.exec("COMMIT");
});

console.log(`\noranlar (tek tek guncellemeye gore): ${(a/b).toFixed(2)}x  ${(a/c).toFixed(2)}x`);
```

```sh
node toplu-guncelleme.mjs
```

```
tek tek guncelleme             guncellenen=1000  ornek(7)=2025-07-15  deyim=1000  sure=   0.9 ms
deger listesinden guncelleme   guncellenen=1000  ornek(7)=2025-07-15  deyim=   1  sure=   1.0 ms
hazirlik tablosu uzerinden     guncellenen=1000  ornek(7)=2025-07-15  deyim=1001  sure=   1.0 ms

oranlar (tek tek guncellemeye gore): 0.91x  0.98x
```

Üç yolun süresi yerel ölçümde aynı. Bu, olumsuz bir sonuç değil, bir sonuçtur: aynı işlem
sınırının içinde kaldıkları sürece deyim sayısı yerel veritabanında maliyeti belirlemiyor.
Ayrım deyim sütununda: bin, bir ve bin bir. Veritabanı ağın ötesindeyse bu sayı doğrudan
gidiş-dönüş sayısıdır ve o zaman aradaki fark bine katlanır.

Üçüncü yolun deyim sayısı en yüksek olduğu hâlde ayrı bir değeri vardır. **Hazırlık
tablosu** verilerin önce ayrı bir bağıntıya yüklenmesini, orada doğrulanmasını ve ancak
sonra hedefe uygulanmasını sağlar. Hedef bağıntıya tek bir deyimle dokunulur; hatalı satır
hedefe hiç ulaşmaz. Hazırlık tablosunun yüklenmesi de kendi içinde bir toplu ekleme
işidir ve bu dersin ilk ölçümündeki kurallara uyar.

#### Özet

- Aynı iki bin satır dört yoldan yazıldı: her satır ayrı işlemde 376 ms, tek işlemde 3 ms,
  hazır deyim yeniden kullanılarak 1 ms sürdü.
- En büyük kazanç işlem sınırından geldi; iki bin kesinleştirme yerine bir kesinleştirme
  yapmak süreyi yüz yirmi kattan fazla düşürdü.
- Parti büyüklüğünde kazanç erken düzleşiyor: 1'den 10'a geçmek süreyi üçte bire indirdi,
  50'den 1000'e geçmek ölçülebilir bir katkı vermedi; sorgu metni ise doğrusal büyüdü.
- Toplu güncellemede üç yolun yerel süresi eşit çıktı; ayrım deyim sayısındadır (1000, 1,
  1001) ve ağ üzerinden bu sayı gidiş-dönüş sayısına dönüşür.
- Hazırlık tablosu, veriyi hedefe uygulamadan önce doğrulama olanağı verir ve hedefe tek
  deyimle dokunur.

#### Sonraki Adım

Buraya kadar ölçülen her şey uygulamanın kendi yaptığı işti: kaç sorgu çalıştı, kaç bayt
taşındı, kaç deyim gönderildi. Bir sorgu bu ölçütlerin hepsini geçip yine de yavaş olabilir.
Tek bir sorgu, tek bir işlemde, yalnız gereken sütunlarla çalışıp saniyeler sürebilir; çünkü
veritabanı o sorguyu nasıl yürüteceğine kendi karar verir. Sonraki ders o kararı uygulamanın
içinden okur, yavaş sorguyu kütüğe düşürür ve planın neden değiştiğini gösterir.

### Sorgu Planı Okuma

Source: https://academia.sh/tr/kurslar/veri-erisim-katmani/sorgu-plani-okuma

Yavaş sorgunun uygulamadan tanılanması: her sorguyu süren ve eşiği aşanı planıyla kütüğe düşüren bir sarmalayıcı, aynı sorgunun dizinsiz ve dizinli planı, koşul yazımının planı değiştirmesi ve yavaş sorguların sorgu imzasına göre toplanması.

Buraya kadar ölçülen her şey uygulamanın kendi yaptığı işti: kaç sorgu çalıştı, kaç bayt
taşındı, kaç deyim gönderildi. Bir sorgu bu ölçütlerin hepsini geçip yine de yavaş olabilir.

Tek bir sorgu, tek bir işlemde, yalnız gereken sütunlarla çalışıp saniyeler sürebilir; çünkü
sorgunun nasıl yürütüleceğine veritabanı karar verir. İleri SQL kursunda bu karar **sorgu
planı** olarak tanıtıldı ve doğrudan veritabanı kabuğundan okundu. Bu ders aynı kararı
uygulamanın içinden okur ve yavaş sorguyu üretim koşullarında yakalayacak bir düzenek kurar.

#### Ölçüm Verisi

Tarama ile dizin arasındaki farkın ölçülebilmesi için bağıntının yeterince büyük olması
gerekir.

```js
// uret.mjs — 50000 odunc kaydi; tarama ile dizin farkinin olculebilmesi icin
import { DatabaseSync } from "node:sqlite";
import { rmSync } from "node:fs";
rmSync("kutuphane.db", { force: true });

const db = new DatabaseSync("kutuphane.db");
db.exec(`
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL);
CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL);
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL,
                    uye_id INTEGER NOT NULL, alis_tarihi TEXT NOT NULL, iade_tarihi TEXT);`);
db.exec("BEGIN");
const u = db.prepare("INSERT INTO uye VALUES (?,?)");
for (let i = 1; i <= 60; i++) u.run(i, `Uye${i}`);
const k = db.prepare("INSERT INTO kitap VALUES (?,?)");
for (let i = 1; i <= 200; i++) k.run(i, `Kitap ${i}`);
const o = db.prepare("INSERT INTO odunc VALUES (?,?,?,?,?)");
let tohum = 20250727;                       // belirlenimci sozde rastgele dizi
const sonraki = (n) => { tohum = (tohum * 48271) % 2147483647; return tohum % n; };
for (let i = 1; i <= 50000; i++) {
  const yil = 2022 + sonraki(4);
  const ay = String(sonraki(12) + 1).padStart(2, "0");
  const gun = String(sonraki(28) + 1).padStart(2, "0");
  o.run(i, sonraki(200) + 1, sonraki(60) + 1, `${yil}-${ay}-${gun}`,
        sonraki(5) === 0 ? null : `${yil}-12-31`);
}
db.exec("COMMIT");
db.exec("ANALYZE");
console.log("odunc:", db.prepare("SELECT count(*) AS n FROM odunc").get().n,
            " acik:", db.prepare("SELECT count(*) AS n FROM odunc WHERE iade_tarihi IS NULL").get().n);
```

```sh
node uret.mjs
```

```
odunc: 50000  acik: 10006
```

Veri sözde rastgele üretiliyor ama tohum sabit olduğu için her çalıştırmada aynı satırlar
oluşuyor. `ANALYZE` çağrısı, İleri SQL kursunda tanıtılan istatistikleri toplar; eniyileyici
seçiciliği bu istatistiklerden tahmin eder.

#### Planı Uygulamadan Okumak

Plan, veritabanı kabuğunda okunabildiği gibi uygulama bağlantısından da okunabilir. Aşağıdaki
sarmalayıcı her sorguyu sürer ve eşiği aşan sorguyu planıyla birlikte kütüğe koyar.

```js
// izleyen.mjs — her sorguyu sureler, esigi asani plani ile birlikte kutuge yazar
import { DatabaseSync } from "node:sqlite";

export function izleyenBaglanti(dosya, esikMs) {
  const db = new DatabaseSync(dosya);
  const kutuk = [];
  const imza = (sql) => sql.replace(/\s+/g, " ").trim();
  return {
    kutuk,
    sorgu(sql, ...d) {
      const t = performance.now();
      const satirlar = db.prepare(sql).all(...d);
      const sure = performance.now() - t;
      if (sure >= esikMs) {
        const plan = db.prepare(`EXPLAIN QUERY PLAN ${sql}`).all(...d).map((p) => p.detail);
        kutuk.push({ imza: imza(sql), sure, satir: satirlar.length, plan });
      }
      return satirlar;
    },
    calistir(sql) { db.exec(sql); },
    plan(sql, ...d) { return db.prepare(`EXPLAIN QUERY PLAN ${sql}`).all(...d).map((p) => p.detail); },
    sure(sql, ...d) {
      const t = performance.now();
      const n = db.prepare(sql).all(...d).length;
      return { sure: performance.now() - t, satir: n };
    },
  };
}
```

Tasarımda iki karar var. Plan yalnız eşiği aşan sorgu için alınır; her sorgu için alınsaydı
ölçüm işin kendisinden pahalı olurdu. Ve plan, sorgunun **çalıştığı bağlı değerlerle**
alınır; farklı değerler farklı plan üretebileceği için bu ayrıntı önemlidir.

#### Aynı Sorgu, İki Plan

```js
// plan-oku.mjs — ayni sorgu dizinsiz ve dizinli, plan ve sure ile
import { izleyenBaglanti } from "./izleyen.mjs";
const db = izleyenBaglanti("kutuphane.db", 0);
const SORGU = "SELECT odunc_id, alis_tarihi FROM odunc WHERE uye_id = ? AND iade_tarihi IS NULL";

const yaz = (etiket) => {
  const { sure, satir } = db.sure(SORGU, 17);
  console.log(`${etiket}`);
  for (const p of db.plan(SORGU, 17)) console.log(`  plan: ${p}`);
  console.log(`  satir=${satir}  sure=${sure.toFixed(2)} ms`);
};

yaz("dizin yokken");
db.calistir("CREATE INDEX odunc_uye_acik ON odunc (uye_id) WHERE iade_tarihi IS NULL");
yaz("kismi dizin eklendikten sonra");
db.calistir("DROP INDEX odunc_uye_acik");
```

```sh
node plan-oku.mjs
```

```
dizin yokken
  plan: SCAN odunc
  satir=180  sure=1.27 ms
kismi dizin eklendikten sonra
  plan: SEARCH odunc USING INDEX odunc_uye_acik (uye_id=?)
  satir=180  sure=0.07 ms
```

Sonuç aynı: 180 satır. Plan farklı. Dizinsiz durumda `SCAN` yazıyor; elli bin satırın tamamı
okunup koşul her satıra uygulanıyor. Dizinli durumda `SEARCH` yazıyor ve hangi dizinin,
hangi koşulla kullanıldığı belirtiliyor.

Plan metinlerinin biçimi motora göre değişir; buradaki `SCAN` ve `SEARCH` sözcükleri
SQLite'ın yazımıdır. Değişmeyen ayrım, planın bir bağıntının tamamını mı okuduğu yoksa
aradığı satırlara doğrudan mı gittiğidir. İlişkisel Veritabanı Yönetimi kursunda tanıtılan
**kısmi dizin** burada kullanıldı: dizin yalnız iade edilmemiş kayıtları kapsıyor, yani elli
bin satırın onda ikisi kadar yer tutuyor.

Süredeki fark bu ölçekte yaklaşık on sekiz kat. Bağıntı büyüdükçe fark büyür: tarama satır
sayısıyla doğrusal, dizin araması logaritmik artar.

#### Yazımın Planı Değiştirmesi

Dizin bulunması, kullanılacağı anlamına gelmez. Koşulun yazımı dizini devre dışı bırakabilir.

```js
// yazim-farki.mjs — ayni kosulun iki yazimi, ayni dizin altinda
import { izleyenBaglanti } from "./izleyen.mjs";
const db = izleyenBaglanti("kutuphane.db", 0);
db.calistir("CREATE INDEX IF NOT EXISTS odunc_alis ON odunc (alis_tarihi)");

const yazimlar = {
  "islev uygulanmis": "SELECT count(*) AS n FROM odunc WHERE substr(alis_tarihi,1,4) = '2024'",
  "aralik kosulu   ": "SELECT count(*) AS n FROM odunc WHERE alis_tarihi >= '2024-01-01' AND alis_tarihi < '2025-01-01'",
};

for (const [ad, sql] of Object.entries(yazimlar)) {
  const sonuc = db.sorgu(sql)[0].n;
  const { sure } = db.sure(sql);
  console.log(`${ad}  sonuc=${sonuc}  sure=${sure.toFixed(2)} ms`);
  for (const p of db.plan(sql)) console.log(`  plan: ${p}`);
}
db.calistir("DROP INDEX odunc_alis");
```

```sh
node yazim-farki.mjs
```

```
islev uygulanmis  sonuc=12574  sure=1.25 ms
  plan: SCAN odunc USING COVERING INDEX odunc_alis
aralik kosulu     sonuc=12574  sure=0.14 ms
  plan: SEARCH odunc USING COVERING INDEX odunc_alis (alis_tarihi>? AND alis_tarihi<?)
```

İki sorgu aynı sayıyı döndürdü, aynı dizini kullandı, ama biri dizini tarayarak, öteki
arayarak. Nedeni ilk yazımdaki işlev çağrısıdır: dizin `alis_tarihi` değerlerine göre
sıralıdır, `substr(alis_tarihi,1,4)` değerlerine göre değil. Eniyileyici, dizinin sırasından
yararlanamaz ve dizini baştan sona okur.

İkinci yazım aynı koşulu aralık olarak ifade ediyor. İleri SQL kursunda bu tür koşullara
**dizine uygun koşul** dendi. Kural sadedir: **dizinli sütun koşulun bir tarafında yalın
durmalıdır.** Sütuna uygulanan işlev, tür dönüşümü ya da hesaplama dizini devre dışı
bırakır.

Her iki planda geçen `COVERING INDEX` ifadesi ikinci bir bilgidir: sorgunun ihtiyaç duyduğu
bütün sütunlar dizinde bulunduğu için bağıntının kendisine hiç dokunulmamış. Bu, İlişkisel
Veritabanı Yönetimi kursunda **kapsayan dizin** olarak geçen durumdur.

#### Yavaş Sorgu Kütüğü

Tek tek sorgu incelemek geliştirme sırasında işe yarar; üretimde hangi sorgunun yavaş olduğu
önceden bilinmez. Bunun için eşiği aşan sorgular toplanır ve **sorgu imzasına** göre
gruplanır. İmza, bağlı değerlerden arındırılmış sorgu metnidir; aynı sorgunun farklı
değerlerle çalışan bin çağrısı tek bir satırda toplanır.

```js
// yavas-kutuk.mjs — karisik yuk calistirilir, esigi asan sorgular imzaya gore toplanir
import { izleyenBaglanti } from "./izleyen.mjs";
const db = izleyenBaglanti("kutuphane.db", 0.5);

for (let i = 0; i < 20; i++) {
  db.sorgu("SELECT odunc_id FROM odunc WHERE odunc_id = ?", (i * 37) % 50000 + 1);
  db.sorgu("SELECT odunc_id FROM odunc WHERE uye_id = ? AND iade_tarihi IS NULL", (i % 60) + 1);
  db.sorgu("SELECT count(*) AS n FROM odunc WHERE substr(alis_tarihi,1,4) = ?", String(2022 + (i % 4)));
}

const gruplar = new Map();
for (const k of db.kutuk) {
  const g = gruplar.get(k.imza) ?? { sayi: 0, toplam: 0, enUzun: 0, plan: k.plan };
  g.sayi += 1; g.toplam += k.sure; g.enUzun = Math.max(g.enUzun, k.sure);
  gruplar.set(k.imza, g);
}

console.log(`esigi asan sorgu sayisi: ${db.kutuk.length} / 60`);
for (const [imza, g] of [...gruplar].sort((a, b) => b[1].toplam - a[1].toplam)) {
  console.log(`\ntoplam=${g.toplam.toFixed(1)} ms  cagri=${g.sayi}  en_uzun=${g.enUzun.toFixed(2)} ms`);
  console.log(`  ${imza}`);
  for (const p of g.plan) console.log(`  plan: ${p}`);
}
```

```sh
node yavas-kutuk.mjs
```

```
esigi asan sorgu sayisi: 40 / 60

toplam=33.0 ms  cagri=20  en_uzun=1.93 ms
  SELECT count(*) AS n FROM odunc WHERE substr(alis_tarihi,1,4) = ?
  plan: SCAN odunc

toplam=17.8 ms  cagri=20  en_uzun=1.22 ms
  SELECT odunc_id FROM odunc WHERE uye_id = ? AND iade_tarihi IS NULL
  plan: SCAN odunc
```

Altmış sorgunun kırkı eşiği aştı; birincil anahtarla arayan yirmi sorgu hiç görünmedi.
Kütük iki imza üretti ve ikisini de toplam süreye göre sıraladı.

Sıralamanın toplam süreye göre yapılması bilinçlidir. En uzun süren tek çağrıya bakmak
yanıltır; bir sorgu iki milisaniye sürüp saniyede bin kez çağrılıyorsa, on milisaniye sürüp
dakikada bir çağrılandan daha çok yük üretir. İlişkisel Veritabanı Yönetimi kursunda bu
ayrım belirti ile neden arasındaki fark olarak geçmişti.

#### Planın Söylemedikleri

Plan bir tahmindir, ölçüm değildir. Üç sınırı akılda tutulur.

Plan **istatistiklere** dayanır. İstatistikler eskimişse eniyileyici seçiciliği yanlış
tahmin eder ve yanlış planı seçer; bağıntı büyüklüğü hızla değişen sistemlerde istatistiğin
tazelenmesi bakım işidir.

Plan **bağlı değere** göre değişebilir. Yaygın bir değer için tarama, seyrek bir değer için
dizin araması seçilebilir. Bu yüzden plan, sorgunun gerçekten çalıştığı değerlerle
alınmalıdır.

Plan **süreyi vermez**. Yukarıdaki ölçümlerin hepsinde plan ile süre birlikte yazdırıldı;
plan neden yavaş olduğunu, süre ne kadar yavaş olduğunu söyler. Bir tanı ancak ikisi
birlikte okunduğunda tamamlanır.

#### Özet

- Sorgu planı uygulama bağlantısından okunabilir; eşiği aşan sorgu için plan almak, her
  sorgu için almaktan ucuzdur.
- Aynı sorgu dizinsiz `SCAN`, kısmi dizinle `SEARCH` planı üretti ve süre 1,27 ms'den
  0,07 ms'ye indi.
- Aynı koşulun işlev uygulanmış yazımı dizini taradı, aralık yazımı aradı; dizinli sütun
  koşulun bir tarafında yalın durmalıdır.
- Yavaş sorgu kütüğü, bağlı değerlerden arındırılmış sorgu imzasına göre gruplanır ve toplam
  süreye göre sıralanır; en uzun tek çağrı değil, en çok yük üreten imza aranır.
- Plan istatistiklere ve bağlı değere bağlı bir tahmindir; süreyi vermez, tanı plan ile süre
  birlikte okunarak tamamlanır.

#### Sonraki Adım

Bu dersteki sorguların hepsi sonuç kümesinin tamamını istedi. Uygulamalar çoğu listeyi
parça parça gösterir ve SQL Temelleri kursunda tanıtılan atlama–sınırlama çifti bunun en
bilinen yoludur. Yazımı kısa, planı sade, ilk sayfalarda hızlıdır. Derin sayfalarda ise
maliyeti atlanan satır sayısıyla birlikte büyür ve bu büyüme sorgu metnine bakarak
görülmez. Sonraki ders aynı listeyi artan atlama değerleriyle çekerek maliyeti ölçer,
anahtar tabanlı yaklaşımla karşılaştırır ve iki yaklaşımın hangi arayüz kısıtını getirdiğini
ele alır.

### Sayfalama Maliyeti

Source: https://academia.sh/tr/kurslar/veri-erisim-katmani/sayfalama-maliyeti

Derin sayfalamanın iki sorunu: atlama değeri büyüdükçe artan süre, anahtar tabanlı sayfalamanın sabit maliyeti, sonuç kümesi değiştiğinde atlanan kayıtlar ve iki yaklaşımın arayüze getirdiği kısıtlar.

Önceki dersteki sorguların hepsi sonuç kümesinin tamamını istedi. Uygulamalar çoğu listeyi
parça parça gösterir; SQL Temelleri kursunda tanıtılan atlama–sınırlama çifti bunun en
bilinen yoludur.

Yazımı kısa, planı sade, ilk sayfalarda hızlıdır. Derin sayfalarda maliyeti atlanan satır
sayısıyla birlikte büyür ve bu büyüme sorgu metnine bakılarak görülmez. Bu ders maliyeti
ölçer, alternatifi kurar ve iki yaklaşımın arayüze getirdiği farkı ele alır.

#### Ölçüm Verisi

Ölçüm için önceki dersteki elli bin kayıtlık veri kümesi kullanılır.

```js
// uret.mjs — 50000 odunc kaydi
import { DatabaseSync } from "node:sqlite";
import { rmSync } from "node:fs";
rmSync("kutuphane.db", { force: true });

const db = new DatabaseSync("kutuphane.db");
db.exec(`
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL);
CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL);
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL,
                    uye_id INTEGER NOT NULL, alis_tarihi TEXT NOT NULL, iade_tarihi TEXT);`);
db.exec("BEGIN");
const u = db.prepare("INSERT INTO uye VALUES (?,?)");
for (let i = 1; i <= 60; i++) u.run(i, `Uye${i}`);
const k = db.prepare("INSERT INTO kitap VALUES (?,?)");
for (let i = 1; i <= 200; i++) k.run(i, `Kitap ${i}`);
const o = db.prepare("INSERT INTO odunc VALUES (?,?,?,?,?)");
let tohum = 20250727;                       // belirlenimci sozde rastgele dizi
const sonraki = (n) => { tohum = (tohum * 48271) % 2147483647; return tohum % n; };
for (let i = 1; i <= 50000; i++) {
  const yil = 2022 + sonraki(4);
  const ay = String(sonraki(12) + 1).padStart(2, "0");
  const gun = String(sonraki(28) + 1).padStart(2, "0");
  o.run(i, sonraki(200) + 1, sonraki(60) + 1, `${yil}-${ay}-${gun}`,
        sonraki(5) === 0 ? null : `${yil}-12-31`);
}
db.exec("COMMIT");
db.exec("ANALYZE");
console.log("odunc:", db.prepare("SELECT count(*) AS n FROM odunc").get().n,
            " acik:", db.prepare("SELECT count(*) AS n FROM odunc WHERE iade_tarihi IS NULL").get().n);
```

```sh
node uret.mjs
```

```
odunc: 50000  acik: 10006
```

#### Atlamanın Bedeli

İki sorgu aynı sayfayı üretir. Birincisi kaç satır atlanacağını söyler; ikincisi bir önceki
sayfanın son anahtarından devam eder.

```js
// sayfalama.mjs — ayni sayfa boyutu, artan atlama degerleri ile anahtar tabanli yaklasim
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("kutuphane.db");
const SAYFA = 20;

const ATLAMALI = `SELECT odunc_id, alis_tarihi FROM odunc
                  WHERE iade_tarihi IS NOT NULL ORDER BY odunc_id LIMIT ? OFFSET ?`;
const ANAHTARLI = `SELECT odunc_id, alis_tarihi FROM odunc
                   WHERE iade_tarihi IS NOT NULL AND odunc_id > ? ORDER BY odunc_id LIMIT ?`;

const olc = (calistir, tekrar = 20) => {
  calistir();
  const t = performance.now();
  for (let i = 0; i < tekrar; i++) calistir();
  return (performance.now() - t) / tekrar;
};

console.log("atlama    atlamali_ms   anahtarli_ms   oran");
for (const atlama of [0, 1000, 10000, 30000, 39000]) {
  // Anahtarli yaklasim icin ayni sayfanin baslangic anahtari bulunur.
  const sinir = db.prepare(
    `SELECT odunc_id FROM odunc WHERE iade_tarihi IS NOT NULL
     ORDER BY odunc_id LIMIT 1 OFFSET ?`).get(Math.max(0, atlama - 1));
  const oncekiAnahtar = atlama === 0 ? 0 : sinir.odunc_id;

  const a = olc(() => db.prepare(ATLAMALI).all(SAYFA, atlama));
  const b = olc(() => db.prepare(ANAHTARLI).all(oncekiAnahtar, SAYFA));
  console.log(`${String(atlama).padStart(6)}  ${a.toFixed(3).padStart(10)}  ${b.toFixed(3).padStart(12)}  ` +
    `${(a / b).toFixed(1).padStart(6)}x`);
}
```

```sh
node sayfalama.mjs
```

```
atlama    atlamali_ms   anahtarli_ms   oran
     0       0.012         0.013     1.0x
  1000       0.030         0.012     2.4x
 10000       0.203         0.013    16.2x
 30000       0.616         0.011    57.1x
 39000       0.793         0.011    70.7x
```

İlk sayfada iki yaklaşım eşit. Atlama büyüdükçe atlamalı sorgunun süresi doğrusal artıyor:
10000'de yaklaşık on altı, 39000'de yetmiş kat. Anahtar tabanlı sorgunun süresi
değişmiyor.

Nedeni sorgu planında değil, planın anlamındadır. `OFFSET` bir "atla" komutu değildir;
veritabanı atlanan satırları **üretmek zorundadır**, sonra atar. Otuz bininci sayfayı almak
için otuz bin satır okunur ve çöpe atılır. Anahtar tabanlı sorguda ise koşulun kendisi
dizinde bir başlangıç noktası verir; okunan satır sayısı sayfa boyutu kadardır.

Bu, önceki dersin ölçütüne uyuyor: iki sorgu da dizin kullanır, ikisinin planı da
"arama" der. Fark, planın kaç satır ürettiğindedir.

#### Gezinti Toplamı

Tek bir derin sayfa milisaniyeler alıyor; asıl fark bütün liste taranırken görünür. Dışa
aktarım ve toplu işler bunu sürekli yapar.

```js
// tam-gezinti.mjs — butun sayfalarin bastan sona gezilmesinin toplam maliyeti
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("kutuphane.db");
const SAYFA = 100;

function atlamaliGezinti() {
  const deyim = db.prepare(`SELECT odunc_id FROM odunc WHERE iade_tarihi IS NOT NULL
                            ORDER BY odunc_id LIMIT ? OFFSET ?`);
  let atlama = 0, toplam = 0, sayfa = 0;
  for (;;) {
    const satirlar = deyim.all(SAYFA, atlama);
    if (satirlar.length === 0) break;
    toplam += satirlar.length; atlama += SAYFA; sayfa += 1;
  }
  return { toplam, sayfa };
}

function anahtarliGezinti() {
  const deyim = db.prepare(`SELECT odunc_id FROM odunc WHERE iade_tarihi IS NOT NULL
                            AND odunc_id > ? ORDER BY odunc_id LIMIT ?`);
  let son = 0, toplam = 0, sayfa = 0;
  for (;;) {
    const satirlar = deyim.all(son, SAYFA);
    if (satirlar.length === 0) break;
    son = satirlar[satirlar.length - 1].odunc_id;
    toplam += satirlar.length; sayfa += 1;
  }
  return { toplam, sayfa };
}

for (const [ad, gez] of [["atlamali", atlamaliGezinti], ["anahtarli", anahtarliGezinti]]) {
  const t = performance.now();
  const { toplam, sayfa } = gez();
  console.log(`${ad.padEnd(10)} sayfa=${sayfa}  satir=${toplam}  sure=${(performance.now() - t).toFixed(0)} ms`);
}
```

```sh
node tam-gezinti.mjs
```

```
atlamali   sayfa=400  satir=39994  sure=170 ms
anahtarli  sayfa=400  satir=39994  sure=7 ms
```

Aynı dört yüz sayfa, aynı otuz dokuz bin satır, yirmi dört kat süre farkı. Atlamalı
gezintide okunan toplam satır sayısı sayfa sayısının karesiyle orantılıdır; her sayfa
kendinden öncekilerin tamamını yeniden üretir. Anahtar tabanlı gezintide her satır bir kez
okunur.

#### Kayan Sonuç Kümesi

Maliyetin yanında bir doğruluk sorunu vardır. Sayfalar arasında sonuç kümesi değişirse,
atlama değeri artık aynı satırı göstermez.

```js
// kayan-sayfa.mjs — sayfalar arasinda kume degisince atlamali sayfalama kayit atlar
import { DatabaseSync } from "node:sqlite";
import { rmSync } from "node:fs";
rmSync("kucuk.db", { force: true });

const db = new DatabaseSync("kucuk.db");
db.exec(`CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, iade_tarihi TEXT);
         INSERT INTO odunc (odunc_id, iade_tarihi)
         VALUES (1,NULL),(2,NULL),(3,NULL),(4,NULL),(5,NULL),
                (6,NULL),(7,NULL),(8,NULL),(9,NULL),(10,NULL);`);

const atlamali = (atlama) => db.prepare(
  "SELECT odunc_id FROM odunc WHERE iade_tarihi IS NULL ORDER BY odunc_id LIMIT 5 OFFSET ?")
  .all(atlama).map((r) => r.odunc_id);
const anahtarli = (sonAnahtar) => db.prepare(
  "SELECT odunc_id FROM odunc WHERE iade_tarihi IS NULL AND odunc_id > ? ORDER BY odunc_id LIMIT 5")
  .all(sonAnahtar).map((r) => r.odunc_id);

const s1 = atlamali(0);
db.prepare("UPDATE odunc SET iade_tarihi = '2025-07-20' WHERE odunc_id = 2").run();  // kume kucululdu
const s2 = atlamali(5);
console.log("atlamali  sayfa1:", s1.join(","), " sayfa2:", s2.join(","));
console.log("  goruntulenen:", [...s1, ...s2].join(","));

db.prepare("UPDATE odunc SET iade_tarihi = NULL WHERE odunc_id = 2").run();          // durum geri alinir
const a1 = anahtarli(0);
db.prepare("UPDATE odunc SET iade_tarihi = '2025-07-20' WHERE odunc_id = 2").run();
const a2 = anahtarli(a1[a1.length - 1]);
console.log("anahtarli sayfa1:", a1.join(","), " sayfa2:", a2.join(","));
console.log("  goruntulenen:", [...a1, ...a2].join(","));
```

```sh
node kayan-sayfa.mjs
```

```
atlamali  sayfa1: 1,2,3,4,5  sayfa2: 7,8,9,10
  goruntulenen: 1,2,3,4,5,6 numarali kayit atlandi
anahtarli sayfa1: 1,2,3,4,5  sayfa2: 6,7,8,9,10
  goruntulenen: 1,2,3,4,5,6,7,8,9,10
```

İki numaralı kayıt açık listeden çıkınca küme dokuza indi; ikinci sayfanın başlangıcı bir
sıra kaydı ve altı numaralı kayıt hiç görünmedi. Anahtar tabanlı yaklaşımda ikinci sayfa
"beşten büyük olanlar" diye soruldu; kümenin küçülmesi bu koşulu etkilemedi.

Ters yön de mümkündür: küme büyürse aynı kayıt iki sayfada birden görünür. İkisi de sessiz
hatadır; kullanıcı eksik ya da yinelenen bir liste görür, hiçbir uyarı üretilmez. Yalıtım
düzeyleri dersindeki gözlem burada tekrarlanıyor: ayrı ayrı doğru olan iki sorgu birlikte
tutarsız olabilir.

#### İki Yaklaşımın Kısıtları

Anahtar tabanlı sayfalama bedelsiz değildir; arayüze iki kısıt getirir.

**Sıra atlanamaz.** "Yirmi yedinci sayfaya git" isteği karşılanamaz, çünkü o sayfanın
başlangıç anahtarı bilinmiyor. Yalnız "sonraki" ve — sıralama tersine çevrilerek — "önceki"
verilebilir. API Tasarımı kursunda tanıtılan imleç tabanlı sayfalama bu kısıta dayanır;
istemciye sayfa numarası yerine bir imleç verilir.

**Sıralama belirlenimci olmalıdır.** Sıralama anahtarı benzersiz değilse eşit değerli
satırlar arasındaki sıra tanımsızdır ve sayfa sınırında kayıt atlanır ya da yinelenir. Bu
yüzden sıralama anahtarına benzersiz bir sütun eklenir; alış tarihine göre sıralanan bir
listede koşul "tarih daha büyük, ya da tarih eşit ve kimlik daha büyük" biçimini alır.

Atlamalı sayfalamanın da yeri vardır. Sayfa sayısı küçük kalan listeler, yönetim ekranları
ve toplam sayfa sayısının gösterilmesi gereken arayüzler için yeterlidir. Ölçüt basittir:
**atlama değerinin üst sınırı biliniyorsa** atlamalı sayfalama kullanılabilir; liste
sınırsız büyüyebiliyorsa anahtar tabanlı yaklaşım seçilir.

Üçüncü bir ayrıntı toplam sayı ile ilgilidir. Arayüzde "1245 kayıt" yazmak için ayrı bir
sayma sorgusu çalışır ve bu sorgu bütün kümeyi üretir; sayfalama ne kadar ucuzlarsa
ucuzlasın, sayma sorgusu maliyeti geri getirir. Bu yüzden büyük listelerde kesin toplam
yerine "daha fazlası var" bilgisi verilir; bu bilgi, sayfa boyutundan bir fazla satır
istenerek elde edilir.

#### Özet

- Atlamalı sayfalamada süre atlama değeriyle doğrusal arttı: 10000'de 16, 39000'de 70 kat.
  Anahtar tabanlı sorgunun süresi değişmedi.
- `OFFSET` atlanan satırları üretmemeyi sağlamaz; veritabanı onları okur ve atar.
- Dört yüz sayfanın tamamı gezildiğinde atlamalı yaklaşım 170 ms, anahtar tabanlı yaklaşım
  7 ms sürdü; atlamalı gezintinin maliyeti sayfa sayısının karesiyle orantılıdır.
- Sayfalar arasında sonuç kümesi küçülünce atlamalı sayfalama bir kaydı atladı; anahtar
  tabanlı sayfalama etkilenmedi.
- Anahtar tabanlı sayfalama sıra atlamaya izin vermez ve belirlenimci sıralama gerektirir;
  atlamalı sayfalama, atlama değerinin üst sınırı bilinen listeler için yeterlidir.

#### Sonraki Adım

Başarım Sorunları konusu veri erişim katmanının ölçülebilir yanını kapattı: sorgu sayısı,
aktarılan bayt, deyim sayısı, sorgu planı ve sayfalama maliyeti. Bu ölçütlerin hepsi
katmanın nasıl çalıştığıyla ilgiliydi; hiçbiri katmanın **ne yapması gerektiğini**
söylemiyor. Ödünç verme kuralı nerede duruyor, doğrulama hangi katmanda yapılıyor, dışarıya
dönen kayıt ile alan modeli aynı şey mi? Sonraki konu bu soruları ele alır ve ilk ders
sunum, uygulama, alan ve altyapı katmanlarının sorumluluklarını ayırıp bağımlılık yönünü
ithal grafiği üzerinden denetler.

## İş Mantığı Yerleşimi

Sunum, uygulama, alan ve altyapı katmanlarının sorumluluk ayrımı, dış sözleşmeyi iç modelden ayıran veri aktarım nesneleri, girdi doğrulama ile alan kuralı arasındaki sınır, yan etkileri gevşek bağlayan alan olayları ve kalıcılıktan bağımsız alan modeli.

### Katman Sorumlulukları

Source: https://academia.sh/tr/kurslar/veri-erisim-katmani/katman-sorumluluklari

Sunum, uygulama, alan ve altyapı katmanlarının ayrımı: bağımlılık yönü kuralının ithal grafiği üzerinden denetlenmesi, ihlalli sürümdeki bulguların sayılması ve sözleşmeyi alan katmanına taşıyarak bağımlılığın tersine çevrilmesi.

Başarım Sorunları konusu veri erişim katmanının nasıl çalıştığını ölçtü: sorgu sayısı,
aktarılan bayt, deyim sayısı, sorgu planı, sayfalama maliyeti. Hiçbiri katmanın ne yapması
gerektiğini söylemedi. Ödünç verme kuralı nerede duruyor, doğrulama hangi katmanda
yapılıyor, dışarıya dönen kayıt ile alan modeli aynı şey mi?

Bu konu o soruları ele alıyor ve ilki en temeldeki soru: kod hangi parçalara ayrılır ve bu
parçalar birbirini hangi yönde çağırabilir? Ayrımın kâğıt üzerinde iddia edilmesi yeterli
değildir; bir kural olarak yazılıp **ithal grafiği** (import graph) üzerinden denetlenmesi
gerekir.

#### Dört Katman

Yerleşim dört sorumluluğa ayrılır. Adları değişse de ayrımın gerekçesi aynıdır: her katman
farklı bir nedenle değişir.

| Katman | Sorumluluk | Değişme nedeni |
|---|---|---|
| Sunum | Dış dünyanın biçimi: yol, gövde çözümleme, durum kodu | Arayüz ya da protokol değişince |
| Uygulama | İş akışının sırası: kimi çağır, işlemi nerede aç, sonucu kime ver | Kullanım senaryosu değişince |
| Alan | İş kuralları: neyin geçerli, neyin yasak olduğu | Kurum kuralı değişince |
| Altyapı | Dış kaynaklara erişim: veritabanı, dosya, ağ | Araç ya da şema değişince |

Katman sayısı kadar önemli olan, aralarındaki **bağımlılık yönüdür**. Kural tek cümleyle
yazılabilir: alan katmanı hiçbir katmana bağımlı olamaz. Kalan yönler bu kuraldan türer.

```
sunum  →  uygulama  →  alan  ←  altyapı
```

Altyapının oku ters yöne bakıyor. Veritabanı erişimi alan katmanını çağırmaz; alan
katmanının tanımladığı bir sözleşmeyi karşılar. Bu ters yön dersin asıl konusu.

#### İhlalli Sürüm

Aşağıdaki dört modül ödünç verme isteğini karşılar. Dizin adları katmanı verir; kod önce
akla gelen sırayla, doğrudan çağrılarla yazılmıştır.

```sh
mkdir -p ihlalli/sunum ihlalli/uygulama ihlalli/alan ihlalli/altyapi
```

```js
// ihlalli/alan/odunc-kurallari.mjs — is kurallari, ama iki alt katmani dogrudan cagiriyor
import { SqliteOduncDeposu } from "../altyapi/sqlite-odunc-deposu.mjs";
import { yanit } from "../sunum/yanit.mjs";

export const UYE_SINIRI = 3;

export function oduncVer(db, uyeId, kitapId, tarih) {
  const depo = new SqliteOduncDeposu(db);
  if (depo.kitabinAcikKaydi(kitapId) !== null) return yanit(409, "kitap zaten odunc verilmis");
  if (depo.uyeninAcikKayitlari(uyeId).length >= UYE_SINIRI) {
    return yanit(409, `uye siniri asildi (${UYE_SINIRI})`);
  }
  return yanit(201, String(depo.ekle({ kitapId, uyeId, alisTarihi: tarih })));
}
```

```js
// ihlalli/altyapi/sqlite-odunc-deposu.mjs — SQL ve surucu burada
import { DatabaseSync } from "node:sqlite";

export const baglan = (dosya) => new DatabaseSync(dosya);

export class SqliteOduncDeposu {
  constructor(db) { this.db = db; }
  kitabinAcikKaydi(kitapId) {
    return this.db.prepare(
      "SELECT odunc_id FROM odunc WHERE kitap_id = ? AND iade_tarihi IS NULL").get(kitapId) ?? null;
  }
  uyeninAcikKayitlari(uyeId) {
    return this.db.prepare(
      "SELECT odunc_id FROM odunc WHERE uye_id = ? AND iade_tarihi IS NULL").all(uyeId);
  }
  ekle(k) {
    return Number(this.db.prepare(
      "INSERT INTO odunc (kitap_id, uye_id, alis_tarihi, iade_tarihi) VALUES (?,?,?,NULL)")
      .run(k.kitapId, k.uyeId, k.alisTarihi).lastInsertRowid);
  }
}
```

```js
// ihlalli/sunum/yanit.mjs — HTTP yaniti bicimleyen sunum yardimcisi
export const yanit = (durum, govde) => ({ durum, govde });
```

```js
// ihlalli/uygulama/odunc-uygulama.mjs — istegi karsilayan uygulama servisi
import { oduncVer } from "../alan/odunc-kurallari.mjs";
import { baglan } from "../altyapi/sqlite-odunc-deposu.mjs";

export function oduncIstegi(istek) {
  const db = baglan("kutuphane.db");
  return oduncVer(db, istek.uyeId, istek.kitapId, istek.tarih);
}
```

```js
// ihlalli/sunum/http-ucu.mjs — govdeyi cozup uygulama servisini cagirir
import { oduncIstegi } from "../uygulama/odunc-uygulama.mjs";

export const POST_odunc = (govde) => oduncIstegi(JSON.parse(govde));
```

Kod çalışır. Sorun davranışta değil, bağımlılık yönünde: iş kuralı hem SQL sınıfını hem
HTTP biçimleyicisini ithal ediyor.

#### Kuralın Denetlenmesi

Bağımlılık yönü bir belge maddesi olarak kaldığı sürece ihlal edilir. Denetlenebilmesi için
ölçülebilir olması gerekir. Aşağıdaki betik dizin ağacındaki bütün modülleri okur, ithal
deyimlerini çıkarır, her modülü dizinine göre bir katmana yerleştirir ve izin verilmeyen
her kenarı bir bulgu olarak bildirir.

```js
// katman-denetle.mjs — ithal grafigini cikarir ve bagimlilik yonunu denetler
import { readdirSync, readFileSync, statSync } from "node:fs";
import { join, relative, dirname, resolve } from "node:path";

// Her katmanin ithal edebilecegi katmanlar. Liste disindaki her kenar bir ihlaldir.
const IZIN = {
  sunum: ["sunum", "uygulama"],
  uygulama: ["uygulama", "alan"],
  altyapi: ["altyapi", "alan", "standart"],
  alan: ["alan", "standart"],
  kok: ["kok", "sunum", "uygulama", "alan", "altyapi", "standart"],
};

const katman = (yol) => yol.split("/")[0] in IZIN ? yol.split("/")[0] : "kok";
const disKatman = (ad) => (ad === "node:sqlite" ? "altyapi" : "standart");

function dosyalar(kok, dizin = kok) {
  return readdirSync(dizin).flatMap((ad) => {
    const tam = join(dizin, ad);
    if (statSync(tam).isDirectory()) return dosyalar(kok, tam);
    return ad.endsWith(".mjs") ? [relative(kok, tam)] : [];
  });
}

const ITHAL = /^\s*(?:import|export)[^;'"]*from\s+["']([^"']+)["']/gm;

function ithaller(kok, dosya) {
  const metin = readFileSync(join(kok, dosya), "utf8");
  return [...metin.matchAll(ITHAL)].map((m) => m[1]).map((h) =>
    h.startsWith(".") ? relative(kok, resolve(kok, dirname(dosya), h)) : h);
}

export function denetle(kok) {
  const bulgular = [];
  for (const d of dosyalar(kok)) {
    for (const h of ithaller(kok, d)) {
      const kaynak = katman(d);
      const hedef = h.startsWith("node:") ? disKatman(h) : katman(h);
      if (!IZIN[kaynak].includes(hedef)) bulgular.push({ d, h, kaynak, hedef });
    }
  }
  return bulgular;
}

// Bir modulden baslayarak ulasilan butun modullerin kumesi.
export function kapanis(kok, giris) {
  const gorulen = new Set();
  const yigit = [giris];
  while (yigit.length > 0) {
    const su = yigit.pop();
    if (gorulen.has(su) || su.startsWith("node:")) { gorulen.add(su); continue; }
    gorulen.add(su);
    for (const h of ithaller(kok, su)) yigit.push(h);
  }
  return gorulen;
}

const kok = process.argv[2];
const bulgular = denetle(kok);
for (const b of bulgular) console.log(`ihlal: ${b.d} -> ${b.h}   (${b.kaynak} -> ${b.hedef})`);
console.log(`bulgu sayisi = ${bulgular.length}`);

const k = kapanis(kok, "alan/odunc-kurallari.mjs");
console.log(`alan kapanisi = ${k.size} modul, node:sqlite iceride = ${k.has("node:sqlite") ? "evet" : "hayir"}`);
```

Sürücü ithali de bir katman sayılır: `node:sqlite` altyapı, kalan standart kitaplık
modülleri her yerden ithal edilebilen "standart" olarak işaretlenir. Ağacın kökündeki
dosyalar hiçbir katmana ait değildir; bunlar **bileşim kökü** (composition root) sayılır ve
her katmanı ithal edebilir.

```sh
node katman-denetle.mjs ihlalli
```

```
ihlal: alan/odunc-kurallari.mjs -> altyapi/sqlite-odunc-deposu.mjs   (alan -> altyapi)
ihlal: alan/odunc-kurallari.mjs -> sunum/yanit.mjs   (alan -> sunum)
ihlal: uygulama/odunc-uygulama.mjs -> altyapi/sqlite-odunc-deposu.mjs   (uygulama -> altyapi)
bulgu sayisi = 3
alan kapanisi = 4 modul, node:sqlite iceride = evet
```

Üç bulgu ve bir ölçü. Son satır ihlallerin sonucunu gösteriyor: iş kuralını içeren modüle
dokunmak için dört modülün tamamı ve veritabanı sürücüsü gerekiyor.

#### Bağımlılığın Tersine Çevrilmesi

Bulguların ikisi aynı nedene dayanıyor: alan katmanı ihtiyaç duyduğu şeyi **kendisi
kurmaya** çalışıyor. Çözüm, ihtiyacın alan katmanında **sözleşme** olarak yazılması,
gerçekleştirimin altyapıda kalmasıdır. Depo Kalıbı dersinde kurulan depo arayüzü bunun bir
örneğiydi; burada sözleşme açıkça bir modüle taşınıyor.

```sh
mkdir -p duzeltilmis/sunum duzeltilmis/uygulama duzeltilmis/alan duzeltilmis/altyapi
```

```js
// duzeltilmis/alan/odunc-deposu.mjs — alan katmaninin ihtiyac duydugu depo sozlesmesi
export const DEPO_SOZLESMESI = ["kitabinAcikKaydi", "uyeninAcikKayitlari", "ekle"];

export function depoyuDogrula(depo) {
  const eksik = DEPO_SOZLESMESI.filter((ad) => typeof depo?.[ad] !== "function");
  if (eksik.length > 0) throw new TypeError(`depo sozlesmesi eksik: ${eksik.join(", ")}`);
  return depo;
}
```

```js
// duzeltilmis/alan/odunc-kurallari.mjs — yalniz is kurallari; HTTP yok, SQL yok
import { depoyuDogrula } from "./odunc-deposu.mjs";

export const UYE_SINIRI = 3;

export function oduncVer(depo, uyeId, kitapId, tarih) {
  depoyuDogrula(depo);
  if (depo.kitabinAcikKaydi(kitapId) !== null) {
    return { sonuc: "reddedildi", sebep: "kitap_oduncte" };
  }
  if (depo.uyeninAcikKayitlari(uyeId).length >= UYE_SINIRI) {
    return { sonuc: "reddedildi", sebep: "uye_siniri", sinir: UYE_SINIRI };
  }
  return { sonuc: "verildi", oduncId: depo.ekle({ kitapId, uyeId, alisTarihi: tarih }) };
}
```

Dönüş değeri de değişti. İhlalli sürüm HTTP durum kodu üretiyordu; bu sürüm alan diliyle
konuşuyor. Durum kodu eşlemesi sunum katmanının işidir.

```js
// duzeltilmis/altyapi/sqlite-odunc-deposu.mjs — sozlesmenin SQL ile gerceklestirimi
import { DatabaseSync } from "node:sqlite";
import { depoyuDogrula } from "../alan/odunc-deposu.mjs";

export const baglan = (dosya) => new DatabaseSync(dosya);

export class SqliteOduncDeposu {
  constructor(db) { this.db = db; depoyuDogrula(this); }
  kitabinAcikKaydi(kitapId) {
    return this.db.prepare(
      "SELECT odunc_id FROM odunc WHERE kitap_id = ? AND iade_tarihi IS NULL").get(kitapId) ?? null;
  }
  uyeninAcikKayitlari(uyeId) {
    return this.db.prepare(
      "SELECT odunc_id FROM odunc WHERE uye_id = ? AND iade_tarihi IS NULL").all(uyeId);
  }
  ekle(k) {
    return Number(this.db.prepare(
      "INSERT INTO odunc (kitap_id, uye_id, alis_tarihi, iade_tarihi) VALUES (?,?,?,NULL)")
      .run(k.kitapId, k.uyeId, k.alisTarihi).lastInsertRowid);
  }
}
```

Altyapı modülü artık alan katmanını ithal ediyor. Ok tersine döndü ve bu, izinli yöndür.

```js
// duzeltilmis/uygulama/odunc-uygulama.mjs — depoyu disaridan alir, kurali cagirir
import { oduncVer } from "../alan/odunc-kurallari.mjs";

export class OduncUygulamasi {
  constructor(depo) { this.depo = depo; }
  ver(istek) { return oduncVer(this.depo, istek.uyeId, istek.kitapId, istek.tarih); }
}
```

```js
// duzeltilmis/sunum/http-ucu.mjs — alan sonucunu HTTP yanitina cevirir
const DURUM = { verildi: 201, kitap_oduncte: 409, uye_siniri: 409 };

export function POST_odunc(uygulama, govde) {
  const s = uygulama.ver(JSON.parse(govde));
  return s.sonuc === "verildi"
    ? { durum: DURUM.verildi, govde: { oduncId: s.oduncId } }
    : { durum: DURUM[s.sebep], govde: { sebep: s.sebep } };
}
```

Hiçbir katman kendi bağımlılığını kurmuyor. Bağlantıyı, depoyu ve uygulama servisini bir
araya getiren tek yer bileşim köküdür.

```js
// duzeltilmis/kur.mjs — bilesim koku: katmanlari yalniz burasi birbirine baglar
import { baglan, SqliteOduncDeposu } from "./altyapi/sqlite-odunc-deposu.mjs";
import { OduncUygulamasi } from "./uygulama/odunc-uygulama.mjs";
import { POST_odunc } from "./sunum/http-ucu.mjs";

const uygulama = new OduncUygulamasi(new SqliteOduncDeposu(baglan(process.argv[2])));
for (const govde of ['{"uyeId":6,"kitapId":1,"tarih":"2025-07-15"}',
                     '{"uyeId":6,"kitapId":7,"tarih":"2025-07-15"}']) {
  console.log(govde, "->", JSON.stringify(POST_odunc(uygulama, govde)));
}
```

Denetim yeniden çalıştırılır.

```sh
node katman-denetle.mjs duzeltilmis
```

```
bulgu sayisi = 0
alan kapanisi = 2 modul, node:sqlite iceride = hayir
```

Bulgu sıfır. İkinci satır ilkinden daha çok şey söylüyor: iş kuralına ulaşmak için gereken
modül sayısı dörtten ikiye indi ve veritabanı sürücüsü kapanışın dışında kaldı.

Bu sürüm gerçek veriyle de çalışır. Aşağıdaki komut SQL Temelleri kursundaki kütüphane
şemasının ödünç bağıntısını kurar ve bileşim kökünü çalıştırır.

```sh
rm -f kutuphane.db
sqlite3 kutuphane.db <<'SQL'
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL,
                    uye_id INTEGER NOT NULL, alis_tarihi TEXT NOT NULL, iade_tarihi TEXT);
INSERT INTO odunc VALUES (1,1,1,'2025-01-10','2025-01-24'),(2,2,1,'2025-02-02','2025-02-20'),
  (3,1,2,'2025-02-11',NULL),(4,3,3,'2025-03-01','2025-03-15'),(5,4,3,'2025-03-18','2025-04-02'),
  (6,1,4,'2025-04-05','2025-04-19'),(7,5,4,'2025-04-21',NULL),(8,2,5,'2025-05-02','2025-05-30'),
  (9,7,1,'2025-05-14','2025-05-28'),(10,3,5,'2025-06-03',NULL),(11,6,2,'2025-06-11','2025-06-25'),
  (12,4,4,'2025-06-20','2025-07-04');
SQL
node duzeltilmis/kur.mjs kutuphane.db
```

```
{"uyeId":6,"kitapId":1,"tarih":"2025-07-15"} -> {"durum":409,"govde":{"sebep":"kitap_oduncte"}}
{"uyeId":6,"kitapId":7,"tarih":"2025-07-15"} -> {"durum":201,"govde":{"oduncId":13}}
```

#### Kapanışın Pratik Karşılığı

Modül sayısı soyut bir ölçü gibi görünür. Karşılığı somuttur: kapanış küçüldükçe iş kuralı
başka bir şey vermeden çağrılabilir hâle gelir. İki sürüme de sözleşmeyi karşılayan sahte
bir nesne verildiğinde fark görünür.

```js
// sahteyle-dene.mjs — ayni is kurali, veritabani olmadan, sahte bir depoyla
import { oduncVer as ihlalli } from "./ihlalli/alan/odunc-kurallari.mjs";
import { oduncVer as duzeltilmis } from "./duzeltilmis/alan/odunc-kurallari.mjs";

const sahteDepo = {
  kitabinAcikKaydi: () => null,
  uyeninAcikKayitlari: () => [],
  ekle: () => 42,
};

for (const [ad, kural] of [["ihlalli", ihlalli], ["duzeltilmis", duzeltilmis]]) {
  try {
    console.log(`${ad.padEnd(12)} ->`, JSON.stringify(kural(sahteDepo, 6, 7, "2025-07-15")));
  } catch (e) {
    console.log(`${ad.padEnd(12)} -> ${e.constructor.name}: ${e.message}`);
  }
}
```

```sh
node sahteyle-dene.mjs
```

```
ihlalli      -> TypeError: this.db.prepare is not a function
duzeltilmis  -> {"sonuc":"verildi","oduncId":42}
```

İhlalli sürüm sahte depoyu kabul etmiyor, çünkü verilen nesneyi bir bağlantı sanıp kendi
SQL sınıfını kuruyor. Düzeltilmiş sürüm sözleşmeye baktığı için sahteyi kabul ediyor.
Bağımlılık yönü kuralının sınanabilirlikle ilişkisi budur: kural bir üslup tercihi değil,
alan katmanının bağımsız çalışabilmesinin ön koşuludur.

#### Uygulama ile Alan Arasındaki Sınır

Dört katman içinde en çok karışan ikisi uygulama ile alandır. Ayrım şu soruyla yapılır:
kural, kurumun kuralı mı yoksa bu senaryonun akışı mı?

"Ödünçteki kitap yeniden verilemez" kurumun kuralıdır; kütüphane bunu ödünç verme
ekranında da, toplu içe aktarımda da uygular. Alan katmanına aittir. "Önce üyeyi bul, sonra
kuralı çalıştır, sonra işlemi kesinleştir, sonra bildirim gönder" ise bu senaryonun
sırasıdır; başka bir senaryoda sıra değişir. Uygulama katmanına aittir.

İşlem sınırı da uygulama katmanının kararıdır. İşlem Sınırları dersinde iş biriminin
sınırının servis katmanında tutulması gerektiği gösterilmişti; bu ders o kararı katman
adıyla yerleştiriyor. Alan katmanı `BEGIN` yazmaz, çünkü bir kuralın işlem içinde mi yoksa
dışında mı değerlendirildiğini bilmesi gerekmez.

Sunum ile uygulama arasındaki sınır daha kolaydır. Sunum katmanı protokolün biçimini bilir:
hangi yolun hangi işleve karşılık geldiği, gövdenin nasıl çözüleceği, sonucun hangi durum
koduna eşleneceği. Bu bilginin uygulama katmanına sızması, aynı senaryonun ikinci bir giriş
noktasından (zamanlanmış iş, kabuk komutu, kuyruk tüketicisi) çağrılmasını engeller.

#### Özet

- Dört katman farklı değişme nedenlerine göre ayrılır; ayrımın çekirdeği katman sayısı
  değil, aralarındaki bağımlılık yönüdür.
- Bağımlılık yönü ithal grafiğinden denetlenebilir: ihlalli sürümde üç bulgu çıktı, sözleşme
  alan katmanına taşınınca bulgu sıfıra indi.
- Alan katmanının ithal kapanışı dört modülden iki modüle indi ve veritabanı sürücüsü
  kapanışın dışında kaldı.
- İhlalli sürümdeki iş kuralı sahte bir depoyla çağrılamadı; düzeltilmiş sürüm aynı sahteyle
  çalıştı. Bağımlılık yönü kuralı, alan katmanının bağımsız sınanabilmesinin ön koşuludur.
- İş kuralı alan katmanına, senaryo sırası ve işlem sınırı uygulama katmanına, protokol
  biçimi sunum katmanına aittir; bağımlılıkları birbirine bağlayan tek yer bileşim köküdür.

#### Sonraki Adım

Düzeltilmiş sürümde alan katmanı `{ sonuc, sebep }` biçiminde bir nesne döndürdü, sunum
katmanı bunu HTTP gövdesine çevirdi. Bu çeviri şimdilik üç alanlıydı ve neredeyse birebirdi;
kayıt büyüdüğünde soru keskinleşir. Ödünç kaydının dışarıya dönen biçimi ile alan
modelinin kendisi aynı şey midir? Alan modeline yeni bir alan eklendiğinde istemcinin
gördüğü gövde değişmeli midir? Sonraki ders aynı alan nesnesinden iki farklı dış sözleşme
üretir, iç modele alan eklendiğinde dış sözleşmenin değişmediğini sınamayla gösterir ve
alan nesnesini doğrudan döndürmenin kaç iç alanı dışarıya sızdırdığını sayar.

### Veri Aktarım Nesneleri

Source: https://academia.sh/tr/kurslar/veri-erisim-katmani/veri-aktarim-nesneleri

Dış sözleşmenin iç modelden ayrılması: aynı alan nesnesinden iki farklı görünüm üretilmesi, iç modele alan eklendiğinde dış sözleşmenin değişmediğinin sınanması, alan nesnesini doğrudan döndürmenin sızdırdığı alanların sayılması ve gelen yönde aşırı atama.

Önceki dersin düzeltilmiş sürümünde alan katmanı `{ sonuc, sebep }` biçiminde bir nesne
döndürdü ve sunum katmanı bunu HTTP gövdesine çevirdi. Çeviri üç alanlıydı, neredeyse
birebirdi; ara katman gereksiz bir el değiştirme gibi görünüyordu.

Kayıt büyüdüğünde soru keskinleşir. Ödünç kaydının dışarıya dönen biçimi ile alan modelinin
kendisi aynı şey midir? Alan modeline yeni bir alan eklendiğinde istemcinin gördüğü gövde
değişmeli midir? Bu dersin konusu, iki tarafın arasına konan **veri aktarım nesnesi** (data
transfer object): dışarıyla konuşmak için üretilen, alan davranışı taşımayan düz kayıt.

#### Aynı Kayıt, İki Sözleşme

Ödünç kaydının alan modeli veriyi ve o veriden türeyen kararları taşır. Aşağıdaki sınıf on
bir alan tutar; ikisi türetilmiş bilgi üretir. Dersin dosyaları iki dizine yerleşir.

```sh
mkdir -p alan sunum
```

```js
// alan/odunc.mjs — odunc kaydinin alan modeli
export class Odunc {
  constructor(v) {
    this.oduncId = v.oduncId;
    this.kitapId = v.kitapId;
    this.kitapBaslik = v.kitapBaslik;
    this.uyeId = v.uyeId;
    this.uyeAd = v.uyeAd;
    this.uyeEposta = v.uyeEposta;
    this.alisTarihi = v.alisTarihi;
    this.sonTeslimTarihi = v.sonTeslimTarihi;
    this.iadeTarihi = v.iadeTarihi;
    this.personelNotu = v.personelNotu;
    this.kayitSurumu = v.kayitSurumu;
  }
  get acikMi() { return this.iadeTarihi === null; }
  gecikmeGunu(bugun) {
    const bitis = this.iadeTarihi ?? bugun;
    const fark = (Date.parse(bitis) - Date.parse(this.sonTeslimTarihi)) / 86400000;
    return Math.max(0, Math.round(fark));
  }
  durum(bugun) {
    if (!this.acikMi) return "iade_edildi";
    return this.gecikmeGunu(bugun) > 0 ? "gecikmis" : "acik";
  }
}
```

Üç alan dışarıya ait değildir. `uyeEposta` kişisel veridir ve yalnız belirli rollere
gösterilir; `personelNotu` iç yazışmadır; `kayitSurumu` İyimser ve Kötümser Kilitleme
dersindeki sürüm sütununun kendisidir, yani bir kalıcılık ayrıntısıdır.

Dışarısı ise tek bir biçim istemez. Liste ekranı satır başına dört alanla yetinir; ayrıntı
ekranı ilişkileri de görmek ister.

```js
// sunum/gorunumler.mjs — ayni alan nesnesinden iki farkli dis sozlesme
export const listeGorunumu = (o, bugun) => ({
  oduncId: o.oduncId,
  kitap: o.kitapBaslik,
  sonTeslimTarihi: o.sonTeslimTarihi,
  durum: o.durum(bugun),
});

export const ayrintiGorunumu = (o, bugun) => ({
  oduncId: o.oduncId,
  kitap: { kitapId: o.kitapId, baslik: o.kitapBaslik },
  uye: { uyeId: o.uyeId, ad: o.uyeAd },
  alisTarihi: o.alisTarihi,
  sonTeslimTarihi: o.sonTeslimTarihi,
  iadeTarihi: o.iadeTarihi,
  durum: o.durum(bugun),
  gecikmeGunu: o.gecikmeGunu(bugun),
});
```

Eşleme iki iş yapıyor: iç alanları dışarıda bırakıyor ve türetilmiş alanları ekliyor.
İkincisi çoğu zaman gözden kaçar. `durum` ile `gecikmeGunu` alan modelinde saklanan değerler
değil, hesaplanan değerlerdir; istemcinin bunları kendisi hesaplaması, iş kuralının
istemciye kopyalanması demektir.

```js
// gorunum.mjs — tek alan nesnesi, iki dis sozlesme
import { Odunc } from "./alan/odunc.mjs";
import { listeGorunumu, ayrintiGorunumu } from "./sunum/gorunumler.mjs";

const BUGUN = "2025-06-20";
const kayit = new Odunc({
  oduncId: 7, kitapId: 5, kitapBaslik: "Körlük", uyeId: 2, uyeAd: "Ayşe Demir",
  uyeEposta: "ayse@ornek.test", alisTarihi: "2025-06-01", sonTeslimTarihi: "2025-06-15",
  iadeTarihi: null, personelNotu: "telefonla arandi", kayitSurumu: 3,
});

console.log("liste  :", JSON.stringify(listeGorunumu(kayit, BUGUN)));
console.log("ayrinti:", JSON.stringify(ayrintiGorunumu(kayit, BUGUN)));
```

```sh
node gorunum.mjs
```

```
liste  : {"oduncId":7,"kitap":"Körlük","sonTeslimTarihi":"2025-06-15","durum":"gecikmis"}
ayrinti: {"oduncId":7,"kitap":{"kitapId":5,"baslik":"Körlük"},"uye":{"uyeId":2,"ad":"Ayşe Demir"},"alisTarihi":"2025-06-01","sonTeslimTarihi":"2025-06-15","iadeTarihi":null,"durum":"gecikmis","gecikmeGunu":5}
```

Aynı nesne, iki gövde. Liste gövdesinde ilişkiler düzleştirildi, ayrıntı gövdesinde
gruplandı. Bu bir biçim tercihi değil, iki farklı sözleşmedir: liste ekranı değişse ayrıntı
sözleşmesi etkilenmez.

#### Sözleşmenin İç Değişimden Yalıtılması

Ayrımın asıl gerekçesi buradadır. Alan modeline yeni bir alan eklendiğinde ne olur?
Aşağıdaki dosya, aynı modelin iki iç alan eklendikten sonraki hâlidir.

```js
// alan/odunc-2.mjs — ayni modelin iki ic alan eklendikten sonraki hali
export class Odunc {
  constructor(v) {
    this.oduncId = v.oduncId;
    this.kitapId = v.kitapId;
    this.kitapBaslik = v.kitapBaslik;
    this.uyeId = v.uyeId;
    this.uyeAd = v.uyeAd;
    this.uyeEposta = v.uyeEposta;
    this.alisTarihi = v.alisTarihi;
    this.sonTeslimTarihi = v.sonTeslimTarihi;
    this.iadeTarihi = v.iadeTarihi;
    this.personelNotu = v.personelNotu;
    this.kayitSurumu = v.kayitSurumu;
    this.yenilemeSayisi = v.yenilemeSayisi ?? 0;        // yeni
    this.sonHatirlatmaTarihi = v.sonHatirlatmaTarihi ?? null;  // yeni
  }
  get acikMi() { return this.iadeTarihi === null; }
  gecikmeGunu(bugun) {
    const bitis = this.iadeTarihi ?? bugun;
    const fark = (Date.parse(bitis) - Date.parse(this.sonTeslimTarihi)) / 86400000;
    return Math.max(0, Math.round(fark));
  }
  durum(bugun) {
    if (!this.acikMi) return "iade_edildi";
    return this.gecikmeGunu(bugun) > 0 ? "gecikmis" : "acik";
  }
}
```

Sınama, iki sürümün ürettiği gövdeleri karşılaştırır.

```js
// gorunum.test.mjs — dis sozlesmenin ic model degisiminden etkilenmedigini sinar
import { test } from "node:test";
import assert from "node:assert/strict";
import { Odunc as Odunc1 } from "./alan/odunc.mjs";
import { Odunc as Odunc2 } from "./alan/odunc-2.mjs";
import { listeGorunumu, ayrintiGorunumu } from "./sunum/gorunumler.mjs";

const BUGUN = "2025-06-20";
const TEMEL = {
  oduncId: 7, kitapId: 5, kitapBaslik: "Körlük", uyeId: 2, uyeAd: "Ayşe Demir",
  uyeEposta: "ayse@ornek.test", alisTarihi: "2025-06-01", sonTeslimTarihi: "2025-06-15",
  iadeTarihi: null, personelNotu: "telefonla arandi", kayitSurumu: 3,
};
// Surum 2 ayni kayda iki ic alan ekler.
const TEMEL2 = { ...TEMEL, yenilemeSayisi: 2, sonHatirlatmaTarihi: "2025-06-18" };

const LISTE_SOZLESMESI = ["oduncId", "kitap", "sonTeslimTarihi", "durum"];
const AYRINTI_SOZLESMESI = ["oduncId", "kitap", "uye", "alisTarihi", "sonTeslimTarihi",
                            "iadeTarihi", "durum", "gecikmeGunu"];
const IC_ALANLAR = ["uyeEposta", "personelNotu", "kayitSurumu",
                    "yenilemeSayisi", "sonHatirlatmaTarihi"];

test("liste gorunumu sozlesmedeki alanlari uretir", () => {
  assert.deepEqual(Object.keys(listeGorunumu(new Odunc1(TEMEL), BUGUN)), LISTE_SOZLESMESI);
});

test("ayrinti gorunumu sozlesmedeki alanlari uretir", () => {
  assert.deepEqual(Object.keys(ayrintiGorunumu(new Odunc1(TEMEL), BUGUN)), AYRINTI_SOZLESMESI);
});

test("ic alanlar hicbir gorunumde gecmiyor", () => {
  const metin = JSON.stringify(listeGorunumu(new Odunc2(TEMEL2), BUGUN)) +
                JSON.stringify(ayrintiGorunumu(new Odunc2(TEMEL2), BUGUN));
  for (const alan of IC_ALANLAR) assert.equal(metin.includes(alan), false, alan);
});

test("ic modele alan eklenince liste gorunumu birebir ayni kalir", () => {
  assert.deepEqual(listeGorunumu(new Odunc2(TEMEL2), BUGUN),
                   listeGorunumu(new Odunc1(TEMEL), BUGUN));
});

test("ic modele alan eklenince ayrinti gorunumu birebir ayni kalir", () => {
  assert.deepEqual(ayrintiGorunumu(new Odunc2(TEMEL2), BUGUN),
                   ayrintiGorunumu(new Odunc1(TEMEL), BUGUN));
});
```

```sh
node --test gorunum.test.mjs
```

```
✔ liste gorunumu sozlesmedeki alanlari uretir (0.556ms)
✔ ayrinti gorunumu sozlesmedeki alanlari uretir (0.068083ms)
✔ ic alanlar hicbir gorunumde gecmiyor (1.237958ms)
✔ ic modele alan eklenince liste gorunumu birebir ayni kalir (0.062917ms)
✔ ic modele alan eklenince ayrinti gorunumu birebir ayni kalir (0.064041ms)
ℹ tests 5
ℹ suites 0
ℹ pass 5
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 35.5965
```

Parantez içindeki süreler her çalıştırmada değişir; anlamlı olan `pass 5` satırıdır. İki
sürüm arasında dış gövdeler `deepEqual` ile birebir eşit çıktı. İç model değişti, dış
sözleşme değişmedi. Bu, Web API Tasarımı kursundaki geriye dönük uyumluluk kuralının
uygulama içindeki karşılığıdır: sözleşmenin değişmesi bir karar olmalıdır, bir yan etki
değil.

#### Doğrudan Dönmenin Bedeli

Ters yön ölçülebilir. Uç nokta alan nesnesini doğrudan döndürürse dış gövde ile sözleşme
arasında iki yönlü bir fark oluşur.

```js
// sizinti-say.mjs — alan nesnesi dogrudan donduruldugunde dis sozlesme ne oluyor
import { Odunc as Odunc1 } from "./alan/odunc.mjs";
import { Odunc as Odunc2 } from "./alan/odunc-2.mjs";
import { listeGorunumu } from "./sunum/gorunumler.mjs";

const BUGUN = "2025-06-20";
const TEMEL = {
  oduncId: 7, kitapId: 5, kitapBaslik: "Körlük", uyeId: 2, uyeAd: "Ayşe Demir",
  uyeEposta: "ayse@ornek.test", alisTarihi: "2025-06-01", sonTeslimTarihi: "2025-06-15",
  iadeTarihi: null, personelNotu: "telefonla arandi", kayitSurumu: 3,
};
const TEMEL2 = { ...TEMEL, yenilemeSayisi: 2, sonHatirlatmaTarihi: "2025-06-18" };

// Istemcinin gordugu gövde, nesnenin JSON'a cevrilmis halidir.
const disAlanlar = (nesne) => Object.keys(JSON.parse(JSON.stringify(nesne))).sort();
const fark = (a, b) => a.filter((x) => !b.includes(x));

for (const [ad, kayit] of [["surum 1", new Odunc1(TEMEL)], ["surum 2", new Odunc2(TEMEL2)]]) {
  const eslemeli = disAlanlar(listeGorunumu(kayit, BUGUN));
  const dogrudan = disAlanlar(kayit);
  console.log(`${ad}  eslemeli=${eslemeli.length} alan   dogrudan=${dogrudan.length} alan`);
  console.log(`  sizan  (${fark(dogrudan, eslemeli).length}): ${fark(dogrudan, eslemeli).join(", ")}`);
  console.log(`  eksilen (${fark(eslemeli, dogrudan).length}): ${fark(eslemeli, dogrudan).join(", ")}`);
}
```

```sh
node sizinti-say.mjs
```

```
surum 1  eslemeli=4 alan   dogrudan=11 alan
  sizan  (9): alisTarihi, iadeTarihi, kayitSurumu, kitapBaslik, kitapId, personelNotu, uyeAd, uyeEposta, uyeId
  eksilen (2): durum, kitap
surum 2  eslemeli=4 alan   dogrudan=13 alan
  sizan  (11): alisTarihi, iadeTarihi, kayitSurumu, kitapBaslik, kitapId, personelNotu, sonHatirlatmaTarihi, uyeAd, uyeEposta, uyeId, yenilemeSayisi
  eksilen (2): durum, kitap
```

Üç bulgu var. Birincisi sızıntının kendisi: liste sözleşmesinde dört alan varken doğrudan
dönen gövdede on bir alan çıktı, dokuzu sözleşmede yok. İçlerinden `uyeEposta` kişisel veri,
`personelNotu` iç yazışma, `kayitSurumu` kalıcılık ayrıntısıdır.

İkincisi büyümenin kendiliğinden olması: iki iç alan eklenince doğrudan dönen gövde on
üç alana çıktı, eşlemeli gövde dört alanda kaldı. Doğrudan dönen uçta her model değişikliği
bir sözleşme değişikliğidir ve bunun için kimse karar vermemiştir.

Üçüncüsü ters yöndeki kayıptır. Doğrudan dönen gövdede `durum` ve `kitap` alanları yok,
çünkü bunlar saklanan değil türetilen alanlardır. Aşırı Veri Çekme dersinde ölçülen fazla
bayt burada ikinci bir maliyet kazanıyor: gövde hem gereğinden büyük hem de istemcinin
ihtiyaç duyduğu bilgi bakımından eksik.

#### Gelen Yön

Aynı sınır ters yönde de gereklidir. İstek gövdesi doğrudan iç modele atanırsa istemci
yazmaması gereken alanları belirler. Aşağıdaki karşılaştırma bunu sayıyor.

```js
// gelen-yon.mjs — istek govdesi ic modele dogrudan atandiginda istemci neyi belirliyor
const VARSAYILAN = {
  oduncId: null, kitapId: null, kitapBaslik: null, uyeId: null, uyeAd: null,
  uyeEposta: null, alisTarihi: null, sonTeslimTarihi: null, iadeTarihi: null,
  personelNotu: null, kayitSurumu: 1,
};

// Istemcinin yolladigi govde: uc mesru alan ve uc fazlalik.
const ISTEK = {
  kitapId: 5, uyeId: 2, alisTarihi: "2025-06-01",
  oduncId: 1, kayitSurumu: 99, personelNotu: "kaydi kapali gorun",
};

const korumasiz = (istek) => ({ ...VARSAYILAN, ...istek });
const esleyerek = (istek) => ({
  ...VARSAYILAN,
  kitapId: istek.kitapId, uyeId: istek.uyeId, alisTarihi: istek.alisTarihi,
});

const istemcininBelirledigi = (nesne) =>
  Object.keys(nesne).filter((a) => JSON.stringify(nesne[a]) !== JSON.stringify(VARSAYILAN[a]));

for (const [ad, kur] of [["korumasiz", korumasiz], ["esleyerek", esleyerek]]) {
  const alanlar = istemcininBelirledigi(kur(ISTEK));
  console.log(`${ad.padEnd(10)} istemcinin belirledigi alan = ${alanlar.length}: ${alanlar.join(", ")}`);
}
```

```sh
node gelen-yon.mjs
```

```
korumasiz  istemcinin belirledigi alan = 6: oduncId, kitapId, uyeId, alisTarihi, personelNotu, kayitSurumu
esleyerek  istemcinin belirledigi alan = 3: kitapId, uyeId, alisTarihi
```

Korumasız sürümde istemci kaydın kimliğini, sürüm sayacını ve personel notunu belirledi.
Sürüm sayacının istemciden gelmesi iyimser kilitlemeyi işlevsiz kılar: çatışma denetimi,
sunucunun bildiği sürüm numarasına dayanır. Bu davranışa **aşırı atama** (mass assignment)
denir ve gövdeye alan eklendikçe sessizce genişler.

Eşlemeli sürümde istek gövdesi bir veri aktarım nesnesidir; adı geçmeyen alan iç modele
ulaşamaz. Sorgu Nesneleri ve Belirtimler dersindeki tanıtıcı izin listesiyle aynı ilkedir:
dışarıdan gelen adın iç yapıya çevrilmesi sabit bir listeye dayanır.

#### Eşlemenin Bedeli ve Sınırı

Ayrım bedelsiz değildir. Her görünüm ek kod, ek sınama ve model değiştiğinde ek bakım
demektir. Alan sayısı arttıkça eşleme işlevleri de uzar ve bir kısmı alan adını olduğu gibi
kopyalar.

Bedelin karşılığı üç durumda net olarak alınır: dış sözleşmenin iç modelden farklı bir
hızda değişmesi gerekiyorsa, iç modelde dışarıya gösterilmeyecek alan varsa ve aynı kayıttan
birden çok görünüm üretiliyorsa. Kimlik Doğrulama ve Yetkilendirme kursundaki rol ayrımı
üçüncü maddeyi doğrudan besler: aynı ödünç kaydı üyeye, personele ve rapor arayüzüne farklı
alan kümeleriyle görünür; bu farkı taşıyacak yer görünüm işlevleridir.

Karşı durum da vardır. İç modeli olmayan, tabloyu okuyup listeleyen bir yönetim ekranında
ayrı bir aktarım nesnesi katmanı yalnızca kopya üretir. Ölçüt şudur: **eşleme işlevi hiçbir
alanı gizlemiyor, hiçbir alan türetmiyor ve hiçbir adı değiştirmiyorsa** o eşleme henüz bir
sözleşme taşımıyordur.

#### Özet

- Veri aktarım nesnesi dışarıyla konuşmak için üretilen düz kayıttır; alan modeli ise veriyi
  ve o veriden türeyen kararları taşır.
- Aynı alan nesnesinden liste ve ayrıntı olmak üzere iki ayrı sözleşme üretildi; eşleme hem
  iç alanları gizledi hem türetilmiş alanları ekledi.
- İç modele iki alan eklendiğinde iki görünüm de birebir aynı gövdeyi üretti; beş sınamanın
  tamamı geçti.
- Alan nesnesi doğrudan döndürüldüğünde liste sözleşmesine göre dokuz alan sızdı, iki
  türetilmiş alan eksildi; model büyüyünce sızan alan sayısı on bire çıktı.
- Gelen yönde istek gövdesinin iç modele doğrudan atanması istemcinin altı alanı
  belirlemesine yol açtı; adları sayılan bir eşlemede bu sayı üçe indi.

#### Sonraki Adım

Bu dersin gelen yön örneğinde istek gövdesindeki alanlar hiç denetlenmeden kabul edildi:
`kitapId` bir sayı mı, `alisTarihi` geçerli bir tarih mi, üye gerçekten var mı, o üye şu
anda ödünç almaya uygun mu? Bu soruların hepsi "doğrulama" başlığı altında toplanır ama
aynı yere ait değildir. Biri gövdenin biçimiyle ilgilidir ve veriye bakmadan yanıtlanır;
öteki kurumun kuralıdır ve veritabanına bakmadan yanıtlanamaz. Sonraki ders aynı hatalı
girdiyi iki katmanda çalıştırır, ürettikleri yanıtların farkını gösterir ve alan kuralının
neden girdi doğrulamasına indirgenemediğini ortaya koyar.

### Doğrulama Katmanları

Source: https://academia.sh/tr/kurslar/veri-erisim-katmani/dogrulama-katmanlari

Girdi doğrulaması ile alan kuralının ayrımı: aynı gövdenin iki katmanda ürettiği farklı yanıtlar, kararın kaç sorgu gerektirdiğinin sayılması, alan kuralının veri durumuna bağlılığı ve son güvenceyi veren veritabanı kısıtı.

Önceki dersin gelen yön örneğinde istek gövdesi hiç denetlenmeden kabul edildi. `kitapId`
bir sayı mı, `alisTarihi` geçerli bir tarih mi, üye gerçekten var mı, o üye şu anda ödünç
almaya uygun mu?

Bu soruların hepsi "doğrulama" başlığı altında toplanır ama aynı yere ait değildir.
Birincisi ile ikincisi gövdeye bakarak yanıtlanır; üçüncüsü ile dördüncüsü veritabanına
bakmadan yanıtlanamaz. Ayrımın adı **girdi doğrulaması** (input validation) ile **alan
kuralı** (domain rule) ayrımıdır ve bu ders farkı ölçerek gösterir.

Ölçümler aşağıdaki veritabanı üzerinde yapılır. Bir üye askıya alınmış, bir üye üç açık
kayda ulaşmış, bir kitap ödünçtedir.

```sh
# kur.sh — dersin veritabanini sifirdan kurar
rm -f kutuphane.db
sqlite3 kutuphane.db <<'SQL'
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL,
                  durum TEXT NOT NULL CHECK (durum IN ('aktif','askida')));
CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL);
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY,
                    kitap_id INTEGER NOT NULL REFERENCES kitap(kitap_id),
                    uye_id INTEGER NOT NULL REFERENCES uye(uye_id),
                    alis_tarihi TEXT NOT NULL, iade_tarihi TEXT);
INSERT INTO uye VALUES (1,'Ayse Demir','aktif'),(2,'Bora Kaya','askida'),(3,'Cem Yilmaz','aktif');
INSERT INTO kitap VALUES (1,'Korluk'),(2,'Tutunamayanlar'),(3,'Kum Kitabi'),(4,'Puslu Kitalar'),
                         (5,'Saatleri Ayarlama'),(6,'Cocukluk'),(7,'Aylak Adam');
INSERT INTO odunc VALUES (1,1,1,'2025-06-01',NULL),(2,2,3,'2025-06-02',NULL),
                         (3,3,3,'2025-06-03',NULL),(4,4,3,'2025-06-04',NULL);
SQL
```

#### Girdi Doğrulaması

Dersin dosyaları üç dizine yerleşir.

```sh
mkdir -p sunum alan altyapi
```

Girdi doğrulaması gövdenin biçimini denetler: alan var mı, türü doğru mu, deseni tutuyor
mu. Kararı yalnızca gövdeye bakarak verir. Web API Tasarımı kursundaki Doğrulama Hataları
dersi bu katmanın çıktısını bir liste olarak tanımlamıştı: her hata bir gövde yolu ve bir
kod taşır.

```js
// sunum/girdi-dogrulama.mjs — govdenin bicimini denetler; veriye hic bakmaz
const KURALLAR = {
  zorunlu: (d) => (d === undefined || d === null || d === "" ? { kod: "zorunlu" } : null),
  tamsayi: (d) => (Number.isInteger(d) && d > 0 ? null : { kod: "tamsayi_degil" }),
  tarih: (d) => (/^\d{4}-\d{2}-\d{2}$/.test(String(d)) ? null : { kod: "tarih_degil" }),
};

export const SEMA = {
  "/uyeId": ["zorunlu", "tamsayi"],
  "/kitapId": ["zorunlu", "tamsayi"],
  "/alisTarihi": ["zorunlu", "tarih"],
};

// Donen liste Dogrulama Hatalari dersindeki { yol, kod } bicimindedir.
export function girdiyiDogrula(sema, govde) {
  const hatalar = [];
  for (const [yol, kurallar] of Object.entries(sema)) {
    const deger = govde[yol.slice(1)];
    for (const ad of kurallar) {
      if (ad !== "zorunlu" && (deger === undefined || deger === null || deger === "")) break;
      const sonuc = KURALLAR[ad](deger);
      if (sonuc) { hatalar.push({ yol, ...sonuc }); break; }   // alan basina tek hata
    }
  }
  return hatalar;
}
```

İşlevin imzasında veritabanı yok. Bu, katmanın tanımıdır: aynı gövde her koşulda aynı hata
listesini üretir.

#### Alan Kuralı

Alan kuralı kurumun kuralını sorar: bu üye ödünç alabilir mi, bu kitap uygun mu? Kararı
gövdeden çıkarılamaz.

```js
// alan/odunc-kurallari.mjs — kurum kurallari; karar icin veri gerekir
export const UYE_SINIRI = 3;

export function oduncVerilebilirMi(depo, uyeId, kitapId) {
  const uye = depo.uyeyiBul(uyeId);
  if (uye === null) return { sonuc: "reddedildi", sebep: "uye_yok" };
  if (uye.durum === "askida") return { sonuc: "reddedildi", sebep: "uye_askida" };
  if (!depo.kitapVarMi(kitapId)) return { sonuc: "reddedildi", sebep: "kitap_yok" };
  if (depo.kitabinAcikKaydi(kitapId) !== null) return { sonuc: "reddedildi", sebep: "kitap_oduncte" };
  if (depo.uyeninAcikSayisi(uyeId) >= UYE_SINIRI) {
    return { sonuc: "reddedildi", sebep: "uye_siniri", sinir: UYE_SINIRI };
  }
  return { sonuc: "kabul" };
}
```

Kural bir depo alıyor ve tek bir sebep döndürüyor. İki fark burada belirir: kararın girdisi
gövde değil **durum**dur, çıktısı ise liste değil **tek bir olgu**dur.

Kararın kaça mal olduğunu görmek için depo çalıştırdığı sorguyu sayar.

```js
// altyapi/sayan-depo.mjs — sozlesmeyi karsilar ve calistirdigi sorguyu sayar
import { DatabaseSync } from "node:sqlite";

export class SayanDepo {
  constructor(dosya) { this.db = new DatabaseSync(dosya); this.sorgu = 0; }
  #tek(sql, ...p) { this.sorgu++; return this.db.prepare(sql).get(...p) ?? null; }
  uyeyiBul(uyeId) { return this.#tek("SELECT uye_id, durum FROM uye WHERE uye_id = ?", uyeId); }
  kitapVarMi(kitapId) {
    return this.#tek("SELECT kitap_id FROM kitap WHERE kitap_id = ?", kitapId) !== null;
  }
  kitabinAcikKaydi(kitapId) {
    return this.#tek(
      "SELECT odunc_id FROM odunc WHERE kitap_id = ? AND iade_tarihi IS NULL", kitapId);
  }
  uyeninAcikSayisi(uyeId) {
    return this.#tek(
      "SELECT count(*) AS n FROM odunc WHERE uye_id = ? AND iade_tarihi IS NULL", uyeId).n;
  }
}
```

#### Aynı Girdi, İki Katman

Beş gövde sırayla iki katmandan geçirilir. İlki biçimsel olarak bozuk; kalan dördü biçimsel
olarak kusursuz.

```js
// iki-katman.mjs — ayni govdeler once girdi dogrulamasindan, sonra alan kuralindan gecer
import { girdiyiDogrula, SEMA } from "./sunum/girdi-dogrulama.mjs";
import { oduncVerilebilirMi } from "./alan/odunc-kurallari.mjs";
import { SayanDepo } from "./altyapi/sayan-depo.mjs";

const TABAN = "https://ornek.kutuphane/sorunlar/";
const depo = new SayanDepo("kutuphane.db");

const GOVDELER = [
  { uyeId: "iki", kitapId: 0, alisTarihi: "15/06/2025" },
  { uyeId: 2, kitapId: 6, alisTarihi: "2025-06-20" },
  { uyeId: 1, kitapId: 1, alisTarihi: "2025-06-20" },
  { uyeId: 3, kitapId: 6, alisTarihi: "2025-06-20" },
  { uyeId: 1, kitapId: 6, alisTarihi: "2025-06-20" },
];

for (const govde of GOVDELER) {
  console.log(`govde: ${JSON.stringify(govde)}`);
  const hatalar = girdiyiDogrula(SEMA, govde);
  console.log(`  girdi dogrulama: ${hatalar.length} hata, sorgu = 0`);
  if (hatalar.length > 0) {
    console.log(`  yanit 422 ${JSON.stringify({ type: TABAN + "dogrulama", hatalar })}`);
    continue;
  }
  depo.sorgu = 0;
  const karar = oduncVerilebilirMi(depo, govde.uyeId, govde.kitapId);
  console.log(`  alan kurali: ${karar.sonuc}${karar.sebep ? ` (${karar.sebep})` : ""}, sorgu = ${depo.sorgu}`);
  console.log(karar.sonuc === "kabul"
    ? "  yanit 201 " + JSON.stringify({ durum: "verildi" })
    : "  yanit 409 " + JSON.stringify({ type: TABAN + karar.sebep.replaceAll("_", "-") }));
}
```

```sh
sh kur.sh
node iki-katman.mjs
```

```
govde: {"uyeId":"iki","kitapId":0,"alisTarihi":"15/06/2025"}
  girdi dogrulama: 3 hata, sorgu = 0
  yanit 422 {"type":"https://ornek.kutuphane/sorunlar/dogrulama","hatalar":[{"yol":"/uyeId","kod":"tamsayi_degil"},{"yol":"/kitapId","kod":"tamsayi_degil"},{"yol":"/alisTarihi","kod":"tarih_degil"}]}
govde: {"uyeId":2,"kitapId":6,"alisTarihi":"2025-06-20"}
  girdi dogrulama: 0 hata, sorgu = 0
  alan kurali: reddedildi (uye_askida), sorgu = 1
  yanit 409 {"type":"https://ornek.kutuphane/sorunlar/uye-askida"}
govde: {"uyeId":1,"kitapId":1,"alisTarihi":"2025-06-20"}
  girdi dogrulama: 0 hata, sorgu = 0
  alan kurali: reddedildi (kitap_oduncte), sorgu = 3
  yanit 409 {"type":"https://ornek.kutuphane/sorunlar/kitap-oduncte"}
govde: {"uyeId":3,"kitapId":6,"alisTarihi":"2025-06-20"}
  girdi dogrulama: 0 hata, sorgu = 0
  alan kurali: reddedildi (uye_siniri), sorgu = 4
  yanit 409 {"type":"https://ornek.kutuphane/sorunlar/uye-siniri"}
govde: {"uyeId":1,"kitapId":6,"alisTarihi":"2025-06-20"}
  girdi dogrulama: 0 hata, sorgu = 0
  alan kurali: kabul, sorgu = 4
  yanit 201 {"durum":"verildi"}
```

Üç fark ölçüldü.

**Yanıtın biçimi farklı.** Girdi doğrulaması üç ayrı alan hatasını tek yanıtta topladı; her
biri bir gövde yoluyla işaretli, yani istemci hangi giriş kutusunu boyayacağını biliyor.
Alan kuralı tek bir sebep döndürdü ve gövde yolu vermedi: "üye askıda" bir alanın hatası
değildir, işlemin kendisinin reddidir. Durum kodu da bu yüzden ayrışır; biri gövdenin
düzeltilebilir olduğunu, öteki isteğin şu anki durumda karşılanamayacağını söyler.

**Maliyet farklı.** Girdi doğrulaması sıfır sorguyla karar verdi. Alan kuralının kararı bir
ile dört sorgu arasında değişti; kural erken reddettiğinde sorgu sayısı düştü. Bu, katman
sırasının gerekçesidir: biçimsel olarak bozuk gövde alan katmanına hiç ulaşmadı ve dört
sorgu boşa gitmedi.

**Bilgi kaynağı farklı.** Girdi doğrulaması gövdedeki değere baktı; alan kuralı gövdede
olmayan bir bilgiye baktı. İstek `uyeId: 2` diyor, üyenin askıda olduğunu söylemiyor. Bu
bilgi gövdeye eklenebilir olsaydı zaten istemciye sorulmuş olurdu ve istemci onu istediği
gibi doldururdu.

#### Kuralın Veriye Bağlılığı

Üçüncü farkın kanıtı, gövdeyi sabit tutup veriyi değiştirmektir.

```js
// veriye-bagli.mjs — ayni govde, degismeyen bicim, degisen veri
import { DatabaseSync } from "node:sqlite";
import { girdiyiDogrula, SEMA } from "./sunum/girdi-dogrulama.mjs";
import { oduncVerilebilirMi } from "./alan/odunc-kurallari.mjs";
import { SayanDepo } from "./altyapi/sayan-depo.mjs";

const GOVDE = { uyeId: 2, kitapId: 6, alisTarihi: "2025-06-20" };
const yazan = new DatabaseSync("kutuphane.db");
const depo = new SayanDepo("kutuphane.db");

for (const durum of ["askida", "aktif", "askida"]) {
  yazan.prepare("UPDATE uye SET durum = ? WHERE uye_id = 2").run(durum);
  const hata = girdiyiDogrula(SEMA, GOVDE).length;
  const karar = oduncVerilebilirMi(depo, GOVDE.uyeId, GOVDE.kitapId);
  console.log(`uye 2 durumu=${durum.padEnd(7)} girdi hatasi=${hata}  ` +
              `alan karari=${karar.sonuc}${karar.sebep ? ` (${karar.sebep})` : ""}`);
}
```

```sh
node veriye-bagli.mjs
```

```
uye 2 durumu=askida  girdi hatasi=0  alan karari=reddedildi (uye_askida)
uye 2 durumu=aktif   girdi hatasi=0  alan karari=kabul
uye 2 durumu=askida  girdi hatasi=0  alan karari=reddedildi (uye_askida)
```

Gövde hiç değişmedi. Girdi doğrulaması üç turda da sıfır hata verdi; alan kuralı iki farklı
karar üretti. Üçüncü tur ilk durumu geri getiriyor, böylece betik tekrar çalıştırıldığında
aynı çıktıyı üretiyor.

Bu, alan kuralının girdi doğrulamasına indirgenememesinin biçimsel gerekçesidir. Girdi
doğrulaması gövdenin bir işlevidir; alan kuralı ise gövde ile durumun ortak işlevidir. Girdi
doğrulaması sabit bir şemayla anlatılabilir, alan kuralı anlatılamaz.

#### Üçüncü Kat

İki katman da geçildikten sonra bir boşluk kalır. Alan kuralı "kitap ödünçte değil" dedi ve
yazma bunun ardından yapıldı; arada başka bir istek aynı kitabı yazmışsa kural yanlış
bilgiye dayanmış olur. İyimser ve Kötümser Kilitleme dersindeki denetle–sonra–yaz açığının
aynısıdır.

Aşağıdaki betik iki isteğin de yazmadan önce kuralı değerlendirdiği sırayı kurar ve aynı
sırayı kısıtlı ve kısıtsız iki veritabanında dener.

```js
// ucuncu-kat.mjs — alan kurali gecti, arada baska bir istek girdi: son soz kisitin
import { DatabaseSync } from "node:sqlite";
import { rmSync } from "node:fs";
import { oduncVerilebilirMi } from "./alan/odunc-kurallari.mjs";

function kur(dosya, kisitli) {
  rmSync(dosya, { force: true });
  const db = new DatabaseSync(dosya);
  db.exec(`CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, durum TEXT NOT NULL);
           CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL);
           CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL,
                               uye_id INTEGER NOT NULL, alis_tarihi TEXT NOT NULL, iade_tarihi TEXT);
           INSERT INTO uye VALUES (1,'Ayse Demir','aktif'),(2,'Bora Kaya','aktif');
           INSERT INTO kitap VALUES (6,'Cocukluk');`);
  if (kisitli) {
    db.exec("CREATE UNIQUE INDEX acik_odunc ON odunc(kitap_id) WHERE iade_tarihi IS NULL");
  }
  return db;
}

const depoKur = (db) => ({
  uyeyiBul: (id) => db.prepare("SELECT uye_id, durum FROM uye WHERE uye_id = ?").get(id) ?? null,
  kitapVarMi: (id) => db.prepare("SELECT kitap_id FROM kitap WHERE kitap_id = ?").get(id) !== undefined,
  kitabinAcikKaydi: (id) => db.prepare(
    "SELECT odunc_id FROM odunc WHERE kitap_id = ? AND iade_tarihi IS NULL").get(id) ?? null,
  uyeninAcikSayisi: (id) => db.prepare(
    "SELECT count(*) AS n FROM odunc WHERE uye_id = ? AND iade_tarihi IS NULL").get(id).n,
});

for (const kisitli of [false, true]) {
  const db = kur(kisitli ? "kisitli.db" : "kisitsiz.db", kisitli);
  const depo = depoKur(db);
  const yaz = (uyeId) => db.prepare(
    "INSERT INTO odunc (kitap_id, uye_id, alis_tarihi, iade_tarihi) VALUES (6,?,'2025-06-20',NULL)")
    .run(uyeId);

  // Iki istek ayni kitabi istiyor; ikisi de yazmadan once kurali degerlendiriyor.
  const karar1 = oduncVerilebilirMi(depo, 1, 6).sonuc;
  const karar2 = oduncVerilebilirMi(depo, 2, 6).sonuc;
  yaz(1);
  let ikinci;
  try { yaz(2); ikinci = "yazildi"; }
  catch (e) { ikinci = `reddedildi (${e.message})`; }

  const acik = db.prepare(
    "SELECT count(*) AS n FROM odunc WHERE kitap_id = 6 AND iade_tarihi IS NULL").get().n;
  console.log(`kisit ${kisitli ? "var" : "yok"}: karar1=${karar1} karar2=${karar2}  ` +
              `ikinci yazma=${ikinci}  kitap 6 acik kayit=${acik}`);
}
```

```sh
node ucuncu-kat.mjs
```

```
kisit yok: karar1=kabul karar2=kabul  ikinci yazma=yazildi  kitap 6 acik kayit=2
kisit var: karar1=kabul karar2=kabul  ikinci yazma=reddedildi (UNIQUE constraint failed: odunc.kitap_id)  kitap 6 acik kayit=1
```

İki durumda da alan kuralı iki isteği de kabul etti; kural yanlış değil, zamanlaması
elverişsizdi. Kısıt yokken kitabın iki açık kaydı oluştu ve bu, iş kuralının doğrudan
ihlalidir. Kısıt varken ikinci yazma reddedildi ve açık kayıt sayısı birde kaldı.

Katmanların rolü böylece ayrışıyor. Alan kuralı **kararın gerekçesini** üretir: kullanıcıya
"bu kitap ödünçte" denebilmesinin tek yolu odur; kısıt hatası bu cümleyi veremez. Kısıt ise
**değişmezi** garanti eder ve kural atlandığında ya da yarıştığında son sözü söyler. İkisi
birbirinin yerine geçmez: yalnız kısıta güvenen bir uygulama kullanıcıya anlamlı hata
veremez, yalnız kurala güvenen bir uygulama bozuk veriye açıktır.

#### Sınırın Çizilmesi

Yeni bir kuralın hangi katmana ait olduğu tek soruyla belirlenir: **kural, gövdeye bakarak
yanıtlanabiliyor mu?**

"İade tarihi geçmişte olamaz" gövdeye bakarak yanıtlanır; bir tarih karşılaştırmasıdır ve
girdi doğrulamasına aittir. "Üyenin gecikmiş kaydı varken yeni ödünç verilemez" gövdeye
bakarak yanıtlanamaz; alan kuralıdır. "ISBN on üç haneli olmalı" biçimdir. "Bu ISBN
katalogda olmalı" veridir.

Ara bir durum vardır: gövde içi tutarlılık. "Alış tarihi son teslim tarihinden önce olmalı"
kuralı iki alanı karşılaştırır, veri istemez ve girdi doğrulamasında kalır. Doğrulama
Hataları dersindeki şema bu tür kuralları alan yolu üzerinden ifade edebiliyordu; ölçüt
değişmiyor.

Sıra da bu ayrımdan çıkar: önce biçim, sonra kural, en sonda kısıt. Ters sıra iki maliyet
üretir. Biçimsiz gövdeyle sorgu çalıştırmak boşa iştir; kural yerine kısıt hatasını
kullanıcıya göstermek ise sorunun ne olduğunu anlatmayan bir yanıttır.

#### Özet

- Girdi doğrulaması gövdenin işlevidir ve sıfır sorguyla karar verdi; alan kuralı gövde ile
  durumun ortak işlevidir ve kararı bir ile dört sorgu arasında değişti.
- İki katmanın yanıtı biçim olarak farklıdır: biri gövde yollarıyla işaretli bir hata
  listesi, öteki tek bir ret sebebi üretti.
- Gövde sabit tutulup üyenin durumu değiştirildiğinde girdi doğrulaması üç turda da sıfır
  hata verdi, alan kuralı iki farklı karar üretti.
- Alan kuralı denetle–sonra–yaz açığına açıktır: kısıtsız veritabanında aynı kitabın iki
  açık kaydı oluştu, kısıtlı veritabanında ikinci yazma reddedildi.
- Kural gerekçeyi, kısıt değişmezi verir; biri kullanıcıya anlamlı hata üretir, öteki veriyi
  garanti eder.

#### Sonraki Adım

Beşinci gövde iki katmandan da geçti ve ödünç kaydının yazılması sırası geldi. Yazmayla
birlikte başlaması gereken başka işler de var: üyeye bildirim gitmeli, günlük istatistik
güncellenmeli, ilerideki raporlar için hareket kaydı düşülmeli. Bunların hepsini ödünç verme işlevinin içine yazmak
o işlevi her yeni yan etkide değiştirmek demektir. Sonraki ders ödünç verme kararının
ürettiği olguyu bir **alan olayı** olarak yayımlar, iki dinleyici bağlar ve olayın işlem
sınırıyla ilişkisini ölçer: işlem geri alındığında dinleyicinin ürettiği yanlış bildirim
sayılır ve giden kutusu düzeniyle sıfıra indirilir.

### Alan Olayları

Source: https://academia.sh/tr/kurslar/veri-erisim-katmani/alan-olaylari

Yan etkilerin olay yayımıyla gevşek bağlanması: aynı yayın çağrısına birden çok dinleyicinin bağlanması, işlem içinde yayımlanan olayın geri alma sonrası ürettiği yanlış bildirimlerin sayılması ve giden kutusu düzeniyle bu sayının sıfıra indirilmesi.

Ödünç verme isteği iki doğrulama katmanından geçti ve kayıt yazılabilir duruma geldi.
Yazmayla birlikte başlaması gereken başka işler de var: üyeye bildirim gitmeli, günlük
istatistik güncellenmeli, ilerideki raporlar için kütüğe satır düşülmeli.

Bunların hepsini ödünç verme işlevinin içine yazmak, o işlevi her yeni yan etkide
değiştirmek demektir. **Alan olayı** (domain event), aradaki bağı gevşetir: ödünç verme
işlevi olup biteni bildirir, kimin ne yapacağını bilmez. Bu dersin asıl konusu bildirimin
zamanıdır — olay ne zaman yayımlanırsa doğru olur?

#### Olay, Yayınlayıcı, Dinleyici

Olay geçmiş zamanda bir olguyu anlatır: "ödünç verildi". Bir istek değildir, bir emir
değildir; olmuş bir şeyin kaydıdır. Yayınlayıcı, olay türüne bağlı dinleyicileri çağırır.
Dersin dosyaları iki dizine yerleşir.

```sh
mkdir -p alan uygulama
```

```js
// alan/olay-yayinlayici.mjs — olay turune gore dinleyici cagiran en kucuk yayinlayici
export class OlayYayinlayici {
  constructor() { this.dinleyiciler = new Map(); }
  abone(tur, dinleyici) {
    if (!this.dinleyiciler.has(tur)) this.dinleyiciler.set(tur, []);
    this.dinleyiciler.get(tur).push(dinleyici);
    return this;
  }
  yayinla(olay) {
    for (const d of this.dinleyiciler.get(olay.tur) ?? []) d(olay);
    return (this.dinleyiciler.get(olay.tur) ?? []).length;
  }
}
```

Dinleyiciler yan etkilerdir. Üçü de aynı olayı alır, hiçbiri diğerini bilmez.

```js
// uygulama/dinleyiciler.mjs — olaya tepki veren uc yan etki
export const bildirimDinleyicisi = (kutu) => (olay) =>
  kutu.push({ oduncId: olay.oduncId, uyeId: olay.uyeId, kitapId: olay.kitapId,
              metin: `Kitap ${olay.kitapId} ${olay.tarih} tarihinde odunc verildi.` });

export const istatistikDinleyicisi = (sayac) => (olay) => {
  sayac[olay.tarih] = (sayac[olay.tarih] ?? 0) + 1;
};

export const kutukDinleyicisi = (satirlar) => (olay) =>
  satirlar.push(`${olay.tarih} ${olay.tur} odunc=${olay.oduncId}`);
```

Gevşek bağlanmanın ölçüsü şudur: dinleyici eklendiğinde üreten tarafın kodu değişmez.

```js
// yayin.mjs — ayni yayin cagrisi, farkli sayida dinleyici
import { OlayYayinlayici } from "./alan/olay-yayinlayici.mjs";
import { bildirimDinleyicisi, istatistikDinleyicisi, kutukDinleyicisi }
  from "./uygulama/dinleyiciler.mjs";

const OLAYLAR = [
  { tur: "odunc_verildi", oduncId: 1, uyeId: 1, kitapId: 5, tarih: "2025-06-20" },
  { tur: "odunc_verildi", oduncId: 2, uyeId: 3, kitapId: 7, tarih: "2025-06-20" },
];

for (const dinleyiciSayisi of [2, 3]) {
  const kutu = [], sayac = {}, kutuk = [];
  const yayinlayici = new OlayYayinlayici()
    .abone("odunc_verildi", bildirimDinleyicisi(kutu))
    .abone("odunc_verildi", istatistikDinleyicisi(sayac));
  if (dinleyiciSayisi === 3) yayinlayici.abone("odunc_verildi", kutukDinleyicisi(kutuk));

  // Ureten taraf her iki durumda da ayni tek satiri calistirir.
  for (const olay of OLAYLAR) yayinlayici.yayinla(olay);

  console.log(`dinleyici=${dinleyiciSayisi}  bildirim=${kutu.length}  ` +
              `istatistik=${JSON.stringify(sayac)}  kutuk=${kutuk.length}`);
}
```

```sh
node yayin.mjs
```

```
dinleyici=2  bildirim=2  istatistik={"2025-06-20":2}  kutuk=0
dinleyici=3  bildirim=2  istatistik={"2025-06-20":2}  kutuk=2
```

Üçüncü yan etki, yayımlayan satıra dokunulmadan eklendi. Aboneliğin kurulduğu yer uygulama
katmanıdır; alan katmanı yalnız olayın kendisini üretir ve ona kimin abone olduğunu bilmez.

#### Olayın İşlem Sınırıyla İlişkisi

Yayının nereye konduğu, gevşek bağlanmadan daha ince bir sorundur. Aşağıdaki veritabanında
ödünç verme iki yazma yapar: kayıt eklenir ve kitabın stoğu bir azalır. Stok sıfırın altına
inemez; bu kısıt bazı isteklerin ikinci adımda başarısız olmasını sağlar.

```sh
# kur.sh — dersin veritabanini sifirdan kurar
rm -f kutuphane.db
sqlite3 kutuphane.db <<'SQL'
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL);
CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL,
                    stok INTEGER NOT NULL CHECK (stok >= 0));
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL,
                    uye_id INTEGER NOT NULL, alis_tarihi TEXT NOT NULL, iade_tarihi TEXT);
CREATE TABLE giden_kutusu (ileti_id INTEGER PRIMARY KEY, govde TEXT NOT NULL,
                           durum TEXT NOT NULL DEFAULT 'bekliyor');
INSERT INTO uye VALUES (1,'Ayse Demir'),(2,'Bora Kaya');
INSERT INTO kitap VALUES (1,'Korluk',1),(2,'Tutunamayanlar',0),
                         (3,'Kum Kitabi',1),(4,'Puslu Kitalar',0);
SQL
```

İlk sürüm olayı kayıt eklenir eklenmez, yani işlem sınırının içinde yayımlar.

```js
// islem-icinde.mjs — olay islem icinde yayinlanir; islem geri alinsa bile dinleyici calisti
import { DatabaseSync } from "node:sqlite";
import { OlayYayinlayici } from "./alan/olay-yayinlayici.mjs";
import { bildirimDinleyicisi, istatistikDinleyicisi } from "./uygulama/dinleyiciler.mjs";

const db = new DatabaseSync("kutuphane.db");
const kutu = [], sayac = {};
const yayinlayici = new OlayYayinlayici()
  .abone("odunc_verildi", bildirimDinleyicisi(kutu))
  .abone("odunc_verildi", istatistikDinleyicisi(sayac));

function oduncVer(uyeId, kitapId, tarih) {
  db.exec("BEGIN");
  try {
    const oduncId = Number(db.prepare(
      "INSERT INTO odunc (kitap_id, uye_id, alis_tarihi, iade_tarihi) VALUES (?,?,?,NULL)")
      .run(kitapId, uyeId, tarih).lastInsertRowid);
    yayinlayici.yayinla({ tur: "odunc_verildi", oduncId, uyeId, kitapId, tarih });
    db.prepare("UPDATE kitap SET stok = stok - 1 WHERE kitap_id = ?").run(kitapId);
    db.exec("COMMIT");
    return `verildi (odunc ${oduncId})`;
  } catch (e) {
    db.exec("ROLLBACK");
    return `geri alindi (${e.message.split("\n")[0]})`;
  }
}

const ISTEKLER = [[1, 1], [1, 2], [2, 3], [2, 4]];
for (const [uyeId, kitapId] of ISTEKLER) {
  console.log(`uye ${uyeId} kitap ${kitapId}: ${oduncVer(uyeId, kitapId, "2025-06-20")}`);
}

// Bir bildirim, veritabaninda karsiligi olmayan bir odunc icin gonderilmisse yanlistir.
const yanlis = kutu.filter((b) => db.prepare(
  "SELECT 1 FROM odunc WHERE odunc_id = ? AND kitap_id = ?").get(b.oduncId, b.kitapId) === undefined);
console.log(`odunc satiri = ${db.prepare("SELECT count(*) AS n FROM odunc").get().n}`);
console.log(`gonderilen bildirim = ${kutu.length}, yanlis bildirim = ${yanlis.length}`);
console.log(`yanlis olanlar: ${yanlis.map((b) => `odunc=${b.oduncId} kitap=${b.kitapId}`).join("  ")}`);
console.log(`istatistik = ${JSON.stringify(sayac)}`);
```

```sh
sh kur.sh
node islem-icinde.mjs
```

```
uye 1 kitap 1: verildi (odunc 1)
uye 1 kitap 2: geri alindi (CHECK constraint failed: stok >= 0)
uye 2 kitap 3: verildi (odunc 2)
uye 2 kitap 4: geri alindi (CHECK constraint failed: stok >= 0)
odunc satiri = 2
gonderilen bildirim = 4, yanlis bildirim = 2
yanlis olanlar: odunc=2 kitap=2  odunc=3 kitap=4
istatistik = {"2025-06-20":4}
```

Veritabanında iki kayıt var, gönderilen bildirim dört. İkisi hiç var olmamış ödünçler için
gitti: iki üye kendilerine verilmeyen kitapların bildirimini aldı. İstatistik de dördü
saydı; günlük rapor gerçeğin iki katını gösteriyor.

Yanlış olanlardan biri ayrıca dikkat çekicidir. `odunc=2 kitap=2` satırındaki iki numaralı
kimlik veritabanında **vardır**, ama üç numaralı kitaba aittir. Geri alınan işlemin kullandığı
kimlik serbest kaldı ve sonraki kayıt onu aldı. Bildirimi yalnız kimliğe bakarak
denetleseydik bu yanlışı göremezdik; denetim kimlik ile kitabın birlikte tutulmasına
dayanıyor.

Sorunun kaynağı yayının yerindedir. Dinleyici çağrıldığında işlem henüz kesinleşmemişti;
kesinleşmeyeceği de belli değildi. Bildirim gönderimi ise geri alınamaz — ileti gitmişse
gitmiştir. Böylece işlemin atomikliği yalnız veritabanı yazmalarını kapsadı, yan etkileri
kapsamadı.

#### Giden Kutusu

Çözüm, İşlemler konusunda kurulan düzenle aynıdır: olay, doğrudan dinleyiciye değil, aynı
işlemin içinde bir tabloya yazılır. İşlem kesinleşirse ileti orada olur, geri alınırsa
olmaz. Gönderim kesinleşmeden sonra ayrı bir adımda yapılır.

```js
// giden-kutusu.mjs — olay ayni islemde tabloya yazilir, kesinlesince dinleyiciye gider
import { DatabaseSync } from "node:sqlite";
import { OlayYayinlayici } from "./alan/olay-yayinlayici.mjs";
import { bildirimDinleyicisi, istatistikDinleyicisi } from "./uygulama/dinleyiciler.mjs";

const db = new DatabaseSync("kutuphane.db");
const kutu = [], sayac = {};
const yayinlayici = new OlayYayinlayici()
  .abone("odunc_verildi", bildirimDinleyicisi(kutu))
  .abone("odunc_verildi", istatistikDinleyicisi(sayac));

function oduncVer(uyeId, kitapId, tarih) {
  db.exec("BEGIN");
  try {
    const oduncId = Number(db.prepare(
      "INSERT INTO odunc (kitap_id, uye_id, alis_tarihi, iade_tarihi) VALUES (?,?,?,NULL)")
      .run(kitapId, uyeId, tarih).lastInsertRowid);
    const olay = { tur: "odunc_verildi", oduncId, uyeId, kitapId, tarih };
    db.prepare("INSERT INTO giden_kutusu (govde) VALUES (?)").run(JSON.stringify(olay));
    db.prepare("UPDATE kitap SET stok = stok - 1 WHERE kitap_id = ?").run(kitapId);
    db.exec("COMMIT");
    return `verildi (odunc ${oduncId})`;
  } catch (e) {
    db.exec("ROLLBACK");
    return `geri alindi (${e.message.split("\n")[0]})`;
  }
}

function gidenKutusunuBosalt() {
  const bekleyen = db.prepare(
    "SELECT ileti_id, govde FROM giden_kutusu WHERE durum = 'bekliyor' ORDER BY ileti_id").all();
  for (const i of bekleyen) {
    yayinlayici.yayinla(JSON.parse(i.govde));
    db.prepare("UPDATE giden_kutusu SET durum = 'gonderildi' WHERE ileti_id = ?").run(i.ileti_id);
  }
  return bekleyen.length;
}

const ISTEKLER = [[1, 1], [1, 2], [2, 3], [2, 4]];
for (const [uyeId, kitapId] of ISTEKLER) {
  console.log(`uye ${uyeId} kitap ${kitapId}: ${oduncVer(uyeId, kitapId, "2025-06-20")}`);
}
console.log(`giden kutusunda bekleyen = ${db.prepare(
  "SELECT count(*) AS n FROM giden_kutusu WHERE durum = 'bekliyor'").get().n}`);
console.log(`gonderilen ileti = ${gidenKutusunuBosalt()}`);

const yanlis = kutu.filter((b) => db.prepare(
  "SELECT 1 FROM odunc WHERE odunc_id = ? AND kitap_id = ?").get(b.oduncId, b.kitapId) === undefined);
console.log(`odunc satiri = ${db.prepare("SELECT count(*) AS n FROM odunc").get().n}`);
console.log(`gonderilen bildirim = ${kutu.length}, yanlis bildirim = ${yanlis.length}`);
console.log(`istatistik = ${JSON.stringify(sayac)}`);
```

```sh
sh kur.sh
node giden-kutusu.mjs
```

```
uye 1 kitap 1: verildi (odunc 1)
uye 1 kitap 2: geri alindi (CHECK constraint failed: stok >= 0)
uye 2 kitap 3: verildi (odunc 2)
uye 2 kitap 4: geri alindi (CHECK constraint failed: stok >= 0)
giden kutusunda bekleyen = 2
gonderilen ileti = 2
odunc satiri = 2
gonderilen bildirim = 2, yanlis bildirim = 0
istatistik = {"2025-06-20":2}
```

İstekler aynı, sonuçlar aynı; yanlış bildirim sayısı ikiden sıfıra indi. Geri alınan iki
işlemin giden kutusu satırı da geri alındığı için ortada gönderilecek ileti kalmadı.
İstatistik de artık iki diyor.

Kazanılan şey, olayın **kesinleşmiş bir olguyu** anlatmasıdır. İşlem içinde yayımlanan olay
bir olgu değil, bir tahmindir; giden kutusundaki olay ise veritabanına yazılmış bir gerçeğin
kaydıdır.

#### Yan Etki Ana İşlemi Belirlememeli

Yayının yeri ikinci bir sorunu daha çözer. Dinleyici doğrudan çağrılıyorsa, dinleyicinin
hatası ödünç verme işlemini de geçersiz kılar. Aşağıdaki betik bildirim servisinin bir kitap
için yanıt vermediği durumu iki düzende de dener.

```js
// dinleyici-hatasi.mjs — bildirim gonderimi hata verirse odunc kaydinin akibeti ne olur
import { DatabaseSync } from "node:sqlite";
import { rmSync } from "node:fs";
import { OlayYayinlayici } from "./alan/olay-yayinlayici.mjs";

function kur(dosya) {
  rmSync(dosya, { force: true });
  const db = new DatabaseSync(dosya);
  db.exec(`CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL,
                               uye_id INTEGER NOT NULL, alis_tarihi TEXT NOT NULL, iade_tarihi TEXT);
           CREATE TABLE giden_kutusu (ileti_id INTEGER PRIMARY KEY, govde TEXT NOT NULL,
                                      durum TEXT NOT NULL DEFAULT 'bekliyor');`);
  return db;
}

// Bildirim servisi kitap 3 icin bir kez hata veriyor.
const arizaliDinleyici = (kutu, arizali) => (olay) => {
  if (arizali.has(olay.kitapId)) throw new Error(`bildirim servisi yanit vermedi (kitap ${olay.kitapId})`);
  kutu.push(olay.oduncId);
};

const ISTEKLER = [[1, 1], [2, 3], [1, 5]];

function dogrudanYayin() {
  const db = kur("dogrudan.db");
  const kutu = [], arizali = new Set([3]);
  const y = new OlayYayinlayici().abone("odunc_verildi", arizaliDinleyici(kutu, arizali));
  for (const [uyeId, kitapId] of ISTEKLER) {
    db.exec("BEGIN");
    try {
      const oduncId = Number(db.prepare(
        "INSERT INTO odunc (kitap_id, uye_id, alis_tarihi, iade_tarihi) VALUES (?,?,'2025-06-20',NULL)")
        .run(kitapId, uyeId).lastInsertRowid);
      y.yayinla({ tur: "odunc_verildi", oduncId, uyeId, kitapId });
      db.exec("COMMIT");
    } catch { db.exec("ROLLBACK"); }
  }
  return { odunc: db.prepare("SELECT count(*) AS n FROM odunc").get().n, bildirim: kutu.length };
}

function gidenKutusuyla() {
  const db = kur("kutulu.db");
  const kutu = [], arizali = new Set([3]);
  const y = new OlayYayinlayici().abone("odunc_verildi", arizaliDinleyici(kutu, arizali));
  for (const [uyeId, kitapId] of ISTEKLER) {
    db.exec("BEGIN");
    const oduncId = Number(db.prepare(
      "INSERT INTO odunc (kitap_id, uye_id, alis_tarihi, iade_tarihi) VALUES (?,?,'2025-06-20',NULL)")
      .run(kitapId, uyeId).lastInsertRowid);
    db.prepare("INSERT INTO giden_kutusu (govde) VALUES (?)")
      .run(JSON.stringify({ tur: "odunc_verildi", oduncId, uyeId, kitapId }));
    db.exec("COMMIT");
  }
  const bosalt = () => {
    for (const i of db.prepare(
      "SELECT ileti_id, govde FROM giden_kutusu WHERE durum = 'bekliyor' ORDER BY ileti_id").all()) {
      try {
        y.yayinla(JSON.parse(i.govde));
        db.prepare("UPDATE giden_kutusu SET durum = 'gonderildi' WHERE ileti_id = ?").run(i.ileti_id);
      } catch { /* ileti bekliyor durumunda kalir, sonraki turda denenir */ }
    }
    return db.prepare("SELECT count(*) AS n FROM giden_kutusu WHERE durum = 'bekliyor'").get().n;
  };
  const ilkTur = bosalt();
  arizali.clear();                       // bildirim servisi ayaga kalkti
  const ikinciTur = bosalt();
  return { odunc: db.prepare("SELECT count(*) AS n FROM odunc").get().n,
           bildirim: kutu.length, ilkTur, ikinciTur };
}

const a = dogrudanYayin();
console.log(`dogrudan yayin : odunc satiri=${a.odunc}  bildirim=${a.bildirim}`);
const b = gidenKutusuyla();
console.log(`giden kutusu   : odunc satiri=${b.odunc}  bildirim=${b.bildirim}  ` +
            `bekleyen ileti: ilk tur=${b.ilkTur} ikinci tur=${b.ikinciTur}`);
```

```sh
node dinleyici-hatasi.mjs
```

```
dogrudan yayin : odunc satiri=2  bildirim=2
giden kutusu   : odunc satiri=3  bildirim=3  bekleyen ileti: ilk tur=1 ikinci tur=0
```

Doğrudan yayında üç isteğin ikisi kaydedildi. Üçüncü kitabın ödüncü, kitapla ya da üyeyle
ilgili hiçbir sorun olmamasına rağmen kaybedildi; kaydın geri alınmasına bildirim servisinin
erişilemez olması karar verdi. Giden kutusunda üç kayıt da yazıldı, iletinin biri ilk turda
gönderilemeyip beklemede kaldı, servis yeniden erişilebilir olunca ikinci turda gönderildi.

Ayrım, kalıcılık ile teslimin farklı güvenceler taşımasıdır. Ödünç kaydının yazılması
işlemin garantisidir; bildirimin gönderilmesi ise **sonunda** olacak bir iştir. Etkisiz
İşlemler dersindeki koşul burada da geçerlidir: bekleyen ileti yeniden denendiği için
dinleyici aynı olayı iki kez alabilir ve bunun ikinci kez etki üretmemesi gerekir.

#### Olayın Sınırları

Olay yayımı bedelsiz değildir; iki tuzağı vardır.

Birincisi **gizli denetim akışı**dır. Ödünç verme işlevini okuyan biri yayın satırını görür
ama ne olacağını göremez; abonelikler başka bir dosyadadır. Bir kural gerçekten iş akışının
zorunlu parçasıysa — örneğin kitabın durumunun güncellenmesi — onu olaya bağlamak akışı
görünmez kılar. Ölçüt şudur: **işlemin doğruluğu için gerekli olan iş dinleyiciye
verilmez.** Dinleyiciye verilen iş, yapılmasa da ödünç kaydının geçerli kaldığı iştir.

İkincisi **sıra ve zamanlama**dır. Giden kutusu düzeninde dinleyici işlemden sonra çalışır;
arada geçen sürede veri değişmiş olabilir. Dinleyici, olayın taşıdığı bilgiyle yetinmeli ya
da veriyi yeniden okuyup güncel duruma göre davranmalıdır. Olayın içine kararı verecek
bilginin konması bu yüzden yararlıdır: `oduncId` ve `kitapId` olayın içinde olduğu için
dinleyici sorgu yapmadan bildirim metnini kurabildi.

#### Özet

- Alan olayı geçmiş zamanda bir olguyu anlatır; yayımlayan taraf kimin abone olduğunu
  bilmez. Üçüncü dinleyici eklendiğinde yayımlayan satır değişmedi.
- Olay işlem içinde yayımlandığında geri alınan iki işlem için de bildirim gitti: iki kayıt
  karşılığında dört bildirim, ikisi yanlış; istatistik gerçeğin iki katını saydı.
- Geri alınan işlemin kimliği serbest kaldığı için yanlış bildirimlerden biri var olan bir
  kimliği taşıyordu; denetimin kimlik ile kitabı birlikte kontrol etmesi gerekti.
- Olay aynı işlemde giden kutusu tablosuna yazılıp kesinleşmeden sonra gönderildiğinde
  yanlış bildirim sayısı sıfıra indi.
- Dinleyici doğrudan çağrıldığında bildirim servisinin hatası geçerli bir ödünç kaydını geri
  aldırdı; giden kutusunda kayıt yazıldı, ileti beklemede kaldı ve sonraki turda gönderildi.

#### Sonraki Adım

Bu konudaki dört ders alan katmanını adım adım yalnızlaştırdı: bağımlılık yönü kuralıyla
altyapıdan, veri aktarım nesneleriyle dış sözleşmeden, doğrulama ayrımıyla girdi biçiminden,
olaylarla yan etkilerden ayrıldı. Geriye son bir soru kalıyor. Alan kuralları hâlâ bir depo
alıyor, bir işlem sınırının içinde çalışıyor ve sınanmak için bir veritabanı dosyası
bekliyor. Kuralların kendisi bunların hiçbirine ihtiyaç duyuyor mu? Kursun son dersi iş
kurallarını saf fonksiyonlarla yazar, veritabanı olmadan sınar, aynı kuralın veritabanına bağlı
sürümüyle sınama süresini karşılaştırır ve alan modülünde kalıcılık izi kalmadığını bir
taramayla gösterir.

### Kalıcılık Bağımsız Alan Modeli

Source: https://academia.sh/tr/kurslar/veri-erisim-katmani/kaliciliktan-bagimsiz-alan-modeli

İş kurallarının saf fonksiyonlarla yazılması: veritabanısız ve veritabanına bağlı iki sürümün karar eşitliği, 64 durumluk taramada süre oranı ve alan modülündeki kalıcılık izinin sayılması.

Bu konudaki dört ders alan katmanını adım adım yalnızlaştırdı: bağımlılık yönü kuralıyla
altyapıdan, veri aktarım nesneleriyle dış sözleşmeden, doğrulama ayrımıyla girdi biçiminden,
olaylarla yan etkilerden ayrıldı.

Geriye son bir bağ kalıyor. Kurallar hâlâ bir depo alıyor ve sınanmak için bir veritabanı
bekliyor. Depo bir sözleşmedir; ama kural onu **çağırdığı** sürece hangi sorguların hangi
sırayla doğacağı kuralın parçası olmayı sürdürür. Bu ders son bağı keser: kural veriyi
kendisi istemez, kendisine verilir.

#### Kuralın Girdisi Durumun Kendisi

Aşağıdaki modülde kural bir depo değil, kararı vermek için gereken bilginin tamamını taşıyan
düz bir nesne alır. Bileşen Tabanlı Arayüz Geliştirme kursunda tanımlanan **saf fonksiyon**
(pure function) tanımına uyar: aynı girdiye aynı çıktıyı verir, dışarıda bir şey değiştirmez.
Dersin dosyaları iki dizine yerleşir.

```sh
mkdir -p alan bagli
```

```js
// alan/odunc-kurallari.mjs — is kurallari saf islevler olarak
export const UYE_SINIRI = 3;
export const ODUNC_GUNU = 14;
export const GUNLUK_CEZA = 2;

export function gunEkle(tarih, gun) {
  return new Date(Date.parse(tarih) + gun * 86400000).toISOString().slice(0, 10);
}

// durum: { uyeVarMi, uyeDurumu, uyeAcikSayisi, kitapVarMi, kitapAcikMi }
export function oduncKarari(durum, bugun) {
  if (!durum.uyeVarMi) return { sonuc: "reddedildi", sebep: "uye_yok" };
  if (durum.uyeDurumu === "askida") return { sonuc: "reddedildi", sebep: "uye_askida" };
  if (!durum.kitapVarMi) return { sonuc: "reddedildi", sebep: "kitap_yok" };
  if (durum.kitapAcikMi) return { sonuc: "reddedildi", sebep: "kitap_oduncte" };
  if (durum.uyeAcikSayisi >= UYE_SINIRI) return { sonuc: "reddedildi", sebep: "uye_siniri" };
  return { sonuc: "kabul", sonTeslimTarihi: gunEkle(bugun, ODUNC_GUNU) };
}

// kayit: { sonTeslimTarihi, iadeTarihi }
export function gecikmeGunu(kayit, bugun) {
  const bitis = kayit.iadeTarihi ?? bugun;
  return Math.max(0, Math.round((Date.parse(bitis) - Date.parse(kayit.sonTeslimTarihi)) / 86400000));
}

export function ceza(kayit, bugun) {
  return gecikmeGunu(kayit, bugun) * GUNLUK_CEZA;
}
```

İki ayrıntı önemlidir. Bugünün tarihi de dışarıdan geliyor; kural sistem saatini okumaz,
yoksa çıktısı çalıştırma anına bağlı olurdu. `durum` nesnesi ise bir **anlık görüntüdür**
(snapshot): veriyi toplamak uygulama katmanının işidir, kural toplanmış bilgiyi yorumlar.

#### Veritabanısız Sınama

Kuralın sınanması için kurulacak hiçbir şey yoktur.

```js
// saf.test.mjs — kurallar veritabani olmadan sinanir
import { test } from "node:test";
import assert from "node:assert/strict";
import { oduncKarari, gecikmeGunu, ceza, gunEkle } from "./alan/odunc-kurallari.mjs";

const UYGUN = { uyeVarMi: true, uyeDurumu: "aktif", uyeAcikSayisi: 0,
                kitapVarMi: true, kitapAcikMi: false };
const BUGUN = "2025-06-20";

test("uygun durumda odunc verilir ve son teslim tarihi hesaplanir", () => {
  assert.deepEqual(oduncKarari(UYGUN, BUGUN),
                   { sonuc: "kabul", sonTeslimTarihi: "2025-07-04" });
});

test("askiya alinmis uye odunc alamaz", () => {
  assert.equal(oduncKarari({ ...UYGUN, uyeDurumu: "askida" }, BUGUN).sebep, "uye_askida");
});

test("acik kaydi olan kitap yeniden verilemez", () => {
  assert.equal(oduncKarari({ ...UYGUN, kitapAcikMi: true }, BUGUN).sebep, "kitap_oduncte");
});

test("uye siniri asilinca reddedilir, sinirin altinda kabul edilir", () => {
  assert.equal(oduncKarari({ ...UYGUN, uyeAcikSayisi: 3 }, BUGUN).sebep, "uye_siniri");
  assert.equal(oduncKarari({ ...UYGUN, uyeAcikSayisi: 2 }, BUGUN).sonuc, "kabul");
});

test("ret sebepleri oncelik sirasina gore verilir", () => {
  const hepsiBozuk = { uyeVarMi: true, uyeDurumu: "askida", uyeAcikSayisi: 9,
                       kitapVarMi: false, kitapAcikMi: true };
  assert.equal(oduncKarari(hepsiBozuk, BUGUN).sebep, "uye_askida");
});

test("gecikme ve ceza iade tarihine gore hesaplanir", () => {
  const kayit = { sonTeslimTarihi: "2025-06-15", iadeTarihi: null };
  assert.equal(gecikmeGunu(kayit, BUGUN), 5);
  assert.equal(ceza(kayit, BUGUN), 10);
  assert.equal(ceza({ ...kayit, iadeTarihi: "2025-06-14" }, BUGUN), 0);
});

test("gun ekleme ay sinirini gecer", () => {
  assert.equal(gunEkle("2025-06-25", 14), "2025-07-09");
});
```

```sh
node --test saf.test.mjs
```

```
✔ uygun durumda odunc verilir ve son teslim tarihi hesaplanir (2.14ms)
✔ askiya alinmis uye odunc alamaz (0.062375ms)
✔ acik kaydi olan kitap yeniden verilemez (0.041792ms)
✔ uye siniri asilinca reddedilir, sinirin altinda kabul edilir (0.045375ms)
✔ ret sebepleri oncelik sirasina gore verilir (0.042625ms)
✔ gecikme ve ceza iade tarihine gore hesaplanir (0.052542ms)
✔ gun ekleme ay sinirini gecer (0.032916ms)
ℹ tests 7
ℹ suites 0
ℹ pass 7
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 36.177083
```

Süre alanları makineye ve o anki yüke bağlıdır; anlamlı olan `pass 7` satırıdır. Her
sınamanın kurulumu tek bir nesne değişikliğidir.

#### Aynı Kural, Veritabanına Bağlı

Aynı kurallar veriyi kendisi çeken bir sürümle de yazıldı.

```js
// bagli/odunc-kurallari.mjs — ayni kurallar, dogrudan veritabanina bagli
import { DatabaseSync } from "node:sqlite";

export const UYE_SINIRI = 3;
export const ODUNC_GUNU = 14;
export const GUNLUK_CEZA = 2;

export class BagliKurallar {
  constructor(dosya) { this.db = new DatabaseSync(dosya); }

  oduncKarari(uyeId, kitapId, bugun) {
    const uye = this.db.prepare("SELECT durum FROM uye WHERE uye_id = ?").get(uyeId);
    if (uye === undefined) return { sonuc: "reddedildi", sebep: "uye_yok" };
    if (uye.durum === "askida") return { sonuc: "reddedildi", sebep: "uye_askida" };
    const kitap = this.db.prepare("SELECT kitap_id FROM kitap WHERE kitap_id = ?").get(kitapId);
    if (kitap === undefined) return { sonuc: "reddedildi", sebep: "kitap_yok" };
    const acik = this.db.prepare(
      "SELECT odunc_id FROM odunc WHERE kitap_id = ? AND iade_tarihi IS NULL").get(kitapId);
    if (acik !== undefined) return { sonuc: "reddedildi", sebep: "kitap_oduncte" };
    const sayi = this.db.prepare(
      "SELECT count(*) AS n FROM odunc WHERE uye_id = ? AND iade_tarihi IS NULL").get(uyeId).n;
    if (sayi >= UYE_SINIRI) return { sonuc: "reddedildi", sebep: "uye_siniri" };
    return { sonuc: "kabul", sonTeslimTarihi: this.db.prepare(
      "SELECT date(?, ?) AS t").get(bugun, `+${ODUNC_GUNU} days`).t };
  }

  ceza(oduncId, bugun) {
    const k = this.db.prepare(
      "SELECT son_teslim_tarihi, iade_tarihi FROM odunc WHERE odunc_id = ?").get(oduncId);
    const gun = this.db.prepare("SELECT max(0, julianday(coalesce(?, ?)) - julianday(?)) AS g")
      .get(k.iade_tarihi, bugun, k.son_teslim_tarihi).g;
    return Math.round(gun) * GUNLUK_CEZA;
  }
}
```

Aynı yedi sınama bu sürüm için yazıldığında kurulum büyür: her durum için şema kurulur ve
satırlar yazılır.

```js
// bagli.test.mjs — ayni kurallar; her sinama once semayi ve satirlari kurar
import { test } from "node:test";
import assert from "node:assert/strict";
import { DatabaseSync } from "node:sqlite";
import { rmSync } from "node:fs";
import { BagliKurallar } from "./bagli/odunc-kurallari.mjs";

const BUGUN = "2025-06-20";
const SEMA = `
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, durum TEXT NOT NULL);
CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL);
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL,
                    uye_id INTEGER NOT NULL, alis_tarihi TEXT NOT NULL,
                    son_teslim_tarihi TEXT NOT NULL, iade_tarihi TEXT);`;

let sayac = 0;
function kur(satirlar = []) {
  const dosya = `sinama-${sayac++}.db`;
  rmSync(dosya, { force: true });
  const db = new DatabaseSync(dosya);
  db.exec(SEMA);
  db.exec(`INSERT INTO uye VALUES (1,'Ayse Demir','aktif'),(2,'Bora Kaya','askida');
           INSERT INTO kitap VALUES (1,'Korluk'),(2,'Tutunamayanlar'),(3,'Kum Kitabi'),
                                    (4,'Puslu Kitalar');`);
  for (const s of satirlar) {
    db.prepare(`INSERT INTO odunc (kitap_id, uye_id, alis_tarihi, son_teslim_tarihi, iade_tarihi)
                VALUES (?,?,?,?,?)`).run(...s);
  }
  return new BagliKurallar(dosya);
}

test("uygun durumda odunc verilir ve son teslim tarihi hesaplanir", () => {
  assert.deepEqual(kur().oduncKarari(1, 1, BUGUN),
                   { sonuc: "kabul", sonTeslimTarihi: "2025-07-04" });
});

test("askiya alinmis uye odunc alamaz", () => {
  assert.equal(kur().oduncKarari(2, 1, BUGUN).sebep, "uye_askida");
});

test("acik kaydi olan kitap yeniden verilemez", () => {
  const k = kur([[1, 2, "2025-06-01", "2025-06-15", null]]);
  assert.equal(k.oduncKarari(1, 1, BUGUN).sebep, "kitap_oduncte");
});

test("uye siniri asilinca reddedilir, sinirin altinda kabul edilir", () => {
  const uc = [[1, 1, "2025-06-01", "2025-06-15", null], [2, 1, "2025-06-02", "2025-06-16", null],
              [3, 1, "2025-06-03", "2025-06-17", null]];
  assert.equal(kur(uc).oduncKarari(1, 4, BUGUN).sebep, "uye_siniri");
  assert.equal(kur(uc.slice(0, 2)).oduncKarari(1, 4, BUGUN).sonuc, "kabul");
});

test("ret sebepleri oncelik sirasina gore verilir", () => {
  const k = kur([[1, 2, "2025-06-01", "2025-06-15", null]]);
  assert.equal(k.oduncKarari(2, 9, BUGUN).sebep, "uye_askida");
});

test("gecikme ve ceza iade tarihine gore hesaplanir", () => {
  const k = kur([[1, 1, "2025-06-01", "2025-06-15", null],
                 [2, 1, "2025-06-01", "2025-06-15", "2025-06-14"]]);
  assert.equal(k.ceza(1, BUGUN), 10);
  assert.equal(k.ceza(2, BUGUN), 0);
});

test("gun ekleme ay sinirini gecer", () => {
  assert.equal(kur().oduncKarari(1, 1, "2025-06-25").sonTeslimTarihi, "2025-07-09");
});
```

```sh
node --test bagli.test.mjs
```

```
✔ uygun durumda odunc verilir ve son teslim tarihi hesaplanir (2.6345ms)
✔ askiya alinmis uye odunc alamaz (1.349208ms)
✔ acik kaydi olan kitap yeniden verilemez (1.527292ms)
✔ uye siniri asilinca reddedilir, sinirin altinda kabul edilir (3.39825ms)
✔ ret sebepleri oncelik sirasina gore verilir (1.243084ms)
✔ gecikme ve ceza iade tarihine gore hesaplanir (1.518125ms)
✔ gun ekleme ay sinirini gecer (1.096625ms)
ℹ tests 7
ℹ suites 0
ℹ pass 7
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 47.056833
```

İki toplam süre yakın, çünkü ikisinde de sürenin çoğunu çalışma ortamının açılışı alıyor.
Fark tek tek sınamalarda: saf sınamalar milisaniyenin onda biri altında, bağlı sınamalar bir
ile üç milisaniye arasında. Yedi sınamada bu önemsizdir; asıl soru sayı büyüdüğünde ne
olduğudur.

#### Bütün Durumların Taranması

Kuralın beş girdisinin birleşimi altmış dört durum üretiyor. Saf sürüm bu durumları doğrudan
üretebilir; bağlı sürüm her durum için veritabanını o duruma getirmek zorundadır. Aşağıdaki
betik ikisini de çalıştırır, kararları karşılaştırır ve süreyi ölçer.

```js
// tarama.mjs — ayni 64 durumun iki surumle taranmasi: karar esitligi ve sure
import { DatabaseSync } from "node:sqlite";
import { rmSync } from "node:fs";
import { oduncKarari } from "./alan/odunc-kurallari.mjs";
import { BagliKurallar } from "./bagli/odunc-kurallari.mjs";

const BUGUN = "2025-06-20";
const DURUMLAR = [];
for (const uyeVarMi of [true, false])
  for (const uyeDurumu of ["aktif", "askida"])
    for (const kitapVarMi of [true, false])
      for (const kitapAcikMi of [true, false])
        for (const uyeAcikSayisi of [0, 1, 2, 3])
          DURUMLAR.push({ uyeVarMi, uyeDurumu, kitapVarMi, kitapAcikMi, uyeAcikSayisi });

rmSync("tarama.db", { force: true });
const db = new DatabaseSync("tarama.db");
db.exec(`CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, durum TEXT NOT NULL);
         CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL);
         CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL,
                             uye_id INTEGER NOT NULL, alis_tarihi TEXT NOT NULL,
                             son_teslim_tarihi TEXT NOT NULL, iade_tarihi TEXT);`);
const bagli = new BagliKurallar("tarama.db");

function durumuKur(d) {
  db.exec("DELETE FROM odunc; DELETE FROM uye; DELETE FROM kitap;");
  if (d.uyeVarMi) db.prepare("INSERT INTO uye VALUES (1,?)").run(d.uyeDurumu);
  if (d.kitapVarMi) db.prepare("INSERT INTO kitap VALUES (1,'Korluk')").run();
  const ekle = db.prepare(`INSERT INTO odunc (kitap_id, uye_id, alis_tarihi,
                           son_teslim_tarihi, iade_tarihi)
                           VALUES (?,?,'2025-06-01','2025-06-15',NULL)`);
  if (d.kitapAcikMi) ekle.run(1, 2);                      // kitap 1 baska bir uyede
  for (let i = 0; i < d.uyeAcikSayisi; i++) ekle.run(100 + i, 1);
}

const olc = (islev) => { const t = performance.now(); islev(); return performance.now() - t; };

let safKararlar, bagliKararlar;
const safSure = olc(() => { safKararlar = DURUMLAR.map((d) => oduncKarari(d, BUGUN)); });
const bagliSure = olc(() => {
  bagliKararlar = DURUMLAR.map((d) => { durumuKur(d); return bagli.oduncKarari(1, 1, BUGUN); });
});

const ayrilan = DURUMLAR.filter((_, i) =>
  JSON.stringify(safKararlar[i]) !== JSON.stringify(bagliKararlar[i]));

console.log(`durum sayisi = ${DURUMLAR.length}`);
console.log(`iki surumun ayrildigi durum = ${ayrilan.length}`);
console.log(`saf surum   : ${safSure.toFixed(1)} ms  (durum basina ${(safSure / DURUMLAR.length).toFixed(4)} ms)`);
console.log(`bagli surum : ${bagliSure.toFixed(1)} ms  (durum basina ${(bagliSure / DURUMLAR.length).toFixed(4)} ms)`);
console.log(`oran = ${(bagliSure / safSure).toFixed(0)}x`);
```

```sh
node tarama.mjs
```

```
durum sayisi = 64
iki surumun ayrildigi durum = 0
saf surum   : 0.6 ms  (durum basina 0.0097 ms)
bagli surum : 71.4 ms  (durum basina 1.1153 ms)
oran = 115x
```

Birinci sonuç doğrulukla ilgilidir: altmış dört durumun hiçbirinde iki sürüm ayrılmadı. Saf
sürüm bir sadeleştirme değil, aynı kuralın başka bir yazımıdır.

İkincisi süredir. Mutlak değerler makineye ve dosya sistemine bağlıdır; aynı betiğin ikinci
çalıştırmasında oran 127 kat çıktı. Değişmeyen, oranın büyüklük mertebesidir: durum başına
maliyet iki basamak farklıdır. Pratik karşılığı, sınanacak durum sayısının serbest
kalmasıdır. Kural karmaşıklaştıkça durum sayısı katlanarak büyür ve bu fark, hangi
durumların sınanacağını belirlemeye başlar.

#### Kalıcılık İzinin Ölçülmesi

Bağımsızlık iddiası taranarak denetlenir. Depo Kalıbı dersindeki sayım burada üç ölçüte
ayrılıyor: SQL deyimi, tablo adı ve sürücü ithali.

```js
// iz-tara.mjs — modul basina kalicilik izi: SQL, tablo adi, surucu ithali
import { readFileSync } from "node:fs";

const SQL = /\b(SELECT|INSERT INTO|UPDATE|DELETE FROM|CREATE TABLE)\b/g;
const TABLO = /\b(?:FROM|INTO|UPDATE|TABLE)\s+([a-z_]+)/g;
const SURUCU = /node:sqlite/g;

const DOSYALAR = ["alan/odunc-kurallari.mjs", "saf.test.mjs",
                  "bagli/odunc-kurallari.mjs", "bagli.test.mjs"];

console.log("dosya                        SQL  tablo  surucu  tablo adlari");
for (const dosya of DOSYALAR) {
  const metin = readFileSync(dosya, "utf8");
  const sql = (metin.match(SQL) ?? []).length;
  const tablolar = [...new Set([...metin.matchAll(TABLO)].map((m) => m[1]))].sort();
  const surucu = (metin.match(SURUCU) ?? []).length;
  console.log(`${dosya.padEnd(28)}${String(sql).padStart(3)}  ${String(tablolar.length).padStart(5)}` +
              `  ${String(surucu).padStart(6)}  ${tablolar.join(", ")}`);
}
```

```sh
node iz-tara.mjs
```

```
dosya                        SQL  tablo  surucu  tablo adlari
alan/odunc-kurallari.mjs      0      0       0  
saf.test.mjs                  0      0       0  
bagli/odunc-kurallari.mjs     7      3       1  kitap, odunc, uye
bagli.test.mjs                6      3       1  kitap, odunc, uye
```

Alan modülünde üç ölçüt de sıfır. Sınama dosyasında da sıfır olması ayrıca anlamlıdır:
kuralın sınanması için kalıcılık bilgisi gerekmedi. Bağlı sürümde tablo
adları hem kural modülüne hem sınama dosyasına yayılmış; şemada `odunc` tablosuna bir sütun
eklendiğinde iki dosya birden etkilenir.

Ölçüm kaba bir araçtır. Bir modül SQL yazmadan da kalıcılığa bağlanabilir: satır sırasının
veritabanının döndürdüğü sıra olduğunu varsaymak ya da kimliğin veritabanı tarafından
üretileceğini beklemek de birer bağımlılıktır. Sayının işlevi, bağın nereye kadar çekildiğini
görünür kılmaktır.

#### Anlık Görüntünün Sınırı

Saf kural bedelsiz değildir; anlık görüntüyü toplama sorumluluğunu uygulama katmanına taşır.
İki tuzağı vardır: kararın kullanmayacağı alanların da çekilmesi (Aşırı Veri Çekme dersindeki
maliyet) ve anlık görüntünün bayatlaması — okunduğu andaki durum, yazma yapılana kadar
değişmiş olabilir. İkincisi Doğrulama Katmanları dersindeki denetle–sonra–yaz açığıdır;
çözümü aynıdır: karar ile yazma aynı işlem sınırında kalır, kritik değişmezler veritabanı
kısıtıyla güvenceye alınır.

Karşı uç da vardır. Alan modeli yalnızca veri taşıyan, hiçbir karar vermeyen bir kabuğa
indirgenirse kurallar yeniden çağrı noktalarına dağılır. Ölçüt şudur: **kararı veren kod ile
kararın dayandığı veriyi tanımlayan kod aynı modülde olmalıdır.**

#### Özet

- Kural, depo çağırmak yerine kararı vermek için gereken bilgiyi taşıyan bir anlık görüntü
  alır; bugünün tarihi de dışarıdan geldiği için işlev saf kalır.
- Yedi sınama veritabanı, şema ve bağlantı olmadan çalıştı; her kurulum tek bir nesne
  değişikliğidir.
- Altmış dört durumun tamamında saf sürüm ile bağlı sürüm aynı kararı verdi; saf sürüm bir
  sadeleştirme değil, aynı kuralın başka yazımıdır.
- Durum başına maliyet iki basamak farklı çıktı (ölçülen oran 115 ve 127 kat); mutlak süreler
  ortama bağlıdır, değişmeyen büyüklük mertebesidir.
- Alan modülünde ve sınamasında SQL, tablo adı ve sürücü ithali sayısı sıfır; bağlı sürümde
  tablo adları hem kural hem sınama dosyasına yayılmış durumda.

#### Kurs Kapanışı

Kurs veri erişimini dört soruyla ele aldı ve her birini ölçerek yanıtladı.

**Veri erişim yaklaşımları** sorgunun nerede yazılacağını sordu: doğrudan sorgu ile eşleyici
ödünleşimi, nesne–ilişki uyumsuzluğu, kararları tek yerde toplayan depo, ölçütü nesneleştiren
belirtimler, bağlantı havuzunun doygunluk davranışı ve genişlet–daralt göçü.

**İşlemler ve tutarlılık** iş biriminin sınırını sordu: sınırın servis katmanında tutulması,
yalıtım düzeyinin uygulamadan görünen etkisi, iyimser ve kötümser kilitlemenin deneme ve
süre farkı, atomikliğin servis sınırını geçememesi ve yeniden denemeyi güvenli kılan etkisiz
biçimler.

**Başarım sorunları** katmanın maliyetini sordu: N+1 sorgu sorunu, aşırı veri çekme, toplu
işlemler, uygulamadan görülen sorgu planı ve derin sayfalamanın maliyeti.

**İş mantığı yerleşimi** ise kuralın nerede duracağını sordu. Bağımlılık yönü ithal grafiği
üzerinden denetlendi, dış sözleşme iç modelden ayrıldı, girdi doğrulaması ile alan kuralı
ürettikleri yanıt ve maliyetle ayrıştırıldı, yan etkiler olaylara bağlandı ve kurallar
kalıcılıktan koparıldı.

Kursun bıraktığı yerde bir varsayım duruyor: her istek verinin kaynağına gidiyor ve her iş
isteğin içinde bitiyor. İkisi de her zaman tutmaz. Aynı sorgunun yanıtı defalarca
üretiliyorsa saklanabilir; bildirim gönderme, rapor üretme, dosya işleme gibi işler isteğin
yanıtını bekletmeden yürütülebilir. Alan Olayları dersindeki giden kutusu bunun ilk adımıydı.

Sonraki kurs olan **Önbellekleme, Kuyruklar ve Eşzamansız İşleme** bu iki yolu ele alır:
önbellek katmanları ve geçersizleştirme stratejileri, mesaj kuyruğu ile akış arasındaki fark,
arka plan işlerinin yeniden deneme ve ölü mektup kutusuyla dayanıklı kurulması. Bu kursta
ölçülen kavramlar orada yeniden karşımıza çıkar: işlem sınırı, etkisizlik ve kalıcılıktan
bağımsız kurallar, eşzamansız işlemenin ön koşuludur.

