Nest支持两种与MongoDB数据库集成的方法。您可以使用此处描述的内置TypeORM模块,该模块具有MongoDB的连接器,或者使用最受欢迎的MongoDB对象建模工具Mongoose。在这一章中,我们将描述后者,使用专用的@nestjs/mongoose
包。
首先安装所需的依赖项:
bash
$ npm i @nestjs/mongoose mongoose
安装过程完成后,我们可以将MongooseModule导入到根AppModule中。
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
:
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 的元数据(和反射)能力,这些属性的模式类型会自动推断。然而,在更复杂的场景中,如果类型不能被隐式地反射(例如,数组或嵌套的对象结构),则必须显式地指明类型,如下所示:
ts
@Prop([String])
tags: string[];
另外,@Prop()
装饰器接受一个选项对象参数(了解更多关于可用选项)。通过这种方式,您可以指示一个属性是否是必需的,指定默认值,或标记它为不可变的。例如:
ts
@Prop({ required: true })
name: string;
如果您想指定与另一个模型的关系,稍后进行填充,您也可以使用 @Prop()
装饰器。例如,如果 Cat
有一个 Owner
,它存储在一个叫做 owners
的不同集合中,属性应该有 type 和 ref。例如:
ts
import * as mongoose from 'mongoose';
import { Owner } from '../owners/schemas/owner.schema';
// 在类定义内部
@Prop({ type: mongoose.Schema.Types.ObjectId, ref: 'Owner' })
owner: Owner;
如果有多个所有者,您的属性配置应如下所示:
ts
@Prop({ type: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Owner' }] })
owner: Owner[];
最后,原始的模式定义也可以传递给装饰器。例如,当属性代表一个未定义为类的嵌套对象时,这是非常有用的。为此,使用来自@nestjs/mongoose
包的raw()
函数,如下所示:
ts
@Prop(raw({
firstName: { type: String },
lastName: { type: String }
}))
details: Record<string, any>;
cat.schema
文件位于cats
目录的一个文件夹中,我们在其中也定义了CatsModule
。虽然你可以根据自己的喜好存储模式文件,但我们建议将它们存储在与相关域对象相邻的位置,在适当的模块目录中。
让我们看一下CatsModule
:
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
中:
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对象。例如,你可能想在connection对象上进行原生的API调用。你可以使用@InjectConnection()
装饰器来注入Mongoose Connection,如下所示:
ts
import { Injectable } from '@nestjs/common';
import { InjectConnection } from '@nestjs/mongoose';
import { Connection } from 'mongoose';
@Injectable()
export class CatsService {
constructor(@InjectConnection() private connection: Connection) {}
}
多数据库
有些项目需要多个数据库连接。这个模块也可以实现这一功能。要使用多个连接,首先创建连接。在这种情况下,连接命名变得是必需的。
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 {}
您还可以为给定的连接注入 Connection:
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) {}
}
为了将给定的 Connection
注入到自定义提供者(例如,工厂提供者),使用 getConnectionToken()
函数,并传递连接的名称作为参数。
ts
{
provide: CatsService,
useFactory: (catsConnection: Connection) => {
return new CatsService(catsConnection);
},
inject: [getConnectionToken('cats')],
}
如果您只是想从一个命名的数据库中注入模型,您可以使用连接名称作为 @InjectModel()
装饰器的第二个参数。
ts
@Injectable()
export class CatsService {
constructor(@InjectModel(Cat.name, 'cats') private catModel: Model<Cat>) {}
}
Hooks(中间件)
中间件(也称为预处理和后处理钩子)是在执行异步函数期间传递控制的函数。中间件在模式级别上指定,对于编写插件(source)很有用。在编译模型后调用 pre()
或 post()
在 Mongoose 中是不起作用的。为了在模型注册之前注册一个钩子,使用 MongooseModule
的 forFeatureAsync()
方法和工厂提供者(即 useFactory
)。使用这种技术,您可以访问一个模式对象,然后使用 pre()
或 post()
方法在该模式上注册一个钩子。请参阅下面的示例:
ts
@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 {}
像其他工厂提供者一样,我们的工厂函数可以是异步
的,并且可以通过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 {}
插件
为了为给定的模式注册一个插件,请使用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
:
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
Discriminators 是一种模式继承机制。它们使您能够在同一个底层的 MongoDB 集合上拥有多个具有重叠模式的模型。
假设您想要在一个集合中跟踪不同类型的事件。每个事件都将有一个时间戳。
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
选项来定义用于鉴别的路径。
SignedUpEvent
和 ClickedLinkEvent
实例将存储在与通用事件相同的集合中。
现在,让我们定义 ClickedLinkEvent
类,如下所示:
ts
@Schema()
export class ClickedLinkEvent {
kind: string;
time: Date;
@Prop({ type: String, required: true })
url: string;
}
export const ClickedLinkEventSchema = SchemaFactory.createForClass(ClickedLinkEvent);
和 SignUpEvent
类:
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
:
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()
函数,该函数基于令牌名称返回一个预先准备好的注入令牌。使用此令牌,您可以轻松地使用任何标准的custom provider技术提供模拟实现,包括 useClass
、useValue
和 useFactory
。例如:
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',
}),
});
像其他工厂providers一样,我们的工厂函数可以是异步
的,并且可以通过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
语法。
ini
MongooseModule.forRootAsync({
imports: [ConfigModule],
useExisting: ConfigService,
});