NestJS Mongo

2023-09-08 16:25 更新

Nest支持兩種與 MongoDB 數(shù)據(jù)庫集成的方式。既使用內(nèi)置的TypeORM 提供的 MongoDB 連接器,或使用最流行的 MongoDB 對(duì)象建模工具 Mongoose。在本章后續(xù)描述中我們使用專用的@nestjs/mongoose包。

首先,我們需要安裝所有必需的依賴項(xiàng):

$ npm install --save @nestjs/mongoose mongoose

安裝過程完成后,我們可以將其 MongooseModule 導(dǎo)入到根目錄 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() 一樣的參數(shù)對(duì)象。參見

模型注入

在Mongoose中,一切都源于 Scheme,每個(gè) Schema 都會(huì)映射到 MongoDB 的一個(gè)集合,并定義集合內(nèi)文檔的結(jié)構(gòu)。Schema 被用來定義模型,而模型負(fù)責(zé)從底層創(chuàng)建和讀取 MongoDB 的文檔。

Schema 可以用 NestJS 內(nèi)置的裝飾器來創(chuàng)建,或者也可以自己動(dòng)手使用 Mongoose的常規(guī)方式。使用裝飾器來創(chuàng)建 Schema 會(huì)極大大減少引用并且提高代碼的可讀性。

我們先定義CatSchema:

schemas/cat.schema.ts
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';

export type CatDocument = Cat & Document;

@Schema()
export class Cat extends Document {
  @Prop()
  name: string;

  @Prop()
  age: number;

  @Prop()
  breed: string;
}

export const CatSchema = SchemaFactory.createForClass(Cat);
注意你也可以通過使用 DefinitionsFactory 類(可以從 @nestjs/mongoose 導(dǎo)入)來生成一個(gè)原始 Schema ,這將允許你根據(jù)被提供的元數(shù)據(jù)手動(dòng)修改生成的 Schema 定義。這對(duì)于某些很難用裝飾器體現(xiàn)所有的極端例子非常有用。

@Schema 裝飾器標(biāo)記一個(gè)類作為Schema 定義,它將我們的 Cat 類映射到 MongoDB 同名復(fù)數(shù)的集合 Cats,這個(gè)裝飾器接受一個(gè)可選的 Schema 對(duì)象。將它想象為那個(gè)你通常會(huì)傳遞給 mongoose.Schema 類的構(gòu)造函數(shù)的第二個(gè)參數(shù)(例如, new mongoose.Schema(_, options)))。 更多可用的 Schema 選項(xiàng)可以 看這里。

@Prop 裝飾器在文檔中定義了一個(gè)屬性。舉個(gè)例子,在上面的 Schema 定義中,我們定義了三個(gè)屬性,分別是:name ,age 和 breed。得益于 TypeScript 的元數(shù)據(jù)(還有反射),這些屬性的 Schema類型會(huì)被自動(dòng)推斷。然而在更復(fù)雜的場(chǎng)景下,有些類型例如對(duì)象和嵌套數(shù)組無法正確推斷類型,所以我們要向下面一樣顯式的指出。

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

另外的 @Prop 裝飾器接受一個(gè)可選的參數(shù),通過這個(gè),你可以指示這個(gè)屬性是否是必須的,是否需要默認(rèn)值,或者是標(biāo)記它作為一個(gè)常量,下面是例子:

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

最后的,原始 Schema 定義也可以被傳遞給裝飾器。這也非常有用,舉個(gè)例子,一個(gè)屬性體現(xiàn)為一個(gè)嵌套對(duì)象而不是一個(gè)定義的類。要使用這個(gè),需要從像下面一樣從 @nestjs/mongoose 包導(dǎo)入 raw()。

@Prop(raw({
  firstName: { type: String },
  lastName: { type: String }
}))
details: Record<string, any>;

或者,如果你不喜歡使用裝飾器,你可以使用 mongoose.Schema 手動(dòng)定義一個(gè) Schema。下面是例子:

