Node.js Backend Development Bootcamp · Ders

Okuma Modelleri ve Projeksiyonlar Oluşturma

Olay akışından sorgu tarafı için eniyileştirilmiş projeksiyonlar türetin ve bunları nihai olarak tutarlı tutun.

3. ders / 413 adım

Okuma Modelleri ve Projeksiyonlar Oluşturma, CoddyKit'te ücretsiz bir Node.js Backend Development Bootcamp dersidir. Bu, 4 dersinin 3. dersidir. Aşağıdan dersin tamamını ücretsiz okuyabilir, sonra tarayıcıda yerleşik kod editörü ve 7/24 yapay zeka koçu ile uygulamalı olarak pratik yapabilirsin. Bu, Node.js Backend Development Bootcamp öğrenme yolunun bir parçasıdır ve ilerlemeniz web ve CoddyKit uygulaması arasında senkronize olur. Node.js Backend Development Bootcamp kursu toplamda 4 dersten oluşur.

Okuma Modelleri Neden Vardır

Olay kaynaklı bir sistemde yazma tarafı, gerçekleri yalnızca ekleme yapılabilen bir olay akışı olarak depolar. Bu akış geçmişi kaydetmek için harikadır, ancak "toplama göre sıralanmış tüm açık siparişleri göster" gibi sorgular için son derece elverişsizdir.

Okuma tarafı bu sorunu çözer. Okuma modeli (aynı zamanda projection olarak da adlandırılır), yalnızca olaylardan türetilen, normalleştirilmemiş ve sorgular için optimize edilmiş bir görünümdür. Bu, CQRS içindeki Q'dur: komutlar olay akışını değiştirir, sorgular ise okuma modellerine yönelir.

  • Tek bir olay akışı, her biri belirli bir sorguya göre şekillendirilmiş birçok okuma modelini besleyebilir.
  • Okuma modelleri geçicidir — bunları istediğiniz zaman silebilir ve olaylardan yeniden oluşturabilirsiniz.
  • Yazma tarafıyla işlemsel olarak değil, sonunda tutarlı olacak şekilde tutarlıdırlar.

Bir İzdüşüm Sol Katlamadır

Temelinde bir izdüşüm, olay akışı üzerinde gerçekleştirilen bir reduce işlemidir: başlangıç durumuyla başlar ve bir sonraki durumu üretmek için her olayı sırayla uygularsınız.

Kavramsal olarak: readModel = events.reduce(apply, initialState). apply işlevi, olay türleri üzerinde çalışan saf bir switch yapısıdır. Katlayabildiğiniz her şeyi izdüşürebilirsiniz.

// Pure projection: fold the event stream into a read model
const events = [
  { type: 'OrderPlaced', orderId: 'o1', total: 50 },
  { type: 'OrderPlaced', orderId: 'o2', total: 20 },
  { type: 'OrderPaid', orderId: 'o1' },
  { type: 'OrderCancelled', orderId: 'o2' },
];

function apply(state, event) {
  const next = { ...state };
  switch (event.type) {
    case 'OrderPlaced':
      next[event.orderId] = { status: 'placed', total: event.total };
      break;
    case 'OrderPaid':
      if (next[event.orderId]) next[event.orderId].status = 'paid';
      break;
    case 'OrderCancelled':
      delete next[event.orderId];
      break;
  }
  return next;
}

const readModel = events.reduce(apply, {});
console.log(readModel);
// { o1: { status: 'paid', total: 50 } }

Bir İzdüşüm Sınıfı

Gerçek bir izdüşüm, tek bir katlama işleminden fazlasıdır. Üç şeyi yönetir:

  • işleyiciler — olay türünden bir güncelleme işlevine eşleme.
  • durum — yönettiği okuma modeli (bellekte, bir tabloda veya önbellekte).
  • konum — akışın ne kadarını tükettiği (denetim noktası).

İlgilenmediği olaylar basitçe yok sayılır. Böylece her izdüşüm, sorgularının ihtiyaç duyduğu veriye tam olarak odaklanır.

