Що таке патерн CQRS і як його реалізувати в NestJS?
CQRS у NestJS
CQRS (Розділення відповідальності за команди та запити) відокремлює операції читання (запити) від операцій запису (команди). NestJS надає пакет @nestjs/cqrs для реалізації цього патерну.
Чому CQRS?
| Без CQRS | З CQRS |
|---|---|
| Одна модель для читання та запису | Окремі моделі |
| Одна база даних | Можна використовувати різні БД |
| Сервіс робить все | Чітке розділення обов'язків |
| Важко масштабувати читання/записи незалежно | Масштабування незалежно |
Налаштування
bash
npm install @nestjs/cqrstypescript
import { Module } from '@nestjs/common';
import { CqrsModule } from '@nestjs/cqrs';
@Module({
imports: [CqrsModule],
// ...
})
export class OrdersModule {}Команди (Операції запису)
Визначити команду
typescript
export class CreateOrderCommand {
constructor(
public readonly userId: string,
public readonly items: { productId: string; quantity: number }[],
public readonly shippingAddress: string,
) {}
}Обробник команди
typescript
import { CommandHandler, ICommandHandler, EventBus } from '@nestjs/cqrs';
@CommandHandler(CreateOrderCommand)
export class CreateOrderHandler implements ICommandHandler<CreateOrderCommand> {
constructor(
private readonly orderRepository: OrderRepository,
private readonly eventBus: EventBus,
) {}
async execute(command: CreateOrderCommand) {
const { userId, items, shippingAddress } = command;
const order = await this.orderRepository.create({
userId,
items,
shippingAddress,
status: 'pending',
});
// Публікація доменної події
this.eventBus.publish(new OrderCreatedEvent(order.id, userId));
return order;
}
}Запити (Операції читання)
Визначити запит
typescript
export class GetOrderByIdQuery {
constructor(public readonly orderId: string) {}
}
export class GetUserOrdersQuery {
constructor(
public readonly userId: string,
public readonly page: number = 1,
) {}
}Обробник запиту
typescript
import { QueryHandler, IQueryHandler } from '@nestjs/cqrs';
@QueryHandler(GetOrderByIdQuery)
export class GetOrderByIdHandler implements IQueryHandler<GetOrderByIdQuery> {
constructor(private readonly orderReadRepository: OrderReadRepository) {}
async execute(query: GetOrderByIdQuery) {
return this.orderReadRepository.findById(query.orderId);
}
}Події
Визначити подію
typescript
export class OrderCreatedEvent {
constructor(
public readonly orderId: string,
public readonly userId: string,
) {}
}Обробник події
typescript
import { EventsHandler, IEventHandler } from '@nestjs/cqrs';
@EventsHandler(OrderCreatedEvent)
export class OrderCreatedHandler implements IEventHandler<OrderCreatedEvent> {
constructor(
private readonly emailService: EmailService,
private readonly analyticsService: AnalyticsService,
) {}
async handle(event: OrderCreatedEvent) {
await this.emailService.sendOrderConfirmation(event.userId, event.orderId);
await this.analyticsService.trackOrder(event.orderId);
}
}Використання в контролері
typescript
import { CommandBus, QueryBus } from '@nestjs/cqrs';
@Controller('orders')
export class OrdersController {
constructor(
private readonly commandBus: CommandBus,
private readonly queryBus: QueryBus,
) {}
@Post()
async create(@Body() dto: CreateOrderDto, @CurrentUser() user: User) {
return this.commandBus.execute(
new CreateOrderCommand(user.id, dto.items, dto.shippingAddress),
);
}
@Get(':id')
async findOne(@Param('id') id: string) {
return this.queryBus.execute(new GetOrderByIdQuery(id));
}
}Реєстрація обробників у модулі
typescript
const CommandHandlers = [CreateOrderHandler, CancelOrderHandler];
const QueryHandlers = [GetOrderByIdHandler, GetUserOrdersHandler];
const EventHandlers = [OrderCreatedHandler, OrderCancelledHandler];
@Module({
imports: [CqrsModule],
controllers: [OrdersController],
providers: [
...CommandHandlers,
...QueryHandlers,
...EventHandlers,
OrderRepository,
OrderReadRepository,
],
})
export class OrdersModule {}Коли використовувати CQRS
| Використовуйте CQRS, коли | Не використовуйте, коли |
|---|---|
| Складна бізнес-логіка | Простий CRUD-додаток |
| Різні моделі читання/запису | Маленькі проекти |
| Архітектура, орієнтована на події | Прототип/МVP |
| Потрібен аудит | Команда не знайома з патерном |
| Незалежне масштабування читання/запису | Ризик надмірної інженерії |
Порада: CQRS додає складність. Починайте з простих сервісів. Вводьте CQRS лише тоді, коли ваші патерни читання та запису суттєво розходяться, або коли вам потрібне подієве джерело.
Коротка відповідь
Для співбесідиPremium
Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.