Mongo
Nest 支持两种与 MongoDB 数据库集成的方式。你可以使用内置的 TypeORM 模块(此处有相关描述),它提供了 MongoDB 连接器;也可以使用 Mongoose——最流行的 MongoDB 对象建模工具。本章将介绍后者,使用专用的 @nestjs/mongoose 包。
首先安装所需的依赖:
$ 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。每个 Schema 映射到一个 MongoDB 集合,并定义该集合中文档的结构。Schema 用于定义 Model。Model 负责从底层 MongoDB 数据库中创建和读取文档。
Schema 可以使用 NestJS 装饰器创建,也可以使用 Mongoose 手动创建。使用装饰器创建 Schema 可以大大减少样板代码,提高整体代码可读性。
让我们定义 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 定义。这允许你手动修改基于提供的元数据生成的 Schema 定义。这对于某些难以完全用装饰器表示的边缘情况非常有用。
@Schema() 装饰器将类标记为 Schema 定义。它将我们的 Cat 类映射到同名的 MongoDB 集合,但会在末尾添加一个 "s"——因此最终的 MongoDB 集合名称将是 cats。这个装饰器接受一个可选参数,即 Schema 选项对象。可以将其视为你通常作为 mongoose.Schema 类构造函数的第二个参数传递的对象(例如 new mongoose.Schema(_, options))。要了解更多可用的 Schema 选项,请参阅此章节。
@Prop() 装饰器定义文档中的属性。例如,在上面的 Schema 定义中,我们定义了三个属性:name、age 和 breed。这些属性的 Schema 类型会借助 TypeScript 的元数据(和反射)功能自动推断。然而,在更复杂的场景中,类型无法被隐式反射(例如数组或嵌套对象结构),需要显式指定类型,如下所示:
@Prop([String])
tags: string[];或者,@Prop() 装饰器接受一个选项对象参数(了解更多可用选项)。通过它,你可以指示属性是否必填、指定默认值或将其标记为不可变。例如:
@Prop({ required: true })
name: string;如果你想指定与另一个模型的关系以便后续进行 populate,也可以使用 @Prop() 装饰器。例如,如果 Cat 有一个 Owner,存储在名为 owners 的不同集合中,该属性应具有 type 和 ref。例如:
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' } })
// 这确保该字段不会与已 populate 的引用混淆
owner: mongoose.Types.ObjectId;然后,当你需要有选择地进行 populate 时,可以使用指定正确类型的仓储函数:
import { Owner } from './schemas/owner.schema';
// 例如在 service 或 repository 中
async findAllPopulated() {
return this.catModel.find().populate<{ owner: Owner }>("owner");
}提示
如果没有可 populate 的外部文档,类型可能是 Owner | null,这取决于你的 Mongoose 配置。或者它可能会抛出错误,在这种情况下类型将是 Owner。
最后,原始 Schema 定义也可以传递给装饰器。当属性表示一个未定义为类的嵌套对象时,这非常有用。为此,请使用 @nestjs/mongoose 包中的 raw() 函数,如下所示:
@Prop(raw({
firstName: { type: String },
lastName: { type: String }
}))
details: Record<string, any>;或者,如果你不想使用装饰器,可以手动定义 Schema。例如:
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 } 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 Connection,如下所示:
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() 来开始一个新会话。这个会话可以用于管理数据库事务,确保跨多个查询的原子操作。启动会话后,请记住根据你的逻辑提交或中止事务。
多数据库
某些项目需要多个数据库连接。这也可以通过本模块实现。要使用多个连接,首先创建连接。在这种情况下,连接命名变成必须的。
// 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>) {}
}钩子(中间件)
中间件(也称为 pre 和 post 钩子)是在异步函数执行期间被传递控制权的函数。中间件在 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:
// 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 {}鉴别器
鉴别器是一种 Schema 继承机制。它们使你能够在同一个底层 MongoDB 集合之上拥有多个具有重叠 Schema 的模型。
假设你想在单个集合中跟踪不同类型的事件。每个事件都有一个时间戳。
// 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 会在你的 Schema 中添加一个名为 __t 的字符串路径,用于跟踪该文档是哪个鉴别器的实例。 你也可以使用 discriminatorKey 选项来定义用于鉴别的路径。
SignedUpEvent 和 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 选项为给定 Schema 注册鉴别器。它同时适用于 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 {}测试
在对应用进行单元测试时,我们通常希望避免任何数据库连接,使测试套件更容易设置、执行更快。但我们的类可能依赖于从连接实例中拉取的模型。如何解决这些类的依赖?解决方案是创建模拟模型。
为了简化此过程,@nestjs/mongoose 包暴露了一个 getModelToken() 函数,该函数根据令牌名称返回一个准备好的注入令牌。使用这个令牌,你可以轻松地使用任何标准的自定义提供者技术提供模拟实现,包括 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',
}),
});与其他工厂提供者一样,我们的工厂函数可以是 async 的,并且可以通过 inject 注入依赖。
MongooseModule.forRootAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
uri: configService.get<string>('MONGODB_URI'),
}),
inject: [ConfigService],
});或者,你可以使用类而不是工厂来配置 MongooseModule,如下所示:
MongooseModule.forRootAsync({
useClass: MongooseConfigService,
});上面的构造在 MongooseModule 内部实例化 MongooseConfigService,使用它来创建所需的选项对象。注意,在这个示例中,MongooseConfigService 必须实现 MongooseOptionsFactory 接口,如下所示。MongooseModule 将在提供的类的实例化对象上调用 createMongooseOptions() 方法。
@Injectable()
export class MongooseConfigService implements MongooseOptionsFactory {
createMongooseOptions(): MongooseModuleOptions {
return {
uri: 'mongodb://localhost/nest',
};
}
}如果你想复用现有的选项提供者而不是在 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>;虚拟属性
在 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 文档中。
示例
一个可运行的示例可以在这里找到。