schemas/cat.schema.ts
import * as mongoose from 'mongoose';

export const CatSchema = new mongoose.Schema({
  name: String,
  age: Number,
  breed: String,
});

該 cat.schema 文件在 cats 目錄下。這個(gè)目錄包含了和 CatsModule模塊有關(guān)的所有文件。你可以決定在哪里保存Schema文件,但我們推薦在他們的域中就近創(chuàng)建,即在相應(yīng)的模塊目錄中。

我們來看看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()方法來配置模塊,包括定義哪些模型應(yīng)該注冊(cè)在當(dāng)前范圍中。如果你還想在另外的模塊中使用這個(gè)模型,將MongooseModule添加到CatsModule的exports部分并在其他模塊中導(dǎo)入CatsModule。

注冊(cè)Schema后,可以使用 @InjectModel() 裝飾器將 Cat 模型注入到 CatsService 中:

cats.service.ts
import { Model } from 'mongoose';
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Cat, CatDocument } from './schemas/cat.schema';
import { CreateCatDto } from './dto/create-cat.dto';

@Injectable()
export class CatsService {
  constructor(@InjectModel('Cat') private catModel: Model<CatDocument>) {}

  async create(createCatDto: CreateCatDto): Promise<Cat> {
    const createdCat = new this.catModel(createCatDto);
    return createdCat.save();
  }

  async findAll(): Promise<Cat[]> {
    return this.catModel.find().exec();
  }
}

連接

有時(shí)你可能需要連接原生的Mongoose 連接對(duì)象,你可能在連接對(duì)象中想使用某個(gè)原生的 API。你可以使用如下的@InjectConnection()裝飾器來注入 Mongoose 連接。

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

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

多數(shù)據(jù)庫

有的項(xiàng)目需要多數(shù)據(jù)庫連接,可以在這個(gè)模塊中實(shí)現(xiàn)。要使用多連接,首先要?jiǎng)?chuàng)建連接,在這種情況下,連接必須**要有名稱。

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

你不能在沒有名稱的情況下使用多連接,也不能對(duì)多連接使用同一個(gè)名稱,否則會(huì)被覆蓋掉。

在設(shè)置中,要告訴MongooseModule.forFeature()方法應(yīng)該使用哪個(gè)連接。

@Module({
  imports: [MongooseModule.forFeature([{ name: 'Cat', schema: CatSchema }], 'cats')],
})
export class AppModule {}

也可以向一個(gè)給定的連接中注入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) {}
}

鉤子(中間件)

中間件(也被稱作預(yù)處理(pre)和后處理(post)鉤子)是在執(zhí)行異步函數(shù)時(shí)傳遞控制的函數(shù)。中間件是針對(duì)Schema層級(jí)的,在寫插件(源碼)時(shí)非常有用。在 Mongoose 編譯完模型后使用pre()或post()不會(huì)起作用。要在模型注冊(cè)前注冊(cè)一個(gè)鉤子,可以在使用一個(gè)工廠提供者(例如 useFactory)是使用MongooseModule中的forFeatureAsync()方法。使用這一技術(shù),你可以訪問一個(gè) Schema 對(duì)象,然后使用pre()或post()方法來在那個(gè) schema 中注冊(cè)一個(gè)鉤子。示例如下:

@Module({
  imports: [
    MongooseModule.forFeatureAsync([
      {
        name: 'Cat',
        useFactory: () => {
          const schema = CatsSchema;
          schema.pre('save', () => console.log('Hello from pre save'));
          return schema;
        },
      },
    ]),
  ],
})
export class AppModule {}

和其他工廠提供者一樣,我們的工廠函數(shù)是異步的,可以通過inject注入依賴。

@Module({
  imports: [
    MongooseModule.forFeatureAsync([
      {
        name: 'Cat',
        imports: [ConfigModule],
        useFactory: (configService: ConfigService) => {
          const schema = CatsSchema;
          schema.pre('save', () => console.log(`${configService.get<string>('APP_NAME')}: Hello from pre save`));
          return schema;
        },
        inject: [ConfigService],
      },
    ]),
  ],
})
export class AppModule {}