class OrderSummaryProjection {
  constructor() {
    this.state = new Map();
    this.handlers = {
      OrderPlaced: (e) => this.state.set(e.orderId, {
        status: 'placed', total: e.total, customer: e.customer,
      }),
      OrderShipped: (e) => {
        const r = this.state.get(e.orderId);
        if (r) r.status = 'shipped';
      },
    };
  }

  when(event) {
    const handler = this.handlers[event.type];
    if (handler) handler(event); // ignore unknown event types
  }

  query(orderId) {
    return this.state.get(orderId) ?? null;
  }
}

const proj = new OrderSummaryProjection();
proj.when({ type: 'OrderPlaced', orderId: 'o1', total: 99, customer: 'Ada' });
proj.when({ type: 'OrderShipped', orderId: 'o1' });
proj.when({ type: 'Irrelevant', foo: 1 });
console.log(proj.query('o1'));
// { status: 'shipped', total: 99, customer: 'Ada' }

Bir Okuma Deposuna Kalıcı Olarak Yazma

Üretim ortamında okuma modeli, sorgular için optimize edilmiş bir veritabanında bulunur — Postgres, MongoDB, Redis, Elasticsearch; sorgu biçimine hangisi uygunsa. Her olay işleyicisi, bu depoya karşı idempotent upsert işlemi gerçekleştirir.

Aşağıdaki SQL'in, order_summary tablosunun zaten var olduğunu varsayan bir parça olduğunu unutmayın; bu nedenle tek başına çalıştırılabilir olmaktan çok açıklama amaçlıdır.

// Postgres read-model upsert inside a projector (using node-postgres)
async function onOrderPlaced(pool, event) {
  await pool.query(
    `INSERT INTO order_summary (order_id, status, total, customer)
     VALUES ($1, 'placed', $2, $3)
     ON CONFLICT (order_id) DO UPDATE
       SET status = EXCLUDED.status,
           total  = EXCLUDED.total,
           customer = EXCLUDED.customer`,
    [event.orderId, event.total, event.customer]
  );
}

async function onOrderShipped(pool, event) {
  await pool.query(
    `UPDATE order_summary SET status = 'shipped' WHERE order_id = $1`,
    [event.orderId]
  );
}

Olay Akışına Abone Olma

Bir izdüşüm, sonsuza kadar körü körüne yoklama yapmaz — olay deposuna abone olur ve olaylar eklendikçe bunları sırayla alır. Her olay deposunun sunduğu sözleşme şudur:

  • Olaylar genel işleme sırasına (veya akış başına sıraya) göre gelir.
  • Her olay, artmayan bir konum (genel uzaklık veya sıra numarası) taşır.
  • Abonelik belirli bir konumdan başlatılabilir — bu, devam etmek için gereklidir.

Alınan her olayı when yöntemine verip ardından denetim noktasını ilerleterek izdüşümü çalıştırırsınız.

// A minimal in-memory event bus a projector can subscribe to
class EventStore {
  constructor() { this.log = []; this.subs = []; }
  append(event) {
    const stored = { ...event, position: this.log.length + 1 };
    this.log.push(stored);
    for (const cb of this.subs) cb(stored);
  }
  subscribeFrom(position, cb) {
    for (const e of this.log) if (e.position > position) cb(e); // catch up
    this.subs.push(cb); // then live
  }
}

const store = new EventStore();
store.append({ type: 'OrderPlaced', orderId: 'o1', total: 10 });
let count = 0;
store.subscribeFrom(0, (e) => { count++; });
store.append({ type: 'OrderPaid', orderId: 'o1' });
console.log('events seen:', count); // 2 (1 catch-up + 1 live)

Denetim Noktaları: Konumunuzu Hatırlama

Hizmetiniz yeniden başlarsa tüm akışı baştan işlemek (yavaş) veya olayları atlamak (veri kaybı) istemezsiniz. Çözüm bir denetim noktasıdır: başarıyla işlenen son olayın konumunu kalıcı olarak saklamak.

Başlangıçta izdüşüm, denetim noktasını okur ve aboneliğe bu konumdan devam eder. Altın kural şudur:

  • Olayı işleyin ve okuma modelini güncelleyin.
  • Ardından denetim noktasını ilerletip kalıcı olarak saklayın.
  • İdeal olarak okuma modeli değişikliğini ve denetim noktasını aynı işlemde yazın.
