从头开始学习nestjs-第五章-技术-数据库

113 阅读9分钟

前置概念:ORM

  • ORM 全称 Object/Relation Mapping,表示对象-关系映射
  • 操作实体类就相当于操作数据库的表(一个实体类对应一张表)
  • 建立映射关系
    • 实体类和表的映射
    • 实体类中的属性和表中字段的映射

MySql

Nest 与数据库无关,可以轻松地与任何 SQL 或 NoSQL 数据库集成。在普通的层面上,将 Nest 连接到数据库只是为数据库加载适当的 Node.js 驱动程序的问题,就像使用 Express 或 Fastify 一样

可以直接使用任何通用 Node.js 数据库集成库或 ORM,例如 MikroORM、Sequelize、Knex.js、TypeORM 和 Prisma

为方便起见,Nest 内置了 TypeORMSequelize, 分别提供了 @nestjs/typeorm@nestjs/sequelize

TypeORM 集成

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

首先安装所需的依赖。TypeORM 提供对许多关系数据库的支持,例如 PostgreSQL、Oracle、Microsoft SQL Server、SQLite,甚至像 MongoDB 这样的 NoSQL 数据库,只需为所选数据库安装关联的客户端 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, // 同步数据库架构
      // 以下内容可选
      retryAttempts: 5, // 尝试连接数据库的次数
      retryDelay: 5, // 连接重试尝试之间的延迟
      autoLoadEntities: false // 实体是否自动加载
    }),
  ],
})
export class AppModule {}

警告:设置 synchronize: true 不应在生产中使用 - 否则可能会丢失生产数据

forRoot() 方法支持 TypeORM 包中的 DataSource 构造函数公开的所有配置属性。此外,还有下面描述的几个额外的配置属性:

retryAttempts尝试连接数据库的次数(默认: XSPACE10)
retryDelay连接重试尝试之间的延迟(毫秒)(默认值: XSPACE3000)
autoLoadEntities如果是 true,实体将自动加载(默认: false)

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

import { DataSource } from 'typeorm';

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

仓库模式

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

定义 User 实体:

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  firstName: string;

  @Column()
  lastName: string;

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

User 实体文件位于 users 目录中。该目录包含与 UsersModule 相关的所有文件。可以自己定义文件的保存位置,但建议与 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 {}

接下来看看 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 {}

该 module 使用 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 的 module 之外使用实体类,则需要重新导出它生成的 provider。可以通过导出整个 module 来完成此操作,如下所示:

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,就可以在后一个 module 的 provider 中使用 @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 {}

关系

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

存在三种类型的关系:

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

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

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[];
}

自动加载实体

手动将实体添加到数据源选项的 entities 数组可能很乏味。此外,从根模块引用实体会破坏应用域边界,并导致将实现细节泄漏到应用的其他部分。为解决此问题,提供了替代解决方案。要自动加载实体,请将配置对象的 autoLoadEntities 属性(传递给forRoot()方法)设置为 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]);
  });
}

订阅监听(subscribers)

使用 TypeORM subscribers,可以监听特定的实体事件

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() {
    // 有关 User 内容就会触发这个事件
    return User;
    // return Post;
  }

  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 特定功能

多个数据库

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

假设有一个 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: ...,
}),

有关详细信息,请参阅 这个问题

此时已使用自己的数据源注册了 User 和 Album 实体。使用此设置,必须告诉 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 注入 provider

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

测试

当谈到对应用进行单元测试时,通常希望避免建立数据库连接,保持测试组件独立并尽可能快地执行它们。但是类可能依赖于从数据源(连接)实例中提取的存储库。如何处理?解决方案是创建模拟存储库。为了实现这一目标,出现了 自定义 provider。每个注册的存储库自动由一个 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,
  }),
});

