NestJS 数据库

2023-09-08 16:17 更新

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

您还可以直接使用任何通用的 Node.js 数据库集成库或 ORM ,例如 Sequelize (recipe)knexjs (tutorial)`和 TypeORM ,以在更高的抽象级别上进行操作。

为了方便起见,Nest 还提供了与现成的 TypeORM 与 @nestjs/typeorm 的紧密集成,我们将在本章中对此进行介绍,而与 @nestjs/mongoose 的紧密集成将在这一章中介绍。这些集成提供了附加的特定于 nestjs 的特性,比如模型/存储库注入、可测试性和异步配置,从而使访问您选择的数据库更加容易。

TypeORM 集成

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

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

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

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

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

forRoot() 方法支持所有TypeORM包中createConnection()函数暴露出的配置属性。其他一些额外的配置参数描述如下:

参数说明
retryAttempts重试连接数据库的次数(默认:10)
retryDelay两次重试连接的间隔(ms)(默认:3000)
autoLoadEntities如果为true,将自动加载实体(默认:false)
keepConnectionAlive如果为true,在应用程序关闭后连接不会关闭(默认:false)

更多连接选项见这里

另外,我们可以创建 ormconfig.json ,而不是将配置对象传递给 forRoot()。

{
  "type": "mysql",
  "host": "localhost",
  "port": 3306,
  "username": "root",
  "password": "root",
  "database": "test",
  "entities": ["dist/**/*.entity{.ts,.js}"],
  "synchronize": true
}

然后,我们可以不带任何选项地调用 forRoot() :

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

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

静态全局路径(例如 dist/**/*.entity{ .ts,.js} )不适用于 Webpack 热重载。

注意,ormconfig.json 文件由typeorm库载入,因此,任何上述参数之外的属性都不会被应用(例如由forRoot()方法内部支持的属性–例如autoLoadEntities和retryDelay())

一旦完成,TypeORM 的Connection和 EntityManager 对象就可以在整个项目中注入(不需要导入任何模块),例如:

app.module.ts
import { Connection } from 'typeorm';

@Module({
  imports: [TypeOrmModule.forRoot(), PhotoModule],
})
export class AppModule {
  constructor(private readonly connection: Connection) {}
}

存储库模式

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

user.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: string): Promise<User> {
    return this.usersRepository.findOne(id);
  }

  async remove(id: string): 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 './user.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可以有多个Photo,可以使用@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数组中的工作会很无聊。此外,在根模块中涉及实体破坏了应用的域边界,并可能将应用的细节泄露给应用的其他部分。针对这一情况,可以使用静态全局路径(例如, dist/*/.entity{.ts,.js})。

注意,webpack不支持全局路径,因此如果你要在单一仓库(Monorepo)中构建应用,可能不能使用全局路径。针对这一问题,有另外一个可选的方案。在配置对象的属性中(传递给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配置被包含。

事务

数据库事务代表在数据库管理系统(DBMS)中针对数据库的一组操作,这组操作是有关的、可靠的并且和其他事务相互独立的。一个事务通常可以代表数据库中的任何变更(了解更多)。

TypeORM 事务中有很多不同策略来处理事务,我们推荐使用QueryRunner类,因为它对事务是完全可控的。

首先,我们需要将Connection对象以正常方式注入:

@Injectable()
export class UsersService {
  constructor(private connection: Connection) {}
}

Connection类需要从typeorm包中导入

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

async createMany(users: User[]) {
  const queryRunner = this.connection.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();
  }
}

注意connection仅用于创建QueryRunner。然而,要测试这个类,就需要模拟整个Connection对象(它暴露出来的几个方法),因此,我们推荐采用一个帮助工厂类(也就是QueryRunnerFactory)并且定义一个包含仅限于维持事务需要的方法的接口。这一技术让模拟这些方法变得非常直接。

可选地,你可以使用一个Connection对象的回调函数风格的transaction方法(阅读更多)。

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

不推荐使用装饰器来控制事务(@Transaction()和@TransactionManager())。

订阅者

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

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

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

  listenTo() {
    return User;
  }

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

事件订阅者不能是请求范围的。

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

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

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

更多实体订阅者内容见这里

迁移

迁移提供了一个在保存数据库中现有数据的同时增量升级数据库使其与应用中的数据模型保持同步的方法。TypeORM 提供了一个专用CLI 命令行工具用于生成、运行以及回滚迁移。

迁移类和Nest应用源码是分开的。他们的生命周期由TypeORM CLI管理,因此,你不能在迁移中使用依赖注入和其他Nest专有特性。在TypeORM 文档 中查看更多关于迁移的内容。

多个数据库

某些项目可能需要多个数据库连接。这也可以通过本模块实现。要使用多个连接,首先要做的是创建这些连接。在这种情况下,连接命名成为必填项。

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

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

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

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

此时,您的User 和 Album 实体中的每一个都已在各自的连接中注册。通过此设置,您必须告诉 TypeOrmModule.forFeature() 方法和 @InjectRepository() 装饰器应该使用哪种连接。如果不传递任何连接名称,则使用 default 连接。

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

您也可以为给定的连接注入 Connection 或 EntityManager:

@Injectable()
export class AlbumsService {
  constructor(
    @InjectConnection('albumsConnection')
    private connection: Connection,
    @InjectEntityManager('albumsConnection')
    private entityManager: EntityManager
  ) {}
}

测试

在单元测试我们的应用程序时,我们通常希望避免任何数据库连接,从而使我们的测试适合于独立,并使它们的执行过程尽可能快。但是我们的类可能依赖于从连接实例中提取的存储库。那是什么?解决方案是创建假存储库。为了实现这一点,我们设置了[自定义提供者]。事实上,每个注册的存储库都由 entitynamereposition 标记表示,其中 EntityName 是实体类的名称。

@nestjs/typeorm 包提供了基于给定实体返回准备好 token 的 getRepositoryToken() 函数。

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

现在, 将使用mockRepository 作为 UsersRepository。每当任何提供程序使用 @InjectRepository() 装饰器请求 UsersRepository 时, Nest 会使用注册的 mockRepository 对象。

定制存储库

TypeORM 提供称为自定义存储库的功能。要了解有关它的更多信息,请访问此页面。基本上,自定义存储库允许您扩展基本存储库类,并使用几种特殊方法对其进行丰富。

要创建自定义存储库,请使用 @EntityRepository() 装饰器和扩展 Repository 类。

@EntityRepository(Author)
export class AuthorRepository extends Repository<Author> {}

@EntityRepository() 和 Repository 来自 typeorm 包。

创建类后,下一步是将实例化责任移交给 Nest。为此,我们必须将 AuthorRepository 类传递给 TypeOrm.forFeature() 函数。

@Module({
  imports: [TypeOrmModule.forFeature([AuthorRepository])],
  controller: [AuthorController],
  providers: [AuthorService],
})
export class AuthorModule {}

之后,只需使用以下构造注入存储库:

@Injectable()
export class AuthorService {
  constructor(private readonly authorRepository: AuthorRepository) {}
}

异步配置

通常,您可能希望异步传递模块选项,而不是事先传递它们。在这种情况下,使用 forRootAsync() 函数,提供了几种处理异步数据的方法。

第一种可能的方法是使用工厂函数:

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

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

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

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

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

上面的构造将 TypeOrmConfigService 在内部进行实例化 TypeOrmModule,并将利用它来创建选项对象。在 TypeOrmConfigService 必须实现 TypeOrmOptionsFactory 的接口。

上面的构造将在TypeOrmModule内部实例化TypeOrmConfigService,并通过调用createTypeOrmOptions()

@Injectable()
class TypeOrmConfigService implements TypeOrmOptionsFactory {
  createTypeOrmOptions(): TypeOrmModuleOptions {
    return {
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'test',
      entities: [__dirname + '/**/*.entity{.ts,.js}'],
      synchronize: true,
    };
  }
}

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

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

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

示例

这儿有一个可用的例子。

Sequelize 集成

另一个使用TypeORM的选择是使用@nestjs/sequelize包中的Sequelize ROM。额外地,我们使用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两次连接之间间隔时间(ms)(默认:3000)
autoLoadModels如果为true,模型将自动载入(默认:false)
keepConnectionAlive如果为true,在应用关闭后连接将不会关闭(默认:false)
synchronize如果为true,自动载入的模型将同步(默认:false)

一旦这些完成了,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<User> {
  @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 {}

接下来我们看看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();
  }
}

不要忘记在根AppModule中导入UsersModule。

如果你要在导入SequelizeModule.forFreature的模块之外使用存储库,你需要重新导出其生成的提供者。你可以像这样将整个模块导出:

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 './user.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可以有多个Photo,可以使用@HasMany()装饰器。

user.entity.ts
import { Column, Model, Table, HasMany } from 'sequelize-typescript';
import { Photo } from '../photos/photo.model';

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

  @Column
  lastName: string;

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

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

阅读本章了解更多关于Sequelize的内容。

自动载入模型

手动将模型一一添加到连接选项的models数组中的工作会很无聊。此外,在根模块中涉及实体破坏了应用的域边界,并可能将应用的细节泄露给应用的其他部分。针对这一情况,在配置对象的属性中(传递给forRoot()方法的)设置autoLoadModels和synchronize属性来自动载入模型,示意如下:

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()方法注册,而仅仅是在实体中被引用(通过关系)的模型。

事务

数据库事务代表在数据库管理系统(DBMS)中针对数据库的一组操作,这组操作是有关的、可靠的并且和其他事务相互独立的。一个事务通常可以代表数据库中的任何变更(了解更多)。

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) {
    // 一旦发生错误,事务会回滚
  }
}

注意Sequelize仅用于开始一个事务。然而,要测试这个类,就需要模拟整个Sequelize对象(它暴露出来的几个方法),因此,我们推荐采用一个帮助工厂类(也就是TransactionRunner)并且定义一个包含仅限于维持事务需要的方法的接口。这一技术让模拟这些方法变得非常直接。

可选地,你可以使用一个Connection对象的回调函数风格的transaction方法(阅读更多)。

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

不推荐使用装饰器来控制事务(@Transaction()和@TransactionManager())。

迁移

迁移提供了一个在保存数据库中现有数据的同时增量升级数据库使其与应用中的数据模型保持同步的方法。Sequelize提供了一个专用CLI 命令行工具用于生成、运行以及回滚迁移。

迁移类和Nest应用源码是分开的。他们的生命周期由TypeORM CLI管理,因此,你不能在迁移中使用依赖注入和其他Nest专有特性。在Sequelize文档 中查看更多关于迁移的内容。

多个数据库

某些项目可能需要多个数据库连接。这也可以通过本模块实现。要使用多个连接,首先要做的是创建这些连接。在这种情况下,连接命名成为必填项。

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

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

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

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

此时,您的User 和 Album 实体中的每一个都已在各自的连接中注册。通过此设置,您必须告诉 SequelizeModule.forFeature() 方法和 @InjectRepository() 装饰器应该使用哪种连接。如果不传递任何连接名称,则使用 default 连接。

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

您也可以为给定的连接注入 Sequelize:

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

测试

在单元测试我们的应用程序时,我们通常希望避免任何数据库连接,从而使我们的测试适合于独立,并使它们的执行过程尽可能快。但是我们的类可能依赖于从连接实例中提取的存储库。那是什么?解决方案是创建假模型。为了实现这一点,我们设置了[自定义提供者]。事实上,每个注册的模型都由 <ModelName>Model 令牌自动表示,其中 ModelName 是模型类的名称。

@nestjs/sequelize 包提供了基于给定模型返回准备好 token 的 getModelToken() 函数。

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

现在, 将使用mockModel 作为 UsersModel。每当任何提供程序使用 @InjectModel() 装饰器请求 UserModel 时, Nest 会使用注册的 mockModel 对象。

异步配置

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

第一种可能的方法是使用工厂函数:

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

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

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

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

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

上面的构造将 SequelizeConfigService 在SequelizeModule内部进行实例化 ,并通过调用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,而不是实例化一个新的 ConfigService。


以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号