Skip to content

数据库

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

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

为了方便起见,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,甚至 MongoDB 等 NoSQL 数据库。本章中介绍的过程对于 TypeORM 支持的任何数据库都是相同的。你只需为所选数据库安装关联的客户端 API 库。

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

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

typescript
// 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连接数据库的重试次数(默认:10
retryDelay连接重试之间的延迟(毫秒)(默认:3000
autoLoadEntities如果为 true,实体将自动加载(默认:false

提示

这里了解更多关于数据源选项的信息。

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

typescript
// app.module.ts
import { DataSource } from 'typeorm';

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

仓库模式

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

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

typescript
// user.entity.ts
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 知道它(除非你使用静态 glob 路径):

typescript
// 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 {}

接下来,让我们看看 UsersModule

typescript
// 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 中:

typescript
// 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 的模块之外使用仓库,则需要重新导出由它生成的提供者。 你可以通过导出整个模块来实现,如下所示:

typescript
// 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)

typescript
// 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 {}

关系

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

有三种类型的关系:

类型说明
One-to-one主表中的每一行在外表中有且仅有一行关联行。使用 @OneToOne() 装饰器来定义此类关系。
One-to-many / Many-to-one主表中的每一行在外表中有一行或多行关联行。使用 @OneToMany()@ManyToOne() 装饰器来定义此类关系。
Many-to-many主表中的每一行在外表中有多行关联行,外表中的每条记录在主表中也有多行关联行。使用 @ManyToMany() 装饰器来定义此类关系。

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

typescript
// user.entity.ts
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,如下所示:

typescript
// 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 设置被包含进来。

分离实体定义

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

typescript
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 实例,例如:

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

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

提示

DataSource 类从 typeorm 包中导入。

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

typescript
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 方法的回调风格方式(了解更多)。

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

订阅者

通过 TypeORM 订阅者,你可以监听特定的实体事件。

typescript
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 数组中:

typescript
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 实体存储在自己的数据库中。

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

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

更多详情请参见此 issue

此时,你已经将 UserAlbum 实体注册到各自的数据源。通过此设置,你需要告诉 TypeOrmModule.forFeature() 方法和 @InjectRepository() 装饰器应该使用哪个数据源。如果不传递任何数据源名称,则使用 default 数据源。

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

你也可以为给定的数据源注入 DataSourceEntityManager

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

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

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

测试

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

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

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

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

异步配置

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

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

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

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

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

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

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

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

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

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

提示

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

自定义 DataSource 工厂

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

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

typescript
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 中。

typescript
// 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 对象将在整个项目中可注入(无需导入任何模块),例如:

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

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

模型

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

typescript
// 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 知道它:

typescript
// 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 {}

接下来,让我们看看 UsersModule

typescript
// 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 中:

typescript
// 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 的模块之外使用模型,则需要重新导出由它生成的提供者。 你可以通过导出整个模块来实现,如下所示:

typescript
// 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)

typescript
// 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 {}

关系

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

有三种类型的关系:

类型说明
One-to-one主表中的每一行在外表中有且仅有一行关联行
One-to-many / Many-to-one主表中的每一行在外表中有一行或多行关联行
Many-to-many主表中的每一行在外表中有多行关联行,外表中的每条记录在主表中也有多行关联行

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

typescript
// user.model.ts
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() 方法)的 autoLoadModelssynchronize 属性都设置为 true 来自动加载模型,如下所示:

typescript
// 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 对象注入到类中:

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

提示

Sequelize 类从 sequelize-typescript 包中导入。

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

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) {
    // 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 实体存储在自己的数据库中。

typescript
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。请注意,你不应该有多个没有名称或具有相同名称的连接,否则它们将被覆盖。

此时,你已经将 UserAlbum 模型注册到各自的连接。通过此设置,你需要告诉 SequelizeModule.forFeature() 方法和 @InjectModel() 装饰器应该使用哪个连接。如果不传递任何连接名称,则使用 default 连接。

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

你也可以为给定的连接注入 Sequelize 实例:

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

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

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

测试

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

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

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

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

异步配置

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

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

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

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

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

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

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

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

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

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

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

示例

一个可运行的示例可以在这里找到。

基于 NestJS 官方文档翻译