Ders 08 / 34
Kaynak Modelleme
Alan kavramlarının adreslenebilir kaynağa dönüşmesi: eylem adlarıyla büyüyen uç nokta listesi, işlemin durum değişimi olarak modellenmesi, hiçbir tabloya karşılık gelmeyen hesaplanmış kaynaklar ve kimlik seçiminin bedeli.
İçindekiler
API Biçemleri konusu seçim işini bitirdi: kaynak temelli, uzak yordam çağrısı ve sorgu temelli yaklaşımların hangi kısıt altında birbirine üstün geldiği, eşzamanlı ve eşzamansız iletişimin nerede ayrıştığı kuruldu. Kütüphane ödünç servisi için karar kaynak temelli biçemden yana verildiğinde iş bitmez; asıl tasarım o noktada başlar. Biçem, “kaynakları adresle ve tek biçim arayüzle konuş” der, ama hangi şeyin kaynak olduğunu söylemez.
Bu ders o boşluğu doldurur. Elde bir alan vardır — kitap, üye, ödünç işlemi, şube, gecikme ücreti, ödünç süresi — ve bu kavramların hepsi aynı türden değildir. Bazıları adreslenir, bazıları yalnızca bir kaynağın içinde bir alan olarak yaşar, bazıları da hiçbir tabloda satırı olmadan kaynak olur. Ayrımı yapan ölçüt kimliktir.
Kaynak Nedir, Ne Değildir
Kaynak (resource), istemcinin ayrı ayrı başvurmak isteyeceği ve zaman içinde aynı kalan bir kimliği olan şeydir. Bu tanımın iki yarısı da işlevseldir. Kimliği olmayan bir şey adreslenemez; başvurulmayan bir şeyin adreslenmesi de yalnız yüzey büyütür.
Ödünç servisinde ölçütü uygulayalım. Kitap kaynaktır: katalogda tek başına aranır, ayrıntısı gösterilir, güncellenir. Üye kaynaktır. Ödünç işlemi kaynaktır — bu, ilk bakışta bir eylem gibi görünse de kendi kimliği, başlangıcı, bitişi ve tarihçesi olan bir şeydir. Şube kaynaktır. Buna karşılık ödünç süresi kaynak değildir: on dört günlük süre bir kuraldır, ödünç kaydının içinde ya da servisin yapılandırmasında yaşar. Gecikme ücreti de tek başına kaynak değildir; bir üyenin borcunun bileşenidir.
Kaynağın kendisi soyuttur. İstemciye giden JSON gövde, kaynağın o andaki gösterimidir (representation). Aynı kaynağın birden çok gösterimi olabilir: özet liste satırı ile ayrıntı sayfası aynı ödünç kaydını farklı ayrıntı düzeyinde anlatır. Kaynak ile gösterimi ayırmak, sonraki derslerde alan seçimi ve kısmi yanıt konularını mümkün kılan ayrımdır.
Eylemleri Adres Adı Yapmanın Bedeli
Kaynak düşüncesi olmadan bir servis yazıldığında adresler eylem adlarına dönüşür:
/oduncVer, /oduncIade, /uyeninOduncleri. Her yeni istemci ihtiyacı yeni bir yol açar.
Aşağıdaki iki sunucu aynı verinin üstünde aynı soruları yanıtlar; farkları adres kümesinin
nasıl büyüdüğüdür.
// eylem-sunucusu.mjs — her istemci ihtiyacina bir yol acan tasarim import { createServer } from "node:http"; const KITAPLAR = [ { isbn: "978-0201896831", ad: "Programlama Sanati", yazar: "Knuth", sube: "S-01" }, { isbn: "978-0262033848", ad: "Algoritmalara Giris", yazar: "Cormen", sube: "S-02" }, ]; const ODUNCLER = [ { id: 1, uye: "U-1001", isbn: "978-0262033848", iade: null }, { id: 2, uye: "U-1002", isbn: "978-0201896831", iade: "2026-03-04" }, ]; const yanitla = (yanit, kod, nesne) => { yanit.writeHead(kod, { "content-type": "application/json; charset=utf-8" }); yanit.end(JSON.stringify(nesne)); }; // Her satir bir istemci ihtiyaci; ihtiyac artinca satir sayisi artar. const YOLLAR = { "/kitapListesi": () => KITAPLAR, "/kitapDetay": (s) => KITAPLAR.find((k) => k.isbn === s.get("isbn")) ?? null, "/kitapEkle": () => ({ sonuc: "eklendi" }), "/kitapSil": () => ({ sonuc: "silindi" }), "/oduncListesi": () => ODUNCLER, "/oduncDetay": (s) => ODUNCLER.find((o) => o.id === Number(s.get("id"))) ?? null, "/oduncVer": () => ({ sonuc: "verildi" }), "/oduncIade": () => ({ sonuc: "iade alindi" }), "/uyeninOduncleri": (s) => ODUNCLER.filter((o) => o.uye === s.get("uye")), "/subedekiKitaplar": (s) => KITAPLAR.filter((k) => k.sube === s.get("sube")), "/acikOduncler": () => ODUNCLER.filter((o) => o.iade === null), }; const sunucu = createServer((istek, yanit) => { const adres = new URL(istek.url, "http://127.0.0.1"); if (adres.pathname === "/yollar") return yanitla(yanit, 200, { sayi: Object.keys(YOLLAR).length }); const isleyici = YOLLAR[adres.pathname]; if (!isleyici) return yanitla(yanit, 404, { hata: "yol_yok" }); yanitla(yanit, 200, isleyici(adres.searchParams)); }); sunucu.listen(8471, "127.0.0.1", () => console.log("eylem sunucusu 127.0.0.1:8471"));
// kaynak-sunucusu.mjs — ayni ihtiyaclari kaynak adresleriyle karsilayan tasarim import { createServer } from "node:http"; const KITAPLAR = [ { isbn: "978-0201896831", ad: "Programlama Sanati", yazar: "Knuth", sube: "S-01" }, { isbn: "978-0262033848", ad: "Algoritmalara Giris", yazar: "Cormen", sube: "S-02" }, ]; const ODUNCLER = [ { id: 1, uye: "U-1001", isbn: "978-0262033848", iade: null }, { id: 2, uye: "U-1002", isbn: "978-0201896831", iade: "2026-03-04" }, ]; const yanitla = (yanit, kod, nesne) => { yanit.writeHead(kod, { "content-type": "application/json; charset=utf-8" }); yanit.end(JSON.stringify(nesne)); }; // Uc adres kalibi; suzme olcutleri sorgu bolumunde tasinir. const KALIPLAR = [ [/^\/kitaplar$/, (e, s) => { const sube = s.get("sube"); return sube ? KITAPLAR.filter((k) => k.sube === sube) : KITAPLAR; }], [/^\/kitaplar\/([\w-]+)$/, (e) => KITAPLAR.find((k) => k.isbn === e[1]) ?? null], [/^\/oduncler$/, (e, s) => { let sonuc = ODUNCLER; if (s.get("uye")) sonuc = sonuc.filter((o) => o.uye === s.get("uye")); if (s.get("durum") === "acik") sonuc = sonuc.filter((o) => o.iade === null); return sonuc; }], [/^\/oduncler\/(\d+)$/, (e) => ODUNCLER.find((o) => o.id === Number(e[1])) ?? null], ]; const sunucu = createServer((istek, yanit) => { const adres = new URL(istek.url, "http://127.0.0.1"); if (adres.pathname === "/kaliplar") return yanitla(yanit, 200, { sayi: KALIPLAR.length }); for (const [kalip, isleyici] of KALIPLAR) { const eslesme = kalip.exec(adres.pathname); if (eslesme) return yanitla(yanit, 200, isleyici(eslesme, adres.searchParams)); } yanitla(yanit, 404, { hata: "yol_yok" }); }); sunucu.listen(8472, "127.0.0.1", () => console.log("kaynak sunucusu 127.0.0.1:8472"));
Her iki sunucu da uzun ömürlüdür; aşağıdaki betik ikisini de başlatır, aynı soruları sorar ve sonunda durdurur.
# Iki sunucuyu sirayla baslatir, ayni dort soruyu her ikisine sorar, durdurur. node eylem-sunucusu.mjs & eylem=$! sleep 0.4 node kaynak-sunucusu.mjs & kaynak=$! sleep 0.4 sor() { printf '%-22s %s\n' "$1" "$(curl -sS "$2")"; } echo "--- eylem tabanli ---" sor "sube S-01 kitaplari" "http://127.0.0.1:8471/subedekiKitaplar?sube=S-01" sor "U-1001 oduncleri" "http://127.0.0.1:8471/uyeninOduncleri?uye=U-1001" sor "acik oduncler" "http://127.0.0.1:8471/acikOduncler" sor "yol sayisi" "http://127.0.0.1:8471/yollar" echo "--- kaynak tabanli ---" sor "sube S-01 kitaplari" "http://127.0.0.1:8472/kitaplar?sube=S-01" sor "U-1001 oduncleri" "http://127.0.0.1:8472/oduncler?uye=U-1001" sor "acik oduncler" "http://127.0.0.1:8472/oduncler?durum=acik" sor "adres kalibi sayisi" "http://127.0.0.1:8472/kaliplar" kill $eylem $kaynak
eylem sunucusu 127.0.0.1:8471
kaynak sunucusu 127.0.0.1:8472
--- eylem tabanli ---
sube S-01 kitaplari [{"isbn":"978-0201896831","ad":"Programlama Sanati","yazar":"Knuth","sube":"S-01"}]
U-1001 oduncleri [{"id":1,"uye":"U-1001","isbn":"978-0262033848","iade":null}]
acik oduncler [{"id":1,"uye":"U-1001","isbn":"978-0262033848","iade":null}]
yol sayisi {"sayi":11}
--- kaynak tabanli ---
sube S-01 kitaplari [{"isbn":"978-0201896831","ad":"Programlama Sanati","yazar":"Knuth","sube":"S-01"}]
U-1001 oduncleri [{"id":1,"uye":"U-1001","isbn":"978-0262033848","iade":null}]
acik oduncler [{"id":1,"uye":"U-1001","isbn":"978-0262033848","iade":null}]
adres kalibi sayisi {"sayi":4}
Yanıt gövdeleri satır satır aynı; sayılar aynı değil. On bir yola karşılık dört adres kalıbı. Fark, kalıpların istemci ihtiyacını değil kaynağı adlandırmasından doğar. “Şubedeki kitaplar” ve “açık ödünçler” ayrı kavramlar değildir; ikisi de var olan bir koleksiyonun süzülmüş görünümüdür ve süzme ölçütü adresin sorgu bölümünde taşınır.
Sayının kendisi asıl sorun değildir. Asıl sorun, eylem tabanlı listenin kapalı olmamasıdır. Bir ekran “geciken ödünçler” isterse liste on ikiye çıkar; başka bir ekran “şubedeki geciken ödünçler” isterse on üçe. Kaynak tabanlı tarafta yeni bir sorgu ölçütü eklenir, adres kümesi aynı kalır. Uç nokta sayısıyla ihtiyaç sayısı arasındaki bu bağın kopması, kaynak modellemenin ölçülebilir çıktısıdır.
İşlem de Bir Kaynağın Durum Değişimidir
Eylem tabanlı listede en dikkat çeken satır /oduncIade idi. İade bir fiildir, dolayısıyla
kaynak gibi görünmez. Ama iade edilen şey, sistemde zaten kimliği olan bir ödünç kaydıdır;
iade o kaydın iade alanının boş olmaktan çıkmasıdır. Yeni adres gerekmez, var olan
kaynağın durumu değişir.
Aşağıdaki servis bunu kütüphane şeması üstünde gösterir. Şema tüm ders boyunca aynıdır: şube, kitap, üye ve ödünç tabloları.
-- sema.sql — kutuphane odunc servisinin cekirdek semasi CREATE TABLE sube ( kod TEXT PRIMARY KEY, ad TEXT NOT NULL ); CREATE TABLE kitap ( isbn TEXT PRIMARY KEY, ad TEXT NOT NULL, yazar TEXT NOT NULL, yil INTEGER NOT NULL, sube TEXT NOT NULL REFERENCES sube(kod) ); CREATE TABLE uye ( kod TEXT PRIMARY KEY, ad TEXT NOT NULL, sube TEXT NOT NULL REFERENCES sube(kod) ); CREATE TABLE odunc ( id INTEGER PRIMARY KEY, uye TEXT NOT NULL REFERENCES uye(kod), isbn TEXT NOT NULL REFERENCES kitap(isbn), verilis TEXT NOT NULL, iade TEXT ); INSERT INTO sube VALUES ('S-01','Merkez'), ('S-02','Yenisehir'); INSERT INTO kitap VALUES ('978-0201896831','Programlama Sanati','Knuth',1968,'S-01'), ('978-0262033848','Algoritmalara Giris','Cormen',1990,'S-02'), ('978-0131103627','C Programlama Dili','Ritchie',1978,'S-01'); INSERT INTO uye VALUES ('U-1001','Aylin Deren','S-01'), ('U-1002','Berk Tuna','S-02'); INSERT INTO odunc (uye, isbn, verilis, iade) VALUES ('U-1001','978-0262033848','2026-03-01',NULL), ('U-1001','978-0131103627','2026-02-10','2026-02-24'), ('U-1002','978-0201896831','2026-03-04',NULL);
// kaynak-servisi.mjs — odunc bir kaynaktir; iade onun durum degisimidir import { createServer } from "node:http"; import { DatabaseSync } from "node:sqlite"; const db = new DatabaseSync("kutuphane.db"); const govdeOku = (istek) => new Promise((coz) => { let veri = ""; istek.on("data", (p) => (veri += p)); istek.on("end", () => coz(veri ? JSON.parse(veri) : {})); }); const yanitla = (yanit, kod, nesne) => { yanit.writeHead(kod, { "content-type": "application/json; charset=utf-8" }); yanit.end(JSON.stringify(nesne)); }; const sunucu = createServer(async (istek, yanit) => { const yol = new URL(istek.url, "http://127.0.0.1").pathname; const odunc = /^\/oduncler\/(\d+)$/.exec(yol); const durum = /^\/uyeler\/([\w-]+)\/durum$/.exec(yol); // Tekil odunc kaydi: bir tablo satirina karsilik gelir. if (odunc && istek.method === "GET") { const satir = db.prepare("SELECT * FROM odunc WHERE id = ?").get(Number(odunc[1])); return satir ? yanitla(yanit, 200, satir) : yanitla(yanit, 404, { hata: "odunc_yok" }); } // Iade: yeni bir adres degil, var olan kaynagin durum degisimi. if (odunc && istek.method === "PATCH") { const govde = await govdeOku(istek); db.prepare("UPDATE odunc SET iade = ? WHERE id = ? AND iade IS NULL") .run(govde.iade, Number(odunc[1])); return yanitla(yanit, 200, db.prepare("SELECT * FROM odunc WHERE id = ?").get(Number(odunc[1]))); } // Uye durumu: hicbir tabloda satiri yok, iki tablodan hesaplanir. if (durum && istek.method === "GET") { const sayilar = db.prepare(` SELECT COUNT(*) AS toplam, SUM(iade IS NULL) AS acik FROM odunc WHERE uye = ?`).get(durum[1]); const uye = db.prepare("SELECT ad, sube FROM uye WHERE kod = ?").get(durum[1]); if (!uye) return yanitla(yanit, 404, { hata: "uye_yok" }); return yanitla(yanit, 200, { uye: durum[1], ad: uye.ad, sube: uye.sube, acikOdunc: sayilar.acik, toplamOdunc: sayilar.toplam, yeniOduncAlabilir: sayilar.acik < 2, }); } yanitla(yanit, 404, { hata: "yol_yok" }); }); sunucu.listen(8473, "127.0.0.1", () => console.log("kaynak servisi 127.0.0.1:8473"));
# Semayi kurar, sunucuyu baslatir, odunc kaydini okur, iade eder, durumu sorar. rm -f kutuphane.db && sqlite3 kutuphane.db < sema.sql node kaynak-servisi.mjs & sunucu=$! sleep 0.4 curl -sS http://127.0.0.1:8473/oduncler/1; echo curl -sS -X PATCH -H 'content-type: application/json' \ -d '{"iade":"2026-03-12"}' http://127.0.0.1:8473/oduncler/1; echo curl -sS http://127.0.0.1:8473/uyeler/U-1001/durum; echo sqlite3 kutuphane.db "SELECT COUNT(*) || ' satir, acik: ' || SUM(iade IS NULL) FROM odunc;" kill $sunucu
kaynak servisi 127.0.0.1:8473
{"id":1,"uye":"U-1001","isbn":"978-0262033848","verilis":"2026-03-01","iade":null}
{"id":1,"uye":"U-1001","isbn":"978-0262033848","verilis":"2026-03-01","iade":"2026-03-12"}
{"uye":"U-1001","ad":"Aylin Deren","sube":"S-01","acikOdunc":0,"toplamOdunc":2,"yeniOduncAlabilir":true}
3 satir, acik: 1
Son satır önemlidir: iade yeni bir satır üretmedi, var olan satırın bir alanı doldu. Tablodaki kayıt sayısı üçtür ve öyle kalır. İade için ayrı bir adres açmak, sistemde ayrı bir şey yaratıldığı yanılsamasını verir; oysa yaratılan bir şey yoktur.
Bunun tersi de doğrudur ve abartıya kaçmamak gerekir. Bir geçişin kendi kuralları, kendi yetkisi ve kendi kaydı varsa — ödüncün başka bir üyeye devri gibi — o geçişi kaynağa bağlı ayrı bir adres olarak açmak savunulabilir. Ölçüt yine kimliktir: geçişin kendisi sorgulanabilir bir kayıt bırakıyorsa kaynak olmayı hak eder, bırakmıyorsa alan değişimidir.
Tablo ile Kaynak Aynı Şey Değil
Çıktının üçüncü satırı /uyeler/U-1001/durum adresine aittir ve bu kaynağın hiçbir tabloda
satırı yoktur. Açık ödünç sayısı, toplam ödünç sayısı ve yeni ödünç alma hakkı iki
tablodan hesaplanır. Yine de kaynaktır: kimliği sabittir, istemci ona doğrudan başvurur ve
sonucu tek bir gösterimdir. Kütüphane görevlisinin ekranı bu tek adresi çağırır; üç ayrı
sorgu yapıp kuralı istemcide uygulamaz.
Karşılık kurmak tek yönlü de değildir. Tek bir tablo birden çok kaynağı besleyebilir:
odunc tablosu hem ödünç kayıtları koleksiyonunu hem de bir üyenin ödünç geçmişini
besler. Kaynak modeli, veritabanı şemasının dışa yansıtılması değildir; şema depolama için
en iyi biçimi arar, kaynak modeli istemcinin başvurmak isteyeceği kavramları arar. İkisinin
çoğu zaman örtüşmesi, aynı şey oldukları anlamına gelmez — ve bu ayrım korunmazsa
veritabanında yapılan her yeniden düzenleme dışarıdaki sözleşmeyi kırar.
Kimlik Seçiminin Bedeli
Kaynak kimliği olarak ne kullanılacağı bir tasarım kararıdır. İki seçenek vardır: Veri Modelleme ve İlişkisel Kuram kursunda tanımlanan doğal anahtar — kitap için ISBN — ve vekil anahtar — ödünç kaydı için üretilen sayı. Doğal anahtar okunabilir ve anlamlıdır; vekil anahtar anlamsızdır ama değişmez.
Fark, veri düzeltildiğinde ortaya çıkar.
# Kaynak kimligi degisince ona yapilan basvurular ne olur? rm -f kimlik.db sqlite3 kimlik.db <<'SQL' CREATE TABLE kitap (isbn TEXT PRIMARY KEY, ad TEXT NOT NULL); CREATE TABLE odunc (id INTEGER PRIMARY KEY, isbn TEXT NOT NULL REFERENCES kitap(isbn)); INSERT INTO kitap VALUES ('978-0262033848', 'Algoritmalara Giris'); INSERT INTO odunc (isbn) VALUES ('978-0262033848'); -- Katalog kaydindaki ISBN yanlis girilmis; duzeltiliyor. UPDATE kitap SET isbn = '978-0262046305' WHERE isbn = '978-0262033848'; SELECT 'kitap tablosu : ' || isbn FROM kitap; SELECT 'odunc tablosu : ' || isbn FROM odunc; SELECT 'yetim kayit : ' || COUNT(*) FROM odunc o LEFT JOIN kitap k ON k.isbn = o.isbn WHERE k.isbn IS NULL; SQL
kitap tablosu : 978-0262046305 odunc tablosu : 978-0262033848 yetim kayit : 1
Tek bir düzeltme bir yetim kayıt üretti. Bu motorda yabancı anahtar denetimi varsayılan
olarak kapalıdır ve PRAGMA foreign_keys = ON ile açılır; açık olsaydı güncelleme
reddedilir, düzeltme yapılamazdı. İki durumda da bedel vardır: ya başvuru kopar ya
düzeltme engellenir.
API tarafındaki karşılığı daha ağırdır, çünkü başvurular sistemin dışındadır.
/kitaplar/978-0262033848 adresi bir kütüğe yazılmış, bir e-postaya konmuş, başka bir
servisin yapılandırmasına gömülmüş olabilir. Kimlik değişince bu adres artık bulunamaz.
Bu yüzden kaynak kimliği için ölçüt “anlamlı mı” değil, “değişmeyeceğinden emin miyiz”
sorusudur. Değişebilecek doğal anahtarlar kaynağın bir alanı olarak durur ve arama ölçütü
diye sunulur; kimlik için vekil anahtar kullanılır.
Özet
- Kaynak, kimliği zaman içinde sabit kalan ve istemcinin doğrudan başvurduğu şeydir; her alan kavramı kaynak değildir, bazıları kaynağın alanı ya da servisin kuralıdır.
- Kaynağın kendisi soyuttur; istemciye giden gövde onun gösterimidir ve aynı kaynağın birden çok gösterimi olabilir.
- Eylem adlarıyla açılan uç noktaların sayısı istemci ihtiyaçlarıyla birlikte büyür; aynı ihtiyaçlar dört adres kalıbıyla karşılandığında bu bağ kopar.
- İade gibi geçişler yeni kaynak yaratmaz, var olan kaynağın durumunu değiştirir; kendi kaydını bırakan geçişler ise ayrı kaynak olmayı hak eder.
- Kaynak modeli veritabanı şemasının yansıması değildir: bir kaynak hiçbir tabloya karşılık gelmeyebilir, bir tablo birden çok kaynağı besleyebilir.
- Kimlik için ölçüt anlamlılık değil değişmezliktir; değişebilen doğal anahtarlar kimlik yerine alan olarak taşınır.
Sonraki Adım
Kaynaklar belirlendi, ama adreslerinin nasıl yazılacağı henüz kararlaştırılmadı. Bu derste
/kitaplar, /oduncler/1 ve /uyeler/U-1001/durum biçimleri sorgulanmadan kullanıldı;
oysa her biri bir seçimdi. Koleksiyon adı tekil mi çoğul mu olur, bir ödünç kaydına
/oduncler/1 üzerinden mi yoksa /uyeler/U-1001/oduncler/1 üzerinden mi başvurulur, iki
yol da açılırsa hangisi doğru sayılır? Sonraki ders yol düzenini bir eşleyici tablosu
kurarak ele alır ve iç içe adreslerin hangi noktada belirsizlik ürettiğini ölçer.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.