ポピュレート

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 でなければなりません。

参照の保存

他のドキュメントへの参照の保存は、通常プロパティを保存する場合と同じ方法で行います。_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 オプションは、ObjectIdNumberString、および Buffer パスに設定できます。populate() は、ObjectId、数値、文字列、およびバッファで動作します。ただし、特に理由がない限り、_id プロパティ(したがって ref プロパティ)には ObjectId を使用することをお勧めします。これは、_id プロパティを持たずに新しいドキュメントを作成した場合、MongoDB が _id を ObjectId に設定するためです。そのため、_id プロパティを数値にすると、数値の _id を持たないドキュメントを挿入しないように特に注意する必要があります。

ポピュレーション

これまでのところ、特に異なることをしていません。単に PersonStory を作成しただけです。それでは、クエリビルダーを使用してストーリーの 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.authornull になります。これは、SQL の 左結合 に類似しています。

await Person.deleteMany({ name: 'Ian Fleming' });

const story = await Story.findOne({ title: 'Casino Royale' }).populate('author');
story.author; // `null`

storySchemaauthors の配列がある場合、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 ドキュメントが取得されます。

たとえば、ストーリーの authorpopulate() し、authormatch を満たさない場合、ストーリーの authornull になります。

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

著者の名前でストーリーをフィルタリングする場合は、非正規化 を使用してください。

limitperDocumentLimit

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.storiesstory オブジェクトがプッシュされたことがないためです。

ここには2つの視点があります。まず、author に自分のストーリーを認識させたい場合があります。通常、スキーマでは、'many' 側にある親ポインタを持つことで、一対多のリレーションシップを解決する必要があります。ただし、子のポインタの配列が必要な正当な理由がある場合は、以下のように配列にドキュメントをプッシュできます。

await story1.save();

author.stories.push(story1);
await author.save();

これにより、findpopulate の組み合わせを実行できます。

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 に個別の blogPostproduct プロパティを定義し、両方のプロパティで 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"

個別の blogPostproduct プロパティを定義する方法は、この単純な例では機能します。ただし、ユーザーが記事やその他のコメントにもコメントできるようにする場合、スキーマにさらにプロパティを追加する必要があります。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 フィールドに基づいてのみポピュレートしていました。ただし、それは最適な選択ではない場合があります。たとえば、AuthorBlogPost の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');

その後、以下のように著者のpostspopulate()できます。

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を表示させたい場合は、スキーマのtoJSONtoObject()オプションに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')は、followersfollowingの両方のフィールドに対して同じミドルウェアをトリガーし、リクエストは無限ループでハングしたままになります。

これを回避するには、_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'