跳到主要内容

关系型数据库

Nest 是数据库无关的,允许您轻松集成任何 SQL 或 NoSQL 数据库。 根据您的喜好,您有许多选项可供选择。 在最一般的层面上,将 Nest 连接到数据库只是加载适用于数据库的适当 Node.js 驱动程序的问题, 就像您在 Express 或 Fastify 中所做的那样。

您还可以直接使用任何通用的 Node.js 数据库集成库或 ORM, 例如 MikroORM(参见 MikroORM recipe)、 Sequelize(参见 Sequelize integration)、 Knex.js(参见 Knex.js tutorial)、 TypeORMPrisma(参见 Prisma recipe), 以在更高的抽象层次上操作。

为了方便起见,Nest 提供了与 TypeORM 和 Sequelize 的紧密集成, 分别通过 @nestjs/typeorm@nestjs/sequelize 软件包提供, 我们将在本章中介绍,以及与 Mongoose@nestjs/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 库即可。

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

此处了解有关数据源选项的更多信息

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

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 { 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 数组中 (除非使用静态 glob 路径),让 TypeORM 知道它:

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

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

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

关系

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

有三种类型的关系:

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

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

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, 如下所示:

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 设置包含, 但仅通过实体引用(通过关系)的实体不会包含在其中。

分离实体定义

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

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 实例:

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方法结合使用(了解更多

订阅者

通过 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 类添加到提供者数组中:

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(https://typeorm.io/#/migrations/creating-a-new-migration)。

迁移类与 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: ...,
}),

请参阅此问题了解更多详细信息

此时,您已经使用各自的数据源注册了 UserAlbum 实体。 使用这种设置,您必须告诉 TypeOrmModule.forFeature() 方法和 @InjectRepository() 装饰器应该使用哪个数据源。 如果不传递任何数据源名称,将使用default数据源。

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

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

@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 属性与 useFactoryuseClassuseValue 属性定义在同一级别。 这将允许 Nest 在适当的注入令牌下正确注册数据源。

自定义DataSource工厂

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

dataSourceFactory 函数接收在使用 useFactoryuseClassuseExisting 进行异步配置时配置的 TypeORM DataSourceOptions,并返回一个解析为 TypeORM DataSourcePromise

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 外,还可以使用 Sequeliz 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

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

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模型。

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相关的所有文件。 您可以自行决定模型文件的存放位置,但我们建议您将其创建在相应模块目录下的域(domain)附近。

要开始使用 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 {}

接下来我们看看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 中:

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

关系

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

关系可分为三种类型:

One-to-one主表中的每一行在外表中都有且仅有一个关联行
One-to-many / Many-to-one主表中的每一行在外表中都有一个或多个相关行
Many-to-many主表中的每一行在外表中都有许多相关的行,并且外表中的每条记录在主表中都有许多相关的行

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

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 来自动加载模型,如下所示:

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

@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) {
// 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实体。

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连接。

@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, 而不是实例化新的模块。

例子

此处提供了一个工作示例。