Toimialuetapahtumat ja AggregateRoot
Tuottakaa ja julkaiskaa toimialuetapahtumia aggregaattijuurista EventBusin ja mergeObjectContextin avulla.
Toimialuetapahtumat ja AggregateRoot on ilmainen NestJS-yritysbackendien API:t-oppitunti CoddyKitissä. Tämä on oppitunti 3/4. Voit lukea koko oppitunnin alta ilmaiseksi ja harjoitella sen jälkeen käytännössä selaimessa sisäänrakennetulla koodieditorilla ja ympäri vuorokauden käytettävissä olevan tekoälytuutorin avulla. Oppitunti kuuluu NestJS-yritysbackendien API:t-oppimispolkuun, ja edistymisesi synkronoituu verkon ja CoddyKit-sovelluksen välillä. NestJS-yritysbackendien API:t-kurssilla on yhteensä 4 oppituntia.
Miksi toimialuetapahtumia?
Tapahtumapohjaisessa CQRS-järjestelmässä toimialuetapahtuma tallentaa jotakin merkityksellistä, joka on jo tapahtunut toimialueellanne: OrderPlaced, PaymentCaptured, UserDeactivated. Ne nimetään imperfektissä, koska ne ovat tosiasioita eivätkä pyyntöjä.
Toimialuetapahtumien avulla voitte irrottaa sivuvaikutukset keskeisestä kirjoituslogiikasta. Sen sijaan että tilauspalvelu kutsuisi suoraan sähköposti-, varasto- ja analytiikkakoodia, se vain lähettää tapahtuman OrderPlaced, johon itsenäiset käsittelijät reagoivat.
- Komento: aikomus muuttaa tilaa (
PlaceOrderCommand). - Tapahtuma: tapahtuneen muutoksen tallenne (
OrderPlacedEvent).
NestJS tarjoaa tähän suoran tuen @nestjs/cqrs-paketin, AggregateRoot-perusluokan ja EventBus-väylän kautta.
AggregateRoot-perusluokka
Aggregaattijuuri on niiden toimialueobjektien muodostaman kokonaisuuden sisääntulopiste, jotka muuttuvat yhdessä ja joiden on säilyttävä yhdenmukaisina. Paketissa @nestjs/cqrs teette luokasta aggregaatin perimällä sen luokan AggregateRoot.
Tämä perusluokka tarjoaa kaksi keskeistä metodia:
apply(event)– tallentaa aggregaatissa tapahtuneen toimialuetapahtuman.commit()– julkaisee kaikki tallennetut tapahtumatEventBus-väylälle.
Aggregaatti puskuroi tapahtumat sisäisesti, kunnes kutsutte nimenomaisesti commit()-metodia. Näin tilaa voidaan muuttaa ensin ja tapahtumat julkaista vasta, kun muutos on tallennettu turvallisesti.
import { AggregateRoot } from '@nestjs/cqrs';
export class OrderPlacedEvent {
constructor(
public readonly orderId: string,
public readonly total: number,
) {}
}
export class Order extends AggregateRoot {
constructor(public readonly id: string) {
super();
}
place(total: number) {
// mutate state here, then record the fact
this.apply(new OrderPlacedEvent(this.id, total));
}
}apply() puskuroi, commit() julkaisee
Kun kutsutte this.apply(event), tapahtuma lisätään aggregaatin sisäiseen taulukkoon. Mitään ei vielä julkaista.
Vasta kun kutsutte aggregate.commit(), NestJS käy puskurin läpi ja luovuttaa jokaisen tapahtuman julkaisijalle, joka välittää ne EventBus-väylälle käsittelijöiden käsiteltäviksi.
Tämä kaksivaiheinen työnkulku on tarkoituksellinen:
- Voitte kutsua
apply-metodia useille tapahtumille yhden toiminnon aikana. - Tallennatte uuden tilan tietokantaan.
- Sen jälkeen kutsutte
commit-metodia, jotta tapahtumat käynnistyvät vasta onnistuneen tallennuksen jälkeen. Näin vältetään sivuvaikutukset tapahtumassa, joka myöhemmin perutaan.
Puuttuvan julkaisijan ongelma
Tässä on kuitenkin yksi ongelma. Jos luotte komennon käsittelijässä yksinkertaisesti objektin new Order(...) ja kutsutte commit()-metodia, mitään ei tapahdu. Aggregaatilla ei ole viitettä oikeaan EventBus-väylään – sen oletusjulkaisija ei tee mitään.
Aggregaatti on yhdistettävä julkaisijaan, joka osaa välittää tapahtumat väylälle. Juuri tämän yhteyden muodostavat EventPublisher.mergeObjectContext (ja mergeClassContext).
Tämän vaiheen unohtaminen on yleisin syy siihen, että aloittelijat ilmoittavat toimialuetapahtumiensa "never fire" NestJS CQRS:ssä.
mergeObjectContext: EventBus-väylän yhdistäminen
EventPublisher.mergeObjectContext(aggregate) ottaa olemassa olevan aggregaatti-instanssin ja injektoi siihen oikean julkaisijan, jotta myöhempi commit()-kutsu todella välittää tapahtumat EventBus-väylälle.
Käyttäkää sitä komennon käsittelijässä:
- Rakentakaa aggregaatti tai ladatkaa se.
- Ympäröikää se:
const order = this.publisher.mergeObjectContext(rawOrder). - Suorittakaa toimialueen toiminta, joka kutsuu sisäisesti
apply-metodia. - Tallentakaa tiedot ja kutsukaa sen jälkeen
order.commit().
Injektoikaa EventPublisher paketista @nestjs/cqrs konstruktorin kautta.
import { CommandHandler, ICommandHandler, EventPublisher } from '@nestjs/cqrs';
import { PlaceOrderCommand } from './place-order.command';
import { Order } from './order.aggregate';
import { OrderRepository } from './order.repository';
@CommandHandler(PlaceOrderCommand)
export class PlaceOrderHandler implements ICommandHandler<PlaceOrderCommand> {
constructor(
private readonly repository: OrderRepository,
private readonly publisher: EventPublisher,
) {}
async execute(command: PlaceOrderCommand): Promise<void> {
const order = this.publisher.mergeObjectContext(
new Order(command.orderId),
);
order.place(command.total); // applies OrderPlacedEvent
await this.repository.save(order);
order.commit(); // now events reach the EventBus
}
}mergeClassContext tehtaita varten
Joskus aggregaatit rekonstruoidaan tehtaassa tai repositoriossa sen sijaan, että ne luotaisiin suoraan käsittelijässä. Tällöin mergeClassContext palauttaa julkaisijan huomioivan aliluokan.
Jokainen yhdistetystä luokasta luotu instanssi sidotaan kontekstiin automaattisesti, joten jokaiselle niistä ei tarvitse kutsua mergeObjectContext-metodia erikseen.
Käyttäkää mergeObjectContext-metodia yksittäiseen hallussanne olevaan instanssiin ja mergeClassContext-metodia, kun tehdas tuottaa useita instansseja.
import { EventPublisher } from '@nestjs/cqrs';
import { Order } from './order.aggregate';
export class OrderFactory {
constructor(private readonly publisher: EventPublisher) {}
create(orderId: string): Order {
// Order becomes a publisher-aware subclass
const ContextOrder = this.publisher.mergeClassContext(Order);
return new ContextOrder(orderId);
}
}Toimialuetapahtuman määrittäminen
NestJS:n toimialuetapahtuma on vain tavallinen luokka, joka yleensä toteuttaa merkkirajapinnan IEvent. Pitäkää tapahtumat muuttumattomina ja sisällyttäkää niihin vain käsittelijöiden tarvitsemat tiedot.
- Käyttäkää
readonly-kenttiä, jotka täytetään konstruktorissa. - Nimetkää tapahtumat imperfektissä:
OrderPlacedEvent, eiPlaceOrder. - Sisällyttäkää tunnisteet ja mahdollisimman pieni hyötykuorma, ei kokonaisia entiteettejä.
Koska tapahtumat voidaan sarjallistaa (event sourcingia tai viestivälittäjiä varten), älkää sijoittako niihin toimintalogiikkaa tai viittauksia palveluihin.
import { IEvent } from '@nestjs/cqrs';
export class OrderPlacedEvent implements IEvent {
constructor(
public readonly orderId: string,
public readonly customerId: string,
public readonly total: number,
public readonly occurredAt: Date = new Date(),
) {}
}Tapahtuman käsittely
Luokka @EventsHandler(OrderPlacedEvent) tilaa tapahtuman väylältä. Sen handle-metodi suorittaa sivuvaikutuksen: lähettää sähköpostin, päivittää lukumallin tai vähentää varastomäärää.
Käsittelijöiden tulisi mahdollisuuksien mukaan olla idempotentteja, koska vähintään kerran tapahtuva toimitus voi hajautetuissa ympäristöissä toistaa tapahtuman. Pitäkää ne pieninä ja kohdennettuina – yksi käsittelijä kutakin vastuualuetta kohti.
Rekisteröikää käsittelijät moduulin providers-taulukkoon, jotta väylä löytää ne.
import { EventsHandler, IEventHandler } from '@nestjs/cqrs';
import { OrderPlacedEvent } from './order-placed.event';
@EventsHandler(OrderPlacedEvent)
export class OrderPlacedHandler implements IEventHandler<OrderPlacedEvent> {
handle(event: OrderPlacedEvent): void {
// side effect: e.g. enqueue a confirmation email
console.log(`Order ${event.orderId} placed for ${event.total}`);
}
}CqrsModule-moduulin yhdistäminen
Käyttääksenne näitä ominaisuuksia tuokaa CqrsModule ja rekisteröikää käsittelijät sekä aggregaattien riippuvuudet palveluntarjoajiksi. Moduuli määrittää CommandBus-, QueryBus- ja EventBus-väylät sekä tarjoaa EventPublisher-palvelun injektoitavaksi.
- Lisätkää komentojen käsittelijät, tapahtumien käsittelijät ja tehtaat
providers-taulukkoon. - Tuokaa
CqrsModuleimports-taulukkoon.
Ilman tätä tuontia EventPublisher- tai EventBus-riippuvuuden injektointi epäonnistuu käynnistyksen aikana ratkaisemattoman riippuvuuden virheen vuoksi.
import { Module } from '@nestjs/common';
import { CqrsModule } from '@nestjs/cqrs';
import { PlaceOrderHandler } from './place-order.handler';
import { OrderPlacedHandler } from './order-placed.handler';
import { OrderRepository } from './order.repository';
@Module({
imports: [CqrsModule],
providers: [PlaceOrderHandler, OrderPlacedHandler, OrderRepository],
})
export class OrdersModule {}Kutsukaa commit tallennuksen jälkeen, ei ennen
Toimintojen järjestyksellä on merkitystä. Suositeltu malli on seuraava:
- 1. Yhdistäkää konteksti aggregaattiin.
- 2. Suorittakaa toimialueen toiminta (tapahtumat puskuroidaan
apply-metodilla). - 3. Tallentakaa aggregaatti transaktion sisällä.
- 4. Kutsukaa
commit()-metodia vasta, kun transaktio onnistuu.
Jos kutsutte commit-metodia ennen tallennusta ja tallennus epäonnistuu, olette jo julkaisseet tapahtumat tilan muutoksesta, jota ei koskaan tallennettu. Tällöin käsittelijät toimivat haamudatan perusteella. Vahvempia takuita varten tiimit käyttävät transaktionaalista outbox-mallia, jossa tapahtumat kirjoitetaan outbox-tauluun samassa transaktiossa ja välitetään eteenpäin sen jälkeen.
Puskuroi ja julkaise -työnkulun mallintaminen
Perusajatus – tapahtumien puskurointi apply-metodilla ja tyhjentäminen commit-metodilla – voidaan havainnollistaa tavallisella TypeScriptillä ilman NestJS:ää. Tässä pieni aggregaatti tallentaa tapahtumat, ja julkaisija tyhjentää ne vasta, kun ne vahvistetaan.
Suorittakaa koodi ja tarkkailkaa, että käsittelijä suoritetaan vasta, kun commit()-metodia kutsutaan.
type DomainEvent = { name: string; payload: unknown };
class MiniAggregate {
private events: DomainEvent[] = [];
private publish: (e: DomainEvent) => void = () => {};
setPublisher(fn: (e: DomainEvent) => void) {
this.publish = fn; // mimics mergeObjectContext
}
apply(event: DomainEvent) {
this.events.push(event); // buffer only
}
commit() {
this.events.forEach((e) => this.publish(e));
this.events = [];
}
}
const order = new MiniAggregate();
order.apply({ name: 'OrderPlaced', payload: { id: 'A1', total: 50 } });
console.log('before commit: nothing published yet');
order.setPublisher((e) => console.log('handled:', e.name, e.payload));
order.commit();
console.log('after commit: buffer flushed');Pikatarkistus
Loitte aggregaatin komennolla new Order(id), kutsuitte metodia, joka käyttää komentoa this.apply(new OrderPlacedEvent(...)), ja kutsuitte sen jälkeen order.commit()-metodia – mutta yksikään tapahtumakäsittelijä ei koskaan suoritu. Mikä on todennäköisin syy?
Yhteenveto
Opitte, miten NestJS CQRS muuttaa aggregaatit tapahtumien lähettäjiksi:
- AggregateRoot tarjoaa
apply()-metodin (tapahtuman puskurointi) jacommit()-metodin (puskurin tyhjentäminenEventBus-väylälle). - Suora
newkäyttää julkaisijaa, joka ei tee mitään. Yhdistäkää oikea julkaisijaEventPublisher.mergeObjectContext-metodilla yksittäistä instanssia varten taimergeClassContext-metodilla tehtaan tuottamia instansseja varten. - Toimialuetapahtumat ovat muuttumattomia, imperfektissä nimettyjä luokkia, jotka toteuttavat
IEvent-rajapinnan ja sisältävät vain tarvittavan hyötykuorman. @EventsHandler-luokat reagoivat tapahtumiin. Pitäkää ne pieninä ja idempotentteina.- Tuokaa
CqrsModuleja rekisteröikää käsittelijätproviders-taulukkoon. - Kutsukaa commit tallennuksen jälkeen, jotta tapahtumat eivät koskaan kuvaa tilaa, jonka tallennus epäonnistui. Vahvempia toimitustakuita varten kannattaa harkita transaktionaalista outbox-mallia.
Opi TypeScript tekoälytuutorin avulla — ilmaiseksi
Kirjoita ja suorita oikeaa koodia selaimessa, saa välitöntä apua tekoälytuutorilta ympäri vuorokauden ja jatka siitä, mihin jäit, verkossa tai sovelluksessa.
- Kurssit
- 20
- Oppitunnit
- 76
Usein kysytyt kysymykset
Onko oppitunti ”Toimialuetapahtumat ja AggregateRoot” ilmainen?
Kyllä – oppitunnin ”Toimialuetapahtumat ja AggregateRoot” koko tekstin voi lukea täällä verkossa ilmaiseksi. Jos haluat harjoitella interaktiivisesti sisäänrakennetulla koodieditorilla ja ympäri vuorokauden käytettävissä olevan tekoälytuutorin avulla sekä avata koko NestJS-yritysbackendien API:t-kurssin, päivitä CoddyKit PROhon. NestJS-yritysbackendien API:t-kurssilla on yhteensä 4 oppituntia.
Mitä opin oppitunnilla ”Toimialuetapahtumat ja AggregateRoot”?
Tuottakaa ja julkaiskaa toimialuetapahtumia aggregaattijuurista EventBusin ja mergeObjectContextin avulla. Harjoittelet NestJS-yritysbackendien API:t-aihetta koodilla, jonka suoritat suoraan selaimessa. Ympäri vuorokauden käytettävissä oleva tekoälytuutori vastaa kysymyksiisi oppitunnin aikana.
Tarvitsenko kokemusta aloittaakseni NestJS-yritysbackendien API:t-opiskelun?
Aiempi kokemus ei ole tarpeen. CoddyKitin NestJS-yritysbackendien API:t-oppimispolku sopii vasta-alkajista edistyneisiin, joten voit aloittaa tästä tai alusta ja edetä omaan tahtiisi. Tämä on oppitunti 3/4.
Kuinka kauan ”Toimialuetapahtumat ja AggregateRoot”-oppitunnin suorittaminen kestää?
Useimmat CoddyKitin oppitunnit kestävät noin 5–10 minuuttia. Jokainen oppitunti on lyhyt ja interaktiivinen, joten edistyt tasaisesti ja voit jatkaa siitä, mihin jäit – sekä verkossa että sovelluksessa.
Voinko kirjoittaa ja suorittaa koodia tällä NestJS-yritysbackendien API:t-oppitunnilla?
Kyllä. Jokainen NestJS-yritysbackendien API:t-oppitunti sisältää sisäänrakennetun koodieditorin, joten voit kirjoittaa ja suorittaa oikeaa koodia suoraan selaimessa ja saada välitöntä palautetta tekoälyltä – paikallista asennusta ei tarvita.
Kaikki tämän kurssin oppitunnit
- Komennot, käsittelijät ja komentoväylä
- Kyselyt ja lukumallin projektiot
- Toimialuetapahtumat ja AggregateRoot
- Pitkäkestoisten työnkulkujen sagat