IlmHamroh
JavaScript Full-stack/30-qism. NestJS47/65-dars16 daqiqa
Mundarija (24)

CQRS NestJS'da: @nestjs/cqrs — CommandBus, QueryBus, EventBus va saga

Qisqacha: CQRS (Command Query Responsibility Segregation) — o'zgartirish (buyruq) va o'qishni (so'rov) ajratish. Nest'da @nestjs/cqrs uchta shina beradi. Masalan, CommandBus.execute(new VazifaniBajar(1)) bitta @CommandHandler ga boradi, QueryBus.execute(...) — bitta @QueryHandler ga. Hodisa — EventBus.publish(...) — istalgancha @EventsHandler ga. AggregateRoot hodisalarni commit() gacha yig'adi, saga esa hodisani yangi buyruqqa aylantiradi. Kuchli naqsh — lekin oddiy CRUD uchun ortiqcha: kanon vazifalar-api ga kiritmadi.

Bu darsda

  • Buyruq, so'rov va hodisa farqini va CQRS qaysi muammoni hal qilishini tushuntirasiz.
  • CqrsModule, @CommandHandler, @QueryHandler, @EventsHandler bilan oqim qurasiz va natijani o'qiysiz.
  • AggregateRoot va EventPublisher bilan hodisalarni commit() gacha ushlab turasiz.
  • @Saga() bilan hodisadan yangi buyruq chiqarasiz va hodisa xatosini UnhandledExceptionBus da ushlaysiz.
  • CQRS va event sourcing qachon kerak, qachon ortiqcha ekanini asoslaysiz.

Oldin bilishingiz kerak: Hodisalar: @nestjs/event-emitter, Mikroservis asoslari NestJS'da, Interceptor'lar va RxJS, Transport'lar chuqur.

Oldingi ikki darsda xizmatlar orasidagi xabarlarni ko'rdik. CQRS esa bitta ilova ichida kodni qanday tartiblash haqida. U mikroservis bilan ko'p birga tilga olinadi, lekin unga bog'liq emas: monolit ichida ham ishlaydi.

1. Nega bu kerak?

VazifalarService ni eslang. Unda o'qish (royxat, ol, teglar) va yozish (yarat, yangila, ochir, toplamYarat) aralash. Hozir bu muammo emas: klass 110 qator atrofida.

Endi katta tizimni tasavvur qiling — masalan, bank. "Hisobdan pul o'tkazish" — o'nlab qoida: limit, valyuta, firibgarlik tekshiruvi, audit. "Oylik ko'chirmani ko'rsatish" — boshqa dunyo: tez qidiruv, yig'indilar, eksport. Ikkalasi bitta klassda bo'lsa:

  • yozish qoidalari va o'qish optimizatsiyalari bir-biriga xalal beradi;
  • o'qish 100 marta ko'p bo'lsa ham, uni alohida kengaytirib bo'lmaydi;
  • har yangi xususiyat katta xizmatga yana bitta metod qo'shadi.

CQRS — shu ikki ishni ajratish: o'zgartiradigan buyruq (command) va o'qiydigan so'rov (query) har biri o'z klassida, o'z handler'ida. Ajratishning eng sodda shakli — kod darajasida. Eng kuchli shakli — o'qish uchun alohida model, hatto alohida baza.

O'xshatish: restoranda oshxona va ofitsiantlar menyusi. Buyurtma (buyruq) oshxonaga boradi, u yerda qoidalar ko'p: mahsulot bormi, navbat qanday. Menyu (so'rov) esa tayyor, chiroyli bosilgan va zalda turadi — mehmon uni ko'rish uchun oshxonaga kirmaydi. Menyu oshxona o'zgarganda yangilanadi (hodisa).

Bu mavzuning nazariy tomoni arxitektura qismida, CQRS va Event Sourcing darslarida. Bu dars — Nest'dagi amaliy vosita.

2. Uch tushuncha

Buyruq (command) So'rov (query) Hodisa (event)
Ma'nosi "buni qil" "buni ber" "bu bo'ldi"
Nomi fe'l: VazifaniBajar ot: VazifalarSoni o'tgan zamon: VazifaBajarildi
Handler soni aynan bitta aynan bitta 0, 1 yoki ko'p
Natija odatda yo'q yoki id ma'lumot yo'q

Jadvaldagi "handler soni" qatori muhim. Buyruqni bajaradigan bitta mas'ul bo'lishi kerak — ikkita bo'lsa, kim haqiqiy? Hodisani esa istalgancha tinglovchi eshitadi — domen hodisalari darsidagi g'oya.

Nomlash ham qoidaning bir qismi. VazifaniBajar — buyruq, rad etilishi mumkin ("allaqachon bajarilgan"). VazifaBajarildi — fakt, uni rad etib bo'lmaydi: u allaqachon bo'ldi.

3. @nestjs/cqrs — birinchi oqim

3.1 O'rnatish va klasslar

bash
npm i --save-exact @nestjs/cqrs@12.1.0

Biz demo loyihada (NestJS 12.2.0, @nestjs/cqrs 12.1.0) buyruq, so'rov va hodisa klasslarini yozdik:

ts
import { Command, Query } from "@nestjs/cqrs";
import type { IEvent } from "@nestjs/cqrs";

class VazifaYarat extends Command<{ id: number; matn: string }> {
  constructor(readonly matn: string) {
    super();
  }
}
class VazifalarSoni extends Query<number> {}
class VazifaYaratildi implements IEvent {
  constructor(readonly id: number, readonly matn: string) {}
}

Buyruq va hodisa — oddiy klass, faqat ma'lumot tashiydi. Command<T> va Query<T> — natija turi: execute(new VazifaYarat("…")) endi TypeScript'da { id, matn } qaytaradi deb biladi. Bu @nestjs/cqrs 11 dan bor qulaylik; eski kodda implements ICommand va qo'lda tur berish uchraydi.

3.2 Handler'lar

ts
@CommandHandler(VazifaYarat)
class YaratHandler implements ICommandHandler<VazifaYarat> {
  constructor(private readonly hodisalar: EventBus) {}
  async execute(c: VazifaYarat) {
    const v = new Vazifa(ombor.size + 1, c.matn);
    ombor.set(v.id, v);
    yoz(`  [CommandHandler] yaratildi #${v.id}`);
    this.hodisalar.publish(new VazifaYaratildi(v.id, v.matn));
    yoz("  [CommandHandler] publish qaytdi");
    return { id: v.id, matn: v.matn };
  }
}

@QueryHandler(VazifalarSoni)
class SoniHandler implements IQueryHandler<VazifalarSoni> {
  async execute() {
    return ombor.size;
  }
}

@EventsHandler(VazifaYaratildi)
class EmailHandler implements IEventHandler<VazifaYaratildi> {
  handle(e: VazifaYaratildi) {
    yoz(`  [EventsHandler] email: "${e.matn}" yaratildi`);
  }
}

@Module({
  imports: [CqrsModule.forRoot()],
  providers: [YaratHandler, SoniHandler, EmailHandler /* ... */],
})
class Ilova {}

Handler — oddiy provider: providers ga yoziladi, DI bilan bog'liqliklarni oladi (bu yerda EventBus). Dekorator Nest'ga "bu klass shu buyruq uchun" deydi. Controller esa handler'ni bilmaydi — faqat shinani:

ts
@Post()
yarat(@Body() d: { matn: string }) {
  return this.buyruqlar.execute(new VazifaYarat(d.matn));
}

3.3 Natija

Controller'siz, to'g'ridan shinalarni chaqirdik (biz ishga tushirdik, Node 24.21.0). Qatorlar tartibiga qarang — qaysi handler qachon ishladi:

text
1) CommandBus.execute(VazifaYarat)
  [CommandHandler] yaratildi #1
  [EventsHandler] email: "Non olish" yaratildi
  [CommandHandler] publish qaytdi
   natija: {"id":1,"matn":"Non olish"}
2) QueryBus.execute(VazifalarSoni) → 1