// Resuming from a stored checkpoint
async function startProjector(store, checkpointRepo, projection) {
  const last = await checkpointRepo.load(projection.name); // e.g. 42

  store.subscribeFrom(last, async (event) => {
    await projection.when(event);              // 1. update read model
    await checkpointRepo.save(
      projection.name, event.position          // 2. advance checkpoint
    );
  });
}

// checkpointRepo example backed by a 'projection_checkpoints' table:
// load:  SELECT position FROM projection_checkpoints WHERE name = $1
// save:  INSERT ... ON CONFLICT (name) DO UPDATE SET position = $2

İdempotentlik: Yeniden Teslimata Dayanma

Denetim noktası işlemeden sonra kaydedildiği için, "okuma modelini güncelle" ile "denetim noktasını kaydet" arasındaki bir çökme, yeniden başlatma sırasında aynı olayın yeniden teslim edilmesine neden olur. Bu, en az bir kez teslimattır ve normaldir.

Bu nedenle her işleyici idempotent olmalıdır — aynı olayın iki kez uygulanması aynı durumu üretmelidir. İki güvenilir teknik vardır:

  • Kontrolsüz eklemeler yerine upsert işlemleri kullanın (yinelenen satır oluşmaz).
  • Idempotent olmayan işlemler (sayaçlar, toplamlar) için uygulanan son konumu satır başına kaydedin ve bu konuma eşit veya daha düşük konumdaki olayları atlayın.
// Guarding a running total against redelivery using a per-row version
function applyRevenue(state, event) {
  const row = state[event.customer] ?? { revenue: 0, lastPos: 0 };
  if (event.position <= row.lastPos) {
    return state; // already applied — skip duplicate
  }
  row.revenue += event.amount;
  row.lastPos = event.position;
  return { ...state, [event.customer]: row };
}

let s = {};
const paid = { type: 'OrderPaid', customer: 'Ada', amount: 30, position: 5 };
s = applyRevenue(s, paid);
s = applyRevenue(s, paid); // redelivered, ignored
console.log(s.Ada.revenue); // 30, not 60

Sonunda Tutarlılık ve Yazdıklarını Okuma Açığı

İzdüşümler eşzamansız olarak güncellendiğinden, bir komut başarıyla tamamlandıktan hemen sonra okuma modeli bu değişikliği henüz yansıtmayabilir. Bu yazdıklarını okuma açığı kullanıcıları şaşırtır: bir sipariş verirler, ancak liste birkaç milisaniye boyunca hâlâ boş görünür.

Bunu yönetme stratejileri:

  • Yeni durumu komuttan döndürün; böylece arayüz yeniden sorgulamadan iyimser bir şekilde görüntülenebilir.
  • İzdüşümü bekleme: komut olay konumunu döndürür; istemci, denetim noktası bu konuma ulaşana kadar okuma modelini yoklar.
  • Kullanıcı deneyimini kısa süreli güncellik kaybını tolere edecek şekilde tasarlayın (yükleniyor göstergeleri, "işleniyor" durumları).

İzdüşümleri "güvenli olmak için" hiçbir zaman eşzamanlı hâle getirmeye çalışmayın — CQRS'yi değerli kılan ölçeklenebilirliği ve ayrıştırmayı kaybedersiniz.

Tek Bir Akıştan Birden Çok İzdüşüm

CQRS'nin asıl gücü şudur: tek bir olay akışı, her biri farklı bir sorgu için optimize edilmiş birçok bağımsız okuma modelini besler. Aynı OrderPlaced olayı şunları güncelleyebilir:

  • müşterinin sipariş geçmişi için bir OrderList izdüşümünü,
  • analiz için bir DailyRevenue izdüşümünü,
  • Elasticsearch'i besleyen bir SearchIndex izdüşümünü.

Her biri kendi işleyicileri ve kendi denetim noktasıyla çalışır; bu nedenle bağımsız olarak eklenebilir, yeniden oluşturulabilir veya ölçeklendirilebilir.

// One dispatcher fans an event out to many projections
const projections = [];

function register(name, handlers) {
  projections.push({ name, handlers, state: {} });
}

function dispatch(event) {
  for (const p of projections) {
    const h = p.handlers[event.type];
    if (h) h(p.state, event);
  }
}

register('orderCount', {
  OrderPlaced: (s) => { s.count = (s.count ?? 0) + 1; },
});
register('revenue', {
  OrderPlaced: (s) => { s.sum = (s.sum ?? 0) + 1; },
  OrderPaid:   (s, e) => { s.paid = (s.paid ?? 0) + e.total; },
});

dispatch({ type: 'OrderPlaced', orderId: 'o1', total: 40 });
dispatch({ type: 'OrderPaid', orderId: 'o1', total: 40 });
console.log(projections.map((p) => [p.name, p.state]));

Bir İzdüşümü Yeniden Oluşturma

Okuma modelleri türetilmiş veriler olduğundan, onları silip yeniden hesaplayabilirsiniz. Bir izdüşümü şu durumlarda yeniden oluşturursunuz:

  • şemasını değiştirdiğinizde (yeni bir sütun veya normalleştirilmemiş alan eklediğinizde),
  • bir işleyicideki hatayı düzelttiğinizde,
  • geçmiş verilere ihtiyaç duyan yepyeni bir okuma modeli eklediğinizde.

Yeniden oluşturma tarifi:

  • Okuma deposunu sıfırlayın (tabloyu kırpın) ve denetim noktasını 0 olarak ayarlayın.
  • Tüm akışı işleyiciler üzerinden baştan sona yeniden oynatın.
  • Sıfır kesinti için yeni bir tabloda oluşturun ve canlı veriye yetiştiğinde atomik olarak değiştirin (mavi-yeşil dağıtım).
// Rebuild by replaying the whole log into a fresh projection
function rebuild(eventLog, projection) {
  projection.state = {};          // reset read model
  projection.checkpoint = 0;      // reset position
  for (const event of eventLog) {
    projection.when(event);
    projection.checkpoint = event.position;
  }
  return projection;
}

const log = [
  { type: 'OrderPlaced', orderId: 'o1', total: 10, position: 1 },
  { type: 'OrderPlaced', orderId: 'o2', total: 25, position: 2 },
];
const proj = {
  state: {}, checkpoint: 0,
  when(e) { if (e.type === 'OrderPlaced') this.state[e.orderId] = e.total; },
};
rebuild(log, proj);
console.log(proj.state, 'at', proj.checkpoint);
// { o1: 10, o2: 25 } at 2

Sıralama, Hatalar ve Zehirli Olaylar

Bir izdüşüm, sıralamayı korumak için olayları sıralı olarak tüketir — gönderilmiş bir olay, yerleştirilmiş olayından önce hiçbir zaman uygulanmamalıdır. Bu kısıtlama, hataları nasıl ele alacağınızı belirler:

  • Geçici bir hatada (veritabanı kesintisi) artan beklemeyle yeniden deneyin; denetim noktasını ilerletmeyin, böylece olay yeniden uygulanır.
  • Zehirli bir olayda (her zaman hata veren bir olay) tüm izdüşümün sonsuza kadar engellenmesine izin vermemelisiniz. Olayı bir ölü mektup günlüğüne taşıyıp uyarı gönderin; böylece akışın geri kalanı devam eder.
  • Gerekli aktarım hızına ulaşmak için bölümler arasında paralelleştirme yapın (örneğin aggregate id'ye göre); her bölümün içinde sıralamayı koruyun.

Hataların nadir ve yeniden üretilebilir olması için işleyicileri küçük ve belirlenebilir tutun.

Hızlı Kontrol: Denetim Noktası Kararı

Bir izdüşüm, bir Postgres okuma modelini güncelliyor ve denetim noktasını ayrı bir tabloda saklıyor. Hizmet her an çökebilir. Hangi yaklaşım doğruluğu en iyi şekilde garanti eder?

Özet: Okuma Modelleri ve İzdüşümler

Artık olay kaynaklı bir sistemin sorgu tarafının nasıl oluşturulduğunu biliyorsunuz:

  • Bir projection, olay akışını normalleştirilmemiş ve sorgular için optimize edilmiş bir okuma modeline dönüştüren sol katlamadır.
  • Bir izdüşüm işleyicileri, kalıcı durumu ve bir denetim noktasını yönetir; depoya abone olur ve olayları sırayla uygular.
  • Denetim noktasını işlemeden sonra kaydedin ve en az bir kez yeniden teslimata dayanmak için işleyicileri idempotent hâle getirin (upsert işlemleri, satır başına konum denetimleri).
  • Okuma modelleri sonunda tutarlıdır — yazdıklarını okuma açığını iyimser yanıtlarla veya izdüşümü bekleyerek yönetin; izdüşümleri hiçbir zaman eşzamanlı çalışmaya zorlamayın.
  • Tek bir akış birçok okuma modelini besleyebilir ve bunlardan herhangi biri akış yeniden oynatılarak yeniden oluşturulabilir (sıfır kesinti için oluştur ve değiştir).
  • Her bölüm içinde sıralı işleyin; geçici hataları denetim noktasını ilerletmeden yeniden deneyin ve zehirli olayları ölü mektup günlüğüne taşıyın.
Başlamak ücretsiz

Yapay zeka eğitmeniyle JavaScript öğren — ücretsiz

Tarayıcında gerçek kod yaz ve çalıştır, 7/24 yapay zeka eğitmeninden anında yardım al; web'de ya da uygulamada kaldığın yerden devam et.

Kurslar
22
Dersler
92

Sıkça Sorulan Sorular

“Okuma Modelleri ve Projeksiyonlar Oluşturma” dersi ücretsiz mi?

Evet — “Okuma Modelleri ve Projeksiyonlar Oluşturma” dersin tüm metni burada web'de ücretsiz olarak okunabilir. Etkileşimli olarak pratik yapmak (yerleşik kod editörü ve 7/24 yapay zeka koçu) ve Node.js Backend Development Bootcamp kursunun geri kalanını açmak için CoddyKit PRO'ya yükselt. Node.js Backend Development Bootcamp kursu toplamda 4 dersten oluşur.

“Okuma Modelleri ve Projeksiyonlar Oluşturma” dersinde ne öğreneceğim?

Olay akışından sorgu tarafı için eniyileştirilmiş projeksiyonlar türetin ve bunları nihai olarak tutarlı tutun. Node.js Backend Development Bootcamp ile uygulamalı kodu tarayıcıda doğrudan çalıştırarak pratik yaparsın ve 7/24 yapay zeka koçu dersi çalışırken sorularını yanıtlar.

Node.js Backend Development Bootcamp öğrenmeye başlamak için deneyim gerekli mi?

Önceden deneyim gerekmez. CoddyKit'te Node.js Backend Development Bootcamp, başlangıçtan ileri seviyeye kadar yapılandırıldığı için buradan başlayabilir veya başından başlayıp kendi hızında ilerleme yapabilirsin. Bu, 4 dersinin 3. dersidir.

“Okuma Modelleri ve Projeksiyonlar Oluşturma” dersi ne kadar sürer?

Çoğu CoddyKit dersi yaklaşık 5–10 dakika sürer. Her biri kısa ve etkileşimli olduğu için sabit ilerleme yaparsın ve web ile uygulama arasında tam olarak bıraktığın yerden devam edebilirsin.

Bu Node.js Backend Development Bootcamp dersinde kod yazıp çalıştırabilir miyim?

Evet. Her Node.js Backend Development Bootcamp dersi yerleşik bir kod editörü içerir, bu sayede tarayıcıda gerçek kod yazıp çalıştırabilir ve anlık yapay zeka geri bildirimi alırsın — yerel kurulum gerekli değildir.

Bu kursun tüm dersleri

  1. Gerçeğin Kaynağı Olarak Olaylar ve Yalnızca Eklenen Günlük
  2. Toplamalar, Komutlar ve Alan Olayı Modellemesi
  3. Okuma Modelleri ve Projeksiyonlar Oluşturma
  4. Anlık Görüntüler, Sürümleme ve Olay Şeması Evrimi
← Node.js Backend Development Bootcamp Sayfasına Dön