前言

创建一个命令指令发送给执行者去执行的流程跟上文的创建一个查询接口差不多,只是在执行者这里有些不同。z在以往的项目开发中,查询往往是变化最多的,可能需要统计数据,可能需要关联查询几张表,可能需要添加筛选参数,可能需要过滤字段。
这对领域层来说不太友好,因为上下文与上下文之间、聚合与聚合之间都有明显的边界,不能轻易跨越边界去调用。并且领域层不会直接跟数据库打交道,而是跟仓储对接。
最简洁的仓储只需要提供save和get方法就可以了。聚合根可能会被其他聚合根引用,所以一般情况下是不会进行删除的。而update和insert合并为save。
从上面可以看出,边界的限制,仓储只提供简单的查询,所以领域层并不适合复杂查询。因此才引入CQRS架构模式。查询直接与数据库对接,命令则通过领域层定义业务逻辑。
废话不多说,接下来实现一个功能:添加商品

创建Command

command需要引用领域层,所以我们先在Domain层中准备好以下代码:

  1. //src\domain\product\model\product\product.ts
  2. //定义聚合根
  3. export class Product extends AggregateRoot {
  4. constructor(
  5. productId?: string,
  6. productName?: string,
  7. price?: number,
  8. stock?: number
  9. ) {
  10. super();
  11. this.setProductId(productId);
  12. this.setProductName(productName);
  13. this.setPrice(price);
  14. this.setStock(stock);
  15. }
  16. private productId: string;
  17. public getProductId(): string {
  18. return this.productId;
  19. }
  20. public setProductId(v: string): void {
  21. this.productId = v;
  22. }
  23. private productName: string;
  24. public getProductName(): string {
  25. return this.productName;
  26. }
  27. public setProductName(v: string): void {
  28. this.productName = v;
  29. }
  30. private price: number;
  31. public getPrice(): number {
  32. return this.price;
  33. }
  34. public setPrice(v: number): void {
  35. this.price = v;
  36. }
  37. private stock: number;
  38. public getStock(): number {
  39. return this.stock;
  40. }
  41. public setStock(v: number): void {
  42. this.stock = v;
  43. }
  44. }
  45. //src\domain\product\repository\product.ts
  46. //定义仓储接口
  47. import { Product } from '../model/product/product';
  48. export interface IProductRepository {
  49. getById(id: string): Promise<Product>;
  50. save(product: Product): Promise<boolean>;
  51. remove(product: Product): Promise<boolean>;
  52. removeById(id: string): Promise<boolean>;
  53. }

有的人可能会问了,为什么只定义了仓储接口,仓储实现在哪里?
这就是仓储的作用,让我们在开发业务的时候忘记数据库,只需要关注领域模型,真正从面向数据库开发转变为面向领域开发。
接下来我们就来定义Command指令

//src\application\command\product\interface.ts
import { ICommandBase } from '../../../infrastructure/core/application/command';

export interface IAddProductCommand extends ICommandBase {
  productName: string;
  price: number;
  stock: number;
}

//src\application\command\product\impl\add-product.command.ts
import { CommandBase } from '../../../../infrastructure/core/application/command';
import { IAddProductCommand } from '../interface';
export class AddProductCommand
  extends CommandBase
  implements IAddProductCommand {
  productName: string;
  price: number;
  stock: number;

  constructor(productName: string, price: number, stock: number) {
    super();

    this.productName = productName;
    this.price = price;
    this.stock = stock;
  }
}

创建CommandExecutor

//src\application\command.executor\product\interface.ts
export interface IAddProductExecutor extends ICommandExecutorBase {
  productRepository: IProductRepository;
}

//src\application\command.executor\product\impl\add-product.executor.ts
/**
 * 添加商品命令处理器
 */
//订阅命令
@SubscribeCommand(AddProductCommand)
@Provide()
export class AddProductExecutor
  extends CommandExecutorBase
  implements IAddProductExecutor {
  //商品仓储
  @Inject()
  productRepository: IProductRepository;

  //执行命令函数
  async executeCommand<C extends CommandBase>(command: C): Promise<void> {
    if (command instanceof AddProductCommand) {
      const _product = Converter.pojoConvertEntity(command, Product);
      _product.setProductId(UUID.randomUUID());
      await this.productRepository.save(_product);
    } else {
      throw new Error('未定义的命令');
    }
  }
}

执行命令指令

@Provide()
@Controller('/products')
export class ProductController {
  @Inject()
  commandBus: ICommandBus;

  @Inject()
  httpHelper: HttpHelper;

  @Post('/')
  async addProduct(@Body(ALL) product: SaveProductDTO): Promise<void> {
    //通过命令总线发送创建商品命令
    await this.commandBus.send(
      new AddProductCommand(product.productName, product.price, product.stock)
    );
    this.httpHelper.success(null, '新增商品成功');
  }
}

最后实现仓储接口

//src\infrastructure\repository.impl\product.ts

import { Init, Inject, Provide } from '@midwayjs/decorator';
import { IProductRepository } from '../../domain/product/repository/product';
import { Product } from '../../domain/product/model/product/product';
import { Converter } from '../common/util/converter';
import { MongoDbContext } from '../db/mongodb/db-context';

@Provide()
export class ProductRepository implements IProductRepository {
  @Inject('mongoDbContext')
  dbCtx: MongoDbContext;

  @Init()
  async initMethod(): Promise<void> {
    //切换数据库
    this.dbCtx.switchDatabase('admin');
  }

  async save(product: Product): Promise<boolean> {
    const isExist = await this.dbCtx.get('product', {
      productId: product.getProductId(),
    });
    if (isExist) {
      await this.dbCtx.update(
        'product',
        {
          productId: product.getProductId(),
        },
        product
      );
    } else {
      await this.dbCtx.save('product', product);
    }
    return true;
  }

  async removeById(id: string): Promise<boolean> {
    await this.dbCtx.remove('product', { productId: id });
    return true;
  }

  async getById(id: string): Promise<Product> {
    const result = await this.dbCtx.get('product', { productId: id });
    if (result) {
      return Converter.pojoConvertEntity(result, Product);
    } else {
      return null;
    }
  }

  async remove(product: Product): Promise<boolean> {
    await this.dbCtx.remove('product', { productId: product.getProductId() });
    return true;
  }
}