İçeriğe geç
academia.sh

Ders 01 / 21

Doğrudan Sorgu ve Eşleyici

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.

İçindekiler

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.

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','[email protected]','2023-02-14'),
  (2,'Mehmet','Kaya','[email protected]','2023-05-30'),(3,'Zeynep','Arslan',NULL,'2024-01-09'),
  (4,'Emre','Yıldız','[email protected]','2024-03-22'),(5,'Selin','Aydın',NULL,'2024-11-05'),
  (6,'Burak','Şahin','[email protected]','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.

// 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);
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.

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

// 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, " "));
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.

// 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));
}
// 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);
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.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat