ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
## Mongo `Nest`支持两种与 [MongoDB](http://www.mongodb.org/) 数据库集成的方式。既使用内置的[TypeORM](https://github.com/typeorm/typeorm) 提供的 MongoDB 连接器,或使用最流行的 MongoDB 对象建模工具 [Mongoose](http://mongoosejs.com/)。在本章后续描述中我们使用专用的`@nestjs/mongoose`包。 首先,我们需要安装所有必需的依赖项: ```bash $ npm install --save @nestjs/mongoose mongoose ``` 安装过程完成后,我们可以将其 `MongooseModule` 导入到根目录 `AppModule` 中。 > app.module.ts ```typescript import { Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; @Module({ imports: [MongooseModule.forRoot('mongodb://localhost/nest')], }) export class AppModule {} ``` 该 `forRoot()` 和 [mongoose](http://mongoosejs.com/) 包中的 `mongoose.connect()` 一样的参数对象。[参见](https://mongoosejs.com/docs/connections.html)。 ### 模型注入 在`Mongoose`中,一切都源于 [Scheme](http://mongoosejs.com/docs/guide.html),每个 `Schema` 都会映射到 `MongoDB` 的一个集合,并定义集合内文档的结构。`Schema` 被用来定义模型,而模型负责从底层创建和读取 `MongoDB` 的文档。 `Schema` 可以用 `NestJS` 内置的装饰器来创建,或者也可以自己动手使用 `Mongoose`的常规方式。使用装饰器来创建 `Schema` 会极大大减少引用并且提高代码的可读性。 我们先定义`CatSchema`: > schemas/cat.schema.ts ```typescript 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` 选项可以 [看这里](https://mongoosejs.com/docs/guide.html#options)。 `@Prop` 装饰器在文档中定义了一个属性。举个例子,在上面的 `Schema` 定义中,我们定义了三个属性,分别是:`name` ,`age` 和 `breed`。得益于 `TypeScript` 的元数据(还有反射),这些属性的 [`Schema类型`](https://mongoosejs.com/docs/schematypes.html)会被自动推断。然而在更复杂的场景下,有些类型例如对象和嵌套数组无法正确推断类型,所以我们要向下面一样显式的指出。 ```typescript @Prop([String]) tags: string[]; ``` 另外的 `@Prop` 装饰器接受一个可选的参数,通过这个,你可以指示这个属性是否是必须的,是否需要默认值,或者是标记它作为一个常量,下面是例子: ```typescript @Prop({ required: true }) name: string; ``` 最后的,原始 `Schema` 定义也可以被传递给装饰器。这也非常有用,举个例子,一个属性体现为一个嵌套对象而不是一个定义的类。要使用这个,需要从像下面一样从 `@nestjs/mongoose` 包导入 `raw()`。 ```typescript @Prop(raw({ firstName: { type: String }, lastName: { type: String } })) details: Record<string, any>; ``` 或者,如果你不喜欢使用装饰器,你可以使用 `mongoose.Schema` 手动定义一个 `Schema`。下面是例子: > schemas/cat.schema.ts ```typescript 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 ```typescript 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 ```typescript 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 连接](https://mongoosejs.com/docs/api.html#Connection)对象,你可能在连接对象中想使用某个原生的 API。你可以使用如下的`@InjectConnection()`装饰器来注入 Mongoose 连接。 ```typescript 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 ```typescript 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()`方法应该使用哪个连接。 ```typescript @Module({ imports: [MongooseModule.forFeature([{ name: 'Cat', schema: CatSchema }], 'cats')], }) export class AppModule {} ``` 也可以向一个给定的连接中注入`Connection`。 ```typescript 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`层级的,在写插件([源码](https://mongoosejs.com/docs/middleware.html))时非常有用。在 Mongoose 编译完模型后使用`pre()`或`post()`不会起作用。要在模型注册前注册一个钩子,可以在使用一个工厂提供者(例如 `useFactory`)是使用`MongooseModule`中的`forFeatureAsync()`方法。使用这一技术,你可以访问一个 Schema 对象,然后使用`pre()`或`post()`方法来在那个 schema 中注册一个钩子。示例如下: ```typescript @Module({ imports: [ MongooseModule.forFeatureAsync([ { name: 'Cat', useFactory: () => { const schema = CatsSchema; schema.pre('save', () => console.log('Hello from pre save')); return schema; }, }, ]), ], }) export class AppModule {} ``` 和其他[工厂提供者](https://docs.nestjs.com/fundamentals/custom-providers#factory-providers-usefactory)一样,我们的工厂函数是异步的,可以通过`inject`注入依赖。 ```typescript @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 中注册[插件](https://mongoosejs.com/docs/plugins.html),可以使用`forFeatureAsync()`方法。 ```typescript @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 ```typescript 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) [鉴别](https://mongoosejs.com/docs/discriminators.html)器是一种模式继承机制。它们使您能够在同一个基础 MongoDB 集合之上拥有多个具有重叠模式的模型。 假设您想在单个集合中跟踪不同类型的事件。每个事件都会有一个时间戳。 >event.schema.ts ~~~typescript @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` 区分不同判别器模型的方式是`__t`通过默认的“判别器键”。`Mongoose` 添加了一个字符串路径,该路径调用`__t`到您的模式中,用于跟踪该文档是哪个鉴别器的实例。您也可以使用该`discriminatorKey`选项来定义区分路径。 `SignedUpEvent`和`ClickedLinkEvent`实例将存储在与通用事件相同的集合中。 现在,让我们定义`ClickedLinkEvent`类,如下所示: >click-link-event.schema.ts ~~~typescript @Schema() export class ClickedLinkEvent { kind: string; time: Date; @Prop({ type: String, required: true }) url: string; } export const ClickedLinkEventSchema = SchemaFactory.createForClass(ClickedLinkEvent); ~~~ 和`SignUpEvent`类: >sign-up-event.schema.ts ~~~typescript @Schema() export class SignUpEvent { kind: string; time: Date; @Prop({ type: String, required: true }) user: string; } export const SignUpEventSchema = SchemaFactory.createForClass(SignUpEvent); ~~~ 有了这个,使用`discriminators`选项为给定的模式注册一个鉴别器。它适用于`MongooseModule.forFeature`和`MongooseModule.forFeatureAsync`: >event.module.ts ~~~typescript 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 {} ~~~ ### 测试 在单元测试我们的应用程序时,我们通常希望避免任何数据库连接,使我们的测试套件独立并尽可能快地执行它们。但是我们的类可能依赖于从连接实例中提取的模型。如何处理这些类呢?解决方案是创建模拟模型。 为了简化这一过程,`@nestjs/mongoose` 包公开了一个 `getModelToken()` 函数,该函数根据一个 `token` 名称返回一个准备好的`[注入token](https://docs.nestjs.com/fundamentals/custom-providers#di-fundamentals)`。使用此 `token`,你可以轻松地使用任何标准[自定义提供者](https://docs.nestjs.com/fundamentals/custom-providers)技术,包括 `useClass`、`useValue` 和 `useFactory`。例如: ```typescript @Module({ providers: [ CatsService, { provide: getModelToken('Cat'), useValue: catModel, }, ], }) export class CatsModule {} ``` 在本例中,每当任何使用者使用 `@InjectModel()` 装饰器注入模型时,都会提供一个硬编码的 `Model<Cat>` (对象实例)。 ### 异步配置 通常,您可能希望异步传递模块选项,而不是事先传递它们。在这种情况下,使用 `forRootAsync()` 方法,`Nest`提供了几种处理异步数据的方法。 第一种可能的方法是使用工厂函数: ```typescript MongooseModule.forRootAsync({ useFactory: () => ({ uri: 'mongodb://localhost/nest', }), }); ``` 与其他工厂提供程序一样,我们的工厂函数可以是异步的,并且可以通过注入注入依赖。 ```typescript MongooseModule.forRootAsync({ imports: [ConfigModule], useFactory: async (configService: ConfigService) => ({ uri: configService.getString('MONGODB_URI'), }), inject: [ConfigService], }); ``` 或者,您可以使用类而不是工厂来配置 `MongooseModule`,如下所示: ```typescript MongooseModule.forRootAsync({ useClass: MongooseConfigService, }); ``` 上面的构造在 `MongooseModule`中实例化了 `MongooseConfigService`,使用它来创建所需的 `options` 对象。注意,在本例中,`MongooseConfigService` 必须实现 `MongooseOptionsFactory` 接口,如下所示。 `MongooseModule` 将在提供的类的实例化对象上调用 `createMongooseOptions()` 方法。 ```typescript @Injectable() class MongooseConfigService implements MongooseOptionsFactory { createMongooseOptions(): MongooseModuleOptions { return { uri: 'mongodb://localhost/nest', }; } } ``` 为了防止 `MongooseConfigService` 内部创建 `MongooseModule` 并使用从不同模块导入的提供程序,您可以使用 `useExisting` 语法。 ```typescript MongooseModule.forRootAsync({ imports: [ConfigModule], useExisting: ConfigService, }); ``` ### 例子 一个可用的示例见[这里](https://github.com/nestjs/nest/tree/master/sample/06-mongoose)。