NestJS Logo
NestJS 中文文档
v10.0.0
  • 介绍
  • 快速上手
  • 控制器
  • 提供者
  • 模块
  • 中间件
  • 异常过滤器
  • 管道
  • 守卫
  • 拦截器
  • 自定义装饰器
  • 自定义提供者
  • 异步提供者
  • 动态模块
  • 依赖注入作用域
  • 循环依赖
  • 模块引用
  • 懒加载模块
  • 执行上下文
  • 生命周期事件
  • 发现服务
  • 跨平台无关性
  • 测试
迁移指南
API 参考
官方课程
  1. 文档
  2. 数据库集成
  3. MongoDB

数据库概述
身份验证

Mongo 数据库

Nest 支持两种方式集成 MongoDB 数据库。你可以使用内置的 TypeORM 模块(详见此处),该模块内置了 MongoDB 连接器;也可以选择 Mongoose,这是最流行的 MongoDB 对象建模工具。本章将重点介绍后者,即如何使用专用的 @nestjs/mongoose 包。

首先,安装所需依赖:

npm install @nestjs/mongoose mongoose

安装完成后,我们可以在根模块 AppModule 中导入 MongooseModule。

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 集合,并定义该集合内文档的结构。模式用于定义 Model(模型)。模型负责在底层 MongoDB 数据库中创建和读取文档。

模式可以通过 NestJS 装饰器创建,也可以直接用 Mongoose 手动定义。使用装饰器创建模式可以大幅减少模板代码(Boilerplate),并提升整体代码可读性。

我们来定义一个 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() 装饰器用于定义文档中的属性。例如,在上面的模式定义中,我们定义了三个属性:name、age 和 breed。这些属性的模式类型会通过 TypeScript 元数据(和反射机制)自动推断。然而,在更复杂的场景下,如果类型无法被自动推断(如数组或嵌套对象结构),则必须显式指定类型,如下所示:

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

另外,@Prop() 装饰器也可以接收一个选项对象作为参数(详细选项说明请参见官方文档)。通过该对象,你可以指定属性是否为必填项、设置默认值,或将其标记为不可变。示例:

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

如果你希望指定与其他模型的关联(便于后续进行数据填充),同样可以使用 @Prop() 装饰器。例如,假设 Cat 拥有一个 Owner,而 Owner 存储在名为 owners 的另一个集合中,则该属性应包含类型和引用。示例:

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

// 在类定义内部
@Prop({ type: mongoose.Schema.Types.ObjectId, ref: 'Owner' })
owner: Owner

如果存在多个 owner,你的属性配置应如下所示:

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

如果你并不打算每次都填充(populate)对另一个集合的引用,建议将类型声明为 mongoose.Types.ObjectId:

@Prop({ type: { type: mongoose.Schema.Types.ObjectId, ref: 'Owner' } })
// 这样可以确保该字段不会与已填充的引用混淆
owner: mongoose.Types.ObjectId

然后,当你需要在后续有选择性地进行关联填充时,可以使用仓库(repository)函数并指定正确的类型:

import { Owner } from './schemas/owner.schema'

// 例如,在服务(Service)或仓库中
async findAllPopulated() {
  return this.catModel.find().populate<{ owner: Owner }>("owner")
}
提示

如果没有需要填充的外部文档,类型可能为 Owner | null,具体取决于你的 Mongoose 配置。另外,也有可能抛出错误,此时类型为 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:

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() 方法,用于配置模块,包括定义当前作用域内需要注册的模型(Model)。如果你还希望在其他模块中使用这些模型,只需将 MongooseModule 添加到 CatsModule 的 exports 部分,并在其他模块中导入 CatsModule。

注册好模式后,你可以通过 @InjectModel() 装饰器将 Cat 模型注入到 CatsService 中:

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 Connection 对象。例如,你可能希望在连接对象上调用原生 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) {}
}

会话(Sessions)

要在 Mongoose 中启动会话,推荐通过 @InjectConnection 注入数据库连接,而不是直接调用 mongoose.startSession()。这种方式可以更好地与 NestJS 的依赖注入机制集成,确保连接管理的规范性。

