数据库 Database

Nest 与数据库无关,可让您轻松与任何 SQL 或 NoSQL 数据库集成。您可以根据自己的偏好选择多种选项。

从最一般的层面上讲,将 Nest 连接到数据库只需为数据库加载合适的 Node.js 驱动程序,就像使用 Express 或 Fastify 一样。

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

为方便起见,Nest 分别通过 @nestjs/typeorm@nestjs/sequelize 包提供与 TypeORM 和 Sequelize 的开箱即用紧密集成,我们将在本章中介绍,并通过 @nestjs/mongoose 提供与 Mongoose 的紧密集成,这将在 本章 中介绍。这些集成提供了其他 NestJS 特定功能,例如模型/存储库注入、可测试性和异步配置,使访问您选择的数据库更加容易。

TypeORM 集成

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

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

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

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

app.module
ts
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 Number of attempts to connect to the database (default: 10)
retryDelay Delay between connection retry attempts (ms) (default: 3000)
autoLoadEntities If true, entities will be loaded automatically (default: false)
提示

此处 详细了解数据源选项。

完成后,TypeORM DataSourceEntityManager 对象将可用于注入整个项目(无需导入任何模块),例如:

ts
app.module
ts
import { DataSource } from 'typeorm'

@Module({
  imports: [TypeOrmModule.forRoot(), UsersModule],
})
export class AppModule {
  constructor(private dataSource: DataSource) {}
}

存储库模式

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

继续这个例子,我们至少需要一个实体。让我们定义User实体。

user.entity
ts
import { Column, Entity, 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 知道它(除非您使用静态 glob 路径):

app.module
ts
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 {}

Next, let's look at the UsersModule:

users.module
ts
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 中:

ts
users.service
ts
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)
  }
}
注意

不要忘记将 UsersModule 导入到根 AppModule

如果您想在导入 TypeOrmModule.forFeature 的模块之外使用存储库,则需要重新导出由其生成的提供程序。 您可以通过导出整个模块来执行此操作,如下所示:

users.module
ts
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)

users-http.module
ts
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() 装饰器。

user.entity
ts
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } 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,如下所示:

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

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

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

警告

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

分离实体定义

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

ts
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', // the name of the PhotoSchema
    },
  },
})

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

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

ts
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 对象注入类中:

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

DataSource 类从 typeorm 包导入。

现在,我们可以使用该对象创建交易。

ts
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) {
    // since we have errors lets rollback the changes we made
    await queryRunner.rollbackTransaction();
  } finally {
    // you need to release a queryRunner which was manually instantiated
    await queryRunner.release();
  }
}
提示

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

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

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

订阅者

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

ts
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)
  }
}
警告

事件订阅者不能是request-scoped

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

ts
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 实体。

ts
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 之外设置数据源名称。例如:

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

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

此时,您已使用自己的数据源注册了UserAlbum实体。通过此设置,您必须告诉TypeOrmModule.forFeature()方法和@InjectRepository()装饰器应使用哪个数据源。如果您未传递任何数据源名称,则将使用默认数据源。

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

您还可以为给定的数据源注入DataSourceEntityManager

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

也可以将任何DataSource注入到提供程序:

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

测试

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

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

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

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

异步配置

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

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

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

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

ts
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 语法:

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

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

ts
@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语法。

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

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

提示

确保 name 属性与 useFactoryuseClassuseValue 属性定义在同一级别。这将允许 Nest 在适当的注入令牌下正确注册数据源。

自定义数据源工厂

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

dataSourceFactory 接收在异步配置期间使用 useFactoryuseClassuseExisting 配置的 TypeORM DataSourceOptions,并返回解析 TypeORM DataSourcePromise

ts
TypeOrmModule.forRootAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  // Use useFactory, useClass, or useExisting
  // to configure the 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 receives the configured DataSourceOptions
  // and returns a 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 库即可。

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

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

app.module
ts
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 对象将可用于注入整个项目(无需导入任何模块),例如:

ts
app.service
ts
import { Injectable } from '@nestjs/common'
import { Sequelize } from 'sequelize-typescript'

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

模型

Sequelize 实现了 Active Record 模式。使用此模式,您可以直接使用模型类与数据库交互。要继续示例,我们至少需要一个模型。让我们定义用户模型。

user.model
ts
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 知道它:

app.module
ts
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 {}

Next, let's look at the UsersModule:

users.module
ts
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 中:

ts
users.service
ts
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()
  }
}
注意

不要忘记将 UsersModule 导入到根 AppModule

如果您想在导入 SequelizeModule.forFeature 的模块之外使用存储库,则需要重新导出由其生成的提供程序。 您可以通过导出整个模块来执行此操作,如下所示:

users.module
ts
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)

users-http.module
ts
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()装饰器。

user.model
ts
import { Column, HasMany, Model, Table } 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() 方法中)的 autoLoadModelssynchronize 属性都设置为 true 来自动加载模型,如下所示:

app.module
ts
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 对象注入到类中:

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

Sequelize 类从 sequelize-typescript 包导入。

现在,我们可以使用该对象创建交易。

ts
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) {
    // Transaction has been rolled back
    // err is whatever rejected the promise chain returned to the transaction callback
  }
}
提示

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

迁移

迁移 提供了一种逐步更新数据库架构的方法,以使其与应用程序的数据模型保持同步,同时保留数据库中的现有数据。为了生成、运行和恢复迁移,Sequelize 提供了专用的 CLI

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

多个数据库

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

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

ts
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 {}
通知

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

此时,您已使用自己的连接注册了用户专辑模型。使用此设置,您必须告诉SequelizeModule.forFeature()方法和@InjectModel()装饰器应使用哪个连接。如果您没有传递任何连接名称,则使用默认连接。

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

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

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

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

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

测试

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

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

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

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

异步配置

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

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

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

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

ts
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 语法:

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

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

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

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

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

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

Example

A working example is available here.