秘籍

SQL(TypeORM)

!> 在本文中,您将学习如何使用自定义组件从头开始 基于TypeORM 包创建的 DatabaseModule。因此,此解决方案包含许多额外开销,您可以使用开箱即用的 @nestjs/typeorm 。要了解更多信息,请参阅此处

TypeORM 无疑是 node.js 界中最成熟的对象关系映射器(ORM)。由于它是用 TypeScript 编写的,所以它在 Nest 框架下运行得非常好。要开始使用这个库,我们必须安装所有必需的依赖关系:

  1. $ npm install --save typeorm mysql

我们需要做的第一步是使用 从 typeorm 包中的 createConnection() 函数建立与我们数据库的连接。该createConnection() 函数返回 Promise,所以有必要创建一个异步组件。

database.providers.ts

  1. import { createConnection } from 'typeorm';
  2. export const databaseProviders = [
  3. {
  4. provide: 'DbConnectionToken',
  5. useFactory: async () => await createConnection({
  6. type: 'mysql',
  7. host: 'localhost',
  8. port: 3306,
  9. username: 'root',
  10. password: 'root',
  11. database: 'test',
  12. entities: [
  13. __dirname + '/../**/*.entity{.ts,.js}',
  14. ],
  15. autoSchemaSync: true,
  16. }),
  17. },
  18. ];

?> 遵循最佳做法,我们已在具有*.providers.ts后缀的分隔文件中声明了自定义组件。

然后,我们需要导出这些提供程序,以使其可以在应用程序的其它部分访问它们。

database.module.ts

  1. import { Module } from '@nestjs/common';
  2. import { databaseProviders } from './database.providers';
  3. @Module({
  4. components: [...databaseProviders],
  5. exports: [...databaseProviders],
  6. })
  7. export class DatabaseModule {}

这就是所有。现在我们可以通过 Connection 使用 @Inject() 装饰器注入对象。每个依赖于 Connection 异步组件的组件都将等待,直到 Promise 解决。

存储库模式

该TypeORM支持库的设计模式,使每个实体都有自己的仓库。这些存储库可以从数据库连接中获取。

首先,我们至少需要一个实体。我们将重用 Photo 官方文档中的实体。

photo/photo.entity.ts

  1. import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
  2. @Entity()
  3. export class Photo {
  4. @PrimaryGeneratedColumn()
  5. id: number;
  6. @Column({ length: 500 })
  7. name: string;
  8. @Column('text')
  9. description: string;
  10. @Column()
  11. filename: string;
  12. @Column('int')
  13. views: number;
  14. @Column()
  15. isPublished: boolean;
  16. }

该 Photo 实体属于该 photo 目录。这个目录代表了 PhotoModule。这是你决定在哪里保留你的模型文件。从我的观点来看,最好的方法是将它们放在他们的域中, 放在相应的模块目录中。

我们来创建一个 Repository 组件:

photo.providers.ts

  1. import { Connection, Repository } from 'typeorm';
  2. import { Photo } from './photo.entity';
  3. export const photoProviders = [
  4. {
  5. provide: 'PhotoRepositoryToken',
  6. useFactory: (connection: Connection) => connection.getRepository(Photo),
  7. inject: ['DbConnectionToken'],
  8. },
  9. ];

!> 在真实使用中,你应该避免使用魔术字符串。双方 PhotoRepositoryToken 和 DbConnectionToken 应保持在不同的constants.ts 文件。

现在我们可以注入 PhotoRepository 到 PhotoService 使用 @Inject() 装饰器。

photo.service.ts

  1. import { Component, Inject } from '@nestjs/common';
  2. import { Repository } from 'typeorm';
  3. import { Photo } from './photo.entity';
  4. @Component()
  5. export class PhotoService {
  6. constructor(
  7. @Inject('PhotoRepositoryToken') private readonly photoRepository: Repository<Photo>) {}
  8. async findAll(): Promise<Photo[]> {
  9. return await this.photoRepository.find();
  10. }
  11. }

数据库连接是异步的,但 Nest 使最终用户对此进程完全不可见。该 PhotoRepository 组件正在等待数据库连接,并且PhotoService 被推迟直到存储库准备好使用。整个应用程序可以在每个组件实例化时启动。

这是一个 最终 PhotoModule:

