Mongo Mongo

导读

Nest 支持两种与 MongoDB 数据库集成的方法。您可以使用 此处 中描述的内置 TypeORM 模块,该模块具有 MongoDB 连接器,也可以使用 Mongoose,这是最流行的 MongoDB 对象建模工具。在本章中,我们将使用专用的 @nestjs/mongoose 包来描述后者。

首先安装 必需的依赖项

bash
$ npm i @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,所有内容都来自 Schema。每个架构都映射到 MongoDB 集合并定义该集合中文档的形状。架构用于定义 模型。模型负责从底层 MongoDB 数据库创建和读取文档。

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

让我们定义 CatSchema

schemas/cat.schema
ts
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() 装饰器在文档中定义一个属性。例如,在上面的架构定义中,我们定义了三个属性:nameagebreed。得益于 TypeScript 元数据(和反射)功能,这些属性的 架构类型 会自动推断。但是,在更复杂的场景中,类型无法隐式反映(例如数组或嵌套对象结构),必须明确指示类型,如下所示:

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

或者,@Prop() 装饰器接受选项对象参数(阅读更多 了解可用选项)。通过它,您可以指示属性是否是必需的,指定默认值,或将其标记为不可变。例如:

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

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

ts
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;

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

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

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

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

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

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

cat.schema 文件位于 cats 目录中的一个文件夹中,我们还在其中定义了 CatsModule。虽然您可以将架构文件存储在您喜欢的任何位置,但我们建议将它们存储在相关对象附近的适当模块目录中。

Let's look at the 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 添加到 CatsModuleexports 部分,并在另一个模块中导入 CatsModule

注册架构后,您可以使用 @InjectModel() 装饰器将 Cat 模型注入 CatsService

ts
cats.service
ts
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 连接,如下所示:

ts
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() 函数应使用哪个连接。

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

您还可以为给定的连接注入连接

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

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

要将给定的连接注入自定义提供程序(例如,工厂提供程序),请使用getConnectionToken()函数将连接的名称作为参数传递。

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

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

cats.service
ts
@Injectable()
export class CatsService {
  constructor(@InjectModel(Cat.name, 'cats') private catModel: Model<Cat>) {}
}
js
@Injectable()
@Dependencies(getModelToken(Cat.name, 'cats'))
export class CatsService {
  constructor(catModel) {
    this.catModel = catModel;
  }
}

钩子(中间件)

中间件(也称为前置和后置钩子)是在执行异步函数期间传递控制的函数。中间件在架构级别指定,对于编写插件很有用(来源)。在 Mongoose 中,编译模型后调用 pre()post() 不起作用。要在模型注册之前注册钩子,请使用 MongooseModuleforFeatureAsync() 方法以及工厂提供程序(即 useFactory)。使用此技术,您可以访问架构对象,然后使用 pre()post() 方法在该架构上注册钩子。请参阅以下示例:

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

与其他 工厂提供商 一样,我们的工厂函数可以是 async,并且可以通过 inject 注入依赖项。

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

Plugins

要为给定架构注册插件,请使用 forFeatureAsync() 方法。

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

要一次性为所有模式注册插件,请调用 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 {}

鉴别器

鉴别器 是一种架构继承机制。它们使您能够在同一个底层 MongoDB 集合之上拥有多个具有重叠架构的模型。

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

event.schema
ts
@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 选项来定义鉴别的路径。

SignedUpEventClickedLinkEvent 实例将与通用事件存储在同一个集合中。

现在,让我们定义 ClickedLinkEvent 类,如下所示:

click-link-event.schema
ts
@Schema()
export class ClickedLinkEvent {
  kind: string
  time: Date

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

export const ClickedLinkEventSchema = SchemaFactory.createForClass(ClickedLinkEvent)

And SignUpEvent class:

sign-up-event.schema
ts
@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

event.module
ts
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() 函数,该函数根据令牌名称返回准备好的 注入令牌。使用此令牌,您可以使用任何标准 自定义提供程序 技术轻松提供模拟实现,包括 useClassuseValueuseFactory。例如:

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

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

异步配置

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

一种技术是使用工厂函数:

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

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

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

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

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

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

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

如果您想重用现有的选项提供程序,而不是在MongooseModule内创建私有副本,请使用useExisting语法。

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

Example

此处 提供了一个工作示例。