以下是启动会话的示例:

import { InjectConnection } from '@nestjs/mongoose'
import { Connection } from 'mongoose'

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

  async startTransaction() {
    const session = await this.connection.startSession()
    session.startTransaction()
    // 在此编写你的事务逻辑
  }
}

在上述示例中,@InjectConnection() 用于将 Mongoose 连接对象注入到服务中。连接对象注入后,你可以通过 connection.startSession() 启动新的会话。该会话可用于管理数据库事务,确保多条查询的原子性操作。启动会话后,请根据你的业务逻辑选择提交或中止事务。

多数据库连接

在某些项目中,可能需要连接多个数据库。通过本模块也可以实现这一需求。要使用多个连接,首先需要创建这些连接。在这种情况下,必须为每个连接指定名称。

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.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 注入到自定义提供者(例如工厂提供者)中,可以使用 getConnectionToken() 方法,并将连接名称作为参数传入。

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

钩子(中间件)/ Hooks

中间件(也称为前置和后置钩子)是在异步函数执行过程中被调用的函数。中间件是在 schema 层级上指定的,常用于编写插件(参考来源)。在 Mongoose 中,模型编译后再调用 pre() 或 post() 方法不会生效。要在模型注册之前注册钩子,可以结合 MongooseModule 的 forFeatureAsync() 方法和工厂提供者(即 useFactory)来实现。通过这种方式,你可以获取 schema 对象,并使用 pre() 或 post() 方法在该 schema 上注册钩子。示例如下:

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

与其他工厂提供者类似,我们的工厂函数可以是 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 {}

插件(Plugins)

要为指定的 schema 注册插件,请使用 forFeatureAsync() 方法。

