从最一般的层面上讲,将 Nest 连接到数据库只需为数据库加载合适的 Node.js 驱动程序,就像使用 Express 或 Fastify 一样。
您还可以直接使用任何通用的 Node.js 数据库集成库或 ORM,例如 MikroORM(请参阅 MikroORM 配方)、Sequelize(请参阅 Sequelize 集成)、Knex.js(请参阅 Knex.js 教程)、TypeORM 和 Prisma(请参阅 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 库即可。
$ npm install --save @nestjs/typeorm typeorm mysql2
安装过程完成后,我们可以将TypeOrmModule
导入到根AppModule
中。
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 DataSource
和 EntityManager
对象将可用于注入整个项目(无需导入任何模块),例如:
import { DataSource } from 'typeorm'
@Module({
imports: [TypeOrmModule.forRoot(), UsersModule],
})
export class AppModule {
constructor(private dataSource: DataSource) {}
}
存储库模式
TypeORM 支持存储库设计模式,因此每个实体都有自己的存储库。这些存储库可以从数据库数据源中获取。
继续这个例子,我们至少需要一个实体。让我们定义User
实体。
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 路径):
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
:
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
中:
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
的模块之外使用存储库,则需要重新导出由其生成的提供程序。
您可以通过导出整个模块来执行此操作,如下所示:
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)
。
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()
装饰器。
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
,如下所示:
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', // 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) {
// 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
方法结合使用(阅读更多)。
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)
}
}
事件订阅者不能是request-scoped。
现在,将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: ...,
injection: ...,
}),
有关更多详细信息,请参阅此问题。
此时,您已使用自己的数据源注册了User
和Album
实体。通过此设置,您必须告诉TypeOrmModule.forFeature()
方法和@InjectRepository()
装饰器应使用哪个数据源。如果您未传递任何数据源名称,则将使用默认
数据源。
@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 {}
测试
在对应用程序进行单元测试时,我们通常希望避免建立数据库连接,保持测试套件独立并尽可能加快执行过程。但我们的类可能依赖于从数据源(连接)实例中提取的存储库。我们如何处理?解决方案是创建模拟存储库。为了实现这一点,我们设置了自定义提供程序(/fundamentals/custom-providers)。每个注册的存储库都自动由<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 在适当的注入令牌下正确注册数据源。
自定义数据源工厂
结合使用 useFactory
、useClass
或 useExisting
的异步配置,您可以选择指定 dataSourceFactory
函数,该函数允许您提供自己的 TypeORM 数据源,而不是允许 TypeOrmModule
创建数据源。
dataSourceFactory
接收在异步配置期间使用 useFactory
、useClass
或 useExisting
配置的 TypeORM DataSourceOptions
,并返回解析 TypeORM DataSource
的 Promise
。
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 库即可。
$ npm install --save @nestjs/sequelize sequelize sequelize-typescript mysql2
$ npm install --save-dev @types/sequelize
安装过程完成后,我们可以将 SequelizeModule
导入到根 AppModule
中。
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
对象将可用于注入整个项目(无需导入任何模块),例如:
import { Injectable } from '@nestjs/common'
import { Sequelize } from 'sequelize-typescript'
@Injectable()
export class AppService {
constructor(private sequelize: Sequelize) {}
}
模型
Sequelize 实现了 Active Record 模式。使用此模式,您可以直接使用模型类与数据库交互。要继续示例,我们至少需要一个模型。让我们定义用户
模型。
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 知道它:
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
:
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
中:
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
的模块之外使用存储库,则需要重新导出由其生成的提供程序。
您可以通过导出整个模块来执行此操作,如下所示:
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)
。
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()
装饰器。
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()
方法中)的 autoLoadModels
和 synchronize
属性都设置为 true
来自动加载模型,如下所示:
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) {
// 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 {}
如果您没有为连接设置名称
,则其名称将设置为默认
。请注意,您不应有多个没有名称或名称相同的连接,否则它们将被覆盖。
此时,您已使用自己的连接注册了用户
和专辑
模型。使用此设置,您必须告诉SequelizeModule.forFeature()
方法和@InjectModel()
装饰器应使用哪个连接。如果您没有传递任何连接名称,则使用默认
连接。
@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
,而不是实例化新的。
Example
A working example is available here.