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