Mundarija (24)
- Bu darsda
- 1. Nega bu kerak?
- 2. Uch tushuncha
- 3. @nestjs/cqrs — birinchi oqim
- 3.1 O'rnatish va klasslar
- 3.2 Handler'lar
- 3.3 Natija
- 4. Agregat: AggregateRoot va commit()
- 4.1 Hodisalarni ushlab turish
- 4.2 Natija va xato
- 5. Saga
- 6. Handler topilmasa
- 7. Event sourcing — tanishuv
- 8. JS modelida CQRS shinalari
- 9. Qachon CQRS, qachon ortiqcha
- 10. Ko'p uchraydigan xatolar
- 11. Mashqlar
- 1-mashq (oson): bo'shliqlarni to'ldiring
- 2-mashq (o'rta): agregat modeli
- 3-mashq (qiyin): hodisalardan holat
- 12. Loyiha qadami: kanon tajribasi, loyihaga kirmaydi
- 13. Real ishda
- Xulosa
- Manbalar
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/cqrsuchta shina beradi. Masalan,CommandBus.execute(new VazifaniBajar(1))bitta@CommandHandlerga boradi,QueryBus.execute(...)— bitta@QueryHandlerga. Hodisa —EventBus.publish(...)— istalgancha@EventsHandlerga.AggregateRoothodisalarnicommit()gacha yig'adi, saga esa hodisani yangi buyruqqa aylantiradi. Kuchli naqsh — lekin oddiy CRUD uchun ortiqcha: kanonvazifalar-apiga kiritmadi.
Bu darsda
- Buyruq, so'rov va hodisa farqini va CQRS qaysi muammoni hal qilishini tushuntirasiz.
CqrsModule,@CommandHandler,@QueryHandler,@EventsHandlerbilan oqim qurasiz va natijani o'qiysiz.AggregateRootvaEventPublisherbilan hodisalarnicommit()gacha ushlab turasiz.@Saga()bilan hodisadan yangi buyruq chiqarasiz va hodisa xatosiniUnhandledExceptionBusda 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
npm i --save-exact @nestjs/cqrs@12.1.0Biz demo loyihada (NestJS 12.2.0, @nestjs/cqrs 12.1.0) buyruq, so'rov va hodisa klasslarini yozdik:
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
@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:
@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:
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) → 1Nima 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:
3) CQRS: CommandBus.execute → {"id":1,"matn":"Non olish"}
[EventsHandler] statistika yangilandi: #1 Non olish
QueryBus.execute(VazifalarSoni) → 1flowchart 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-emitterham hodisa beradi. CQRS'ningEventBusi 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:
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:
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 bajarilganKetma-ketlikni o'qing:
bajar()dan keyin hech kim hodisani eshitmadi — ucommit()ni kutdi.commit()hodisani yubordi.VazifaBajarildining bitta handler'i xato tashladi.- Xato
commit()ni to'xtatmadi:EventBusuni ushladi, logga yozdi vaUnhandledExceptionBusga uzatdi. Biz shu shinaga obuna bo'lgan edik — xato va uning sababi (VazifaBajarildi) keldi. - Saga ishladi (keyingi bo'lim), keyin
commit qilindi. - 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:
@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:
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}: …`);
}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:
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:
// 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:
[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
// 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:
commit oldidan yuborilgan: 0
yuborildi: {"tur":"VazifaBajarildi","id":1}
Vazifa 1 allaqachon bajarilganbajar() 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
// 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:
{
'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+mapbilan hodisadan buyruq; hodisa xatosiUnhandledExceptionBusga. - Handler topilmasa
executesinxron xato tashlaydi; handlerprovidersda 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
- NestJS hujjati — CQRS: https://docs.nestjs.com/recipes/cqrs
@nestjs/cqrsmanba kodi (12.1.0): https://github.com/nestjs/cqrs- Martin Fowler, "CQRS": https://martinfowler.com/bliki/CQRS.html
- Martin Fowler, "Event Sourcing": https://martinfowler.com/eaaDev/EventSourcing.html
- Kanon (IlmHamroh, 2026-10-28):
CommandBus,EventBus,QueryBustajribasi; saga,AggregateRoot, event sourcing — kanonda sinalmagan. Demo (buyruq, so'rov, hodisa tartibi, agregat vacommit, saga,UnhandledExceptionBus, handler topilmasa) — biz ishga tushirdik (2026-11-02, Node 24.21.0, NestJS 12.2.0,@nestjs/cqrs12.1.0).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!