MongoDBとの連携

MongoDB

NestJSにはMongoDBと連携するために2つの方法をサポートしています。
  • 組み込みのTypeORMモジュールにあるMongoDB用のコネクタ
  • Mongoose(MongoDBのオブジェクトモデリングツール)
本セクションでは後者について、専用の@nestjs/mongooseパッケージを使いながら説明します。
まずは以下のコマンドをインストールしてください。(詳細は公式GitHubへ)
npm install --save @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()と同じ設定オブジェクトを受け取ります。
詳細について知りたければ、MongoDB公式ドキュメントをご確認ください。ざっくり知りたければ以下の動画を確認してください。

モデルのインジェクション

Mongooseでは、すべてがSchema(スキーマ)から派生しています。
各SchemaはMongoDBのコレクションに対応し、そのコレクション内のドキュメントの形を定義します。Schemaはモデルの定義に使われます。モデルはMongoDBからドキュメントを生成したり読み込んだりする役割を果たします。
SchemaはNestJSデコレータで作れますし、Mongoose自身で手動で作成できます。デコレータを使ってスキーマを作成すると、定型文が貼ってコード全体の読みやすさが向上します。
CatSchemaを定義してみましょう。
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
export type CatDocument = Cat & Document;
@Schema()
export class Cat {
@Prop()
name: string;
@Prop()
age: number;
@Prop()
breed: string;
}
export const CatSchema = SchemaFactory.createForClass(Cat);
メモ
DefinitionsFactoryクラス(@nestjs/mongooseにあるパッケージ)を使って生のSchema定義を生成できることには十分に注意してください。これによって、提供されたメタデータに基づいて生成されたSchema定義を手動で修正できます。
これは、デコレータですべてを表現するのが難しいような、ある種のエッジケースに便利です。
@Schema()デコレータは、クラスをSchema定義としてマークします。これはCatクラスを同じ名前のMongoDBコレクションに対応させますが、最後に"s"を追加します。このデコレータが受けるオプションの引数は一つで、スキーマオプションオブジェクトです。これは、通常mongoose.Schemaクラスのコンストラクタの第二引数に渡すオブジェクトだと考えてください。
@Props()デコレータは、ドキュメント内のプロパティを定義します。例えば、上記のスキーマ定義ではnameagebreedという3つのプロパティを定義しています。これらのプロパティのSchemaの型は、TypeScriptのメタデータの機能によって自動的に推測されます。しかし、型を暗黙的に反映できないようなより複雑なシナリオ(例えば、配列やネストしたオブジェクト構造)では、以下のように型を明示的に示す必要があります。
@Prop([String])
tags: string[];
あるいは、@Prop()デコレータにoptionオブジェクトの引数を指定することもできます。これで、そのプロパティが必須かどうかを指定したり、不変であるとマークしたりできます。
@Prop({ required: true })
name: string;
他のモデルとの関係を指定した場合は、@Prop()デコレータを使用できます。例えば、Catownersという別のコレクションに格納されているOwnerを持っている場合、プロパティはtyperefを持つ必要があります。
import * as mongoose from 'mongoose';
import { Owner } from '../owners/schemas/owner.schema';
// クラス定義の内部に書く
// type:やり取りするデータの型, ref:データの説明
@Prop({ type: mongoose.Schema.Types.ObjectId, ref: 'Owner' })
owner: Owner;
複数の所有者(owner)がいる場合、プロパティ構成は以下のようになります。
@Prop({ type: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Owner' }] })
owner: Owner[];
最後に、生のSchema定義もデコレータに渡すことができます。これは、例えばあるプロパティがクラスとして定義されていないネストされたオブジェクトを表している場合に便利です。この場合、@nestjs/mongooseパッケージのraw()関数を以下のようにします。
@Prop(raw({
firstName: { type: String },
lastName: { type: String }
}))
details: Record<string, any>;
また、デコレータを使わずにSchemaを定義したい場合は手動で設定できます。
export const CatSchema = new mongoose.Schema({
name: String,
age: Number,
breed: String,
});
cat.schemaファイルは、catsディレクトリの中のフォルダに存在し、そこでCatsModuleも定義しています。Schemaファイルは好きな場所に保存できますが、関連するドメインオブジェクトの近く、適切なModuleディレクトリに保存します。
CatsModuleを見てみましょう。
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 {}
MongooseModuleforFeature()メソッドを提供しており、現在のスコープでどのモデルを登録するかの定義など、モジュールの設定を行うことができます。他のモジュールでもモデルを使いたい場合は、CatsModuleexportsセクションにMongooseModuleを追加して、他のモジュールでCatsModuleをインポートしてください。
スキーマを登録したら、@InjectModel()デコレータを使ってCatsServiceCatモデルをインジェクトできます。
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.name) 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();
}
}

