填充
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
。
- 保存引用
- 填充
- 检查字段是否已填充
- 设置填充字段
- 如果没有外键文档怎么办?
- 字段选择
- 填充多个路径
- 查询条件和其他选项
limit
与perDocumentLimit
- 引用子项
- 填充现有文档
- 填充多个现有文档
- 跨多个级别填充
- 跨数据库填充
- 通过
refPath
使用动态引用 - 通过
ref
使用动态引用 - 填充虚拟字段
- 填充虚拟字段:计数选项
- 填充虚拟字段:匹配选项
- 填充映射
- 在中间件中填充
- 在中间件中填充多个路径
- 转换填充的文档
保存引用
保存对其他文档的引用与通常保存属性的方式相同,只需分配 _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!
您可以在 ObjectId
、Number
、String
和 Buffer
路径上设置 ref
选项。populate()
支持 ObjectIds、数字、字符串和缓冲区。但是,我们建议使用 ObjectIds 作为 _id
属性(以及 ref
属性的 ObjectIds),除非您有充分的理由不这样做。这是因为 MongoDB 会在您创建没有 _id
属性的新文档时将 _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'
如果您推送非 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
如果您想根据作者姓名过滤故事,您应该使用 反规范化。
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()
,您会发现第二个故事有 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();
这使我们能够执行 find
和 populate
组合
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
上定义单独的 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
属性是可以的。但是,如果您决定允许用户评论文章或其他评论,您需要在模式中添加更多属性。您还需要对每个属性进行额外的 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'