@Module({
  imports: [
    MongooseModule.forFeatureAsync([
      {
        name: Cat.name,
        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 {}

判别器(Discriminators)

判别器是一种模式继承机制。它允许你在同一个底层 MongoDB 集合上,拥有多个具有重叠模式(Schema)的模型。

假设你希望在单个集合中追踪不同类型的事件(Event)。每个事件都包含一个时间戳。

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 区分不同判别器模型的方式是通过「判别器键(discriminator key)」,默认值为 __t。Mongoose 会在你的模式中添加一个名为 __t 的字符串路径,用于标记该文档属于哪个判别器模型。

你也可以通过 discriminatorKey 选项自定义判别路径。

SignUpEvent 和 ClickedLinkEvent 的实例会与通用事件一起存储在同一个集合中。

现在,让我们定义 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)

接下来是 SignUpEvent 类:

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.forFeature 和 MongooseModule.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 {}

测试

在进行单元测试时,我们通常希望避免任何数据库连接,这样可以让测试套件更易于搭建,并且执行速度更快。但我们的类可能依赖于从连接实例中获取的模型。那么,我们该如何解决这些类的依赖问题?解决方案是创建模拟模型(mock model)。

为简化这一过程,@nestjs/mongoose 包提供了一个 getModelToken() 函数,该函数会根据令牌名称返回一个预处理好的注入令牌(Injection Token)。借助这个令牌,你可以通过任何标准的自定义提供者技术(包括 useClass、useValue 和 useFactory)轻松提供一个模拟实现。例如:

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

在上述示例中,每当有消费者通过 @InjectModel() 装饰器注入 Model<Cat> 时,都会注入一个硬编码的 catModel。

异步配置

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

其中一种方式是使用工厂函数:

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

与其他工厂提供者(Factory Provider)类似,我们的工厂函数可以是 async,并且可以通过 inject 注入依赖。

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

另外,你也可以通过类(class)而不是工厂函数来配置 MongooseModule,如下所示:

MongooseModule.forRootAsync({
  useClass: MongooseConfigService,
})

上述写法会在 MongooseModule 内部实例化 MongooseConfigService,并用它来创建所需的配置对象。需要注意的是,在这个例子中,MongooseConfigService 必须实现 MongooseOptionsFactory 接口,如下所示。MongooseModule 会在所提供类的实例对象上调用 createMongooseOptions() 方法。

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

如果你希望复用已有的配置提供者(options provider),而不是在 MongooseModule 内部创建一个私有副本,可以使用 useExisting 语法。

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

连接事件

你可以通过 onConnectionCreate 配置项监听 Mongoose 的连接事件。这样可以在每次建立连接时执行自定义逻辑。例如,你可以为 connected、open、disconnected、reconnected 和 disconnecting 等事件注册监听器,示例如下:

MongooseModule.forRoot('mongodb://localhost/test', {
  onConnectionCreate: (connection: Connection) => {
    connection.on('connected', () => console.log('connected'))
    connection.on('open', () => console.log('open'))
    connection.on('disconnected', () => console.log('disconnected'))
    connection.on('reconnected', () => console.log('reconnected'))
    connection.on('disconnecting', () => console.log('disconnecting'))

    return connection
  },
}),

在上述代码片段中,我们连接到了 mongodb://localhost/test 这个 MongoDB 数据库。通过 onConnectionCreate 选项,你可以为连接状态的变化设置专门的事件监听器:

  • connected:当连接成功建立时触发。
  • open:当连接完全打开并且可以进行操作时触发。
  • disconnected:当连接断开时触发。
  • reconnected:当连接断开后重新建立时触发。
  • disconnecting:当连接正在关闭时触发。

你也可以在使用 MongooseModule.forRootAsync() 创建的异步配置中加入 onConnectionCreate 属性:

MongooseModule.forRootAsync({
  useFactory: () => ({
    uri: 'mongodb://localhost/test',
    onConnectionCreate: (connection: Connection) => {
      // 在这里注册事件监听器
      return connection
    },
  }),
}),

这种方式为管理连接事件提供了灵活的手段,使你能够高效地处理连接状态的变化。

子文档

要在父文档中嵌套子文档,可以按如下方式定义你的 schema:

name.schema.ts
@Schema()
export class Name {
  @Prop()
  firstName: string

  @Prop()
  lastName: string
}

export const NameSchema = SchemaFactory.createForClass(Name)

然后在父级 schema 中引用该子文档:

person.schema.ts
@Schema()
export class Person {
  @Prop(NameSchema)
  name: Name
}

export const PersonSchema = SchemaFactory.createForClass(Person)

export type PersonDocumentOverride = {
  name: Types.Subdocument<Types.ObjectId> & Name
}

export type PersonDocument = HydratedDocument<Person, PersonDocumentOverride>

如果你希望包含多个子文档,可以使用子文档数组。需要注意的是,属性的类型也要相应地进行重写:

name.schema.ts
@Schema()
export class Person {
  @Prop([NameSchema])
  name: Name[]
}

export const PersonSchema = SchemaFactory.createForClass(Person)

export type PersonDocumentOverride = {
  name: Types.DocumentArray<Name>
}

export type PersonDocument = HydratedDocument<Person, PersonDocumentOverride>

虚拟属性(Virtuals)

在 Mongoose 中,虚拟属性是指存在于文档对象上但不会被持久化到 MongoDB 的属性。也就是说,这类属性不会存储在数据库中,而是在每次访问时动态计算得出。虚拟属性通常用于派生值或计算值,例如将多个字段组合(如通过拼接 firstName 和 lastName 创建 fullName 属性),或者用于依赖文档中现有数据生成的新属性。

class Person {
  @Prop()
  firstName: string

  @Prop()
  lastName: string

  @Virtual({
    get: function (this: Person) {
      return `${this.firstName} ${this.lastName}`
    },
  })
  fullName: string
}
提示
@Virtual() 装饰器需从 @nestjs/mongoose 包中导入。

在上述示例中,fullName 虚拟属性是根据 firstName 和 lastName 计算得出的。虽然访问时表现得像普通属性,但它实际上并不会被保存到 MongoDB 文档中。

示例

可用的完整示例请参见这里。