在 NestJS 中链接 MongoDB 有两种方法。一种方法就是使用TypeORM
来进行连接,另外一种方法就是使用Mongoose
。
此笔记主要是记录使用Mongoose
的。所以我们先安装所需的依赖:
npm i @nestjs/mongoose mongoose
安装完成后,需要在AppModule
中引入MongooseModule
。具体实例如下:
import databaseConfig from "./config/database.config";
import { MongooseModule } from "@nestjs/mongoose";
@Module({
imports: [
MongooseModule.forRoot("mongodb://localhost:27017/managementsytem"),
CommodityModule,
],
})
export class AppModule {}
MongooseModule.forRoot
中的配置对象与mongoose.connect()的配置对象一致。
模型注入
在 Mongoose 中最为核心的是模式,因为每个模式都会转换成一个具体的MongoDB集合
,并且模式定义了集合中文档的结构。
在 Mongoose 中模型主要是负责从底层创建数据和读取文档。
在 NestJs 中模式可以使用装饰器来创建,也可以使用 Mongoose 本身手动创建。使用装饰器创建的模式在代码量和代码可读性上都比 Mongoose 本身手动创建的要好。所以建议使用装饰器来创建模式。具体的实例如下:
import { HydratedDocument } from "mongoose";
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
export type CommodityDocument = HydratedDocument<Commodity>;
@Schema()
export class Commodity {
@Prop()
name: string; // 商品名称
@Prop()
price: number; // 商品价格
@Prop()
stock: number; // 商品库存
@Prop([String])
tag: string; // 商品标签
}
export const CommoditySchema = SchemaFactory.createForClass(Commodity);
注意:
您还可以使用 DefinitionsFactory
类生成原始模式定义。这允许您手动修改根据您提供的元数据生成的架构定义。当模型的字段可能会进行扩展和删除的时候,我们就可以使用DefinitionsFactory
来维护模式的数据。
@Schema()
装饰器的作用主要是定义模式。在上述的例子中会把Commodity
类映射到同名的MongoDB
集合中,但是需要注意的是在 MongoDB 集合中的名称会添加一个’s’,即Commoditys
。此装饰器可以接受一个可选参数,此参数主要是用于设置 MongoDB 的模型相关参数,具体的内容可以进入这里查看。
@Prop()
装饰器定义文档中的属性。例如,在上面的模式定义中,我们定义了三个属性:名称、价格和标签。借助 TypeScript 元数据(和反射)功能,可以自动推断这些属性的架构类型。但是,在无法隐式反映类型的更复杂场景(例如数组或嵌套对象结构)中,必须显式指示类型,如下所示:
@Prop([String])
tags: string[];
或者,@Prop() 装饰器接受选项对象参数(阅读有关可用选项的更多信息)。这样,您可以指示属性是否是必需的、指定默认值或将其标记为不可变。例如:
@Prop({ required: true })
name: string;
如果一个模型中带有另外一个属性的话,可以使用@Prop()
装饰器。例如,Commodity 里面包含一个 supply 模型,这样我们需要在 Commodity 里面添加具体的类和引用。具体例子如下:
import * as mongoose from 'mongoose';
import { Supply } from '../owners/schemas/supply.schema';
@Prop({ type: mongoose.Schema.Types.ObjectId, ref: 'Supply' })
supply: Supply;
如果有多个提供商,您的属性配置应如下所示:
@Prop({ type: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Supply' }] })
supply: Supply[];
最后,原始模式定义也可以传递给装饰器。例如,当属性表示未定义为类的嵌套对象时,这很有用。为此,请使用 @nestjs/mongoose 包中的 raw() 函数,如下所示:
@Prop(raw({
firstName: { type: String },
lastName: { type: String }
}))
details: Record<string, any>;
或者,如果您不想使用装饰器,则可以手动定义架构。例如:
export const CommoditySchema = new mongoose.Schema({
name: String,
price: Number,
stock: Number,
tags: [String],
});
当我们定义好模式后,就可以在对应的Module
中进行定义,具体实例如下:
@Module({
imports: [
ConfigModule,
MongooseModule.forFeature([
{ name: Commodity.name, schema: CommoditySchema },
]),
],
controllers: [CommodityController],
providers: [CommodityService],
})
export class CommodityModule {}
MongooseModule 提供了 forFeature() 方法来配置模块,包括定义应在当前范围内注册哪些模型。如果您还想在另一个模块中使用模型的话,你只要在 CommodityModule 中添加 MongooseModule 作为导出部分,并在另一个模块中导入 CommodityModule 就可以。
注册模式后,您可以使用 @InjectModel() 装饰器将 Commodity 模型注入到 CommodityService 中:
import { Injectable } from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
import { Model } from "mongoose";
import { Commodity } from "src/schemas/commodity.schemas";
@Injectable()
export class CommodityService {
constructor(
@InjectModel(Commodity.name) private commodityModel: Model<Commodity>
) {}
create(commodity: Commodity) {
const createdCommdity = new this.commodityModel(commodity);
return createdCommdity.save();
}
}
获取 Mongoose Connection 对象
有时您可能需要访问本机 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) {}
}
多个数据库使用
有些项目需要多个数据库连接。这也可以通过该模块来实现。要使用多个连接,请首先创建连接。在这种情况下,连接命名就成为强制性的。具体实例如下:
import { Module } from "@nestjs/common";
import { CommodityModule } from "./module/commodity.module";
import { AccountModule } from "./module/account.module";
import { ConfigModule } from "@nestjs/config";
import configuration from "./config/configuration";
import databaseConfig from "./config/database.config";
import { MongooseModule } from "@nestjs/mongoose";
@Module({
imports: [
ConfigModule.forRoot({
load: [configuration, databaseConfig],
cache: true,
}),
MongooseModule.forRoot("mongodb://localhost:27017/managementsytem", {
connectionName: "commodity",
}),
MongooseModule.forRoot("mongodb://localhost:27018/user", {
connectionName: "user",
}),
CommodityModule,
AccountModule,
],
})
export class AppModule {}
**注意:**您不应有多个没有名称或名称相同的连接,否则它们将被覆盖。
假如你设置了多个数据链接后,你必须在对应的模块中使用MongooseModule.forFeature()
函数来声明链接哪个数据库。具体的例子如下:
@Module({
imports: [
ConfigModule,
MongooseModule.forFeature(
[{ name: Commodity.name, schema: CommoditySchema }],
"commodity"
),
],
controllers: [CommodityController],
providers: [CommodityService],
exports: [CommodityService],
})
export class CommodityModule {}
如果您在 AppModule 中声明了多个数据库,并在对应的模块中声明了链接的数据库名称,那么我们的提供者就需要在装饰器中添加第二个参数。具体实例如下:
@Injectable()
export class CatsService {
constructor(
@InjectModel(Commodity.name, "commodities")
private commodityModel: Model<Commodity>
) {}
async create(commodity: Commodity) {
const createdCommdity = new this.commodityModel(commodity);
const _res = await createdCommdity.save();
return _res;
}
}
您还可以为给定连接注入连接:
import { Injectable } from "@nestjs/common";
import { InjectConnection } from "@nestjs/mongoose";
import { Connection } from "mongoose";
@Injectable()
export class CommodityService {
constructor(@InjectConnection("commodity") private connection: Connection) {}
}
要将给定连接注入到自定义提供程序(例如工厂提供程序),请使用 getConnectionToken() 函数,将连接名称作为参数传递。
{
provide: CommodityService,
useFactory: (commodityConnection: Connection) => {
return new CommodityService(commodityConnection);
},
inject: [getConnectionToken('commodity')],
}
Mongo 钩子(中间件)
Mongo 钩子的使用场景一般都是在编写自己的插件时才会使用。
Mongo 的中间件是在模式级别指定的,对于编写插件
很有用。编译模型后调用 pre() 或 post() 在 Mongoose 中不起作用。要在模型注册之前注册钩子,请使用 MongooseModule 的 forFeatureAsync() 方法以及工厂提供程序(即 useFactory)。通过这种技术,您可以访问模式对象,然后使用 pre() 或 post() 方法在该模式上注册挂钩。具体实例如下:
@Module({
imports: [
MongooseModule.forFeatureAsync([
{
name: Commodity.name,
useFactory: () => {
const schema = CommoditySchema;
schema.pre("save", function () {
console.log("Hello from pre save");
});
return schema;
},
},
]),
],
})
export class AppModule {}
与其他工厂提供者一样,我们的工厂函数可以是异步的,并且可以通过注入注入依赖项。
@Module({
imports: [
MongooseModule.forFeatureAsync([
{
name: Commodity.name,
imports: [ConfigModule],
useFactory: (configService: ConfigService) => {
const schema = CommoditySchema;
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: Commodity.name,
useFactory: () => {
const schema = CommoditySchema;
schema.plugin(require("mongoose-autopopulate"));
return schema;
},
},
]),
],
})
export class AppModule {}
要一次为所有模式注册插件,请调用 Connection 对象的 .plugin() 方法。您应该在创建模型之前访问连接;为此,请使用连接工厂:
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({ 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);
SignedUpEvent 和 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);
完成此操作后,使用鉴别器选项为给定模式注册鉴别器。它适用于 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 {}
完成上述的代码后,其实模型之间的关系如下:
异步配置
当您需要异步而不是静态地传递模块选项时,请使用 forRootAsync() 方法。与大多数动态模块一样,Nest 提供了多种处理异步配置的技术。
一种技术是使用工厂函数:
MongooseModule.forRootAsync({
useFactory: () => ({
uri: "mongodb://localhost/nest",
}),
});
与其他工厂提供者一样,我们的工厂函数可以是异步的,并且可以通过注入注入依赖项。
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,
});