Nima bo'ldi? [EventsHandler] qatori publish qaytdi dan oldin turibdi: hodisa handler'i publish qaytishidan oldin ishladi — xuddi event-emitter'dagi sinxron emit kabi. Kanon tajribasi ham shu oqimni tasdiqlagan:

text
3) CQRS: CommandBus.execute → {"id":1,"matn":"Non olish"}
   [EventsHandler] statistika yangilandi: #1 Non olish
   QueryBus.execute(VazifalarSoni) → 1
flowchart LR
  C["Controller"] -->|"execute(buyruq)"| CB["CommandBus"]
  CB --> CH["CommandHandler"]
  CH -->|"publish(hodisa)"| EB["EventBus"]
  EB --> E1["EventsHandler: email"]
  EB --> S["Saga"]
  S -->|"yangi buyruq"| CB
  C -->|"execute(so'rov)"| QB["QueryBus"]
  QB --> QH["QueryHandler"]

Diagrammada ikki alohida yo'l bor: yuqorida yozish (buyruq → hodisalar → saga), pastda o'qish. Ular faqat ma'lumot orqali uchrashadi.

Tekshirib ko'ring: @nestjs/event-emitter ham hodisa beradi. CQRS'ning EventBus i undan nimasi bilan farq qiladi?

Javob

