填充 (Populate)
在版本 >= 3.2 中,MongoDB 有類似 join 的 $lookup 聚合運算符。Mongoose 有一個更強大的替代方案,稱為 populate()
,它允許您參考其他集合中的文件。
填充 (Population) 是將文件中指定的路徑自動替換為來自其他集合的文件之過程。我們可以填充單個文件、多個文件、普通物件、多個普通物件,或從查詢返回的所有物件。讓我們看一些範例。
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
。
- 儲存參考 (Saving Refs)
- 填充 (Population)
- 檢查欄位是否已填充
- 設定已填充的欄位
- 如果沒有外部文件會怎樣?
- 欄位選擇
- 填充多個路徑
- 查詢條件和其他選項
limit
與perDocumentLimit
- 子項的參考
- 填充現有文件
- 填充多個現有文件
- 跨多個層級填充
- 跨資料庫填充
- 透過
refPath
的動態參考 - 透過
ref
的動態參考 - 填充虛擬屬性 (Virtuals)
- 填充虛擬屬性:計數選項
- 填充虛擬屬性:比對選項
- 填充映射 (Maps)
- 在中介層中填充
- 在中介層中填充多個路徑
- 轉換已填充的文件
儲存參考
儲存對其他文件的參考與您正常儲存屬性的方式相同,只需指派 _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
屬性 (因此 ObjectIds 用於 ref
屬性)。這是因為如果您建立沒有 _id
屬性的新文件,MongoDB 會將 _id
設定為 ObjectId,因此如果您將 _id
屬性設定為數字,則需要格外小心,避免插入沒有數字 _id
的文件。
填充 (Population)
到目前為止,我們沒有做太多不同的事情。我們僅僅建立了 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 推送到已填充的陣列上,如果它們的 ref
符合,Mongoose 會新增這些文件。
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,因此無論 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 填充的行為不像傳統的 SQL join。當沒有文件時,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
文件。
例如,假設您 populate()
一個故事的 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
知道哪些故事是他們的。通常,您的結構描述應透過在「多」方中具有父指標來解析一對多關係。但是,如果您有充分的理由需要子指標陣列,您可以將文件 push()
到陣列上,如下所示。
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);
除非指定了 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
填充多個現有文件
如果我們有一個或多個 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 也可以根據文件中屬性的值,從多個集合中進行填充 (populate)。假設您正在建立一個用於儲存評論的 schema。使用者可能會評論一篇部落格文章或一個產品。
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
屬性適用於這個簡單的範例。但是,如果您決定允許使用者也評論文章或其他評論,您將需要在 schema 中新增更多屬性。您還需要為每個屬性額外呼叫 populate()
,除非您使用 mongoose-autopopulate。使用 refPath
意味著您只需要 2 個 schema 路徑和一個 populate()
呼叫,無論您的 commentSchema
可以指向多少個模型。
您也可以將函式指派給 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
}
},
});
填充虛擬屬性 (Virtuals)
到目前為止,您只根據 _id
欄位進行填充。然而,有時這並不是正確的選擇。例如,假設您有 2 個模型: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');
以上是一個糟糕的 schema 設計範例。為什麼呢?假設您有一位多產的作者,撰寫了超過 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
}]
});
不幸的是,這兩個 schema 按照目前的方式撰寫,不支援填充作者的部落格文章列表。這就是虛擬填充的用武之地。虛擬填充是指在具有 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()
等函式時顯示填充的虛擬屬性,請在 schema 的 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
});
如果您正在使用填充的投影 (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();
填充虛擬屬性:計數選項
填充的虛擬屬性也支援計算具有匹配 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
})
});
填充映射 (Maps)
Maps 是一種代表具有任意字串鍵的物件的類型。例如,在下面的 schema 中,members
是一個從字串到 ObjectIds 的映射。
const BandSchema = new Schema({
name: String,
members: {
type: Map,
of: {
type: 'ObjectId',
ref: 'Person'
}
}
});
const Band = mongoose.model('Band', bandSchema);
此映射具有 ref
,這表示您可以使用 populate()
來填充映射中的所有 ObjectIds。假設您有以下的 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()
映射中的每個元素。$*
是一種特殊的語法,告訴 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
來 populate()
每本書的作者
const libraries = await Library.find().populate('books.$*.author');
在中介層中填充
您可以在 pre 或 post hook 中進行填充。如果您想要始終填充某個欄位,請查看 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
值,以將參數傳遞給 getters 和虛擬屬性。例如,假設您想要在您的文件中設定一個用於國際化的語言代碼,如下所示。
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'