Techniques

数据库

Nest 与数据库无关,允许您轻松地与任何 SQL 或 NoSQL 数据库集成。根据您的偏好,您有许多选项可用。

数据库

Nest 与数据库无关,允许您轻松地与任何 SQL 或 NoSQL 数据库集成。根据您的偏好,您有许多选项可用。在最通用的级别上,将 Nest 连接到数据库只需为数据库加载适当的 Node.js 驱动程序,就像您使用 Express 或 Fastify 一样。

您还可以直接使用任何通用的 Node.js 数据库集成库或 ORM,例如 MikroORM(参见 MikroORM 配方)、Sequelize(参见 Sequelize 集成)、Knex.js(参见 Knex.js 教程)、TypeORM 和 Prisma(参见 Prisma 配方),以在更高的抽象级别上操作。

为了方便起见,Nest 还提供了与现成的 TypeORM 和 Sequelize 的紧密集成,我们将在本章中分别介绍这些集成,而 Mongoose 则在本章中介绍。这些集成提供了额外的 NestJS 特定功能,例如模型/存储库注入、可测试性和异步配置,使访问您选择的数据库变得更加容易。

TypeORM 集成

为了与 SQL 和 NoSQL 数据库集成,Nest 提供了 @nestjs/typeorm 包。Nest 使用 TypeORM,因为它是 TypeScript 中最成熟的对象关系映射器 (ORM)。由于它是用 TypeScript 编写的,因此与 Nest 框架集成得很好。

要开始使用它,我们首先安装所需的依赖项。在本章中,我们将演示使用流行的 MySQL 关系型数据库管理系统,但 TypeORM 为许多关系型数据库提供支持,例如 PostgreSQL、Oracle、Microsoft SQL Server、SQLite,甚至像 MongoDB 这样的 NoSQL 数据库。我们在本章中介绍的过程对于 TypeORM 支持的任何数据库都是相同的。您只需要为您选择的数据库安装相关的客户端 API 库。

$ npm install --save @nestjs/typeorm typeorm mysql2

安装过程完成后,我们可以将 TypeOrmModule 导入到根 AppModule 中。

@@filename(app.module)
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'test',
      entities: [],
      synchronize: true,
    }),
  ],
})
export class AppModule {}

警告 设置 synchronize: true 不应在生产环境中使用 - 否则您可能会丢失生产数据。

forRoot() 方法支持 TypeORM 包中 DataSource 构造函数暴露的所有配置属性。此外,还有几个额外的配置属性,如下所述。

属性描述
retryAttempts重试连接数据库的次数(默认值:10)
retryDelay连接重试尝试之间的延迟(毫秒)(默认值:3000)
autoLoadEntities如果为 true,实体将自动加载(默认值:false)

提示 了解更多关于连接选项的信息,请参阅 TypeORM 文档。

完成此操作后,DataSource 和 EntityManager 对象将可用于在整个项目中注入(无需导入任何模块),例如:

@@filename(app.service)
import { Injectable } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';

@Injectable()
export class AppService {
  constructor(
    @InjectDataSource()
    private dataSource: DataSource,
  ) {}
}
@@switch
import { Injectable } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';

@Dependencies(InjectDataSource())
@Injectable()
export class AppService {
  constructor(dataSource) {
    this.dataSource = dataSource;
  }
}

存储库模式

TypeORM 支持存储库设计模式,因此每个实体都有自己的存储库。这些存储库可以从数据库数据源获得。

为了继续示例,我们至少需要一个实体。让我们定义 User 实体。

@@filename(user.entity)
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  firstName: string;

  @Column()
  lastName: string;

  @Column({ default: true })
  isActive: boolean;
}

提示 了解更多关于实体的信息,请参阅 TypeORM 文档。

User 实体文件位于 users 目录中。此目录包含与 UsersModule 相关的所有文件。您可以决定在哪里保存模型文件,但我们建议在相应的模块目录中,靠近它们的域创建它们。

要开始使用 User 实体,我们需要通过将其插入到模块 forRoot() 方法选项的 entities 数组中来让 TypeORM 知道它(除非您使用静态全局路径):

@@filename(app.module)
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './users/user.entity';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'test',
      entities: [User],
      synchronize: true,
    }),
  ],
})
export class AppModule {}

