Skip to content

Cell - CQRS 组件 ​

@celljs/cqrs 将改变状态的代码(命令)与读取状态的代码(查询)分离,并让应用的其他部分对发生的事情做出反应(事件),方式参考 @nestjs/cqrs。处理器是 Cell 组件:通过装饰器查找,获得与其他组件相同的注入,在第一条消息分派时创建。除了 @celljs/core 外无其他依赖;仅限后端。

bash
npm install @celljs/cqrs

命令与查询 ​

命令或查询是类的实例;其类决定了唯一的处理器。扩展 CommandMessage<R> / QueryMessage<R> 来给 execute 指定结果类型(可选:没有它的话,execute<R>() 接受类型)。

ts
import { Autowired } from '@celljs/core';
import { Body, Controller, Get, Param, Post } from '@celljs/mvc/lib/node';
import { CommandBus, CommandHandler, CommandMessage, QueryBus, QueryHandler, QueryMessage } from '@celljs/cqrs';

export class CreateUser extends CommandMessage<string> {
    constructor(readonly name: string) {
        super();
    }
}

export class GetUser extends QueryMessage<User | undefined> {
    constructor(readonly id: string) {
        super();
    }
}

@CommandHandler(CreateUser)
export class CreateUserHandler implements CommandHandler<CreateUser> {
    @Autowired(UserRepository)
    protected readonly users: UserRepository;

    execute(command: CreateUser): Promise<string> {
        return this.users.insert(command.name);
    }
}

@QueryHandler(GetUser)
export class GetUserHandler implements QueryHandler<GetUser> {
    @Autowired(UserRepository)
    protected readonly users: UserRepository;

    execute(query: GetUser): Promise<User | undefined> {
        return this.users.findById(query.id);
    }
}

@Controller('users')
export class UserController {
    @Autowired(CommandBus)
    protected readonly commandBus: CommandBus;

    @Autowired(QueryBus)
    protected readonly queryBus: QueryBus;

    @Post()
    create(@Body() body: { name: string }): Promise<string> {
        return this.commandBus.execute(new CreateUser(body.name));
    }

    @Get(':id')
    get(@Param('id') id: string): Promise<User | undefined> {
        return this.queryBus.execute(new GetUser(id));
    }
}

execute 解析为处理器返回的值,拒绝为其抛出的错误。装饰器接受 @Component 的选项作为第二参数(scope、proxy 用于 AOP 等)。

事件与传奇 ​

一个事件类可以有任意数量的 @EventHandler 和 @Saga。传奇将事件转化为随之而来的命令:事件总线使用命令总线按顺序运行它返回的内容(一个命令、命令数组或无)。

ts
import { EventBus, EventHandler, Saga } from '@celljs/cqrs';

export class UserCreated {
    constructor(readonly id: string, readonly name: string) { }
}

@EventHandler(UserCreated)                    // 或 @EventHandler([UserCreated, UserRenamed])
export class UserProjection implements EventHandler<UserCreated> {
    async handle(event: UserCreated): Promise<void> { ... }
}

@Saga(UserCreated)
export class WelcomeSaga implements Saga<UserCreated> {
    handle(event: UserCreated): SendWelcomeMail {
        return new SendWelcomeMail(event.id);
    }
}

await eventBus.publish(new UserCreated(id, name));

publish(event) 并发运行事件的处理器和传奇,在全部完成时解析,因此无服务器函数不会在它们之前返回;publishAll(events) 一个接一个地发布事件。处理器或传奇抛出的错误不会拒绝 publish 也不会阻止其他处理器:它会被记录并交给 UnhandledExceptionBus:

ts
@Component(ApplicationLifecycle)
export class CqrsAlerts implements ApplicationLifecycle<Application> {
    @Autowired(UnhandledExceptionBus)
    protected readonly exceptions: UnhandledExceptionBus;

    initialize(): void {
        this.exceptions.onException(({ error, event, handler }) => { ... });
    }
}

聚合根 ​

AggregateRoot 将其更改记录为事件:apply(event) 记录事件并调用聚合根的 on<EventClassName> 方法(如果存在),以更改状态;commit() 用事件总线发布记录的事件。先用 EventPublisher 将聚合根连接到事件总线。

ts
import { AggregateRoot, EventPublisher } from '@celljs/cqrs';

export class User extends AggregateRoot {
    name: string;

    constructor(readonly id: string) {
        super();
    }

    rename(name: string): void {
        this.apply(new UserRenamed(this.id, name));
    }

    onUserRenamed(event: UserRenamed): void {
        this.name = event.name;
    }
}

@CommandHandler(RenameUser)
export class RenameUserHandler implements CommandHandler<RenameUser> {
    @Autowired(EventPublisher)
    protected readonly publisher: EventPublisher;

    @Autowired(UserRepository)
    protected readonly users: UserRepository;

    async execute(command: RenameUser): Promise<void> {
        const user = this.publisher.mergeObjectContext(await this.users.load(command.id));
        user.rename(command.name);
        await this.users.save(user);
        await user.commit();
    }
}
  • loadFromHistory(events) 从存储的事件重建状态,不再次记录它们。
  • getUncommittedEvents() 返回记录的事件;uncommit() 忘记它们。
  • eventPublisher.mergeClassContext(User) 返回创建时连接实例的子类。
  • 记录的事件不是聚合根的属性:JSON.stringify(user) 仅给出其状态。

错误 ​

总线和 commit() 拒绝时返回 CqrsError,其 code 稳定,其 hint 说明如何修复:

CodeWhen
CELL_CQRS_NO_HANDLER命令或查询的类没有 @CommandHandler / @QueryHandler
CELL_CQRS_DUPLICATE_HANDLER一个命令或查询类有两个处理器
CELL_CQRS_NOT_AN_INSTANCE消息是普通对象、null 或原始值,而不是类的实例
CELL_CQRS_AGGREGATE_NOT_MERGED对 EventPublisher 未连接的聚合根调用 commit();事件保持记录状态

自定义 ​

CommandBus、QueryBus、EventBus、EventPublisher 和 UnhandledExceptionBus 是组件:用 rebind: true 替换其中之一,例如要同时将事件发送给消息代理:

ts
@Component({ id: EventBus, rebind: true })
export class BrokerEventBus extends EventBusImpl {
    override async publish(event: object): Promise<void> {
        await super.publish(event);
        await this.broker.send(event);
    }
}

从 @nestjs/cqrs 迁移 ​

@nestjs/cqrs@celljs/cqrs
CqrsModule.forRoot()添加依赖;模块随后端加载
ICommandHandler、IQueryHandler、IEventHandlerCommandHandler、QueryHandler、EventHandler:装饰器和接口用一个名字
@EventsHandler(A, B)@EventHandler([A, B])
Command<R>、Query<R>CommandMessage<R>、QueryMessage<R>(@celljs/mvc 有 @Query 装饰器)
@Saga() 带 RxJS 流@Saga(Event) 类,其 handle(event) 返回命令;无 RxJS
eventBus.publish() 立即返回await eventBus.publish() 等待处理器和传奇
aggregate.commit()await aggregate.commit();未连接时拒绝,而不是什么都不做
autoCommit不支持:调用 commit()
UnhandledExceptionBus(RxJS subject)UnhandledExceptionBus.onException(Cell Event)