插件

要向給定的 schema 中注冊(cè)插件,可以使用forFeatureAsync()方法。

@Module({
  imports: [
    MongooseModule.forFeatureAsync([
      {
        name: 'Cat',
        useFactory: () => {
          const schema = CatsSchema;
          schema.plugin(require('mongoose-autopopulate'));
          return schema;
        },
      },
    ]),
  ],
})
export class AppModule {}

要向所有 schema 中立即注冊(cè)一個(gè)插件,調(diào)用Connection對(duì)象中的.plugin()方法。你可以在所有模型創(chuàng)建前訪問連接。使用connectionFactory來實(shí)現(xiàn):

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

測(cè)試

在單元測(cè)試我們的應(yīng)用程序時(shí),我們通常希望避免任何數(shù)據(jù)庫連接,使我們的測(cè)試套件獨(dú)立并盡可能快地執(zhí)行它們。但是我們的類可能依賴于從連接實(shí)例中提取的模型。如何處理這些類呢?解決方案是創(chuàng)建模擬模型。

為了簡(jiǎn)化這一過程,@nestjs/mongoose 包公開了一個(gè) getModelToken() 函數(shù),該函數(shù)根據(jù)一個(gè) token 名稱返回一個(gè)準(zhǔn)備好的[注入token](https://docs.nestjs.com/fundamentals/custom-providers#di-fundamentals)。使用此 token,你可以輕松地使用任何標(biāo)準(zhǔn)自定義提供者技術(shù),包括 useClass、useValue 和 useFactory。例如:

@Module({
  providers: [
    CatsService,
    {
      provide: getModelToken('Cat'),
      useValue: catModel,
    },
  ],
})
export class CatsModule {}

在本例中,每當(dāng)任何使用者使用 @InjectModel() 裝飾器注入模型時(shí),都會(huì)提供一個(gè)硬編碼的 Model<Cat> (對(duì)象實(shí)例)。

異步配置

通常,您可能希望異步傳遞模塊選項(xiàng),而不是事先傳遞它們。在這種情況下,使用 forRootAsync() 方法,Nest提供了幾種處理異步數(shù)據(jù)的方法。

第一種可能的方法是使用工廠函數(shù):

MongooseModule.forRootAsync({
  useFactory: () => ({
    uri: 'mongodb://localhost/nest',
  }),
});

與其他工廠提供程序一樣,我們的工廠函數(shù)可以是異步的,并且可以通過注入注入依賴。

MongooseModule.forRootAsync({
  imports: [ConfigModule],
  useFactory: async (configService: ConfigService) => ({
    uri: configService.getString('MONGODB_URI'),
  }),
  inject: [ConfigService],
});

或者,您可以使用類而不是工廠來配置 MongooseModule,如下所示:

MongooseModule.forRootAsync({
  useClass: MongooseConfigService,
});

上面的構(gòu)造在 MongooseModule中實(shí)例化了 MongooseConfigService,使用它來創(chuàng)建所需的 options 對(duì)象。注意,在本例中,MongooseConfigService 必須實(shí)現(xiàn) MongooseOptionsFactory 接口,如下所示。 MongooseModule 將在提供的類的實(shí)例化對(duì)象上調(diào)用 createMongooseOptions() 方法。

@Injectable()
class MongooseConfigService implements MongooseOptionsFactory {
  createMongooseOptions(): MongooseModuleOptions {
    return {
      uri: 'mongodb://localhost/nest',
    };
  }
}

為了防止 MongooseConfigService 內(nèi)部創(chuàng)建 MongooseModule 并使用從不同模塊導(dǎo)入的提供程序,您可以使用 useExisting 語法。

MongooseModule.forRootAsync({
  imports: [ConfigModule],
  useExisting: ConfigService,
});

例子

一個(gè)可用的示例見這里。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)