接下来,让我们看看 UsersModule:

@@filename(users.module)
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { User } from './user.entity';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  providers: [UsersService],
  controllers: [UsersController],
})
export class UsersModule {}

此模块使用 forFeature() 方法来定义在当前作用域中注册哪些存储库。有了这个设置,我们可以使用 @InjectRepository() 装饰器将 UsersRepository 注入到 UsersService 中:

@@filename(users.service)
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>,
  ) {}

  findAll(): Promise<User[]> {
    return this.usersRepository.find();
  }

  findOne(id: number): Promise<User | null> {
    return this.usersRepository.findOneBy({ id });
  }

  async remove(id: number): Promise<void> {
    await this.usersRepository.delete(id);
  }
}
@@switch
import { Injectable, Dependencies } from '@nestjs/common';
import { getRepositoryToken } from '@nestjs/typeorm';
import { User } from './user.entity';

@Injectable()
@Dependencies(getRepositoryToken(User))
export class UsersService {
  constructor(usersRepository) {
    this.usersRepository = usersRepository;
  }

  findAll() {
    return this.usersRepository.find();
  }

  findOne(id) {
    return this.usersRepository.findOneBy({ id });
  }

  async remove(id) {
    await this.usersRepository.delete(id);
  }
}

注意 不要忘记将 UsersModule 导入到根 AppModule 中。

如果您想在导入 TypeOrmModule.forFeature 的模块之外使用存储库,您需要重新导出由其生成的提供者。您可以通过导出整个模块来做到这一点,如下所示:

@@filename(users.module)
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  exports: [TypeOrmModule]
})
export class UsersModule {}

现在,如果我们在 UserHttpModule 中导入 UsersModule,我们可以在后一个模块的提供者中使用 @InjectRepository(User)。

@@filename(users-http.module)
import { Module } from '@nestjs/common';
import { UsersModule } from './users.module';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';

@Module({
  imports: [UsersModule],
  providers: [UsersService],
  controllers: [UsersController]
})
export class UserHttpModule {}

关系

关系是在两个或多个表之间建立的关联。关系基于每个表的公共字段,通常涉及主键和外键。

有三种类型的关系:

类型描述
一对一主表中的每一行在外表中都有一个且仅有一个关联行。使用 @OneToOne() 装饰器来定义这种类型的关系。
一对多 / 多对一主表中的每一行在外表中有一个或多个相关行。使用 @OneToMany() 和 @ManyToOne() 装饰器来定义这种类型的关系。
多对多主表中的每一行在外表中有许多相关行,外表中的每条记录在主表中也有许多相关行。使用 @ManyToMany() 装饰器来定义这种类型的关系。

要在实体中定义关系,请使用相应的装饰器。例如,要定义每个 User 可以有多张照片,请使用 @OneToMany() 装饰器。

@@filename(user.entity)
import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm';
import { Photo } from '../photos/photo.entity';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  firstName: string;

  @Column()
  lastName: string;

  @Column({ default: true })
  isActive: boolean;

  @OneToMany(type => Photo, photo => photo.user)
  photos: Photo[];
}

提示 要了解更多关于 TypeORM 中关系的信息,请访问 TypeORM 文档。

自动加载实体

手动将实体添加到数据源选项的 entities 数组中可能很繁琐。此外,从根模块引用实体会破坏应用程序域边界并导致实现细节泄漏到应用程序的其他部分。为了解决这个问题,提供了一个替代解决方案。要自动加载实体,请将配置对象(传递给 forRoot() 方法)的 autoLoadEntities 属性设置为 true,如下所示:

@@filename(app.module)
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      ...
      autoLoadEntities: true,
    }),
  ],
})
export class AppModule {}

指定该选项后,通过 forFeature() 方法注册的每个实体都将自动添加到配置对象的 entities 数组中。

警告 请注意,未通过 forFeature() 方法注册,但仅从实体(通过关系)引用的实体不会通过 autoLoadEntities 设置包含在内。

分离实体定义

您可以使用装饰器在模型中定义实体及其列。但有些人更喜欢使用"实体模式"在单独的文件中定义实体及其列。

import { EntitySchema } from 'typeorm';
import { User } from './user.entity';

export const UserSchema = new EntitySchema<User>({
  name: 'User',
  target: User,
  columns: {
    id: {
      type: Number,
      primary: true,
      generated: true,
    },
    firstName: {
      type: String,
    },
    lastName: {
      type: String,
    },
    isActive: {
      type: Boolean,
      default: true,
    },
  },
  relations: {
    photos: {
      type: 'one-to-many',
      target: 'Photo', // PhotoSchema 的名称
    },
  },
});

警告 如果您提供 target 选项,name 选项值必须与目标类的名称相同。如果您不提供 target,您可以使用任何名称。

Nest 允许您在期望 Entity 的任何地方使用 EntitySchema 实例,例如:

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserSchema } from './user.schema';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
  imports: [TypeOrmModule.forFeature([UserSchema])],
  providers: [UsersService],
  controllers: [UsersController],
})
export class UsersModule {}

TypeORM 事务

数据库事务象征着在数据库管理系统中对数据库执行的工作单元,并以独立于其他事务的连贯和可靠的方式处理。事务通常表示数据库中的任何更改(了解更多)。

有许多不同的策略来处理 TypeORM 事务。我们建议使用 QueryRunner 类,因为它可以完全控制事务。

首先,我们需要以正常方式将 DataSource 对象注入到类中:

@Injectable()
export class UsersService {
  constructor(private dataSource: DataSource) {}
}

提示 DataSource 类从 typeorm 包导入。

现在,我们可以使用此对象来创建事务。

async createMany(users: User[]) {
  const queryRunner = this.dataSource.createQueryRunner();

  await queryRunner.connect();
  await queryRunner.startTransaction();
  try {
    await queryRunner.manager.save(users[0]);
    await queryRunner.manager.save(users[1]);

    await queryRunner.commitTransaction();
  } catch (err) {
    // 由于我们有错误,让我们回滚我们所做的更改
    await queryRunner.rollbackTransaction();
  } finally {
    // 您需要释放手动实例化的 queryRunner
    await queryRunner.release();
  }
}

提示 请注意,dataSource 仅用于创建 QueryRunner。但是,要测试此类需要模拟整个 DataSource 对象(它暴露了几个方法)。因此,我们建议使用辅助工厂类(例如,QueryRunnerFactory)并定义一个具有维护事务所需的有限方法集的接口。这种技术使模拟这些方法变得非常简单。

或者,您可以使用 DataSource 对象的 transaction 方法的回调样式方法(阅读更多)。

async createMany(users: User[]) {
  await this.dataSource.transaction(async manager => {
    await manager.save(users[0]);
    await manager.save(users[1]);
  });
}

订阅者

使用 TypeORM 订阅者,您可以监听特定的实体事件。

import {
  DataSource,
  EntitySubscriberInterface,
  EventSubscriber,
  InsertEvent,
} from 'typeorm';
import { User } from './user.entity';

@EventSubscriber()
export class UserSubscriber implements EntitySubscriberInterface<User> {
  constructor(dataSource: DataSource) {
    dataSource.subscribers.push(this);
  }

  listenTo() {
    return User;
  }

  beforeInsert(event: InsertEvent<User>) {
    console.log(`BEFORE USER INSERTED: `, event.entity);
  }
}

警告 事件订阅者不能是请求作用域的。

现在,将 UserSubscriber 类添加到 providers 数组中:

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { UserSubscriber } from './user.subscriber';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  providers: [UsersService, UserSubscriber],
  controllers: [UsersController],
})
export class UsersModule {}

迁移

迁移提供了一种增量更新数据库模式以使其与应用程序的数据模型保持同步的方法,同时保留数据库中的现有数据。要生成、运行和还原迁移,TypeORM 提供了专用的 CLI。

迁移类与 Nest 应用程序源代码分离。它们的生命周期由 TypeORM CLI 维护。因此,您无法利用依赖注入和其他 Nest 特定功能进行迁移。要了解更多关于迁移的信息,请遵循 TypeORM 文档中的指南。

多个数据库

一些项目需要多个数据库连接。这也可以通过此模块实现。要使用多个连接,首先创建连接。在这种情况下,数据源命名变为强制性的。

假设您有一个存储在自己数据库中的 Album 实体。

const defaultOptions = {
  type: 'postgres',
  port: 5432,
  username: 'user',
  password: 'password',
  database: 'db',
  synchronize: true,
};

@Module({
  imports: [
    TypeOrmModule.forRoot({
      ...defaultOptions,
      host: 'user_db_host',
      entities: [User],
    }),
    TypeOrmModule.forRoot({
      ...defaultOptions,
      name: 'albumsConnection',
      host: 'album_db_host',
      entities: [Album],
    }),
  ],
})
export class AppModule {}

注意 如果您没有为数据源设置 name,其名称将设置为 default。请注意,您不应该有多个没有名称的连接,或具有相同名称的连接,否则它们将被覆盖。

注意 如果您使用 TypeOrmModule.forRootAsync,您还必须在 useFactory 之外设置数据源名称。例如:

TypeOrmModule.forRootAsync({
  name: 'albumsConnection',
  useFactory: ...,
  inject: ...,
}),

有关更多详细信息,请参阅此问题。

此时,您已经将 User 和 Album 实体注册到它们自己的数据源中。通过此设置,您必须告诉 TypeOrmModule.forFeature() 方法和 @InjectRepository() 装饰器应该使用哪个数据源。如果您不传递任何数据源名称,则使用 default 数据源。

@Module({
  imports: [
    TypeOrmModule.forFeature([User]),
    TypeOrmModule.forFeature([Album], 'albumsConnection'),
  ],
})
export class AppModule {}

您还可以为给定的数据源注入 DataSource 或 EntityManager:

@Injectable()
export class AlbumsService {
  constructor(
    @InjectDataSource('albumsConnection')
    private dataSource: DataSource,
    @InjectEntityManager('albumsConnection')
    private entityManager: EntityManager,
  ) {}
}

也可以将任何 DataSource 注入到提供者中:

@Module({
  providers: [
    {
      provide: AlbumsService,
      useFactory: (albumsConnection: DataSource) => {
        return new AlbumsService(albumsConnection);
      },
      inject: [getDataSourceToken('albumsConnection')],
    },
  ],
})
export class AlbumsModule {}

测试

当涉及到单元测试应用程序时,我们通常希望避免建立数据库连接,保持我们的测试套件独立,并且它们的执行过程尽可能快。但是我们的类可能依赖于从数据源(连接)实例中提取的存储库。我们如何处理这个问题?解决方案是创建模拟存储库。为了实现这一点,我们设置了自定义提供者。每个注册的存储库都自动由一个 <EntityName>Repository 令牌表示,其中 EntityName 是您的实体类的名称。

@nestjs/typeorm 包暴露了 getRepositoryToken() 函数,该函数根据给定实体返回准备好的令牌。

@Module({
  providers: [
    UsersService,
    {
      provide: getRepositoryToken(User),
      useValue: mockRepository,
    },
  ],
})
export class UsersModule {}

现在,替代的 mockRepository 将用作 UsersRepository。每当任何类使用 @InjectRepository() 装饰器请求 UsersRepository 时,Nest 将使用注册的 mockRepository 对象。

异步配置

您可能希望异步而不是静态地传递存储库模块选项。在这种情况下,使用 forRootAsync() 方法,它提供了几种处理异步配置的方法。

一种方法是使用工厂函数:

TypeOrmModule.forRootAsync({
  useFactory: () => ({
    type: 'mysql',
    host: 'localhost',
    port: 3306,
    username: 'root',
    password: 'root',
    database: 'test',
    entities: [],
    synchronize: true,
  }),
});

我们的工厂行为类似于任何其他异步提供者(例如,它可以是 async 并且能够通过 inject 注入依赖项)。

TypeOrmModule.forRootAsync({
  imports: [ConfigModule],
  useFactory: (configService: ConfigService) => ({
    type: 'mysql',
    host: configService.get('HOST'),
    port: +configService.get('PORT'),
    username: configService.get('USERNAME'),
    password: configService.get('PASSWORD'),
    database: configService.get('DATABASE'),
    entities: [],
    synchronize: true,
  }),
  inject: [ConfigService],
});

或者,您可以使用 useClass 语法:

TypeOrmModule.forRootAsync({
  useClass: TypeOrmConfigService,
});

上面的构造将在 TypeOrmModule 内实例化 TypeOrmConfigService 并使用它通过调用 createTypeOrmOptions() 来提供选项对象。请注意,这意味着 TypeOrmConfigService 必须实现 TypeOrmOptionsFactory 接口,如下所示:

@Injectable()
export class TypeOrmConfigService implements TypeOrmOptionsFactory {
  createTypeOrmOptions(): TypeOrmModuleOptions {
    return {
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'test',
      entities: [],
      synchronize: true,
    };
  }
}

为了防止在 TypeOrmModule 内创建 TypeOrmConfigService 并使用从不同模块导入的提供者,您可以使用 useExisting 语法。

TypeOrmModule.forRootAsync({
  imports: [ConfigModule],
  useExisting: ConfigService,
});

这种构造与 useClass 的工作方式相同,但有一个关键区别 - TypeOrmModule 将查找导入的模块以重用现有的 ConfigService,而不是实例化一个新的。

提示 确保 name 属性在与 useFactory、useClass 或 useValue 属性相同的级别上定义。这将允许 Nest 在适当的注入令牌下正确注册数据源。

自定义 DataSource 工厂

结合使用 useFactory、useClass 或 useExisting 的异步配置,您可以选择指定一个 dataSourceFactory 函数,该函数将允许您提供自己的 TypeORM 数据源,而不是允许 TypeOrmModule 创建数据源。

dataSourceFactory 接收在使用 useFactory、useClass 或 useExisting 进行异步配置期间配置的 TypeORM DataSourceOptions,并返回一个解析 TypeORM DataSource 的 Promise。

TypeOrmModule.forRootAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  // 使用 useFactory、useClass 或 useExisting
  // 来配置 DataSourceOptions。
  useFactory: (configService: ConfigService) => ({
    type: 'mysql',
    host: configService.get('HOST'),
    port: +configService.get('PORT'),
    username: configService.get('USERNAME'),
    password: configService.get('PASSWORD'),
    database: configService.get('DATABASE'),
    entities: [],
    synchronize: true,
  }),
  // dataSource 接收配置的 DataSourceOptions
  // 并返回一个 Promise<DataSource>。
  dataSourceFactory: async (options) => {
    const dataSource = await new DataSource(options).initialize();
    return dataSource;
  },
});

提示 DataSource 类从 typeorm 包导入。

示例

一个工作示例可在这里获得。

Sequelize 集成

使用 TypeORM 的替代方案是使用 Sequelize ORM 与 @nestjs/sequelize 包。此外,我们利用 sequelize-typescript 包,它提供了一组额外的装饰器来声明性地定义实体。

要开始使用它,我们首先安装所需的依赖项。在本章中,我们将演示使用流行的 MySQL 关系型数据库管理系统,但 Sequelize 为许多关系型数据库提供支持,例如 PostgreSQL、MySQL、Microsoft SQL Server、SQLite 和 MariaDB。我们在本章中介绍的过程对于 Sequelize 支持的任何数据库都是相同的。您只需要为您选择的数据库安装相关的客户端 API 库。

$ npm install --save @nestjs/sequelize sequelize sequelize-typescript mysql2
$ npm install --save-dev @types/sequelize

安装过程完成后,我们可以将 SequelizeModule 导入到根 AppModule 中。

@@filename(app.module)
import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';

@Module({
  imports: [
    SequelizeModule.forRoot({
      dialect: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'test',
      models: [],
    }),
  ],
})
export class AppModule {}

forRoot() 方法支持 Sequelize 构造函数暴露的所有配置属性(阅读更多)。此外,还有几个额外的配置属性,如下所述。

属性描述
retryAttempts连接数据库的尝试次数(默认值:10)
retryDelay连接重试尝试之间的延迟(毫秒)(默认值:3000)
autoLoadModels如果为 true,模型将自动加载(默认值:false)
keepConnectionAlive如果为 true,连接在应用程序关闭时不会关闭(默认值:false)
synchronize如果为 true,自动加载的模型将被同步(默认值:true)

完成此操作后,Sequelize 对象将可用于在整个项目中注入(无需导入任何模块),例如:

@@filename(app.service)
import { Injectable } from '@nestjs/common';
import { Sequelize } from 'sequelize-typescript';

@Injectable()
export class AppService {
  constructor(private sequelize: Sequelize) {}
}
@@switch
import { Injectable } from '@nestjs/common';
import { Sequelize } from 'sequelize-typescript';

@Dependencies(Sequelize)
@Injectable()
export class AppService {
  constructor(sequelize) {
    this.sequelize = sequelize;
  }
}

模型

Sequelize 实现了活动记录模式。使用此模式,您直接使用模型类与数据库交互。为了继续示例,我们至少需要一个模型。让我们定义 User 模型。

@@filename(user.model)
import { Column, Model, Table } from 'sequelize-typescript';

@Table
export class User extends Model {
  @Column
  firstName: string;

  @Column
  lastName: string;

  @Column({ defaultValue: true })
  isActive: boolean;
}

提示 了解更多关于可用装饰器的信息,请参阅这里。

User 模型文件位于 users 目录中。此目录包含与 UsersModule 相关的所有文件。您可以决定在哪里保存模型文件,但我们建议在相应的模块目录中,靠近它们的域创建它们。

要开始使用 User 模型,我们需要通过将其插入到模块 forRoot() 方法选项的 models 数组中来让 Sequelize 知道它:

@@filename(app.module)
import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';
import { User } from './users/user.model';

@Module({
  imports: [
    SequelizeModule.forRoot({
      dialect: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'test',
      models: [User],
    }),
  ],
})
export class AppModule {}

接下来,让我们看看 UsersModule:

@@filename(users.module)
import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';
import { User } from './user.model';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
  imports: [SequelizeModule.forFeature([User])],
  providers: [UsersService],
  controllers: [UsersController],
})
export class UsersModule {}

此模块使用 forFeature() 方法来定义在当前作用域中注册哪些模型。有了这个设置,我们可以使用 @InjectModel() 装饰器将 UserModel 注入到 UsersService 中:

@@filename(users.service)
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/sequelize';
import { User } from './user.model';

@Injectable()
export class UsersService {
  constructor(
    @InjectModel(User)
    private userModel: typeof User,
  ) {}

  async findAll(): Promise<User[]> {
    return this.userModel.findAll();
  }

  findOne(id: string): Promise<User> {
    return this.userModel.findOne({
      where: {
        id,
      },
    });
  }

  async remove(id: string): Promise<void> {
    const user = await this.findOne(id);
    await user.destroy();
  }
}
@@switch
import { Injectable, Dependencies } from '@nestjs/common';
import { getModelToken } from '@nestjs/sequelize';
import { User } from './user.model';

@Injectable()
@Dependencies(getModelToken(User))
export class UsersService {
  constructor(usersRepository) {
    this.usersRepository = usersRepository;
  }

  async findAll() {
    return this.userModel.findAll();
  }

  findOne(id) {
    return this.userModel.findOne({
      where: {
        id,
      },
    });
  }

  async remove(id) {
    const user = await this.findOne(id);
    await user.destroy();
  }
}

注意 不要忘记将 UsersModule 导入到根 AppModule 中。

如果您想在导入 SequelizeModule.forFeature 的模块之外使用模型,您需要重新导出由其生成的提供者。您可以通过导出整个模块来做到这一点,如下所示:

@@filename(users.module)
import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';
import { User } from './user.entity';

@Module({
  imports: [SequelizeModule.forFeature([User])],
  exports: [SequelizeModule]
})
export class UsersModule {}

现在,如果我们在 UserHttpModule 中导入 UsersModule,我们可以在后一个模块的提供者中使用 @InjectModel(User)。

@@filename(users-http.module)
import { Module } from '@nestjs/common';
import { UsersModule } from './users.module';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';

@Module({
  imports: [UsersModule],
  providers: [UsersService],
  controllers: [UsersController]
})
export class UserHttpModule {}

关系

关系是在两个或多个表之间建立的关联。关系基于每个表的公共字段,通常涉及主键和外键。

有三种类型的关系:

类型描述
一对一主表中的每一行在外表中都有一个且仅有一个关联行
一对多 / 多对一主表中的每一行在外表中有一个或多个相关行
多对多主表中的每一行在外表中有许多相关行,外表中的每条记录在主表中也有许多相关行

要在模型中定义关系,请使用相应的装饰器。例如,要定义每个 User 可以有多张照片,请使用 @HasMany() 装饰器。

@@filename(user.model)
import { Column, Model, Table, HasMany } from 'sequelize-typescript';
import { Photo } from '../photos/photo.model';

@Table
export class User extends Model {
  @Column
  firstName: string;

  @Column
  lastName: string;

  @Column({ defaultValue: true })
  isActive: boolean;

  @HasMany(() => Photo)
  photos: Photo[];
}

提示 要了解更多关于 Sequelize 中关联的信息,请阅读此章节。

自动加载模型

手动将模型添加到连接选项的 models 数组中可能很繁琐。此外,从根模块引用模型会破坏应用程序域边界并导致实现细节泄漏到应用程序的其他部分。要解决此问题,请通过将配置对象(传递给 forRoot() 方法)的 autoLoadModels 和 synchronize 属性都设置为 true 来自动加载模型,如下所示:

@@filename(app.module)
import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';

@Module({
  imports: [
    SequelizeModule.forRoot({
      ...
      autoLoadModels: true,
      synchronize: true,
    }),
  ],
})
export class AppModule {}

指定该选项后,通过 forFeature() 方法注册的每个模型都将自动添加到配置对象的 models 数组中。

警告 请注意,未通过 forFeature() 方法注册,但仅从模型(通过关联)引用的模型不会被包含在内。

Sequelize 事务

数据库事务象征着在数据库管理系统中对数据库执行的工作单元,并以独立于其他事务的连贯和可靠的方式处理。事务通常表示数据库中的任何更改(了解更多)。

有许多不同的策略来处理 Sequelize 事务。下面是托管事务(自动回调)的示例实现。

首先,我们需要以正常方式将 Sequelize 对象注入到类中:

@Injectable()
export class UsersService {
  constructor(private sequelize: Sequelize) {}
}

提示 Sequelize 类从 sequelize-typescript 包导入。

现在,我们可以使用此对象来创建事务。

async createMany() {
  try {
    await this.sequelize.transaction(async t => {
      const transactionHost = { transaction: t };

      await this.userModel.create(
          { firstName: 'Abraham', lastName: 'Lincoln' },
          transactionHost,
      );
      await this.userModel.create(
          { firstName: 'John', lastName: 'Boothe' },
          transactionHost,
      );
    });
  } catch (err) {
    // 事务已回滚
    // err 是返回给事务回调的 promise 链拒绝的任何内容
  }
}

提示 请注意,Sequelize 实例仅用于启动事务。但是,要测试此类需要模拟整个 Sequelize 对象(它暴露了几个方法)。因此,我们建议使用辅助工厂类(例如,TransactionRunner)并定义一个具有维护事务所需的有限方法集的接口。这种技术使模拟这些方法变得非常简单。

迁移

迁移提供了一种增量更新数据库模式以使其与应用程序的数据模型保持同步的方法,同时保留数据库中的现有数据。要生成、运行和还原迁移,Sequelize 提供了专用的 CLI。

迁移类与 Nest 应用程序源代码分离。它们的生命周期由 Sequelize CLI 维护。因此,您无法利用依赖注入和其他 Nest 特定功能进行迁移。要了解更多关于迁移的信息,请遵循 Sequelize 文档中的指南。

多个数据库

一些项目需要多个数据库连接。这也可以通过此模块实现。要使用多个连接,首先创建连接。在这种情况下,连接命名变为强制性的。

假设您有一个存储在自己数据库中的 Album 实体。

const defaultOptions = {
  dialect: 'postgres',
  port: 5432,
  username: 'user',
  password: 'password',
  database: 'db',
  synchronize: true,
};

@Module({
  imports: [
    SequelizeModule.forRoot({
      ...defaultOptions,
      host: 'user_db_host',
      models: [User],
    }),
    SequelizeModule.forRoot({
      ...defaultOptions,
      name: 'albumsConnection',
      host: 'album_db_host',
      models: [Album],
    }),
  ],
})
export class AppModule {}

注意 如果您没有为连接设置 name,其名称将设置为 default。请注意,您不应该有多个没有名称的连接,或具有相同名称的连接,否则它们将被覆盖。

此时,您已经将 User 和 Album 模型注册到它们自己的连接中。通过此设置,您必须告诉 SequelizeModule.forFeature() 方法和 @InjectModel() 装饰器应该使用哪个连接。如果您不传递任何连接名称,则使用 default 连接。

@Module({
  imports: [
    SequelizeModule.forFeature([User]),
    SequelizeModule.forFeature([Album], 'albumsConnection'),
  ],
})
export class AppModule {}

您还可以为给定连接注入 Sequelize 实例:

@Injectable()
export class AlbumsService {
  constructor(
    @InjectConnection('albumsConnection')
    private sequelize: Sequelize,
  ) {}
}

也可以将任何 Sequelize 实例注入到提供者中:

@Module({
  providers: [
    {
      provide: AlbumsService,
      useFactory: (albumsSequelize: Sequelize) => {
        return new AlbumsService(albumsSequelize);
      },
      inject: [getDataSourceToken('albumsConnection')],
    },
  ],
})
export class AlbumsModule {}

测试

当涉及到单元测试应用程序时,我们通常希望避免建立数据库连接,保持我们的测试套件独立,并且它们的执行过程尽可能快。但是我们的类可能依赖于从连接实例中提取的模型。我们如何处理这个问题?解决方案是创建模拟模型。为了实现这一点,我们设置了自定义提供者。每个注册的模型都自动由一个 <ModelName>Model 令牌表示,其中 ModelName 是您的模型类的名称。

@nestjs/sequelize 包暴露了 getModelToken() 函数,该函数根据给定模型返回准备好的令牌。

@Module({
  providers: [
    UsersService,
    {
      provide: getModelToken(User),
      useValue: mockModel,
    },
  ],
})
export class UsersModule {}

现在,替代的 mockModel 将用作 UserModel。每当任何类使用 @InjectModel() 装饰器请求 UserModel 时,Nest 将使用注册的 mockModel 对象。

异步配置

您可能希望异步而不是静态地传递 SequelizeModule 选项。在这种情况下,使用 forRootAsync() 方法,它提供了几种处理异步配置的方法。

一种方法是使用工厂函数:

SequelizeModule.forRootAsync({
  useFactory: () => ({
    dialect: 'mysql',
    host: 'localhost',
    port: 3306,
    username: 'root',
    password: 'root',
    database: 'test',
    models: [],
  }),
});

我们的工厂行为类似于任何其他异步提供者(例如,它可以是 async 并且能够通过 inject 注入依赖项)。

SequelizeModule.forRootAsync({
  imports: [ConfigModule],
  useFactory: (configService: ConfigService) => ({
    dialect: 'mysql',
    host: configService.get('HOST'),
    port: +configService.get('PORT'),
    username: configService.get('USERNAME'),
    password: configService.get('PASSWORD'),
    database: configService.get('DATABASE'),
    models: [],
  }),
  inject: [ConfigService],
});

或者,您可以使用 useClass 语法:

SequelizeModule.forRootAsync({
  useClass: SequelizeConfigService,
});

上面的构造将在 SequelizeModule 内实例化 SequelizeConfigService 并使用它通过调用 createSequelizeOptions() 来提供选项对象。请注意,这意味着 SequelizeConfigService 必须实现 SequelizeOptionsFactory 接口,如下所示:

@Injectable()
class SequelizeConfigService implements SequelizeOptionsFactory {
  createSequelizeOptions(): SequelizeModuleOptions {
    return {
      dialect: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'test',
      models: [],
    };
  }
}

为了防止在 SequelizeModule 内创建 SequelizeConfigService 并使用从不同模块导入的提供者,您可以使用 useExisting 语法。

SequelizeModule.forRootAsync({
  imports: [ConfigModule],
  useExisting: ConfigService,
});

这种构造与 useClass 的工作方式相同,但有一个关键区别 - SequelizeModule 将查找导入的模块以重用现有的 ConfigService,而不是实例化一个新的。

示例

一个工作示例可在这里获得。