技术

数据库

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。

示例

这儿有一个可用的例子。

Mongo

Nest支持两种与 MongoDB 数据库集成的方式。既使用内置的TypeORM 提供的 MongoDB 连接器,或使用最流行的 MongoDB 对象建模工具 Mongoose。在本章后续描述中我们使用专用的@nestjs/mongoose包。

首先,我们需要安装所有必需的依赖项:

$ npm install --save @nestjs/mongoose mongoose
$ npm install --save-dev @types/mongoose

安装过程完成后,我们可以将其 MongooseModule 导入到根目录 AppModule 中。

app.module.ts

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中,一切都源于 Scheme,每个 Schema 都会映射到 MongoDB 的一个集合,并定义集合内文档的结构。Schema 被用来定义模型,而模型负责从底层创建和读取 MongoDB 的文档。

Schema 可以用 NestJS 内置的装饰器来创建,或者也可以自己动手使用 Mongoose的常规方式。使用装饰器来创建 Schema 会极大大减少引用并且提高代码的可读性。

我们先定义CatSchema:

schemas/cat.schema.ts

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

export type CatDocument = Cat & Document;

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

  @Prop()
  age: number;

  @Prop()
  breed: string;
}

export const CatSchema = SchemaFactory.createForClass(Cat);

注意你也可以通过使用 DefinitionsFactory 类(可以从 @nestjs/mongoose 导入)来生成一个原始 Schema ,这将允许你根据被提供的元数据手动修改生成的 Schema 定义。这对于某些很难用装饰器体现所有的极端例子非常有用。

@Schema 装饰器标记一个类作为Schema 定义,它将我们的 Cat 类映射到 MongoDB 同名复数的集合 Cats,这个装饰器接受一个可选的 Schema 对象。将它想象为那个你通常会传递给 mongoose.Schema 类的构造函数的第二个参数(例如, new mongoose.Schema(_, options)))。 更多可用的 Schema 选项可以 看这里。

@Prop 装饰器在文档中定义了一个属性。举个例子,在上面的 Schema 定义中,我们定义了三个属性,分别是:name ,age 和 breed。得益于 TypeScript 的元数据(还有反射),这些属性的 Schema类型会被自动推断。然而在更复杂的场景下,有些类型例如对象和嵌套数组无法正确推断类型,所以我们要向下面一样显式的指出。

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

另外的 @Prop 装饰器接受一个可选的参数,通过这个,你可以指示这个属性是否是必须的,是否需要默认值,或者是标记它作为一个常量,下面是例子:

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

最后的,原始 Schema 定义也可以被传递给装饰器。这也非常有用,举个例子,一个属性体现为一个嵌套对象而不是一个定义的类。要使用这个,需要从像下面一样从 @nestjs/mongoose 包导入 raw()。

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

或者,如果你不喜欢使用装饰器,你可以使用 mongoose.Schema 手动定义一个 Schema。下面是例子:

schemas/cat.schema.ts

import * as mongoose from 'mongoose';

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

该 cat.schema 文件在 cats 目录下。这个目录包含了和 CatsModule模块有关的所有文件。你可以决定在哪里保存Schema文件,但我们推荐在他们的域 中就近创建,即在相应的模块目录中。

我们来看看CatsModule:

cats.module.ts

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()方法来配置模块,包括定义哪些模型应该注册在当前范围中。如果你还想在另外的模块中使用这个模型,将MongooseModule添加到CatsModule的exports部分并在其他模块中导入CatsModule。

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

cats.service.ts

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

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

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

多数据库

有的项目需要多数据库连接,可以在这个模块中实现。要使用多连接,首先要创建连接,在这种情况下, 连接 必须**要有名称。

app.module.ts

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', schema: CatSchema }], 'cats')],
})
export class AppModule {}

也可以向一个给定的连接中注入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) {}
}

钩子(中间件)

中间件(也被称作预处理(pre)和后处理(post)钩子)是在执行异步函数时传递控制的函数。中间件是针对Schema层级的,在写插件(源码)时非常有用。在 Mongoose 编译完模型后使用pre()或post()不会起作用。要在模型注册前注册一个钩子,可以在使用一个工厂提供者(例如 useFactory)是使用MongooseModule中的forFeatureAsync()方法。使用这一技术,你可以访问一个 Schema 对象,然后使用pre()或post()方法来在那个 schema 中注册一个钩子。示例如下:

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

和其他工厂提供者一样,我们的工厂函数是异步的,可以通过inject注入依赖。

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

插件

要向给定的 schema 中注册插件,可以使用forFeatureAsync()方法。

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

要向所有 schema 中立即注册一个插件,调用Connection对象中的.plugin()方法。你可以在所有模型创建前访问连接。使用connectionFactory来实现:

app.module.ts

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

测试

