跳到主要内容

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)。 每个模式都映射到一个 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。 这些属性的模式类型(schema types) 会自动由 TypeScript 的元数据(和反射)功能推断出来。 但是,在无法隐式反映类型的更复杂的情况下(例如,数组或嵌套对象结构),必须明确指定类型, 如下所示:

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

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

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

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

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

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

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

@Prop({ type: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Owner' }] })
owner: 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() 方法来配置模块,其中包括定义应在当前范围注册哪些模型。 如果您还希望在另一个模块中使用这些模型,请将 MongooseModule 添加到 CatsModuleexports 部分, 并在另一个模块中导入 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 Connection, 如下所示:

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

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

多个数据库

有些项目需要多个数据库连接。使用此模块也可以实现这一点。 要使用多个连接,首先创建这些连接。在这种情况下,连接命名变得必要(mandatory)

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

// CatsModule
@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 (中间件)

中间件(也称为预处理和后处理钩子)是在异步函数执行期间传递控制的函数。 中间件在模式级别上指定,适用于编写插件(参见文档)。 在 Mongoose 中,调用 pre()post() 以注册模型之前的钩子并不起作用。 要在模型注册之前注册钩子,请使用 MongooseModuleforFeatureAsync() 方法以及一个工厂提供程序(即 useFactory)。 使用这种技术,您可以访问模式对象,然后使用 pre()post() 方法在该模式上注册钩子。 请参见以下示例:

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

插件

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

@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);

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.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() 函数, 该函数返回基于令牌名称的准备好的[注入令牌(injection token)](https://docs.nestjs.com/fundamentals/custom-providers#di-fundamentals。 使用此令牌,您可以轻松地使用任何标准的自定义提供程序技术, 包括 useClassuseValueuseFactory,提供模拟实现。 例如:

@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,
});

示例

您可以在此处找到一个可运行的示例。