Hodisa klass bilan aniqlanadi, satr bilan emas: @EventsHandler(VazifaYaratildi) — harf xatosi bo'lsa, TypeScript darrov ko'radi; wildcard (vazifa.*) esa yo'q. Va EventBus — RxJS Observable shinasi: unga saga'lar operatorlar bilan ulanadi. Ikkalasi ham bitta jarayon ichida va xotirada — crash'da hodisa yo'qoladi.

4. Agregat: AggregateRoot va commit()

4.1 Hodisalarni ushlab turish

Agregat (aggregate) — o'z qoidalarini o'zi qo'riqlaydigan obyekt: "bajarilgan vazifani qayta bajarib bo'lmaydi" — bu qoida Vazifa ichida yashaydi, xizmatda emas. Bu tushuncha DDD'dan (DDD taktik darsi). @nestjs/cqrs da agregat AggregateRoot dan meros oladi:

ts
class Vazifa extends AggregateRoot {
  bajarildi = false;
  constructor(readonly id: number, readonly matn: string) {
    super();
  }
  bajar() {
    if (this.bajarildi) {
      throw new Error(`Vazifa ${this.id} allaqachon bajarilgan`);
    }
    this.bajarildi = true;
    this.apply(new VazifaBajarildi(this.id)); // hali yuborilmaydi
  }
}

@CommandHandler(VazifaniBajar)
class BajarHandler implements ICommandHandler<VazifaniBajar> {
  constructor(private readonly publisher: EventPublisher) {}
  async execute(c: VazifaniBajar) {
    const v = this.publisher.mergeObjectContext(ombor.get(c.id)!);
    v.bajar();
    yoz("  [CommandHandler] bajar() chaqirildi, commit oldidan");
    v.commit();
    yoz("  [CommandHandler] commit qilindi");
  }
}

apply(hodisa) — hodisani agregat ichida yig'adi, yubormaydi. mergeObjectContext — agregatni EventBus ga ulaydi. commit() — yig'ilganlarni birdan yuboradi. G'oya: avval ma'lumotni saqlaysiz (tranzaksiya), keyin commit(). Saqlash yiqilsa, commit() chaqirilmaydi va hodisalar ketmaydi — event-emitter darsidagi "rollback'dan keyin hodisa ketib qolgan" muammosining Nest'dagi javobi.

4.2 Natija va xato

Demoda VazifaBajarildi ga ikkita tinglovchi ulangan: biri ataylab xato tashlaydi (XatoliHandler), ikkinchisi — saga. Chiqishda commit oldidan va commit qilindi qatorlari orasiga nima tushganiga qarang:

text
3) Agregat + saga: VazifaniBajar
  [CommandHandler] bajar() chaqirildi, commit oldidan
  [UnhandledExceptionBus] bot javob bermadi ← VazifaBajarildi
ERROR [EventBus] "XatoliHandler" has thrown an unhandled exception.
ERROR [EventBus] Error: bot javob bermadi
  [CommandHandler] statistika yangilandi (vazifa #1 bajarildi)
  [CommandHandler] commit qilindi
4) Qoida: ikkinchi marta bajarish
    Vazifa 1 allaqachon bajarilgan

