Ders 04 / 21
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ı.
İçindekiler
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.
// 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.
// 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)); }
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.
// 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.
// 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.
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.
// 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);
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.
// 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);
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.
// 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);
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.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.