Ders 14 / 34
Filtreleme, Sıralama ve Arama
Sorgu parametrelerinin tasarımı: süzgeç ve sıralama adlarının izin listesiyle sütuna çevrilmesi, istemci değerlerinin bağlı değişkenle taşınması, izin listesi dışındaki isteğin reddedilmesi ve sıralama bağlarının sayfalamada kayıt kaybına yol açması.
İçindekiler
Sayfalama dersi koleksiyonun ne kadarının verileceğini çözdü. Hangi kayıtların verileceği ve hangi sırayla verileceği açık kaldı. Kütüphane görevlisi katalogda “Aho’nun kitapları”, “1990 sonrası basımlar”, “adında Ağ geçenler” diye arar; sonucu yıla ya da yazara göre sıralı ister.
Bunların hepsi sorgu bölümünde taşınır ve hepsi SQL’e dönüşür. Dönüşüm iki yerde tasarım kararı ister: istemciden gelen ad hangi sütuna karşılık gelecek, istemciden gelen değer sorguya nasıl girecek. Bu iki soruyu karıştırmamak, bu dersin ana ayrımıdır.
Ad ile Değerin Ayrılması
Bir sorgu parametresi iki farklı türde bilgi taşıyabilir. ?yazar=Aho isteğinde yazar
bir addır — hangi sütuna bakılacağını söyler — Aho ise bir değerdir. İkisi SQL’e aynı
yoldan giremez.
Değer, sorgu metnine hiç girmez; bağlı değişken olarak ayrı taşınır. Motor sorgu metnini bir kez çözümler, değeri sonra yerleştirir; değerin içeriği sorgunun yapısını değiştiremez. İleri SQL kursundaki Dinamik SQL Riskleri dersi bunun neden zorunlu olduğunu ayrıntılandırır: değerler dizgi birleştirmeyle sorgu metnine yazıldığında, değerin içindeki karakterler sorgu yapısının parçası hâline gelir.
Ad ise bağlı değişken olamaz — sütun adı sorgunun yapısıdır, değeri değil. Bu yüzden adlar için başka bir düzenek gerekir: izin listesi. İstemciden gelen ad, sunucudaki sabit bir eşlemede aranır; eşlemede yoksa istek reddedilir. Böylece sorgu metnine yalnızca sunucunun kendi yazdığı sütun adları girer.
// filtre-sunucusu.mjs — sorgu parametrelerini izin listesi ve bagli degiskenle SQL'e tasir import { createServer } from "node:http"; import { DatabaseSync } from "node:sqlite"; const db = new DatabaseSync("kutuphane.db"); const yanitla = (yanit, kod, nesne) => { yanit.writeHead(kod, { "content-type": "application/json; charset=utf-8" }); yanit.end(JSON.stringify(nesne)); }; // Izin listeleri: istemciden gelen ad, sutun adina burada cevrilir. const SUZGECLER = { yazar: "yazar = ?", yilEnAz: "yil >= ?", sube: "sube = ?", ara: "ad LIKE ?", }; const SIRALAMALAR = { yil: "yil", ad: "ad", yazar: "yazar" }; const sunucu = createServer((istek, yanit) => { const adres = new URL(istek.url, "http://127.0.0.1"); if (adres.pathname !== "/kitaplar") return yanitla(yanit, 404, { hata: "yol_yok" }); const s = adres.searchParams; const boyut = Math.min(Number(s.get("boyut") ?? 3), 50); // Suzgecler: yalnizca izin listesindeki adlar kosula donusur. const kosullar = [], degerler = []; for (const [ad, kalip] of Object.entries(SUZGECLER)) { const deger = s.get(ad); if (deger === null) continue; kosullar.push(kalip); degerler.push(ad === "ara" ? `%${deger}%` : deger); // deger, sorgu metnine girmez } // Siralama: izin listesinde yoksa istek reddedilir, varsayilana dusulmez. const siralaAdi = s.get("sirala") ?? "yil"; const sutun = SIRALAMALAR[siralaAdi]; if (!sutun) return yanitla(yanit, 422, { hata: "dogrulama", alan: "sirala", izinli: Object.keys(SIRALAMALAR), }); // Imlec: gevsek kip yalnizca siralama sutununu, kesin kip benzersizlestiriciyi de tasir. const imlec = s.get("imlec"); const kesin = s.get("kip") !== "gevsek"; if (imlec !== null) { if (kesin) { const [d, k] = imlec.split("|"); kosullar.push(`(${sutun}, isbn) > (?, ?)`); degerler.push(d, k); } else { kosullar.push(`${sutun} > ?`); degerler.push(imlec); } } const nerede = kosullar.length ? `WHERE ${kosullar.join(" AND ")}` : ""; const sira = kesin ? `${sutun}, isbn` : sutun; const satirlar = db.prepare( `SELECT isbn, ad, yazar, yil FROM kitap ${nerede} ORDER BY ${sira} LIMIT ?` ).all(...degerler, boyut); const son = satirlar.at(-1); yanitla(yanit, 200, { veri: satirlar.map((r) => `${r.yil} ${r.isbn}`), sayfalama: { sonrakiImlec: son ? (kesin ? `${son[sutun]}|${son.isbn}` : String(son[sutun])) : null, }, }); }); sunucu.listen(8481, "127.0.0.1", () => console.log("filtre sunucusu 127.0.0.1:8481"));
Sorgu metninin nasıl kurulduğuna dikkat edin. kosullar dizisine giren her parça
sunucunun kendi yazdığı sabit bir dizgidir; istemciden gelen hiçbir karakter oraya
ulaşmaz. İstemcinin gönderdiği her şey degerler dizisindedir ve sorguya bağlı değişken
olarak verilir.
Katalog, sıralama bağlarını görebilmek için genişletilir:
-- katalog.sql — siralama baglarini iceren genisletilmis katalog DELETE FROM kitap; INSERT INTO kitap (isbn, ad, yazar, yil, sube) VALUES ('K-01','Bilgisayar Aglari','Tanenbaum',1978,'S-01'), ('K-02','Veri Yapilari','Aho',1983,'S-01'), ('K-03','Derleyiciler','Aho',1983,'S-02'), ('K-04','Isletim Sistemleri','Tanenbaum',1983,'S-02'), ('K-05','Kabuk Betikleri',"O'Reilly",1986,'S-01'), ('K-06','Cizge Algoritmalari','Sedgewick',1990,'S-01'), ('K-07','Sayisal Yontemler','Press',1990,'S-02'), ('K-08','Ag Programlama','Stevens',1990,'S-01'), ('K-09','Bicimsel Diller','Hopcroft',1995,'S-02');
Sayfaları dolaşan istemci de ayrı bir dosyadır; imleci kendisi kurmaz, yanıttan alır.
// yuru.mjs — imleci izleyerek butun sayfalari dolasan istemci // Kullanim: node yuru.mjs <gevsek|kesin> const KIP = process.argv[2]; let imlec = null, sayfa = 0; const toplananlar = []; while (sayfa < 10) { const adres = new URL("http://127.0.0.1:8481/kitaplar"); adres.searchParams.set("kip", KIP); adres.searchParams.set("sirala", "yil"); if (imlec !== null) adres.searchParams.set("imlec", imlec); const yanit = await (await fetch(adres)).json(); if (yanit.veri.length === 0) break; sayfa++; toplananlar.push(...yanit.veri); console.log(`${KIP.padEnd(7)} sayfa ${sayfa}: ${yanit.veri.join(" ")}`); imlec = yanit.sayfalama.sonrakiImlec; } console.log(`${KIP.padEnd(7)} toplam : ${toplananlar.length} satir\n`);
# Izin listesi, bagli degisken ve siralama baginin sayfalamaya etkisi. rm -f kutuphane.db && sqlite3 kutuphane.db < sema.sql && sqlite3 kutuphane.db < katalog.sql node filtre-sunucusu.mjs & sunucu=$! sleep 0.4 al() { curl -sS -G "http://127.0.0.1:8481/kitaplar" "$@"; echo; } echo "--- suzgec ve izin listesi ---" printf 'yazar=Aho : '; al --data-urlencode "yazar=Aho" printf "yazar=O'Reilly : "; al --data-urlencode "yazar=O'Reilly" printf 'ara=Ag : '; al --data-urlencode "ara=Ag" printf 'sirala=sube : '; al --data-urlencode "sirala=sube" echo "--- siralama bagi olan sutunda sayfalama (katalogda 9 kitap var) ---" node yuru.mjs gevsek node yuru.mjs kesin kill $sunucu
filtre sunucusu 127.0.0.1:8481
--- suzgec ve izin listesi ---
yazar=Aho : {"veri":["1983 K-02","1983 K-03"],"sayfalama":{"sonrakiImlec":"1983|K-03"}}
yazar=O'Reilly : {"veri":["1986 K-05"],"sayfalama":{"sonrakiImlec":"1986|K-05"}}
ara=Ag : {"veri":["1978 K-01","1990 K-08"],"sayfalama":{"sonrakiImlec":"1990|K-08"}}
sirala=sube : {"hata":"dogrulama","alan":"sirala","izinli":["yil","ad","yazar"]}
--- siralama bagi olan sutunda sayfalama (katalogda 9 kitap var) ---
gevsek sayfa 1: 1978 K-01 1983 K-02 1983 K-03
gevsek sayfa 2: 1986 K-05 1990 K-06 1990 K-07
gevsek sayfa 3: 1995 K-09
gevsek toplam : 7 satir
kesin sayfa 1: 1978 K-01 1983 K-02 1983 K-03
kesin sayfa 2: 1983 K-04 1986 K-05 1990 K-06
kesin sayfa 3: 1990 K-07 1990 K-08 1995 K-09
kesin toplam : 9 satir
Değerin Sorgu Metnine Girmemesi
İkinci satır bu ayrımın en somut kanıtıdır. O'Reilly değeri kesme işareti içerir ve SQL
dizgi sabitlerini bitiren karakter tam olarak odur. Değer sorgu metnine yazılsaydı, sorgu
o noktada bölünür ve kalan bölüm SQL olarak okunurdu. Bağlı değişkenle taşındığında böyle
bir şey olmaz: değer yalnızca değerdir, kaydı bulur ve döndürür.
Bu, kesme işaretli adların “kaçırılması” sorunu değildir. Kaçış karakteri eklemek de bir çözüm gibi görünür, ama her veri tipi ve her lehçe için ayrı kural gerektirir ve bir yerde unutulur. Bağlı değişkende unutulacak bir şey yoktur; sorgu metni istemciden bağımsız olarak sabittir.
Üçüncü satırdaki ara=Ag süzgeci aynı ilkeyi bir adım öteye taşır. Joker karakterler
sunucuda eklenir — istemcinin gönderdiği Ag değeri %Ag% hâline getirilir — böylece
istemci örüntü yazamaz, yalnızca aranacak metni verir.
İzin Listesi Dışını Reddetmek
Dördüncü satır sirala=sube isteğine 422 döndü ve izinli adları yanıtta bildirdi.
Buradaki iki karar da bilinçlidir.
Birincisi, istek reddedildi; sessizce varsayılan sıralamaya düşülmedi. Varsayılana düşmek, istemcinin yanlış bir ad gönderdiğini hiç öğrenmemesi demektir; ekran yanlış sırada görünür ve kimse nedenini aramaz. Durum Kodu Seçimi dersindeki ölçüt burada da geçerlidir: kabul edilmeyen bir alan değeri 422 ile bildirilir.
İkincisi, yanıt izinli adları sayıyor. Bu, sözleşmenin kendini anlatan bir parçasıdır; istemciyi belgeye gitmeden düzeltmeye yönlendirir.
İzin listesi aynı zamanda sıralamanın maliyetini denetim altında tutar. Her sütuna göre sıralamaya izin verilirse, dizini olmayan bir sütuna göre sıralama isteği tabloyu baştan sona okutur. Listeye yalnızca desteklenmesi kararlaştırılmış — ve gerekiyorsa dizinlenmiş — sütunlar konur.
Sıralama Bağı Kayıt Kaybettirir
Çıktının son bölümü sayfalama dersine geri bağlanır. Katalogda dokuz kitap var; gevşek kip yedisini gösterdi. K-04 ve K-08 hiçbir sayfada görünmedi.
Neden ölçülebilir biçimde ortadadır. Gevşek kipte imleç yalnızca yılı taşır. Birinci sayfa
1983 yılında biter ve imleç 1983 olur; ikinci sayfa yil > 1983 koşuluyla gelir ve
aynı yıla sahip K-04 kaydı bu koşulun dışında kalır. Aynı şey 1990 yılında yinelenir ve
K-08 kaybolur. Sıralama anahtarı benzersiz olmadığı sürece, “bu değerden sonrası” ölçütü
bağların bir bölümünü hep dışarıda bırakır.
Kesin kipte imleç iki alan taşır: sıralama sütunu ve benzersiz kimlik. Koşul da iki alan üzerinde çalışır, sıralama da. Aynı yıla sahip kayıtlar arasında kesin bir sıra kurulduğu için “bu noktadan sonrası” tanımlıdır ve dokuz kaydın dokuzu da görünür.
Buradan iki kural çıkar. Sıralama ölçütü ne olursa olsun, sonuna benzersiz bir alan eklenir — belirlenimci sıralama koşulu budur. İmleç de sıralama ölçütünün tamamını taşır: tek alanlık bir imleç, tek alanlık bir sıralamayı anlatabilir.
Özet
- Sorgu parametresi iki tür bilgi taşır: ad ve değer. Ad izin listesiyle sütuna çevrilir, değer bağlı değişkenle taşınır; ikisi SQL’e aynı yoldan girmez.
- Sorgu metnine yalnızca sunucunun yazdığı sabit parçalar girer; istemciden gelen hiçbir karakter sorgu yapısına ulaşmaz. Kesme işareti içeren bir yazar adı bu yüzden sorunsuz bulunur.
- Arama örüntüleri sunucuda kurulur; istemci aranacak metni verir, joker karakteri değil.
- İzin listesi dışındaki sıralama isteği 422 ile reddedilir ve izinli adlar yanıtta bildirilir; sessizce varsayılana düşmek hatayı görünmez kılar.
- İzin listesi sıralamanın maliyetini de sınırlar: listeye yalnızca desteklenmesi kararlaştırılmış sütunlar konur.
- Sıralama anahtarı benzersiz değilse imleç tabanlı sayfalama bağların bir bölümünü atlar; ölçümde dokuz kayıttan yedisi göründü. Sıralamaya ve imlece benzersiz bir alan eklenmesi bu kaybı ortadan kaldırır.
Sonraki Adım
Hangi kayıtların döneceği ve hangi sırayla döneceği çözüldü. Geriye her kaydın ne kadarının döneceği kaldı. Katalog listesi ekranı yalnızca kitap adı ve yazarı gösterirken sunucu her satırda tüm alanları gönderiyor; ödünç geçmişi ekranı ise tek bir yanıtta bulamadığı bilgiler için ayrı ayrı istek atıyor. İki sorun da aynı yerden doğar: gösterimin ayrıntı düzeyine sunucu tek başına karar veriyor. Sonraki ders istemcinin alan seçmesine izin veren düzeneği kurar ve aktarılan bayt miktarındaki farkı ölçer.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.