photo.module.ts

  1. import { Module } from '@nestjs/common';
  2. import { DatabaseModule } from '../database/database.module';
  3. import { photoProviders } from './photo.providers';
  4. import { PhotoService } from './photo.service';
  5. @Module({
  6. imports: [DatabaseModule],
  7. components: [
  8. ...photoProviders,
  9. PhotoService,
  10. ],
  11. })
  12. export class PhotoModule {}

?> 不要忘记将 PhotoModule 导入根 ApplicationModule。

MongoDB (Mongoose)

(待翻译,不推荐使用)

SQL(Sequelize)

(待翻译)

身份验证(Passport)

与这里相同

CQRS

最简单的CRUD应用程序的流程可以使用以下步骤来描述:

  1. 控制器层处理HTTP请求并将任务委派给服务。
  2. 服务层是正在执行大部分业务逻辑的地方。
  3. 服务使用 存储库或DAOs 来更改/保留实体。
  4. 实体是我们的模型 - 只有容器的值,setters 和 getters 。

这是一个好办法吗?是的。在大多数情况下, 我们不应该使中小型应用程序更复杂。但有时候这样是不够的, 当我们的需求变得更加复杂的时候, 我们希望有可伸缩的系统与简单的数据流。

这就是为什么 Nest 提供了一个轻量级CQRS模块的原因,下面详细描述了这些组件。

Commands

为了使应用程序更易于理解,每个更改都必须以 Command 开头。当发送任何命令时 - 应用程序必须对其作出反应。命令可能会从服务中分派并在适当的 Command 处理程序中使用。

heroes-game.service.ts

  1. @Component()
  2. export class HeroesGameService {
  3. constructor(private readonly commandBus: CommandBus) {}
  4. async killDragon(heroId: string, killDragonDto: KillDragonDto) {
  5. return await this.commandBus.execute(
  6. new KillDragonCommand(heroId, killDragonDto.dragonId)
  7. );
  8. }
  9. }

这里有一个示例服务, 它调度 KillDragonCommand。让我们来看看这个命令:

kill-dragon.command.ts

  1. export class KillDragonCommand implements ICommand {
  2. constructor(
  3. public readonly heroId: string,
  4. public readonly dragonId: string) {}
  5. }

这个 CommandBus 是一个命令「流」。它将命令委托给等效的处理程序。每个命令必须有相应的命令处理程序:

kill-dragon.handler.ts

  1. @CommandHandler(KillDragonCommand)
  2. export class KillDragonHandler implements ICommandHandler<KillDragonCommand> {
  3. constructor(private readonly repository: HeroRepository) {}
  4. async execute(command: KillDragonCommand, resolve: (value?) => void) {
  5. const { heroId, dragonId } = command;
  6. const hero = this.repository.findOneById(+heroId);
  7. hero.killEnemy(dragonId);
  8. await this.repository.persist(hero);
  9. resolve();
  10. }
  11. }

现在, 每个应用程序状态更改都是 Command 发生的结果。逻辑被封装在处理程序中。我们可以简单地在这里添加日志, 甚至我们可以在数据库中保留我们的命令 (例如, 用于诊断目的)。

为什么我们需要 resolve() 函数?有时, 我们可能希望从处理程序返回消息到服务。此外, 我们可以在 execute() 方法的开头调用此函数, 因此应用程序首先返回到服务中, 然后将响应反馈给客户端, 然后异步返回到这里处理发送的命令。

事件(Events)

由于我们在处理程序中封装了命令, 所以我们阻止了它们之间的交互-应用程序结构仍然不灵活, 不具有响应性。解决方案是使用事件。

hero-killed-dragon.event.ts

  1. export class HeroKilledDragonEvent implements IEvent {
  2. constructor(
  3. public readonly heroId: string,
  4. public readonly dragonId: string) {}
  5. }

事件是异步的。他们由模型调用。模型必须扩展这个 AggregateRoot 类。

hero.model.ts

  1. export class Hero extends AggregateRoot {
  2. constructor(private readonly id: string) {
  3. super();
  4. }
  5. killEnemy(enemyId: string) {
  6. // logic
  7. this.apply(new HeroKilledDragonEvent(this.id, enemyId));
  8. }
  9. }