Mongooseの接続

時には、ネイティブのMongoose Connectionオブジェクトにアクセスする必要があるかもしれません。たとえば、接続オブジェクトに対してネイティブAPIを呼び出したいときなどです。
Mongoose Connectionをインジェクトするためには、以下のように@InjectConnection()デコレータを使います。
import { Injectable } from '@nestjs/common';
import { InjectConnection } from '@nestjs/mongoose';
import { Connection } from 'mongoose';
@Injectable()
export class CatsService {
constructor(@InjectConnection() private connection: Connection) {}
}

複数のデータベースを扱う

プロジェクト次第では、複数のデータベースへの雪像が必要な場合があります。これもこのModuleで実現できます。複数の接続を実装するには、まずデータベースへの接続を作成します。この場合、接続の命名が必須となります。
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()関数に指定しなければなりません。
@Module({
imports: [
MongooseModule.forFeature([{ name: Cat.name, schema: CatSchema }], 'cats'),
],
})
export class AppModule {}
また、指定された接続の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をカスタム・プロバイダに注入するためには、引数としてConnectionの名前を渡すgetConnectionToken()関数を使います。
{
provide: CatsService,
useFactory: (catsConnection: Connection) => {
return new CatsService(catsConnection);
},
inject: [getConnectionToken('cats')],
}

フック(Middleware)

Middleware(ミドルウェア)とは、非同期関数の実行時に制御を受け持つ関数のことを指します。Middlewareはスキーマ単位で指定され、プラグインを書くのに便利です。(詳細はこちら)
モデルをコンパイルした後にpre()あるいはpost()を呼び出しても、Mongooseでは動きません。モデル登録前にフックを登録するには、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.Connection()メソッドを使います。
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とはSchema継承の仕組みの一つです。これを使うと、同じMongoDBコレクションの上にSchemaが重複する複数のモデルを置けるようになります。
一つのコレクションでさまざまな種類のイベントを追跡したいとしましょう。すべてのイベントにはタイムスタンプがあります。
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が異なるDiscriminatorのモデルをクベルする方法はdiscriminatorKeyプロパティで、デフォルトでは__tとなっています。MongooseはSchemaに__tという文字列パスを追加し、このドキュメントがどのDiscriminatorのインスタンス7日を追跡するためにそれを使います。
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オプションを使って指定したSchemaに対応するDiscriminatorを登録します。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()関数を公開しています。これは、トークン名に基づいて準備されたインジェクション用トークンを返すものです。
このトークンを使うと、useClassuseValueuseFactoryなどの標準的カスタムプロバイダ技術を使ったモック実装を簡単に提供できます。(例えば以下のようにできます)
@Module({
providers: [
CatsService,
{
provide: getModelToken(Cat.name),
useValue: catModel,
},
],
})
export class CatsModule {}
この例では、@InjectModel()デコレータを使用してModel<Cat>を注入するコンシューマがいる場合、ハードコードされたcatModel(オブジェクトのインスタンス)が提供されます。

非同期設定

Moduleのオプションを静的ではなく非同期で渡す必要がある場合は、forRootAsync()メソッドを活用します。多くの動的Moduleと同様に、NestJSでは非同期設定に対応するためのいくつかのテクニックを提供します。
その一つが、ファクトリー関数を使う方法です。
MongooseModule.forRootAsync({
useFactory: () => ({
uri: 'mongodb://localhost/nest',
}),
});
他のファクトリープロバイダと同様に、私たちのファクトリー関数も非同期で、injectを通じて依存関係を注入できます。
MongooseModule.forRootAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
uri: configService.get<string>('MONGODB_URI'),
}),
inject: [ConfigService],
});
あるいは、次のようにファクトリーの代わりにクラスを使ってMongooseModuleを設定できます。
MongooseModule.forRootAsync({
useClass: MongooseConfigService,
});
上記の例はMongooseConfigServiceMongooseModule内でインスタンス化し、それを用いて必要なオプションオブジェクトを作成します。この例では、MongooseConfigServiceは以下のようにMongooseOptionFactoryインターフェイスを実装しなければいけないことに留意しましょう。
MongooseModuleは、指定したクラスのインスタンス化されたオブジェクトに対してcreateMongooseOptions()メソッドを呼び出します。
@Injectable()
export class MongooseConfigService implements MongooseOptionsFactory {
createMongooseOptions(): MongooseModuleOptions {
return {
uri: 'mongodb://localhost/nest',
};
}
}
MongooseModule内でプライベートなコピーを作るのではなく、既存のオプションプロバイダを再利用したい場合はuseExistingシンタックスを使います。
MongooseModule.forRootAsync({
imports: [ConfigModule],
useExisting: ConfigService,
});