在单元测试我们的应用程序时,我们通常希望避免任何数据库连接,使我们的测试套件独立并尽可能快地执行它们。但是我们的类可能依赖于从连接实例中提取的模型。如何处理这些类呢?解决方案是创建模拟模型。

为了简化这一过程,@nestjs/mongoose 包公开了一个 getModelToken() 函数,该函数根据一个 token 名称返回一个准备好的[注入token](https://docs.nestjs.com/fundamentals/custom-providers#di-fundamentals)。使用此 token,你可以轻松地使用任何标准自定义提供者技术,包括 useClass、useValue 和 useFactory。例如:

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

在本例中,每当任何使用者使用 @InjectModel() 装饰器注入模型时,都会提供一个硬编码的 Model<Cat> (对象实例)。

异步配置

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

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

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

与其他工厂提供程序一样,我们的工厂函数可以是异步的,并且可以通过注入注入依赖。

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

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

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

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

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

为了防止 MongooseConfigService 内部创建 MongooseModule 并使用从不同模块导入的提供程序,您可以使用 useExisting 语法。

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

例子

一个可用的示例见这里。

配置

应用程序通常在不同的环境 中运行。根据环境的不同,应该使用不同的配置设置。例如,通常本地环境依赖于特定的数据库凭据,仅对本地 DB 实例有效。生产环境将使用一组单独的 DB 凭据。由于配置变量会更改,所以最佳实践是将配置变量存储在环境中。

外部定义的环境变量通过 process.env global 在Node.js 内部可见。 我们可以尝试通过在每个环境中分别设置环境变量来解决多个环境的问题。 这会很快变得难以处理,尤其是在需要轻松模拟或更改这些值的开发和测试环境中。

在 Node.js 应用程序中,通常使用 .env 文件,其中包含键值对,其中每个键代表一个特定的值,以代表每个环境。 在不同的环境中运行应用程序仅是交换正确的.env 文件的问题。

在 Nest 中使用这种技术的一个好方法是创建一个 ConfigModule ,它暴露一个 ConfigService ,根据 $NODE_ENV 环境变量加载适当的 .env 文件。虽然您可以选择自己编写这样的模块,但为方便起见,Nest 提供了开箱即用的@ nestjs/config软件包。 我们将在本章中介绍该软件包。

安装

要开始使用它,我们首先安装所需的依赖项。

$ npm i --save @nestjs/config

注意 @nestjs/config 内部使用 dotenv 实现。

开始使用

安装完成之后,我们需要导入ConfigModule模块。通常,我们在根模块AppModule中导入它,并使用。forRoot()静态方法导入它的配置。

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';

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

上述代码将从默认位置(项目根目录)载入并解析一个.env文件,从.env文件和process.env合并环境变量键值对,并将结果存储到一个可以通过ConfigService访问的私有结构。forRoot()方法注册了ConfigService提供者,后者提供了一个get()方法来读取这些解析/合并的配置变量。由于@nestjs/config依赖dotenv,它使用该包的规则来处理冲突的环境变量名称。当一个键同时作为环境变量(例如,通过操作系统终端如export DATABASE_USER=test导出)存在于运行环境中以及.env文件中时,以运行环境变量优先。

一个样例.env文件看起来像这样:

DATABASE_USER=test
DATABASE_PASSWORD=test

自定义 env 文件路径

默认情况下,程序在应用程序的根目录中查找.env文件。 要为.env文件指定另一个路径,请配置forRoot()的配置对象 envFilePath 属性(可选),如下所示:

ConfigModule.forRoot({
  envFilePath: '.development.env',
});

您还可以像这样为.env 文件指定多个路径:

ConfigModule.forRoot({
  envFilePath: ['.env.development.local', '.env.development'],
});

如果在多个文件中发现同一个变量,则第一个变量优先。

禁止加载环境变量

如果您不想加载.env 文件,而是想简单地从运行时环境访问环境变量(如 OS shell 导出,例如export DATABASE_USER = test),则将options对象的ignoreEnvFile属性设置为true,如下所示 :

ConfigModule.forRoot({
  ignoreEnvFile: true,
});

全局使用

当您想在其他模块中使用ConfigModule时,需要将其导入(这是任何 Nest 模块的标准配置)。 或者,通过将options对象的isGlobal属性设置为true,将其声明为全局模块,如下所示。 在这种情况下,将ConfigModule加载到根模块(例如AppModule)后,您无需在其他模块中导入它。

ConfigModule.forRoot({
  isGlobal: true,
});

自定义配置文件

对于更复杂的项目,您可以利用自定义配置文件返回嵌套的配置对象。 这使您可以按功能对相关配置设置进行分组(例如,与数据库相关的设置),并将相关设置存储在单个文件中,以帮助独立管理它们

自定义配置文件导出一个工厂函数,该函数返回一个配置对象。配置对象可以是任意嵌套的普通 JavaScript 对象。process.env对象将包含完全解析的环境变量键/值对(具有如上所述的.env文件和已解析和合并的外部定义变量)。因为您控制了返回的配置对象,所以您可以添加任何必需的逻辑来将值转换为适当的类型、设置默认值等等。例如:

// config/configuration.ts
export default () => ({
  port: parseInt(process.env.PORT, 10) || 3000,
  database: {
    host: process.env.DATABASE_HOST,
    port: parseInt(process.env.DATABASE_PORT, 10) || 5432
  }
});

我们使用传递给ConfigModule.forRoot()方法的 options 对象的load属性来加载这个文件:

import configuration from './config/configuration';

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

ConfigModule 注册一个 ConfigService ,并将其导出为在其他消费模块中可见。此外,我们使用 useValue 语法(参见自定义提供程序)来传递到 .env 文件的路径。此路径将根据 NODE_ENV 环境变量中包含的实际执行环境而不同(例如,‘开发’、’生产‘等)。 > info 注意 分配给load属性的值是一个数组,允许您加载多个配置文件 (e.g. load: [databaseConfig, authConfig])

使用 ConfigService

现在您可以简单地在任何地方注入 ConfigService ,并根据传递的密钥检索特定的配置值。 要从 ConfigService 访问环境变量,我们需要注入它。因此我们首先需要导入该模块。与任何提供程序一样,我们需要将其包含模块ConfigModule导入到将使用它的模块中(除非您将传递给ConfigModule.forRoot()方法的 options 对象中的isGlobal属性设置为true)。 如下所示将其导入功能模块。

// feature.module.ts
@Module({
  imports: [ConfigModule],
  ...
})

然后我们可以使用标准的构造函数注入:

constructor(private configService: ConfigService) {}

在我们的类中使用它:

要从 ConfigService 访问环境变量,我们需要注入它。因此我们首先需要导入该模块。

// get an environment variable
const dbUser = this.configService.get<string>('DATABASE_USER');
// get a custom configuration value
const dbHost = this.configService.get<string>('database.host');

如上所示,使用configService.get()方法通过传递变量名来获得一个简单的环境变量。您可以通过传递类型来执行 TypeScript 类型提示,如上所示(例如,get<string>(…))。get()方法还可以遍历一个嵌套的自定义配置对象(通过自定义配置文件创建,如上面的第二个示例所示)。get()方法还接受一个可选的第二个参数,该参数定义一个默认值,当键不存在时将返回该值,如下所示:

// use "localhost" when "database.host" is not defined
const dbHost = this.configService.get<string>('database.host', 'localhost');

配置命名空间

ConfigModule模块允许您定义和加载多个自定义配置文件,如上面的自定义配置文件所示。您可以使用嵌套的配置对象来管理复杂的配置对象层次结构,如本节所示。或者,您可以使用registerAs()函数返回一个“带名称空间”的配置对象,如下所示:

export default registerAs('database', () => ({
  host: process.env.DATABASE_HOST,
  port: process.env.DATABASE_PORT || 5432,
}));

与自定义配置文件一样,在您的registerAs()工厂函数内部,process.env对象将包含完全解析的环境变量键/值对(带有.env文件和已定义并已合并的外部定义变量)

注意 registerAs 函数是从 @nestjs/config 包导出的。

使用forRoot()的load方法载入命名空间的配置,和载入自定义配置文件方法相同:

// config/database.config.ts
import databaseConfig from './config/database.config';

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

然后我们可以使用标准的构造函数注入,并在我们的类中使用它: 现在,要从数据库命名空间获取host的值,请使用符号.。使用'database'作为属性名称的前缀,该属性名称对应于命名空间的名称(作为传递给registerAs()函数的第一个参数)

const dbHost = this.configService.get<string>('database.host');

一个合理的替代方案是直接注入'database'的命名空间,我们将从强类型中获益:

constructor(
  @Inject(databaseConfig.KEY)
  private dbConfig: ConfigType<typeof databaseConfig>,
) {}

注意 ConfigType 函数是从 @nestjs/config 包导出的。

部分注册

到目前为止,我们已经使用forRoot()方法在根模块(例如,AppModule)中处理了配置文件。也许您有一个更复杂的项目结构,其中特定于功能的配置文件位于多个不同的目录中。与在根模块中加载所有这些文件不同,@nestjs/config包提供了一个称为部分注册的功能,它只引用与每个功能模块相关联的配置文件。使用特性模块中的forFeature()静态方法来执行部分注册,如下所示:

import databaseConfig from './config/database.config';

@Module({
  imports: [ConfigModule.forFeature(databaseConfig)],
})
export class DatabaseModule {}

您可以选择将 ConfigModule 声明为全局模块,而不是在每个模块中导入 ConfigModule。 > info 警告 在某些情况下,您可能需要使用onModuleInit()钩子通过部分注册来访问加载的属性,而不是在构造函数中。这是因为forFeature()方法是在模块初始化期间运行的,而模块初始化的顺序是不确定的。如果您以这种方式访问由另一个模块在构造函数中加载的值,则配置所依赖的模块可能尚未初始化。onModuleInit()方法只在它所依赖的所有模块被初始化之后运行,因此这种技术是安全的

Schema验证

一个标准实践是如果在应用启动过程中未提供需要的环境变量或它们不满足特定的验证规则时抛出异常。@nestjs/config包让我们可以使用Joi npm 包来提供这种类型验证。使用 Joi,你可以定义一个对象Schema对象并验证对应的JavaScript对象。 Install Joi (and its types, for TypeScript users): 安装 Joi(Typescript 用户还需要安装其类型申明)

$ npm install --save @hapi/joi
$ npm install --save-dev @types/hapi__joi

!> 注意 最新版本的“@hapi/joi”要求您运行 Node v12 或更高版本。对于较老版本的 node,请安装”v16.1.8“。这主要是在“v17.0.2”发布之后,它会在构建期间导致错误。更多信息请参考他们的文档和github issue

现在,我们可以定义一个 Joi 验证模式,并通过forRoot()方法的options对象的validationSchema属性传递它,如下所示

// app.module.ts
import * as Joi from '@hapi/joi';

@Module({
  imports: [
    ConfigModule.forRoot({
      validationSchema: Joi.object({
        NODE_ENV: Joi.string().valid('development', 'production', 'test', 'provision').default('development'),
        PORT: Joi.number().default(3000),
      }),
    }),
  ],
})
export class AppModule {}

由于我们为 NODE_ENV 和 PORT 设置了默认值,因此如果不在环境文件中提供这些变量,验证将不会失败。然而, 我们需要明确提供 API_AUTH_ENABLED。如果我们的 .env 文件中的变量不是模式( schema )的一部分, 则验证也会引发错误。此外,Joi 还会尝试将 env 字符串转换为正确的类型。

默认情况下,允许使用未知的环境变量(其键不在模式中出现的环境变量),并且不会触发验证异常。默认情况下,将报告所有验证错误。您可以通过通过forRoot() options 对象的validationOptions键传递一个 options 对象来更改这些行为。此选项对象可以包含由 Joi 验证选项提供的任何标准验证选项属性。例如,要反转上面的两个设置,像这样传递选项:

// app.module.ts
import * as Joi from '@hapi/joi';

@Module({
  imports: [
    ConfigModule.forRoot({
      validationSchema: Joi.object({
        NODE_ENV: Joi.string().valid('development', 'production', 'test', 'provision').default('development'),
        PORT: Joi.number().default(3000),
      }),
      validationOptions: {
        allowUnknown: false,
        abortEarly: true,
      },
    }),
  ],
})
export class AppModule {}

@nestjs/config包使用默认设置:

  • allowUnknown:控制是否允许环境变量中未知的键。默认为true。
  • abortEarly:如果为true,在遇到第一个错误时就停止验证;如果为false,返回所有错误。默认为false。

注意,一旦您决定传递validationOptions对象,您没有显式传递的任何设置都将默认为Joi标准默认值(而不是@nestjs/config默认值)。例如,如果在自定义validationOptions对象中保留allowUnknowns未指定,它的Joi默认值将为false。因此,在自定义对象中指定这两个设置可能是最安全的。

自定义 getter 函数

ConfigService定义了一个通用的get()方法来通过键检索配置值。我们还可以添加getter函数来启用更自然的编码风格:

@Injectable()
export class ApiConfigService {
  constructor(private configService: ConfigService) {}

  get isAuthEnabled(): boolean {
    return this.configService.get('AUTH_ENABLED') === 'true';
  }
}

现在我们可以像下面这样使用getter函数:

// app.service.ts
@Injectable()
export class AppService {
  constructor(apiConfigService: ApiConfigService) {
    if (apiConfigService.isAuthEnabled) {
      // Authentication is enabled
    }
  }
}

扩展变量

@nestjs/config包支持环境变量扩展。使用这种技术,您可以创建嵌套的环境变量,其中一个变量在另一个变量的定义中引用。例如:

APP_URL=mywebsite.com
SUPPORT_EMAIL=support@${APP_URL}

通过这种构造,变量SUPPORT_EMAIL解析为support@mywebsite.com。注意${…}语法来触发解析变量APP_URL在SUPPORT_EMAIL定义中的值。

提示 对于这个特性,@nestjs/config 包内部使用dotenv-expand实现。 使用传递给ConfigModule的forRoot()

(本文超出站点单文档体积上限,已截断)