工厂的行为与任何其他异步 provider 一样(例如,它可以是 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 并使用从不同 module 导入的 provider,可以使用 useExisting 设置别名

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

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

提示:确保 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 包,它提供了一组额外的装饰器来以声明方式定义实体

首先安装所需的依赖。Sequelize 提供对许多关系数据库的支持,例如 PostgreSQL、MySQL、Microsoft SQL Server、SQLite 和 MariaDB,只需为所选数据库安装关联的客户端 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尝试连接数据库的次数(默认: XSPACE10)
retryDelay连接重试尝试之间的延迟(毫秒)(默认值: XSPACE3000)
autoLoadModels如果是 true,模型将自动加载(默认: false)
keepConnectionAlive如果是 true,则应用关闭时连接不会关闭(默认值: false)
synchronize如果是 true,将同步自动加载的模型(默认: true)

完成后,Sequelize 对象将可用于在整个项目中注入(无需导入任何 module),例如:

import { Injectable } from '@nestjs/common';
import { Sequelize } from 'sequelize-typescript';

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

模块

Sequelize 实现了 Active Record 模式。使用此模式,可以直接使用模型类与数据库交互

定义 User 模型

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 Model文件位于 users 目录中。该目录包含与 UsersModule 相关的所有文件。建议 model 和 module 放到一起

要开始使用 User Model,需要通过将其插入模块 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 {}

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

该 module 使用 forFeature() 方法定义在当前作用域内注册哪些 module。有了它,可以使用 @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 的 module 之外使用存储库,则需要重新导出它生成的 provider 。可以通过导出整个 module 来完成此操作,如下所示:

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,就可以在后一个 module 的 provider 中使用 @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 {}

关系

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

存在三种类型的关系:

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

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

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[];
}

自动加载模型

手动将 module 添加到连接选项的 models 数组可能很乏味。此外,从根模块引用 module 会打破应用域边界,并导致将实现细节泄漏到应用的其他部分。为了解决这个问题,通过将配置对象的 autoLoadModels 和 synchronize 属性(传递给 forRoot() 方法)设置为 true 来自动加载 module,如下所示:

import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';

@Module({
  imports: [
    SequelizeModule.forRoot({
      ...
      autoLoadModels: true,
      synchronize: true,
    }),
  ],
})
export class AppModule {}

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

警告:请注意,未通过forFeature()方法注册但仅从 module 引用(通过关联)的模型将不包括在内

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 特定功能

多个数据库

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

假设有一个 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() 方法和 @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 {}

测试

当谈到对应用进行单元测试时,通常希望避免建立数据库连接,保持测试组件独立并尽可能快地执行它们。但是我们的类可能依赖于从连接实例中提取的模型。如何处理?解决方案是创建模拟模型。为了实现这一目标,出现了 自定义 provider。每个注册的模型都自动由一个 Model 标记表示,其中 ModelName 是模型类的名称

@nestjs/sequelize 包公开了 getModelToken()方法,该方法返回基于给定 module 的准备好的令牌

@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: [],
  }),
});

工厂的行为与任何其他异步 provider 一样(例如,它可以是 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: [],
    };
  }
}

为了防止在 TypeOrmModule 中创建 TypeOrmConfigService 并使用从不同 module 导入的 provider,可以使用 useExisting 设置别名

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

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

NoSql-Mongo

Nest 支持两种与 MongoDB 数据库集成的方法。可以使用内置 TypeORM 模块,它具有用于 MongoDB 的连接器,或者使用 Mongoose,最流行的 MongoDB 对象建模工具。这里将使用专用的 @nestjs/mongoose 包来演示

安装必需的依赖:

$ npm i @nestjs/mongoose mongoose

安装过程完成后,可以将 MongooseModule 导入到根 AppModule 中

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';

@Module({
  imports: [MongooseModule.forRoot('mongodb://localhost/nest')],
})
export class AppModule {}

forRoot() 方法从 Mongoose 包中接受与 mongoose.connect() 相同的配置对象

模型注入

使用 Mongoose,一切都源自 Schema。每个模式都映射到一个 MongoDB 集合并定义该集合中文档的形状。模式用于定义模块。模型负责从底层 MongoDB 数据库创建和读取文档

可以使用 NestJS 装饰器或手动使用 Mongoose 本身创建模式。使用装饰器创建模式大大减少了样板文件并提高了整体代码的可读性

定义 CatSchema:

import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument } from 'mongoose';

export type CatDocument = HydratedDocument<Cat>;

@Schema()
export class Cat {
  @Prop()
  name: string;

  @Prop()
  age: number;

  @Prop()
  breed: string;
}