apply() 方法尚未发送事件, 因为模型和 EventPublisher 类之间没有关系。如何辨别 publisher 的模型?我们需要在我们的命令处理程序中使用一个 publisher mergeObjectContext() 方法。

kill-dragon.handler.ts

  1. @CommandHandler(KillDragonCommand)
  2. export class KillDragonHandler implements ICommandHandler<KillDragonCommand> {
  3. constructor(
  4. private readonly repository: HeroRepository,
  5. private readonly publisher: EventPublisher,
  6. ) {}
  7. async execute(command: KillDragonCommand, resolve: (value?) => void) {
  8. const { heroId, dragonId } = command;
  9. const hero = this.publisher.mergeObjectContext(
  10. await this.repository.findOneById(+heroId),
  11. );
  12. hero.killEnemy(dragonId);
  13. hero.commit();
  14. resolve();
  15. }
  16. }

现在, 一切都按我们预期的方式工作。注意, 我们需要 commit() 事件, 因为他们没有立即调用。当然, 一个对象不一定已经存在。我们也可以轻松地合并类型上下文:

  1. const HeroModel = this.publisher.mergeContext(Hero);
  2. new HeroModel('id');

就是这样。模型现在能够发布事件。我们得处理他们

每个事件都可以有许多事件处理程序。他们不必知道对方。

hero-killed-dragon.handler.ts

  1. @EventsHandler(HeroKilledDragonEvent)
  2. export class HeroKilledDragonHandler implements IEventHandler<HeroKilledDragonEvent> {
  3. constructor(private readonly repository: HeroRepository) {}
  4. handle(event: HeroKilledDragonEvent) {
  5. // logic
  6. }
  7. }

现在, 我们可以将写入逻辑移动到事件处理程序中。

Sagas

这种类型的事件驱动架构可以提高应用程序的反应性和可伸缩性。现在, 当我们有了事件, 我们可以简单地以各种方式对他们作出反应。sagas 是从建筑学的观点来看的最后积木。

sagas 是一个非常强大的功能。单身传奇可以监听听 1..* 事件。它可以组合,合并,过滤事件流。RxJS 库是魔术的来源地。简单地说, 每个 sagas 都必须返回一个包含命令的Observable。此命令是异步调用的。

heroes-game.saga.ts

  1. @Component()
  2. export class HeroesGameSagas {
  3. dragonKilled = (events$: EventObservable<any>): Observable<ICommand> => {
  4. return events$.ofType(HeroKilledDragonEvent)
  5. .map((event) => new DropAncientItemCommand(event.heroId, fakeItemID));
  6. }
  7. }

我们宣布一个规则, 当任何英雄杀死龙-它应该得到古老的项目。然后, DropAncientItemCommand 将被适当的处理程序调度和处理。

建立

最后一件事, 我们要处理的是建立整个机制。

heroes-game.module.ts

  1. export const CommandHandlers = [KillDragonHandler, DropAncientItemHandler];
  2. export const EventHandlers = [HeroKilledDragonHandler, HeroFoundItemHandler];
  3. @Module({
  4. imports: [CQRSModule],
  5. controllers: [HeroesGameController],
  6. components: [
  7. HeroesGameService,
  8. HeroesGameSagas,
  9. ...CommandHandlers,
  10. ...EventHandlers,
  11. HeroRepository,
  12. ]
  13. })
  14. export class HeroesGameModule implements OnModuleInit {
  15. constructor(
  16. private readonly moduleRef: ModuleRef,
  17. private readonly command$: CommandBus,
  18. private readonly event$: EventBus,
  19. private readonly heroesGameSagas: HeroesGameSagas) {}
  20. onModuleInit() {
  21. this.command$.setModuleRef(this.moduleRef);
  22. this.event$.setModuleRef(this.moduleRef);
  23. this.event$.register(EventHandlers);
  24. this.command$.register(CommandHandlers);
  25. this.event$.combineSagas([
  26. this.heroesGameSagas.dragonKilled,
  27. ]);
  28. }
  29. }

概要

CommandBus 和 EventBus 都是 Observables。这意味着您可以轻松地订阅整个「流」, 并通过 Event Sourcing 丰富您的应用程序。

完整的源代码在这里可用。

OpenAPI (Swagger)

(遗弃,建议使用 GraphQL)

MongoDB E2E Testing

(待翻译,不推荐使用)