Nest 支持两种方式集成 MongoDB 数据库。你可以使用内置的 TypeORM 模块(详见此处),该模块内置了 MongoDB 连接器;也可以选择 Mongoose,这是最流行的 MongoDB 对象建模工具。本章将重点介绍后者,即如何使用专用的 @nestjs/mongoose 包。
首先,安装所需依赖:
npm install @nestjs/mongoose mongoose安装完成后,我们可以在根模块 AppModule 中导入 MongooseModule。
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:
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:
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 中:
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) {}
}要在 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() 启动新的会话。该会话可用于管理数据库事务,确保多条查询的原子性操作。启动会话后,请根据你的业务逻辑选择提交或中止事务。
在某些项目中,可能需要连接多个数据库。通过本模块也可以实现这一需求。要使用多个连接,首先需要创建这些连接。在这种情况下,必须为每个连接指定名称。
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() 装饰器中将连接名称作为第二个参数传入。
@Injectable()
export class CatsService {
constructor(@InjectModel(Cat.name, 'cats') private catModel: Model<Cat>) {}
}中间件(也称为前置和后置钩子)是在异步函数执行过程中被调用的函数。中间件是在 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 {}要为指定的 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:
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 集合上,拥有多个具有重叠模式(Schema)的模型。
假设你希望在单个集合中追踪不同类型的事件(Event)。每个事件都包含一个时间戳。
@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 类,如下所示:
@Schema()
export class ClickedLinkEvent {
kind: string
time: Date
@Prop({ type: String, required: true })
url: string
}
export const ClickedLinkEventSchema =
SchemaFactory.createForClass(ClickedLinkEvent)接下来是 SignUpEvent 类:
@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:
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:
@Schema()
export class Name {
@Prop()
firstName: string
@Prop()
lastName: string
}
export const NameSchema = SchemaFactory.createForClass(Name)然后在父级 schema 中引用该子文档:
@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>如果你希望包含多个子文档,可以使用子文档数组。需要注意的是,属性的类型也要相应地进行重写:
@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>在 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 文档中。
可用的完整示例请参见这里。