填充

MongoDB 在版本 >= 3.2 中具有类似联接的 $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);

到目前为止,我们已经创建了两个 模型。我们的 Person 模型的 stories 字段被设置为 ObjectId 数组。ref 选项告诉 Mongoose 在填充过程中使用哪个模型,在本例中是 Story 模型。我们存储在这里的所有 _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!

您可以在 ObjectIdNumberStringBuffer 路径上设置 ref 选项。populate() 支持 ObjectIds、数字、字符串和缓冲区。但是,我们建议使用 ObjectIds 作为 _id 属性(以及 ref 属性的 ObjectIds),除非您有充分的理由不这样做。这是因为 MongoDB 会在您创建没有 _id 属性的新文档时将 _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'

如果您推送非 POJO 和非文档值(如 ObjectId),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 getter,因此您可以使用 story.author._id,无论 author 是否已填充。

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 填充不像传统的 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 方法的第二个参数来实现

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' });

查询条件和其他选项

如果我们想根据粉丝的年龄填充粉丝数组,并只选择他们的姓名呢?

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,并且 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

如果您想根据作者姓名过滤故事,您应该使用 反规范化

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(),您会发现第二个故事有 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 对象,我们可能无法获得故事列表。这是因为从未将任何 story 对象‘推送’到 author.stories 中。

这里有两个观点。首先,您可能希望 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);

我们可以争论说,我们是否真的想要两组指针,因为它们可能会不同步。相反,我们可以跳过填充并直接 find() 我们感兴趣的故事。

const stories = await Story.
  find({ author: author._id }).
  exec();
console.log('The stories are an array: ', stories);

查询填充 返回的文档将变为完全可用的、可 remove 的、可 save 的文档,除非指定了 lean 选项。不要将它们与 子文档 混淆。在调用其 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

填充多个现有文档

如果我们有一个或多个 mongoose 文档,甚至是普通对象(mapReduce 输出),我们可以使用 Model.populate() 方法填充它们。这就是 Document#populate()Query#populate() 用于填充文档的方式。

跨多个级别填充

假设您有一个用于跟踪用户朋友的用户模式。

const userSchema = new Schema({
  name: String,
  friends: [{ type: ObjectId, ref: 'User' }]
});

Populate 允许您获取用户的友列表,但是如果您还想获取用户的友的朋友列表呢?指定 populate 选项告诉 mongoose 填充所有用户朋友的 friends 数组

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 指的是同一个连接上的模型名称。在上面的示例中,对话模型是在 db2 上注册的,而不是 db1

// 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 属性是可以的。但是,如果您决定允许用户评论文章或其他评论,您需要在模式中添加更多属性。您还需要对每个属性进行额外的 populate() 调用,除非您使用 mongoose-autopopulate。使用 refPath 意味着无论您的 commentSchema 可以指向多少个模型,您只需要 2 个模式路径和一个 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`。

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');

以上是一个糟糕的模式设计示例。为什么?假设你有一个非常多产的作者,他写了超过 10000 篇博客文章。该 `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
  }]
});

不幸的是,这两个模式按原样编写不支持填充作者的博客文章列表。这就是虚拟填充的用武之地。虚拟填充意味着在具有 `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`。

const author = await Author.findOne().populate('posts');

author.posts[0].title; // Title of the first blog post

请记住,默认情况下,虚拟属性不会包含在 `toJSON()` 和 `toObject()` 输出中。如果你希望在使用像 Express 的 `res.json()` 函数 或 `console.log()` 这样的函数时显示填充的虚拟属性,请在你的模式的 `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
});

如果你使用填充投影,请确保 `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();

填充虚拟字段:计数选项

填充虚拟属性也支持统计具有匹配 `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`。此选项向 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` 选项设置为函数。这样可以根据正在填充的文档配置 `match`。例如,假设你只想填充 `tags` 包含作者 `favoriteTags` 之一的博客文章。

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`,这意味着你可以使用 `populate()` 来填充映射中的所有 ObjectId。假设你拥有以下 `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.$*` 来填充映射中的每个元素。`$*` 是一种特殊语法,它告诉 Mongoose 查看映射中的每个键。

const band = await Band.findOne({ name: 'Motley Crue' }).populate('members.$*');

band.members.get('singer'); // { _id: ..., name: 'Vince Neil' }

你也可以使用 `$*` 填充子文档映射中的路径。例如,假设你拥有以下 `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` 来填充每本书的作者。

const libraries = await Library.find().populate('books.$*.author');

在中间件中填充

你可以在 钩子 的前或后进行填充。如果你希望始终填充某个字段,请查看 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();
  });
});

在中间件中填充多个路径

在中间件中填充多个路径在始终希望填充某些字段时很有用。但是,实现比你想象的要稍微复杂一些。以下是你的预期工作方式。

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` 选项,以便我们的中间件避免递归填充。以下示例将使其按预期工作。

userSchema.pre('find', function(next) {
  if (this.options._recursed) {
    return next();
  }
  this.populate({ path: 'followers following', options: { _recursed: true } });
  next();
});

或者,你可以查看 mongoose-autopopulate 插件

转换填充的文档

你可以使用 `transform` 选项操作填充的文档。如果你指定了 `transform` 函数,Mongoose 将使用两个参数(填充的文档和用于填充文档的原始 id)在结果中的每个填充文档上调用此函数。这让你可以更好地控制 `populate()` 执行的结果。当你填充多个文档时,它特别有用。

`transform` 选项的 最初动机 是提供如果未找到文档则保留未填充的 `_id` 的功能,而不是将值设置为 `null`。

// 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()` 来“扁平化”填充的文档,如下所示。

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()` 的另一个用例是在填充的文档上设置 `$locals` 值以将参数传递给 getter 和虚拟属性。例如,假设你希望为国际化目的在你的文档上设置语言代码,如下所示。

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);

你可以像下面这样在所有填充的练习上设置语言代码。

// 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'