Ketma-ketlikni o'qing:

  1. bajar() dan keyin hech kim hodisani eshitmadi — u commit() ni kutdi.
  2. commit() hodisani yubordi. VazifaBajarildi ning bitta handler'i xato tashladi.
  3. Xato commit() ni to'xtatmadi: EventBus uni ushladi, logga yozdi va UnhandledExceptionBus ga uzatdi. Biz shu shinaga obuna bo'lgan edik — xato va uning sababi (VazifaBajarildi) keldi.
  4. Saga ishladi (keyingi bo'lim), keyin commit qilindi.
  5. Ikkinchi VazifaniBajar — agregat qoidasi rad etdi.

Tarjimasi: "XatoliHandler" has thrown an unhandled exception — "XatoliHandler ushlanmagan istisno tashladi". UnhandledExceptionBus — shunday xatolarni bitta joyda yig'ish uchun: monitoringga yuborish, qayta urinish, ogohlantirish.

5. Saga

Saga — hodisalar oqimini tinglab, yangi buyruq chiqaradigan jarayon. "Vazifa bajarildi → statistikani yangila". "Buyurtma to'landi → yetkazishni boshla → kuryer topilmasa — pulni qaytar". Ko'p qadamli biznes jarayonlari shu bilan quriladi.

Nest'da saga — RxJS funksiyasi:

ts
@Injectable()
class VazifaSagalari {
  @Saga()
  bajarilganda = (hodisalar$: Observable<IEvent>) =>
    hodisalar$.pipe(
      ofType(VazifaBajarildi),
      map((e) => new StatistikaYangila(`vazifa #${e.id} bajarildi`)),
    );
}

hodisalar$ — hamma hodisalar oqimi. ofType(VazifaBajarildi) — faqat keraklisini filtrlaydi. map — buyruqqa aylantiradi. Saga chiqargan har buyruqni Nest o'zi CommandBus.execute ga beradi. Yuqoridagi chiqishdagi statistika yangilandi (vazifa #1 bajarildi) qatori — saga ishi.

Saga bilan ehtiyot bo'ling: hodisa → buyruq → hodisa → buyruq zanjiri ko'rinmas bo'lib ketadi. "Nega statistika yangilandi?" savoliga javob — kod qidiruvi. Va saga holatsiz — "uchinchi qadamda yiqilsa, qayerdan davom etamiz?" degan savolga javob bermaydi. Uzoq, ishonchli jarayonlar uchun durable workflow'lar (Temporal kabi) mo'ljallangan.

6. Handler topilmasa

Handler'i yo'q buyruqni yubordik:

ts
class Ochir extends Command<void> {}
try {
  const p = cmd.execute(new Ochir());
  console.log("   Promise qaytdi");
  await p;
} catch (x) {
  console.log(`   sinxron tashlandi — ${(x as Error).name}: …`);
}
text
5) handler yo'q buyruq
   sinxron tashlandi — Error: No handler found for the command: "Ochir".

Tarjimasi: "Ochir buyrug'i uchun handler topilmadi". Qiziq joy: "Promise qaytdi" chiqmadi. execute rad etilgan Promise qaytarmadi — xatoni sinxron tashladi. Birinchi urinishda biz cmd.execute(...).catch(...) yozgan edik — .catch ishlamadi va butun jarayon yiqildi. Qoida: await + try/catch ishlating, .catch() zanjiriga tayanmang.

Ikkinchi holat — handler klassi yozilgan, lekin providers ga qo'shilmagan:

text
   ro'yxatga olinmagan handler — No handler found for the command: "Yangi".

Xuddi shu xabar. @CommandHandler dekoratori yetarli emas — klass providers da bo'lishi shart. Hodisa handler'i uchun esa hatto xato ham bo'lmaydi: hodisa jim yo'qoladi.

7. Event sourcing — tanishuv

Event sourcing — holatni emas, hodisalarni saqlash. Bazada "vazifa 1: bajarildi" yozuvi yo'q. Unda faqat jurnal bor: "Yaratildi", "Matn o'zgardi", "Bajarildi". Joriy holat — jurnalni boshidan o'qib yig'ish (3-mashqda o'zimiz yozamiz).

Foydasi: to'liq tarix (kim, qachon, nima), o'tgan istalgan paytdagi holatni tiklash, audit. Bank hisob-kitobi tabiatan shunday: balans — kirim-chiqimlar yig'indisi (ikki yoqlama yozuv).

