ポピュレート
MongoDB はバージョン 3.2 以上で、join に似た $lookup 集約演算子を持っています。Mongoose は、populate()
と呼ばれる、より強力な代替手段を提供しており、他のコレクションのドキュメントを参照できます。
ポピュレーションとは、ドキュメント内の指定されたパスを、他のコレクションからのドキュメントに自動的に置き換えるプロセスです。単一ドキュメント、複数のドキュメント、プレーンオブジェクト、複数のプレーンオブジェクト、またはクエリから返されたすべてのオブジェクトをポピュレートできます。いくつかの例を見てみましょう。
const mongoose = require('mongoose');
const { Schema } = mongoose;
const personSchema = Schema({
_id: Schema.Types.ObjectId,
name: String,
age: Number,
stories: [{ type: Schema.Types.ObjectId, ref: 'Story' }]
});
const storySchema = Schema({
author: { type: Schema.Types.ObjectId, ref: 'Person' },
title: String,
fans: [{ type: Schema.Types.ObjectId, ref: 'Person' }]
});
const Story = mongoose.model('Story', storySchema);
const Person = mongoose.model('Person', personSchema);
これまでは、モデルを2つ作成しました。Person
モデルの stories
フィールドは、ObjectId
の配列に設定されています。ref
オプションは、ポピュレーション中に使用するモデル(この場合は Story
モデル)を Mongoose に指示するものです。ここに保存するすべての _id
は、Story
モデルのドキュメント _id
でなければなりません。
- 参照の保存
- ポピュレーション
- フィールドがポピュレートされているかどうかを確認する
- ポピュレートされたフィールドの設定
- 外部ドキュメントがない場合
- フィールドの選択
- 複数のパスのポピュレート
- クエリ条件とその他のオプション
limit
対perDocumentLimit
- 子への参照
- 既存のドキュメントのポピュレート
- 複数の既存ドキュメントのポピュレート
- 複数レベルにわたるポピュレート
- データベース間でのポピュレート
refPath
による動的な参照ref
による動的な参照- バーチャルのポピュレート
- バーチャルのポピュレート:Count オプション
- バーチャルのポピュレート:Match オプション
- マップのポピュレート
- ミドルウェアでのポピュレート
- ミドルウェアでの複数パスのポピュレート
- ポピュレートされたドキュメントの変換
参照の保存
他のドキュメントへの参照の保存は、通常プロパティを保存する場合と同じ方法で行います。_id
値を割り当てるだけです。
const author = new Person({
_id: new mongoose.Types.ObjectId(),
name: 'Ian Fleming',
age: 50
});
await author.save();
const story1 = new Story({
title: 'Casino Royale',
author: author._id // assign the _id from the person
});
await story1.save();
// that's it!
ref
オプションは、ObjectId
、Number
、String
、および Buffer
パスに設定できます。populate()
は、ObjectId、数値、文字列、およびバッファで動作します。ただし、特に理由がない限り、_id
プロパティ(したがって ref
プロパティ)には ObjectId を使用することをお勧めします。これは、_id
プロパティを持たずに新しいドキュメントを作成した場合、MongoDB が _id
を ObjectId に設定するためです。そのため、_id
プロパティを数値にすると、数値の _id
を持たないドキュメントを挿入しないように特に注意する必要があります。
ポピュレーション
これまでのところ、特に異なることをしていません。単に Person
と Story
を作成しただけです。それでは、クエリビルダーを使用してストーリーの author
をポピュレートしてみましょう。
const story = await Story.
findOne({ title: 'Casino Royale' }).
populate('author').
exec();
// prints "The author is Ian Fleming"
console.log('The author is %s', story.author.name);
ポピュレートされたパスは、元の _id
に設定されなくなります。その値は、結果を返す前に個別のクエリを実行することでデータベースから返された mongoose ドキュメントに置き換えられます。
参照の配列は同じように動作します。クエリで populate メソッドを呼び出すだけで、元の _id
の代わりにドキュメントの配列がその場で返されます。
ポピュレートされたフィールドの設定
ドキュメントを設定することで、プロパティを手動でポピュレートできます。ドキュメントは、ref
プロパティが参照するモデルのインスタンスである必要があります。
const story = await Story.findOne({ title: 'Casino Royale' });
story.author = author;
console.log(story.author.name); // prints "Ian Fleming"
ポピュレートされた配列にドキュメントまたは POJO をプッシュすることもできます。Mongoose は、それらのドキュメントの ref
が一致する場合は、それらのドキュメントを追加します。
const fan1 = await Person.create({ name: 'Sean' });
await Story.updateOne({ title: 'Casino Royale' }, { $push: { fans: { $each: [fan1._id] } } });
const story = await Story.findOne({ title: 'Casino Royale' }).populate('fans');
story.fans[0].name; // 'Sean'
const fan2 = await Person.create({ name: 'George' });
story.fans.push(fan2);
story.fans[1].name; // 'George'
story.fans.push({ name: 'Roger' });
story.fans[2].name; // 'Roger'
ObjectId のような、非 POJO か非ドキュメントの値をプッシュすると、Mongoose >= 8.7.0
は配列全体をデポピュレートします。
const fan4 = await Person.create({ name: 'Timothy' });
story.fans.push(fan4._id); // Push the `_id`, not the full document
story.fans[0].name; // undefined, `fans[0]` is now an ObjectId
story.fans[0].toString() === fan1._id.toString(); // true
フィールドがポピュレートされているかどうかを確認する
フィールドがポピュレートされているかどうかを確認するには、populated()
関数を呼び出すことができます。populated()
が 真偽値 を返す場合、そのフィールドはポピュレートされていると見なすことができます。
story.populated('author'); // truthy
story.depopulate('author'); // Make `author` not populated anymore
story.populated('author'); // undefined
パスがポピュレートされているかどうかを確認する一般的な理由は、author
ID を取得することです。ただし、便宜上、Mongoose は ObjectId インスタンスに _id
ゲッター を追加するため、author
がポピュレートされているかどうかに関係なく、story.author._id
を使用できます。
story.populated('author'); // truthy
story.author._id; // ObjectId
story.depopulate('author'); // Make `author` not populated anymore
story.populated('author'); // undefined
story.author instanceof ObjectId; // true
story.author._id; // ObjectId, because Mongoose adds a special getter
外部ドキュメントがない場合
Mongoose の populate は、従来の SQL ジョイン とは動作が異なります。ドキュメントがない場合、story.author
は null
になります。これは、SQL の 左結合 に類似しています。
await Person.deleteMany({ name: 'Ian Fleming' });
const story = await Story.findOne({ title: 'Casino Royale' }).populate('author');
story.author; // `null`
storySchema
に authors
の配列がある場合、populate()
は空の配列を返します。
const storySchema = Schema({
authors: [{ type: Schema.Types.ObjectId, ref: 'Person' }],
title: String
});
// Later
const story = await Story.findOne({ title: 'Casino Royale' }).populate('authors');
story.authors; // `[]`
フィールドの選択
ポピュレートされたドキュメントについて、特定のフィールドだけを返す場合、populate メソッドの第2引数として、通常の フィールド名構文 を渡すことで実現できます。
const story = await Story.
findOne({ title: /casino royale/i }).
populate('author', 'name').
exec(); // only return the Persons name
// prints "The author is Ian Fleming"
console.log('The author is %s', story.author.name);
// prints "The authors age is null"
console.log('The authors age is %s', story.author.age);
複数のパスのポピュレート
複数のパスを同時にポピュレートしたい場合はどうすればよいでしょうか?
await Story.
find({ /* ... */ }).
populate('fans').
populate('author').
exec();
同じパスで populate()
を複数回呼び出すと、最後に呼び出されたものだけが有効になります。
// The 2nd `populate()` call below overwrites the first because they
// both populate 'fans'.
await Story.
find().
populate({ path: 'fans', select: 'name' }).
populate({ path: 'fans', select: 'email' });
// The above is equivalent to:
await Story.find().populate({ path: 'fans', select: 'email' });
クエリ条件とその他のオプション
年齢に基づいて fans 配列をポピュレートし、名前だけを選択したい場合はどうすればよいでしょうか?
await Story.
find().
populate({
path: 'fans',
match: { age: { $gte: 21 } },
// Explicitly exclude `_id`, see http://bit.ly/2aEfTdB
select: 'name -_id'
}).
exec();
match
オプションは、Story
ドキュメントをフィルタリングしません。match
を満たすドキュメントがない場合、fans
配列が空の Story
ドキュメントが取得されます。
たとえば、ストーリーの author
を populate()
し、author
が match
を満たさない場合、ストーリーの author
は null
になります。
const story = await Story.
findOne({ title: 'Casino Royale' }).
populate({ path: 'author', match: { name: { $ne: 'Ian Fleming' } } }).
exec();
story.author; // `null`
一般的に、populate()
を使用して、ストーリーの author
のプロパティに基づいてストーリーをフィルタリングすることはできません。たとえば、以下のクエリは、author
がポピュレートされている場合でも、結果を返しません。
const story = await Story.
findOne({ 'author.name': 'Ian Fleming' }).
populate('author').
exec();
story; // null
著者の名前でストーリーをフィルタリングする場合は、非正規化 を使用してください。
limit
対 perDocumentLimit
Populate は limit
オプションをサポートしていますが、下位互換性のために、現在、ドキュメント単位で制限されていません。たとえば、2つのストーリーがあるとします。
await Story.create([
{ title: 'Casino Royale', fans: [1, 2, 3, 4, 5, 6, 7, 8] },
{ title: 'Live and Let Die', fans: [9, 10] }
]);
limit
オプションを使用して populate()
すると、2番目のストーリーのファンが0人であることがわかります。
const stories = await Story.find().populate({
path: 'fans',
options: { limit: 2 }
});
stories[0].name; // 'Casino Royale'
stories[0].fans.length; // 2
// 2nd story has 0 fans!
stories[1].name; // 'Live and Let Die'
stories[1].fans.length; // 0
これは、ドキュメントごとに個別のクエリを実行しないために、Mongoose が numDocuments * limit
を制限としてファンをクエリするためです。正しい limit
が必要な場合は、perDocumentLimit
オプション(Mongoose 5.9.0 で新規追加)を使用してください。ただし、populate()
はストーリーごとに個別のクエリを実行するため、populate()
の速度が低下する可能性があることに注意してください。
const stories = await Story.find().populate({
path: 'fans',
// Special option that tells Mongoose to execute a separate query
// for each `story` to make sure we get 2 fans for each story.
perDocumentLimit: 2
});
stories[0].name; // 'Casino Royale'
stories[0].fans.length; // 2
stories[1].name; // 'Live and Let Die'
stories[1].fans.length; // 2
子への参照
ただし、author
オブジェクトを使用すると、ストーリーのリストを取得できないことがわかります。これは、author.stories
に story
オブジェクトがプッシュされたことがないためです。
ここには2つの視点があります。まず、author
に自分のストーリーを認識させたい場合があります。通常、スキーマでは、'many' 側にある親ポインタを持つことで、一対多のリレーションシップを解決する必要があります。ただし、子のポインタの配列が必要な正当な理由がある場合は、以下のように配列にドキュメントをプッシュできます。
await story1.save();
author.stories.push(story1);
await author.save();
これにより、find
と populate
の組み合わせを実行できます。
const person = await Person.
findOne({ name: 'Ian Fleming' }).
populate('stories').
exec(); // only works if we pushed refs to children
console.log(person);
同期がずれる可能性があるため、2つのポインタセットが必要かどうかは議論の余地があります。代わりに、ポピュレートをスキップし、必要なストーリーを直接 find()
できます。
const stories = await Story.
find({ author: author._id }).
exec();
console.log('The stories are an array: ', stories);
クエリポピュレーション から返されたドキュメントは、lean オプションが指定されていない限り、完全に機能する、remove
可能で、save
可能なドキュメントになります。サブドキュメント と混同しないでください。その remove メソッドを呼び出す際には注意してください。データベースから削除されるため、配列から削除されるだけではありません。
既存のドキュメントのポピュレート
既存の mongoose ドキュメントがあり、そのパスのいくつかをポピュレートする場合は、Document#populate() メソッドを使用できます。
const person = await Person.findOne({ name: 'Ian Fleming' });
person.populated('stories'); // null
// Call the `populate()` method on a document to populate a path.
await person.populate('stories');
person.populated('stories'); // Array of ObjectIds
person.stories[0].name; // 'Casino Royale'
Document#populate()
メソッドは、チェーンをサポートしていません。複数パスをポピュレートするには、populate()
を複数回呼び出すか、パス配列で呼び出す必要があります。
await person.populate(['stories', 'fans']);
person.populated('fans'); // Array of ObjectIds
複数の既存ドキュメントのポピュレート
1つまたは複数の mongoose ドキュメント、またはプレーンオブジェクト(mapReduce の出力など)がある場合は、Model.populate() メソッドを使用してポピュレートできます。これは、Document#populate()
と Query#populate()
がドキュメントをポピュレートするために使用するものです。
複数レベルにわたるポピュレート
ユーザーの友達を追跡するユーザーのスキーマがあるとします。
const userSchema = new Schema({
name: String,
friends: [{ type: ObjectId, ref: 'User' }]
});
Populate を使用すると、ユーザーの友達のリストを取得できますが、ユーザーの友達の友達も取得したい場合はどうすればよいでしょうか?populate
オプションを指定して、ユーザーのすべての友達の friends
配列をポピュレートするように mongoose に指示します。
await User.
findOne({ name: 'Val' }).
populate({
path: 'friends',
// Get friends of friends - populate the 'friends' array for every friend
populate: { path: 'friends' }
});
データベース間のポピュレート
イベントを表すスキーマと、会話を表すスキーマがあるとします。各イベントには、対応する会話スレッドがあります。
const db1 = mongoose.createConnection('mongodb://127.0.0.1:27000/db1');
const db2 = mongoose.createConnection('mongodb://127.0.0.1:27001/db2');
const conversationSchema = new Schema({ numMessages: Number });
const Conversation = db2.model('Conversation', conversationSchema);
const eventSchema = new Schema({
name: String,
conversation: {
type: ObjectId,
ref: Conversation // `ref` is a **Model class**, not a string
}
});
const Event = db1.model('Event', eventSchema);
上記の例では、イベントと会話は別々の MongoDB データベースに保存されています。文字列 ref
はこの状況では機能しません。Mongoose は、文字列 ref
が同じ接続上のモデル名を参照すると想定しているためです。上記の例では、会話モデルは db1
ではなく db2
に登録されています。
// Works
const events = await Event.
find().
populate('conversation');
これは、「データベース間のポピュレート」と呼ばれ、MongoDB データベース間、さらには MongoDB インスタンス間でポピュレートできます。
eventSchema
を定義するときにモデルインスタンスにアクセスできない場合は、モデルインスタンスを populate()
のオプションとして渡す こともできます。
const events = await Event.
find().
// The `model` option specifies the model to use for populating.
populate({ path: 'conversation', model: Conversation });
refPath
による動的な参照
Mongoose は、ドキュメントのプロパティの値に基づいて、複数のコレクションからポピュレートすることもできます。コメントを保存するためのスキーマを作成しているとします。ユーザーは、ブログ投稿または製品のいずれかにコメントできます。
const commentSchema = new Schema({
body: { type: String, required: true },
doc: {
type: Schema.Types.ObjectId,
required: true,
// Instead of a hardcoded model name in `ref`, `refPath` means Mongoose
// will look at the `docModel` property to find the right model.
refPath: 'docModel'
},
docModel: {
type: String,
required: true,
enum: ['BlogPost', 'Product']
}
});
const Product = mongoose.model('Product', new Schema({ name: String }));
const BlogPost = mongoose.model('BlogPost', new Schema({ title: String }));
const Comment = mongoose.model('Comment', commentSchema);
refPath
オプションは、ref
のより洗練された代替手段です。ref
が文字列の場合、Mongoose は常に同じモデルをクエリして、ポピュレートされたサブドキュメントを見つけます。refPath
を使用すると、ドキュメントごとに Mongoose が使用するモデルを構成できます。
const book = await Product.create({ name: 'The Count of Monte Cristo' });
const post = await BlogPost.create({ title: 'Top 10 French Novels' });
const commentOnBook = await Comment.create({
body: 'Great read',
doc: book._id,
docModel: 'Product'
});
const commentOnPost = await Comment.create({
body: 'Very informative',
doc: post._id,
docModel: 'BlogPost'
});
// The below `populate()` works even though one comment references the
// 'Product' collection and the other references the 'BlogPost' collection.
const comments = await Comment.find().populate('doc').sort({ body: 1 });
comments[0].doc.name; // "The Count of Monte Cristo"
comments[1].doc.title; // "Top 10 French Novels"
別の方法として、commentSchema
に個別の blogPost
と product
プロパティを定義し、両方のプロパティで populate()
することができます。
const commentSchema = new Schema({
body: { type: String, required: true },
product: {
type: Schema.Types.ObjectId,
required: true,
ref: 'Product'
},
blogPost: {
type: Schema.Types.ObjectId,
required: true,
ref: 'BlogPost'
}
});
// ...
// The below `populate()` is equivalent to the `refPath` approach, you
// just need to make sure you `populate()` both `product` and `blogPost`.
const comments = await Comment.find().
populate('product').
populate('blogPost').
sort({ body: 1 });
comments[0].product.name; // "The Count of Monte Cristo"
comments[1].blogPost.title; // "Top 10 French Novels"
個別の blogPost
と product
プロパティを定義する方法は、この単純な例では機能します。ただし、ユーザーが記事やその他のコメントにもコメントできるようにする場合、スキーマにさらにプロパティを追加する必要があります。mongoose-autopopulate を使用しない限り、プロパティごとに追加の populate()
呼び出しが必要になります。refPath
を使用すると、commentSchema
がポイントできるモデルの数に関わらず、2つのスキーマパスと1つの populate()
呼び出しのみが必要になります。
refPath
に関数を割り当てることもできます。これは、ポピュレートされるドキュメントの値に応じて、Mongoose が refPath を選択することを意味します。
const commentSchema = new Schema({
body: { type: String, required: true },
commentType: {
type: String,
enum: ['comment', 'review']
},
entityId: {
type: Schema.Types.ObjectId,
required: true,
refPath: function () {
return this.commentType === 'review' ? this.reviewEntityModel : this.commentEntityModel; // 'this' refers to the document being populated
}
},
commentEntityModel: {
type: String,
required: true,
enum: ['BlogPost', 'Review']
},
reviewEntityModel: {
type: String,
required: true,
enum: ['Vendor', 'Product']
}
});
ref
による動的な参照
refPath
と同様に、ref
にも関数を割り当てることができます。
const commentSchema = new Schema({
body: { type: String, required: true },
verifiedBuyer: Boolean
doc: {
type: Schema.Types.ObjectId,
required: true,
ref: function() {
return this.verifiedBuyer ? 'Product' : 'BlogPost'; // 'this' refers to the document being populated
}
},
});
バーチャルのポピュレート
これまでは、_id
フィールドに基づいてのみポピュレートしていました。ただし、それは最適な選択ではない場合があります。たとえば、Author
と BlogPost
の2つのモデルがあるとします。
const AuthorSchema = new Schema({
name: String,
posts: [{ type: mongoose.Schema.Types.ObjectId, ref: 'BlogPost' }]
});
const BlogPostSchema = new Schema({
title: String,
comments: [{
author: { type: mongoose.Schema.Types.ObjectId, ref: 'Author' },
content: String
}]
});
const Author = mongoose.model('Author', AuthorSchema, 'Author');
const BlogPost = mongoose.model('BlogPost', BlogPostSchema, 'BlogPost');
上記は不適切なスキーマ設計の一例です。なぜでしょうか?非常に多作な著者で、1万件以上のブログ記事を書いていると仮定しましょう。そのauthor
ドキュメントは非常に大きくなり、12KBを超える可能性があります。大きなドキュメントは、サーバーとクライアントの両方でパフォーマンスの問題につながります。最小カーディナリティの原則では、著者とブログ記事のような一対多の関係は、「多」側に格納するべきだと述べられています。つまり、ブログ記事はauthor
を格納するべきであり、著者はすべてのposts
を格納するべきではありません。
const AuthorSchema = new Schema({
name: String
});
const BlogPostSchema = new Schema({
title: String,
author: { type: mongoose.Schema.Types.ObjectId, ref: 'Author' },
comments: [{
author: { type: mongoose.Schema.Types.ObjectId, ref: 'Author' },
content: String
}]
});
残念ながら、このように記述された2つのスキーマでは、著者のブログ記事リストをpopulateすることができません。そこで、仮想populateが登場します。仮想populateとは、以下に示すように、ref
オプションを持つ仮想プロパティに対してpopulate()
を呼び出すことを意味します。
// Specifying a virtual with a `ref` property is how you enable virtual
// population
AuthorSchema.virtual('posts', {
ref: 'BlogPost',
localField: '_id',
foreignField: 'author'
});
const Author = mongoose.model('Author', AuthorSchema, 'Author');
const BlogPost = mongoose.model('BlogPost', BlogPostSchema, 'BlogPost');
その後、以下のように著者のposts
をpopulate()
できます。
const author = await Author.findOne().populate('posts');
author.posts[0].title; // Title of the first blog post
virtualsは、デフォルトではtoJSON()
とtoObject()
の出力には含まれていないことに注意してください。Expressのres.json()
関数やconsole.log()
などの関数を使用する際に、populateされたvirtualsを表示させたい場合は、スキーマのtoJSON
とtoObject()
オプションにvirtuals: true
を設定します。
const authorSchema = new Schema({ name: String }, {
toJSON: { virtuals: true }, // So `res.json()` and other `JSON.stringify()` functions include virtuals
toObject: { virtuals: true } // So `console.log()` and other functions that use `toObject()` include virtuals
});
populate projectionを使用する場合は、projectionにforeignField
を含めるようにしてください。
let authors = await Author.
find({}).
// Won't work because the foreign field `author` is not selected
populate({ path: 'posts', select: 'title' }).
exec();
authors = await Author.
find({}).
// Works, foreign field `author` is selected
populate({ path: 'posts', select: 'title author' }).
exec();
バーチャルのポピュレート:Count オプション
Populate virtualsは、ドキュメント自体ではなく、一致するforeignField
を持つドキュメントの数をカウントすることもサポートしています。仮想プロパティにcount
オプションを設定します。
const PersonSchema = new Schema({
name: String,
band: String
});
const BandSchema = new Schema({
name: String
});
BandSchema.virtual('numMembers', {
ref: 'Person', // The model to use
localField: 'name', // Find people where `localField`
foreignField: 'band', // is equal to `foreignField`
count: true // And only get the number of docs
});
// Later
const doc = await Band.findOne({ name: 'Motley Crue' }).
populate('numMembers');
doc.numMembers; // 2
バーチャルのポピュレート:Match オプション
Populate virtualsのもう一つのオプションはmatch
です。このオプションは、Mongooseがpopulate()
するために使用するクエリに追加のフィルタ条件を追加します。
// Same example as 'Populate Virtuals' section
AuthorSchema.virtual('posts', {
ref: 'BlogPost',
localField: '_id',
foreignField: 'author',
match: { archived: false } // match option with basic query selector
});
const Author = mongoose.model('Author', AuthorSchema, 'Author');
const BlogPost = mongoose.model('BlogPost', BlogPostSchema, 'BlogPost');
// After population
const author = await Author.findOne().populate('posts');
author.posts; // Array of not `archived` posts
match
オプションを関数に設定することもできます。これにより、populateされるドキュメントに基づいてmatch
を構成できます。例えば、著者のfavoriteTags
のいずれかを含むtags
を持つブログ記事のみをpopulateしたいとします。
AuthorSchema.virtual('posts', {
ref: 'BlogPost',
localField: '_id',
foreignField: 'author',
// Add an additional filter `{ tags: author.favoriteTags }` to the populate query
// Mongoose calls the `match` function with the document being populated as the
// first argument.
match: author => ({ tags: author.favoriteTags })
});
populate()
を呼び出す際に、match
オプションを上書きすることができます。
// Overwrite the `match` option specified in `AuthorSchema.virtual()` for this
// single `populate()` call.
await Author.findOne().populate({ path: posts, match: {} });
populate()
呼び出しでmatch
オプションを関数に設定することもできます。populate()
のmatch
オプションを上書きするのではなくマージしたい場合は、以下を使用します。
await Author.findOne().populate({
path: posts,
// Add `isDeleted: false` to the virtual's default `match`, so the `match`
// option would be `{ tags: author.favoriteTags, isDeleted: false }`
match: (author, virtual) => ({
...virtual.options.match(author),
isDeleted: false
})
});
マップのポピュレート
マップは、任意の文字列キーを持つオブジェクトを表すタイプです。例えば、以下のスキーマでは、members
は文字列からObjectIdへのマップです。
const BandSchema = new Schema({
name: String,
members: {
type: Map,
of: {
type: 'ObjectId',
ref: 'Person'
}
}
});
const Band = mongoose.model('Band', bandSchema);
このマップにはref
があるので、マップ内のすべてのObjectIdをpopulate()
を使用してpopulateできます。以下のband
ドキュメントがあるとします。
const person1 = new Person({ name: 'Vince Neil' });
const person2 = new Person({ name: 'Mick Mars' });
const band = new Band({
name: 'Motley Crue',
members: {
singer: person1._id,
guitarist: person2._id
}
});
特別なパスmembers.$*
をpopulateすることで、マップ内のすべての要素をpopulate()
できます。$*
は、Mongooseにマップ内のすべてのキーを参照するように指示する特別な構文です。
const band = await Band.findOne({ name: 'Motley Crue' }).populate('members.$*');
band.members.get('singer'); // { _id: ..., name: 'Vince Neil' }
サブドキュメントのマップ内のパスも$*
を使用してpopulateできます。例えば、以下のlibrarySchema
があるとします。
const librarySchema = new Schema({
name: String,
books: {
type: Map,
of: new Schema({
title: String,
author: {
type: 'ObjectId',
ref: 'Person'
}
})
}
});
const Library = mongoose.model('Library', librarySchema);
books.$*.author
をpopulateすることで、すべての本の著者をpopulateできます。
const libraries = await Library.find().populate('books.$*.author');
ミドルウェアでのポピュレート
プリフックまたはポストフックのいずれかでpopulateできます。フック特定のフィールドを常にpopulateしたい場合は、mongoose-autopopulateプラグインを確認してください。
// Always attach `populate()` to `find()` calls
MySchema.pre('find', function() {
this.populate('user');
});
// Always `populate()` after `find()` calls. Useful if you want to selectively populate
// based on the docs found.
MySchema.post('find', async function(docs) {
for (const doc of docs) {
if (doc.isPublic) {
await doc.populate('user');
}
}
});
// `populate()` after saving. Useful for sending populated data back to the client in an
// update API endpoint
MySchema.post('save', function(doc, next) {
doc.populate('user').then(function() {
next();
});
});
ミドルウェアでの複数パスのポピュレート
ミドルウェアで複数のパスをpopulateすることは、常にいくつかのフィールドをpopulateしたい場合に役立ちますが、実装は思っているよりも少し複雑です。期待される動作は次のとおりです。
const userSchema = new Schema({
email: String,
password: String,
followers: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }],
following: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }]
});
userSchema.pre('find', function(next) {
this.populate('followers following');
next();
});
const User = mongoose.model('User', userSchema);
しかし、これは機能しません。デフォルトでは、ミドルウェアで複数のパスをpopulate()
に渡すと、無限再帰が発生します。つまり、基本的にpopulate()
メソッドに提供されたすべてのパスに対して同じミドルウェアがトリガーされます。例えば、this.populate('followers following')
は、followers
とfollowing
の両方のフィールドに対して同じミドルウェアをトリガーし、リクエストは無限ループでハングしたままになります。
これを回避するには、_recursed
オプションを追加して、ミドルウェアが再帰的にpopulateされないようにする必要があります。以下の例では、期待通りに動作します。
userSchema.pre('find', function(next) {
if (this.options._recursed) {
return next();
}
this.populate({ path: 'followers following', options: { _recursed: true } });
next();
});
あるいは、mongoose-autopopulateプラグインを確認することもできます。
ポピュレートされたドキュメントの変換
transform
オプションを使用して、populateされたドキュメントを操作できます。transform
関数を指定すると、Mongooseは結果内のすべてのpopulateされたドキュメントに対してこの関数を呼び出します。引数は2つあり、populateされたドキュメントと、ドキュメントをpopulateするために使用された元のIDです。これにより、populate()
の実行結果をより細かく制御できます。複数のドキュメントをpopulateする場合に特に便利です。
transform
オプションの最初の動機は、ドキュメントが見つからない場合に、値をnull
に設定する代わりに、populateされていない_id
を残す機能を提供することでした。
// With `transform`
doc = await Parent.findById(doc).populate([
{
path: 'child',
// If `doc` is null, use the original id instead
transform: (doc, id) => doc == null ? id : doc
}
]);
doc.child; // 634d1a5744efe65ae09142f9
doc.children; // [ 634d1a67ac15090a0ca6c0ea, { _id: 634d1a4ddb804d17d95d1c7f, name: 'Luke', __v: 0 } ]
transform()
から任意の値を返すことができます。例えば、以下のようにtransform()
を使用してpopulateされたドキュメントを「フラット化」できます。
let doc = await Parent.create({ children: [{ name: 'Luke' }, { name: 'Leia' }] });
doc = await Parent.findById(doc).populate([{
path: 'children',
transform: doc => doc == null ? null : doc.name
}]);
doc.children; // ['Luke', 'Leia']
transform()
のもう1つのユースケースは、ゲッターとvirtualsにパラメーターを渡すために、populateされたドキュメントに$locals
値を設定することです。例えば、国際化のためにドキュメントに言語コードを設定したいとします。
const internationalizedStringSchema = new Schema({
en: String,
es: String
});
const ingredientSchema = new Schema({
// Instead of setting `name` to just a string, set `name` to a map
// of language codes to strings.
name: {
type: internationalizedStringSchema,
// When you access `name`, pull the document's locale
get: function(value) {
return value[this.$locals.language || 'en'];
}
}
});
const recipeSchema = new Schema({
ingredients: [{ type: mongoose.ObjectId, ref: 'Ingredient' }]
});
const Ingredient = mongoose.model('Ingredient', ingredientSchema);
const Recipe = mongoose.model('Recipe', recipeSchema);
すべてのpopulateされた演習に言語コードを設定できます。
// Create some sample data
const { _id } = await Ingredient.create({
name: {
en: 'Eggs',
es: 'Huevos'
}
});
await Recipe.create({ ingredients: [_id] });
// Populate with setting `$locals.language` for internationalization
const language = 'es';
const recipes = await Recipe.find().populate({
path: 'ingredients',
transform: function(doc) {
doc.$locals.language = language;
return doc;
}
});
// Gets the ingredient's name in Spanish `name.es`
recipes[0].ingredients[0].name; // 'Huevos'