NestJS Mongo

2023-09-08 16:25 更新

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

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

$ npm install --save @nestjs/mongoose 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,
});

例子

一个可用的示例见这里


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

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号