Narxi katta. Hodisa formatini o'zgartirish qiyin: eski hodisalar abadiy qoladi. Joriy holatni o'qish uchun alohida "proyeksiya" jadvallari kerak. Xatoni tuzatish — yangi tuzatuvchi hodisa yozish. @nestjs/cqrs event sourcing'ni bermaydi — AggregateRoot da loadFromHistory metodi bor, lekin hodisalar ombori, proyeksiyalar — o'zingiz yoki alohida kutubxona (EventStoreDB kabi). Biz uni sinamadik — kanonda ham sinalmagan.

8. JS modelida CQRS shinalari

Nest'ning uch shinasi va saga ichida nima bor? Paketsiz modelda yozamiz — blok brauzerda ishlaydi:

js
// CQRS shinalari: buyruq → bitta handler, hodisa → ko'p handler
class Shinalar {
  #buyruqlar = new Map(); // klass → handler
  #hodisalar = new Map(); // klass → [handler]
  #sagalar = [];
  buyruqHandler(Klass, fn) {
    this.#buyruqlar.set(Klass, fn);
  }
  hodisaHandler(Klass, fn) {
    const bor = this.#hodisalar.get(Klass) ?? [];
    this.#hodisalar.set(Klass, [...bor, fn]);
  }
  saga(Klass, fn) {
    this.#sagalar.push({ Klass, fn });
  }
  execute(buyruq) {
    const fn = this.#buyruqlar.get(buyruq.constructor);
    if (!fn) {
      const nom = buyruq.constructor.name;
      throw new Error(`No handler found for the command: "${nom}"`);
    }
    return fn(buyruq);
  }
  publish(hodisa) {
    for (const fn of this.#hodisalar.get(hodisa.constructor) ?? []) {
      try {
        fn(hodisa);
      } catch (e) {
        console.log(`  ERROR [EventBus] ${e.message}`);
      }
    }
    for (const s of this.#sagalar) {
      if (hodisa instanceof s.Klass) this.execute(s.fn(hodisa));
    }
  }
}

class VazifaniBajar { constructor(id) { this.id = id; } }
class VazifaBajarildi { constructor(id) { this.id = id; } }
class StatistikaYangila { constructor(sabab) { this.sabab = sabab; } }

const s = new Shinalar();
s.buyruqHandler(VazifaniBajar, (b) => {
  console.log(`  [buyruq] vazifa #${b.id} bajarildi`);
  s.publish(new VazifaBajarildi(b.id));
});
s.hodisaHandler(VazifaBajarildi, (h) => {
  console.log(`  [hodisa] email: #${h.id}`);
});
s.hodisaHandler(VazifaBajarildi, () => {
  throw new Error("bot javob bermadi");
});
s.saga(VazifaBajarildi, (h) => new StatistikaYangila(`#${h.id}`));
s.buyruqHandler(StatistikaYangila, (b) => {
  console.log(`  [buyruq] statistika (${b.sabab})`);
});

s.execute(new VazifaniBajar(1));
try {
  s.execute(new VazifaBajarildi(1)); // hodisani buyruq qilib yuborish
} catch (e) {
  console.log(e.message);
}

Konsolda:

text
  [buyruq] vazifa #1 bajarildi
  [hodisa] email: #1
  ERROR [EventBus] bot javob bermadi
  [buyruq] statistika (#1)
No handler found for the command: "VazifaBajarildi"

Nest chiqishi bilan bir xil manzara: buyruq → hodisalar (biri yiqildi, lekin oqim to'xtamadi) → saga → yangi buyruq. Oxirgi qator — hodisani buyruq shinasiga yuborish xatosi. Kalit sifatida klassning o'zi ishlatildi (buyruq.constructor), satr emas — Nest ham klassga metama'lumot yozib, shu bilan topadi. Soddalashtirilgan joyi: saga — RxJS oqimi emas, oddiy funksiya; commit() va UnhandledExceptionBus yo'q (2-mashqda agregatni qo'shamiz).

9. Qachon CQRS, qachon ortiqcha

Vaziyat Tanlov
Oddiy CRUD, bitta jamoa yo'q — xizmat + domen hodisalari
Murakkab yozish qoidalari, ko'p jarayon @nestjs/cqrs (kod darajasida)
O'qish yuki yozishdan juda katta alohida o'qish modeli (proyeksiya, kesh)
To'liq tarix va audit — biznes talabi event sourcing (alohida vosita bilan)

CQRS'ning yashirin narxi — fayllar soni. vazifalar-api ning bitta yarat metodi CQRS'da: buyruq klassi, handler klassi, hodisa klassi, hodisa handler'i — to'rt fayl. Oddiy loyihada bu "boilerplate" foydasidan og'ir.

Kanon qarori. vazifalar-api ga CQRS kirmadi: vazifalar CRUD uchun bitta xizmat va domen hodisalari yetarli. Kanon tajribasi CommandBus, EventBus, QueryBus ishlashini tasdiqladi; saga, AggregateRoot va event sourcing kanonda sinalmagan — bu darsdagi agregat va saga chiqishlari — o'z demomiz.

10. Ko'p uchraydigan xatolar

Xato Nima bo'ladi Tuzatish
Handler providers da yo'q No handler found (hodisa — jim) modulga qo'shing
execute(...).catch() sinxron xato ushlanmaydi await + try/catch
apply() dan keyin commit() yo'q hodisalar hech qachon ketmaydi mergeObjectContext + commit()
Hodisani buyruq qilib yuborish No handler found EventBus.publish
Har CRUD'ga CQRS fayllar ko'payadi, foyda yo'q oddiy xizmat
Hodisa handler xatosiga e'tibor yo'q xato faqat logda UnhandledExceptionBus ga obuna

11. Mashqlar

Mashqlar kurs/mashqlar/30/47-cqrs/ da.

1-mashq (oson): bo'shliqlarni to'ldiring

  • "Buni qil" — (command)
  • Hodisa nomi qaysi zamonda yoziladi? —
  • Bitta buyruq uchun nechta handler bo'ladi? —
  • Agregatdagi hodisalarni yuboradigan metod —
  • Hodisadan yangi buyruq chiqaradigan jarayon —
Yechim
  • buyruq — VazifaniBajar; so'rov — "buni ber".
  • o'tgan — VazifaBajarildi: fakt, rad etib bo'lmaydi.
  • 1 — hodisada esa 0, 1 yoki ko'p.
  • commit — apply() faqat yig'adi.
  • saga — @Saga() + ofType + map.

2-mashq (o'rta): agregat modeli

AggregateRoot modelini yozing: apply(hodisa) — ichki ro'yxatga qo'shadi, commit(publish) — hammasini publish funksiyasiga beradi va ro'yxatni tozalaydi. Vazifa undan meros olsin: bajar() — qoida (ikkinchi marta — xato) va apply({ tur: "VazifaBajarildi", id }). Ishora: #yigilgan — xususiy maydon (klasslar); super() constructor'da.

Yechim
js
// Agregat: hodisalar commit() gacha yig'iladi
class AggregateRoot {
  #yigilgan = [];
  apply(hodisa) {
    this.#yigilgan.push(hodisa);
  }
  commit(publish) {
    for (const h of this.#yigilgan) publish(h);
    this.#yigilgan = [];
  }
}

class Vazifa extends AggregateRoot {
  constructor(id) {
    super();
    this.id = id;
    this.bajarildi = false;
  }
  bajar() {
    if (this.bajarildi) {
      throw new Error(`Vazifa ${this.id} allaqachon bajarilgan`);
    }
    this.bajarildi = true;
    this.apply({ tur: "VazifaBajarildi", id: this.id });
  }
}

const v = new Vazifa(1);
v.bajar();
console.log("commit oldidan yuborilgan: 0");
v.commit((h) => console.log("yuborildi:", JSON.stringify(h)));
try {
  v.bajar();
} catch (e) {
  console.log(e.message);
}

Konsolda:

text
commit oldidan yuborilgan: 0
yuborildi: {"tur":"VazifaBajarildi","id":1}
Vazifa 1 allaqachon bajarilgan

bajar() hech narsa yubormadi — faqat commit yubordi. Qoida esa agregat ichida: xizmat "bajarilganmi?" deb tekshirmaydi, Vazifa o'zi biladi. Nest'ning AggregateRoot i ham shunday ishlaydi, faqat publish ni mergeObjectContext ulaydi.

3-mashq (qiyin): hodisalardan holat

Event sourcing g'oyasini yozing: hodisalar ro'yxatidan joriy holatni yig'adigan qayta(royxat) funksiyasi. Hodisa turlari: Yaratildi (id, matn), MatnOzgardi (id, matn), Bajarildi (id). Natija — { id: { matn, bajarildi } } obyekti. Keyin ro'yxatning faqat birinchi ikki hodisasidan holat yig'ing — "o'tmishga sayohat". Ishora: Map va Object.fromEntries (Object metodlari); slice(0, 2).

Yechim
js
// Event sourcing g'oyasi: holat — hodisalar yig'indisi
const hodisalar = [
  { tur: "Yaratildi", id: 1, matn: "Non olish" },
  { tur: "MatnOzgardi", id: 1, matn: "Non va sut olish" },
  { tur: "Bajarildi", id: 1 },
  { tur: "Yaratildi", id: 2, matn: "Sport" },
];

function qayta(royxat) {
  const holat = new Map();
  for (const h of royxat) {
    if (h.tur === "Yaratildi") {
      holat.set(h.id, { matn: h.matn, bajarildi: false });
    } else if (h.tur === "MatnOzgardi") {
      holat.get(h.id).matn = h.matn;
    } else if (h.tur === "Bajarildi") {
      holat.get(h.id).bajarildi = true;
    }
  }
  return Object.fromEntries(holat);
}

console.log(qayta(hodisalar));
console.log(qayta(hodisalar.slice(0, 2))); // o'tmishga "sayohat"

Konsolda:

text
{
  '1': { matn: 'Non va sut olish', bajarildi: true },
  '2': { matn: 'Sport', bajarildi: false }
}
{ '1': { matn: 'Non va sut olish', bajarildi: false } }

Ikkinchi chaqiruv — vazifa hali bajarilmagan paytdagi holat. Oddiy CRUD bazasida bu ma'lumot yo'qolgan bo'lardi (UPDATE eski qiymatni o'chiradi). Narxini ham ko'ring: har o'qishda butun jurnalni aylanish. Haqiqiy tizimlarda shuning uchun "snapshot" (oraliq holat) va proyeksiya jadvallari saqlanadi.

12. Loyiha qadami: kanon tajribasi, loyihaga kirmaydi

vazifalar-api o'zgarmaydi. Demo loyihangizda VazifalarService.yangila ni CQRS shaklida qayta yozib ko'ring: VazifaniBajar buyrug'i, Vazifa agregati, VazifaBajarildi hodisasi va webhook handler'i. Keyin fayllar sonini va qator sonini oddiy xizmat bilan solishtiring — "qachon ortiqcha" savoliga o'zingiz raqam bilan javob berasiz.

13. Real ishda

  • Qayerda uchraydi: bank, sug'urta, logistika, e-commerce buyurtma tizimlari — murakkab qoidalar va uzoq jarayonlar. Nest + CQRS ko'pincha DDD bilan birga keladi.
  • Kesh va o'qish modeli: CQRS'ning eng ko'p uchraydigan "yengil" shakli — yozish Postgres'ga, o'qish esa Redis yoki Elasticsearch'dagi tayyor ko'rinishdan.
  • Intervyu savollari: "CQRS nima va qachon kerak?", "Buyruq va hodisa farqi?", "Saga nima?", "Event sourcing'ning kamchiliklari?".
  • Kod review'da: "bu oddiy CRUD — CQRS kerakmi?" savoli ham, "bu hodisa commit() siz qolib ketmaydimi?" ham.

Xulosa

  • CQRS — buyruq ("qil") va so'rovni ("ber") ajratish; hodisa — "bo'ldi".
  • @nestjs/cqrs: CommandBus → bitta @CommandHandler, QueryBus → bitta @QueryHandler, EventBus → ko'p @EventsHandler; Command<T> natija turini beradi.
  • AggregateRoot.apply() yig'adi, commit() yuboradi — saqlash muvaffaqiyatli bo'lganda.
  • Saga — ofType + map bilan hodisadan buyruq; hodisa xatosi UnhandledExceptionBus ga.
  • Handler topilmasa execute sinxron xato tashlaydi; handler providers da bo'lishi shart.
  • Oddiy CRUD uchun CQRS ortiqcha — kanon xizmat + domen hodisalarida qoldi; event sourcing — alohida, katta qaror.

Keyingi dars: Modular monolit va hexagonal arxitektura NestJS'da — mikroservissiz chegaralar: modul public API'si, portlar va adapterlar, modullararo hodisalar — vazifalar-api misolida.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
CQRS NestJS'da: @nestjs/cqrs — CommandBus, QueryBus, EventBus va saga — IlmHamroh