Ders 25 / 30
Aktif Kayıt ve Veri Eşleyici
Nesne ile saklanan kayıt arasındaki çevrimin nereye konulacağı: çevrimi nesnenin üzerine koyan aktif kayıt ile ayrı bir eşleyiciye veren veri eşleyicinin karşılaştırılması, alan modülündeki kalıcılık izinin ve ithal kapanışının sayılması, iki sürümün gidiş-dönüşte eşitliği ve ikinci bir saklama biçiminin alan modülüne dokunmadan eklenmesi.
İçindekiler
Önceki ders iş mantığının iki düzenlenişini karşılaştırdı ve ikisini de bellekte bıraktı. Gönderi saklanmaya başladığı anda nesne ile saklanan kayıt arasında bir çevrim doğar: nesnenin alanları bir satıra, satır geri nesneye dönüştürülmelidir. Bu çevrimin nereye konulacağı iki kalıp üretir. Aktif kayıt (active record) çevrimi ve saklama erişimini nesnenin kendi üzerine koyar; nesne kendi satırını okur ve yazar. Veri eşleyici (data mapper) çevrimi ayrı bir nesneye verir; alan nesnesi saklandığından habersiz kalır.
Bu iki kalıp Veri Erişim Katmanı kursunda kurulmuş ve orada üretilen sorgu sayısıyla ölçülmüştü. Burada ölçü bir tasarım ölçüsüdür: alan modülünün kalıcılık hakkında bilmek zorunda olduğu ad sayısı, ithal kapanışının boyutu ve saklama biçimi değiştiğinde dokunulan dosya sayısı. Bağlantı havuzu, sorgu planı ve dizin ayrıntısı o kursa aittir; burada saklama yerine sade bir bellek içi depo konur.
mkdir -p aktif esleyici
// bellek-depo.mjs — sade nesne deposu: tablo adi ve kimlige gore satir tutar const TABLOLAR = new Map(); const tablo = (ad) => TABLOLAR.get(ad) ?? TABLOLAR.set(ad, new Map()).get(ad); export const depo = { oku: (ad, id) => tablo(ad).get(id) ?? null, yaz: (ad, satir) => { tablo(ad).set(satir.id, { ...satir }); }, say: (ad) => tablo(ad).size, };
Aktif Kayıt
Kalıbın çözümü tek cümledir: satırın alanlarını taşıyan sınıf, o satırı okuyan ve yazan yöntemleri de taşır.
// aktif/gonderi.mjs — nesne kendi satirini okur ve yazar import { depo } from "../bellek-depo.mjs"; const KADEME = [[1, 3900], [5, 6400], [Infinity, 11800]]; const ASGARI_UCRET = 3990; export class Gonderi { constructor(satir) { Object.assign(this, satir); } net() { const taban = KADEME.find(([ust]) => this.agirlik <= ust)[1]; return Math.max(Math.round(taban * this.katsayi * (1 - this.indirim)), ASGARI_UCRET); } kaydet() { depo.yaz("gonderi", { id: this.id, agirlik: this.agirlik, katsayi: this.katsayi, indirim: this.indirim }); return this; } static bul(id) { const satir = depo.oku("gonderi", id); return satir === null ? null : new Gonderi(satir); } }
Kazanç dosya sayısındadır: bir kavram, bir sınıf, bir dosya. Kaybı, kural ile kalıcılığın aynı dosyada durmasıdır.
Veri Eşleyici
Aynı iş iki dosyaya bölünür. Alan nesnesi yalnız kuralı taşır.
// esleyici/gonderi.mjs — alan nesnesi: yalniz kural, kalicilik adi yok const KADEME = [[1, 3900], [5, 6400], [Infinity, 11800]]; const ASGARI_UCRET = 3990; export class Gonderi { constructor({ id, agirlik, katsayi, indirim }) { Object.assign(this, { id, agirlik, katsayi, indirim }); } net() { const taban = KADEME.find(([ust]) => this.agirlik <= ust)[1]; return Math.max(Math.round(taban * this.katsayi * (1 - this.indirim)), ASGARI_UCRET); } }
// esleyici/gonderi-esleyici.mjs — cevrimi ve depo erisimini tasiyan esleyici import { Gonderi } from "./gonderi.mjs"; import { depo } from "../bellek-depo.mjs"; export const gonderiEsleyici = { nesneye: (satir) => new Gonderi(satir), satira: (g) => ({ id: g.id, agirlik: g.agirlik, katsayi: g.katsayi, indirim: g.indirim }), bul(id) { const satir = depo.oku("gonderi", id); return satir === null ? null : this.nesneye(satir); }, kaydet(g) { depo.yaz("gonderi", this.satira(g)); return g; }, };
Bağımlılık oku yön değiştirdi: alan nesnesi eşleyiciyi bilmiyor, eşleyici alan nesnesini biliyor.
İzin ve Kapanışın Sayılması
Ölçü üç sayıdır. Birincisi kalıcılık izi: alan modülünde depo adının, tablo adının ve satır sözcüğünün kaç kez geçtiği. İkincisi alan modülünün ithal kapanışı: o modülü yüklemek için yüklenmesi gereken başka modül sayısı. Üçüncüsü hem kuralı hem depo erişimini taşıyan dosya sayısı.
// iz-say.mjs — alan modulunde kalicilik izi, ithal kapanisi ve depoya baglanan dosya sayisi import { readFileSync } from "node:fs"; import { dirname, join, normalize } from "node:path"; const IZ = { "depo adi": /\bdepo\b/g, "tablo adi": /"gonderi"/g, "satir sozcugu": /\bsatir\b/g }; const ITHAL = /from "([^"]+)"/g; function kapanis(baslangic) { const gorulen = new Set(), yigit = [normalize(baslangic)]; while (yigit.length > 0) { const dosya = yigit.pop(); if (gorulen.has(dosya)) continue; gorulen.add(dosya); for (const [, yol] of readFileSync(dosya, "utf8").matchAll(ITHAL)) { if (yol.startsWith(".")) yigit.push(normalize(join(dirname(dosya), yol))); } } gorulen.delete(normalize(baslangic)); return gorulen; } const SURUM = { "aktif kayit": { alan: "aktif/gonderi.mjs", dosyalar: ["aktif/gonderi.mjs"] }, "veri esleyici": { alan: "esleyici/gonderi.mjs", dosyalar: ["esleyici/gonderi.mjs", "esleyici/gonderi-esleyici.mjs"], }, }; for (const [ad, s] of Object.entries(SURUM)) { const metin = readFileSync(s.alan, "utf8"); const izler = Object.entries(IZ).map(([k, r]) => `${k} = ${[...metin.matchAll(r)].length}`); const baglanan = s.dosyalar.filter((d) => readFileSync(d, "utf8").includes("bellek-depo.mjs")); const kuralDosyasi = s.dosyalar.filter((d) => readFileSync(d, "utf8").includes("ASGARI_UCRET")); const ikisi = kuralDosyasi.filter((d) => baglanan.includes(d)); console.log(ad); console.log(` alan modulu (${s.alan}) izleri: ${izler.join(", ")}`); console.log(` alan modulu ithal kapanisi = ${kapanis(s.alan).size}`); console.log(` depoya baglanan dosya = ${baglanan.length} / ${s.dosyalar.length} (${baglanan.join(", ")})`); console.log(` hem kural hem depo tasiyan dosya = ${ikisi.length}`); }
node iz-say.mjs
aktif kayit alan modulu (aktif/gonderi.mjs) izleri: depo adi = 4, tablo adi = 2, satir sozcugu = 5 alan modulu ithal kapanisi = 1 depoya baglanan dosya = 1 / 1 (aktif/gonderi.mjs) hem kural hem depo tasiyan dosya = 1 veri esleyici alan modulu (esleyici/gonderi.mjs) izleri: depo adi = 0, tablo adi = 0, satir sozcugu = 0 alan modulu ithal kapanisi = 0 depoya baglanan dosya = 1 / 2 (esleyici/gonderi-esleyici.mjs) hem kural hem depo tasiyan dosya = 0
Aktif kayıtta alan modülü depo sözcüğünü 4, tablo adını 2, satır sözcüğünü 5 kez taşıyor; eşleyici sürümünde üçü de 0. İthal kapanışı 1’den 0’a iniyor: kural dosyasını yüklemek için depo modülünün yüklenmesi gerekmiyor. Son satırlar farkın adını veriyor: aktif kayıtta kural ile depo erişimi aynı dosyada, eşleyici sürümünde ayrı dosyalarda.
Gidiş-Dönüş Eşitliği
İki kalıp aynı işi yapıyor mu, sınanır.
// gidis-donus.mjs — iki surum ayni satirlari yazip okuyunca ayni neti veriyor mu import { Gonderi as AktifGonderi } from "./aktif/gonderi.mjs"; import { Gonderi as AlanGonderi } from "./esleyici/gonderi.mjs"; import { gonderiEsleyici } from "./esleyici/gonderi-esleyici.mjs"; const SATIRLAR = [ { id: "G1", agirlik: 0.6, katsayi: 1, indirim: 0 }, { id: "G2", agirlik: 3, katsayi: 1.35, indirim: 0.1 }, { id: "G3", agirlik: 12, katsayi: 1.8, indirim: 0.4 }, ]; let ayrilan = 0; for (const satir of SATIRLAR) { new AktifGonderi(satir).kaydet(); const a = AktifGonderi.bul(satir.id).net(); gonderiEsleyici.kaydet(new AlanGonderi(satir)); const e = gonderiEsleyici.bul(satir.id).net(); if (a !== e) ayrilan += 1; console.log(`${satir.id} aktif kayit ${String(a).padStart(6)} veri esleyici ${String(e).padStart(6)}`); } console.log(`ayrilan sonuc = ${ayrilan} / ${SATIRLAR.length}`);
node gidis-donus.mjs
G1 aktif kayit 3990 veri esleyici 3990 G2 aktif kayit 7776 veri esleyici 7776 G3 aktif kayit 12744 veri esleyici 12744 ayrilan sonuc = 0 / 3
Üç satırın üçünde sonuç aynı. Fark doğrulukta değil, saklama kararının nereye yazıldığında.
İkinci Saklama Biçimi
Kalıcılık ayrımının bedelini ödemenin karşılığı burada görünür. İkinci bir depo eklenir: yazmaları bir kütüğe ekleyen, okumada son satırı veren bir depo.
// kutuk-depo.mjs — ikinci saklama bicimi: her yazma bir kutuk satiri, okuma son satiri verir const KUTUK = []; export const kutukDepo = { yaz: (ad, satir) => { KUTUK.push({ ad, satir: { ...satir } }); }, oku: (ad, id) => [...KUTUK].reverse().find((k) => k.ad === ad && k.satir.id === id)?.satir ?? null, uzunluk: () => KUTUK.length, };
// esleyici/kutuk-esleyici.mjs — ayni alan nesnesi, ikinci depoya yazan ikinci esleyici import { Gonderi } from "./gonderi.mjs"; import { kutukDepo } from "../kutuk-depo.mjs"; export const kutukEsleyici = { satira: (g) => ({ id: g.id, agirlik: g.agirlik, katsayi: g.katsayi, indirim: g.indirim }), bul(id) { const satir = kutukDepo.oku("gonderi", id); return satir === null ? null : new Gonderi(satir); }, kaydet(g) { kutukDepo.yaz("gonderi", this.satira(g)); return g; }, };
// ikinci-depo.mjs — alan nesnesi degismeden ikinci depoya yazilabiliyor mu import { readFileSync } from "node:fs"; import { Gonderi } from "./esleyici/gonderi.mjs"; import { gonderiEsleyici } from "./esleyici/gonderi-esleyici.mjs"; import { kutukEsleyici } from "./esleyici/kutuk-esleyici.mjs"; import { kutukDepo } from "./kutuk-depo.mjs"; const SATIR = { id: "G2", agirlik: 3, katsayi: 1.35, indirim: 0.1 }; const g = new Gonderi(SATIR); gonderiEsleyici.kaydet(g); kutukEsleyici.kaydet(g); kutukEsleyici.kaydet(new Gonderi({ ...SATIR, indirim: 0.2 })); console.log(`bellek deposundan net = ${gonderiEsleyici.bul("G2").net()}`); console.log(`kutuk deposundan net = ${kutukEsleyici.bul("G2").net()}`); console.log(`kutuk satiri = ${kutukDepo.uzunluk()}`); const alan = readFileSync("esleyici/gonderi.mjs", "utf8"); console.log(`alan modulunde depo adi = ${[...alan.matchAll(/\bdepo\b/gi)].length}`);
node ikinci-depo.mjs
bellek deposundan net = 7776 kutuk deposundan net = 6912 kutuk satiri = 2 alan modulunde depo adi = 0
Aynı alan nesnesi iki depoya yazıldı ve ikisinden de okundu; alan modülünde depo adı hâlâ 0.
İkinci saklama biçimini eklemek için düzenlenen var olan dosya sayısı 0, eklenen dosya sayısı
2 oldu. Aktif kayıtta aynı gereksinim aktif/gonderi.mjs dosyasının düzenlenmesini gerektirir
ve o dosya kuralı taşıdığı için kural da yeniden doğrulanmak zorunda kalır.
Ne Zaman Uygulanmaz
Veri eşleyicinin bedeli sayılabilir. Aktif kayıt sürümü 1 dosya tutuyor, eşleyici sürümü depo ile birlikte 3 dosya. Bir yazma aktif kayıtta 1 çağrıdır, eşleyicide 2 çağrıya bölünür: önce satıra çevirme, sonra yazma. Kaydı okuyup nesneyi elde etmek için izlenmesi gereken dosya sayısı 1’den 2’ye çıkar.
Satır ile nesne arasında bire bir örtüşme varsa ve kural tek satırlık bir hesaptan
oluşuyorsa, eşleyicinin çevirdiği hiçbir şey yoktur: satira yöntemi alanları aynı adlarla
kopyalar. Bu durumda ödenen 2 ek dosya ve 1 ek dolaylılık basamağı karşılıksızdır; aktif kayıt
daha az parçayla aynı işi yapar.
Aktif kayıt ise nesne modeli ile satır düzeni ayrıştığında pahalanır. Kural birden çok satırın birleşiminden karar üretiyorsa nesne artık bir satırı temsil etmez ve kaydetme yönteminin hangi satırları yazacağı belirsizleşir. Bu ayrışmanın adı Veri Erişim Katmanı kursunda konulmuştu: nesne–ilişki uyumsuzluğu. Ayrışma büyüdükçe kalıcılık kodu kural dosyasının içinde şişer ve ölçüm ilk çıktıdaki iz sayılarını yukarı taşır.
Özet
- Aktif kayıt çevrimi ve saklama erişimini alan nesnesinin üzerine koyar; veri eşleyici ayrı bir nesneye verir ve bağımlılık okunu alan nesnesinden dışarı çevirir.
- Alan modülünde kalıcılık izi aktif kayıtta depo adı 4, tablo adı 2, satır sözcüğü 5 iken eşleyici sürümünde üçü de 0 çıktı; ithal kapanışı 1’den 0’a indi.
- Üç satırın üçünde iki sürüm aynı net ücreti verdi; kalıp seçimi doğruluğu değil, saklama kararının hangi dosyada durduğunu değiştirir.
- İkinci saklama biçimi eşleyici sürümünde 0 var olan dosya düzenlenerek ve 2 dosya eklenerek bağlandı; alan modülünde depo adı 0 kaldı.
- Eşleyicinin bedeli 2 ek dosya, yazma başına 1 ek çağrı ve okunması gereken 1 ek dosyadır; satır ile nesne bire bir örtüşüyorsa bu bedel karşılıksız kalır.
Sonraki Adım
Bu dersin eşleyicisi tek bir nesneyi tek bir satıra çeviriyor. Gerçek bir senaryo tek nesneye dokunmaz: bir gönderi kaydedilirken rotası ve indirim satırları da yazılır, gönderi satırındaki indirim toplamı da yenilenir. Çağıran taraf hangi nesneleri hangi sırayla kaydedeceğini bilmek zorunda kalırsa kalıcılık kararı yeniden yukarı sızar. Sonraki ders erişimi alan diliyle veren depo kalıbını ve değişen satırları tek bir yazma noktasında toplayan iş birimini kurar, elle kaydetmeyle karşılaştırıp uygulama katmanındaki yazma noktalarını ve aynı senaryonun üç düzende ürettiği yazma çağrısı sayısını sayar.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.