export const CatSchema = SchemaFactory.createForClass(Cat);

提示:请注意,还可以使用 DefinitionsFactory 类(来自 nestjs/mongoose)生成原始模式定义。这允许手动修改根据你提供的元数据生成的架构定义。这对于某些可能很难用装饰器表示所有内容的边缘情况很有用

@Schema() 装饰器将类标记为模式定义。它将 Cat 类映射到同名的 MongoDB 集合,但末尾有一个额外的s - 所以最终的 mongo 集合名称将是 cats。这个装饰器接受一个可选参数,它是一个模式选项对象。将其视为通常作为 mongoose.Schema 类构造函数(例如,new mongoose.Schema(_, options))的第二个参数传递的对象

@Prop() 装饰器在文档中定义了一个属性。例如,在上面的模式定义中,定义了三个属性:name、age 和 breed。由于 TypeScript 元数据(和反射)功能,这些属性的类型会自动推断出来。但是,在更复杂的不能隐式反映类型的场景中(例如,数组或嵌套对象结构),必须显式指示类型,如下所示:

@Prop([String])
tags: string[];

或者, @Prop() 装饰器接受一个选项对象参数。有了它,可以指示属性是否是必需的,指定默认值,或将其标记为不可变的。例如:

@Prop({ required: true })
name: string;

如果想指定与另一个模型的关系,稍后进行填充,也可以使用 @Prop() 装饰器。例如,如果 Cat 具有存储在名为 owners 的不同集合中的 Owner,则该属性应具有类型和引用。例如:

import * as mongoose from 'mongoose';
import { Owner } from '../owners/schemas/owner.schema';

// inside the class definition
@Prop({ type: mongoose.Schema.Types.ObjectId, ref: 'Owner' })
owner: Owner;

如果有多个所有者,属性配置应如下所示:

@Prop({ type: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Owner' }] })
owner: Owner[];

最后,原始模式定义也可以传递给装饰器。例如,当属性表示未定义为类的嵌套对象时,这很有用。为此,使用 @nestjs/mongoose 包中的 raw() 方法,如下所示:

@Prop(raw({
  firstName: { type: String },
  lastName: { type: String }
}))
details: Record<string, any>;

或者,如果不想使用装饰器,则可以手动定义架构。例如:

export const CatSchema = new mongoose.Schema({
  name: String,
  age: Number,
  breed: String,
});

cat.schema 文件位于 cats 目录下的一个文件夹中,在这里定义了 CatsModule。虽然可以将架构文件存储在任何位置,但建议将它们存储在相应的模块目录中靠近其相关域对象的位置

接下来配置 CatsModule:

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
import { Cat, CatSchema } from './schemas/cat.schema';

@Module({
  imports: [MongooseModule.forFeature([{ name: Cat.name, schema: CatSchema }])],
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {}

MongooseModule 提供了 forFeature() 方法来配置 module,包括定义哪些 module 应该在当前作用域内注册。如果还想在另一个 module 中使用这些模型,请将 MongooseModule 添加到 CatsModule 的 exports 部分并在另一个 module 中导入 CatsModule

注册模式后,可以使用 @InjectModel() 装饰器将 Cat 模型注入到 CatsService 中:

import { Model } from 'mongoose';
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Cat } from './schemas/cat.schema';
import { CreateCatDto } from './dto/create-cat.dto';

@Injectable()
export class CatsService {
  constructor(@InjectModel(Cat.name) private catModel: Model<Cat>) {}

  async create(createCatDto: CreateCatDto): Promise<Cat> {
    const createdCat = new this.catModel(createCatDto);
    return createdCat.save();
  }

  async findAll(): Promise<Cat[]> {
    return this.catModel.find().exec();
  }
}

使用原生连接对象

有时可能需要使用原生Mongoose 连接对象。例如,可能希望对连接对象进行原生 API 调用。可以使用 @InjectConnection() 装饰器注入 Mongoose 连接,如下所示:

import { Injectable } from '@nestjs/common';
import { InjectConnection } from '@nestjs/mongoose';
import { Connection } from 'mongoose';

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

多个数据库

一些项目需要多个数据库连接。这也可以通过这个 module 来实现。要使用多个连接,首先要创建连接。在这种情况下,连接命名就成为强制性的

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';

@Module({
  imports: [
    MongooseModule.forRoot('mongodb://localhost/test', {
      connectionName: 'cats',
    }),
    MongooseModule.forRoot('mongodb://localhost/users', {
      connectionName: 'users',
    }),
  ],
})
export class AppModule {}

注意:请注意,不应该有多个没有名称或具有相同名称的连接,否则它们将被覆盖

使用此设置,必须告诉 MongooseModule.forFeature() 方法应该使用哪个连接

@Module({
  imports: [
    MongooseModule.forFeature([{ name: Cat.name, schema: CatSchema }], 'cats'),
  ],
})
export class CatsModule {}

还可以为给定连接注入 Connection

import { Injectable } from '@nestjs/common';
import { InjectConnection } from '@nestjs/mongoose';
import { Connection } from 'mongoose';

@Injectable()
export class CatsService {
  constructor(@InjectConnection('cats') private connection: Connection) {}
}

要将给定的 Connection 注入自定义provider(例如,工厂provider),请使用 getConnectionToken()方法将连接名称作为参数传递

{
  provide: CatsService,
  useFactory: (catsConnection: Connection) => {
    return new CatsService(catsConnection);
  },
  inject: [getConnectionToken('cats')],
}

如果只是想从命名数据库中注入模型,可以使用连接名称作为 @InjectModel() 装饰器的第二个参数

@Injectable()
export class CatsService {
  constructor(@InjectModel(Cat.name, 'cats') private catModel: Model<Cat>) {}
}

钩子(中间件)

中间件(也称为前钩子和后钩子)是在异步函数执行期间传递控制的方法。中间件在模式级别指定,对于编写插件很有用(source)。编译模型后调用 pre()post() 在 Mongoose 中不起作用。要在模型注册之前注册钩子,请使用 MongooseModule 的 forFeatureAsync() 方法以及工厂provider(即 useFactory)。使用这种技术,可以访问架构对象,然后使用 pre()post() 方法在该架构上注册钩子。示例:

@Module({
  imports: [
    MongooseModule.forFeatureAsync([
      {
        name: Cat.name,
        useFactory: () => {
          const schema = CatsSchema;
          schema.pre('save', function () {
            console.log('Hello from pre save');
          });
          return schema;
        },
      },
    ]),
  ],
})
export class AppModule {}

和其他 工厂provider 一样,工厂方法可以是 async,可以通过 inject 注入依赖

@Module({
  imports: [
    MongooseModule.forFeatureAsync([
      {
        name: Cat.name,
        imports: [ConfigModule],
        useFactory: (configService: ConfigService) => {
          const schema = CatsSchema;
          schema.pre('save', function() {
            console.log(
              `${configService.get('APP_NAME')}: Hello from pre save`,
            ),
          });
          return schema;
        },
        inject: [ConfigService],
      },
    ]),
  ],
})
export class AppModule {}

插件

要为给定模式注册 plugin,请使用 forFeatureAsync() 方法

@Module({
  imports: [
    MongooseModule.forFeatureAsync([
      {
        name: Cat.name,
        useFactory: () => {
          const schema = CatsSchema;
          schema.plugin(require('mongoose-autopopulate'));
          return schema;
        },
      },
    ]),
  ],
})
export class AppModule {}

要一次为所有模式注册插件,请调用 Connection 对象的 .plugin() 方法。要在创建 model 之前访问连接,请使用 connectionFactory

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';

@Module({
  imports: [
    MongooseModule.forRoot('mongodb://localhost/test', {
      connectionFactory: (connection) => {
        connection.plugin(require('mongoose-autopopulate'));
        return connection;
      }
    }),
  ],
})
export class AppModule {}

鉴别器(Discriminators)

鉴别器模式继承机制。它们能够在相同的底层 MongoDB 集合之上拥有多个具有重叠模式的模型

假设想在单个集合中跟踪不同类型的事件。每个事件都会有一个时间戳

@Schema({ discriminatorKey: 'kind' })
export class Event {
  @Prop({
    type: String,
    required: true,
    enum: [ClickedLinkEvent.name, SignUpEvent.name],
  })
  kind: string;

  @Prop({ type: Date, required: true })
  time: Date;
}

export const EventSchema = SchemaFactory.createForClass(Event);

提示:Mongoose 区分不同判别器模型之间的差异的方式是通过 鉴别键,默认情况下是 __tMongoose 将一个名为 __t 的字符串路径添加到你的模式中,它用于跟踪该文档是哪个鉴别器的实例。也可以使用 discriminatorKey 选项来定义区分路径

SignedUpEventClickedLinkEvent 实例将存储在与通用事件相同的集合中

定义 ClickedLinkEvent 类,如下所示:

@Schema()
export class ClickedLinkEvent {
  kind: string;
  time: Date;

  @Prop({ type: String, required: true })
  url: string;
}

export const ClickedLinkEventSchema = SchemaFactory.createForClass(ClickedLinkEvent);

还有 SignUpEvent 类:

@Schema()
export class SignUpEvent {
  kind: string;
  time: Date;

  @Prop({ type: String, required: true })
  user: string;
}

export const SignUpEventSchema = SchemaFactory.createForClass(SignUpEvent);

使用 discriminators 选项为给定模式注册鉴别器。它适用于 MongooseModule.forFeatureMongooseModule.forFeatureAsync

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';

@Module({
  imports: [
    MongooseModule.forFeature([
      {
        name: Event.name,
        schema: EventSchema,
        discriminators: [
          { name: ClickedLinkEvent.name, schema: ClickedLinkEventSchema },
          { name: SignUpEvent.name, schema: SignUpEventSchema },
        ],
      },
    ]),
  ]
})
export class EventsModule {}

测试

避免任何数据库连接,从而使测试组件,解决方案是创建模拟模型当谈到对应用进行单元测试时,通常希望避免建立数据库连接,保持测试组件独立并尽可能快地执行它们。但是我们的类可能依赖于从连接实例中提取的模型。如何处理?解决方案是创建模拟模型。为了实现这一目标,出现了 自定义 provider。每个注册的模型都自动由一个 Model 标记表示,其中 ModelName 是模型类的名称 @nestjs/sequelize 包公开了 getModelToken()方法,该方法返回基于给定 module 的准备好的令牌

为了使这更容易,@nestjs/mongoose 包公开了一个 getModelToken() 函数,该函数返回一个基于令牌名称的准备好的 注入令牌。使用此令牌,可以使用任何标准 定制提供器 技术(包括 useClass、useValue 和 useFactory)轻松提供模拟实现。例如:

@Module({
  providers: [
    CatsService,
    {
      provide: getModelToken(Cat.name),
      useValue: catModel,
    },
  ],
})
export class CatsModule {}

在此示例中,每当任何消费者使用 @InjectModel() 装饰器注入 Model 时,都会提供硬编码的 catModel(对象实例)

异步配置

当需要异步而不是静态地传递模块选项时,请使用 forRootAsync() 方法。与大多数动态模块一样,Nest 提供了几种处理异步配置的技术

一种技术是使用工厂方法:

MongooseModule.forRootAsync({
  useFactory: () => ({
    uri: 'mongodb://localhost/nest',
  }),
});

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

MongooseModule.forRootAsync({
  imports: [ConfigModule],
  useFactory: async (configService: ConfigService) => ({
    uri: configService.get<string>('MONGODB_URI'),
  }),
  inject: [ConfigService],
});

或者,可以使用类而不是工厂来配置 MongooseModule,如下所示:

MongooseModule.forRootAsync({
  useClass: MongooseConfigService,
});

上面的构造在 MongooseModule 中实例化了 MongooseConfigService,使用它来创建所需的选项对象。请注意,在此示例中,MongooseConfigService 必须实现 MongooseOptionsFactory 接口,如下所示。MongooseModule 将在所提供类的实例化对象上调用 createMongooseOptions() 方法

@Injectable()
export class MongooseConfigService implements MongooseOptionsFactory {
  createMongooseOptions(): MongooseModuleOptions {
    return {
      uri: 'mongodb://localhost/nest',
    };
  }
}

如果要重用现有选项提供程序而不是在 MongooseModule 中创建私有副本,请使用 useExisting

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