填充 (Populate)
MongoDB 3.2 之后,也有像 sql 里 join 的聚合操作,那就是 $lookup 而 Mongoose,拥有更强大的 populate()
,可以让你在别的 collection 中引用 document。
Population 可以自动替换 document 中的指定字段,替换内容从其他 collection 获取。 我们可以填充(populate)单个或多个 document、单个或多个纯对象,甚至是 query 返回的一切对象。 下面我们看看例子:
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var personSchema = Schema({
_id: Schema.Types.ObjectId,
name: String,
age: Number,
stories: [{ type: Schema.Types.ObjectId, ref: 'Story' }]
});
var storySchema = Schema({
author: { type: Schema.Types.ObjectId, ref: 'Person' },
title: String,
fans: [{ type: Schema.Types.ObjectId, ref: 'Person' }]
});
var Story = mongoose.model('Story', storySchema);
var Person = mongoose.model('Person', personSchema);
现在我们创建了两个 Model。Person
model 的 stories
字段设为 ObjectId
数组。 ref
选项告诉 Mongoose 在填充的时候使用哪个 model,本例中为 Story
model。 所有储存在此的 _id
都必须是 Story
model 中 document 的 _id
。
注意: ObjectId
、Number
、String
以及 Buffer
都可以作为 refs 使用。 但是最好还是使用 ObjectId
,除非你是进阶玩家,并且有充分理由使用其他类型 作为 refs。
保存 refs
保存 refs 与保存普通属性一样,把 _id
的值赋给它就好了:
var author = new Person({
_id: new mongoose.Types.ObjectId(),
name: 'Ian Fleming',
age: 50
});
author.save(function (err) {
if (err) return handleError(err);
var story1 = new Story({
title: 'Casino Royale',
author: author._id // assign the _id from the person
});
story1.save(function (err) {
if (err) return handleError(err);
// thats it!
});
});
Population
至此我们做的东西还是跟平常差不多,只是创建了 Person
和 Story
。 现在我们试试对 query 填充 story 的 author
: So far we haven't done anything much different. We've merely created a Person
and a Story
. Now let's take a look at populating our story's author
using the query builder:
Story.
findOne({ title: 'Casino Royale' }).
populate('author').
exec(function (err, story) {
if (err) return handleError(err);
console.log('The author is %s', story.author.name);
// prints "The author is Ian Fleming"
});
被填充的字段已经不是原来的 _id
,而是被指定的 document 代替,这个 document 由另一条 query 从数据库返回。
refs 数组的原理也与此相似。对 query 对象调用 populate 方法, 就能返回装载对应 _id
的 document 数组。
设置被填充字段
Mongoose 4.0 之后,你可以手动填充一个字段。
Story.findOne({ title: 'Casino Royale' }, function(error, story) {
if (error) {
return handleError(error);
}
story.author = author;
console.log(story.author.name); // prints "Ian Fleming"
});
字段选择
如果我们只需要填充的 document 其中一部分字段怎么办? 第二参数传入 field name syntax 就能实现。
Story.
findOne({ title: /casino royale/i }).
populate('author', 'name'). // only return the Persons name
exec(function (err, story) {
if (err) return handleError(err);
console.log('The author is %s', story.author.name);
// prints "The author is Ian Fleming"
console.log('The authors age is %s', story.author.age);
// prints "The authors age is null'
});
填充多个字段
要一次填充多个字段怎么办?
Story.
find(...).
populate('fans').
populate('author').
exec();
如果对同一路径 populate()
两次,只有最后一次生效。
// 第二个 `populate()` 覆盖了第一个,因为它们都填充 fans
Story.
find().
populate({ path: 'fans', select: 'name' }).
populate({ path: 'fans', select: 'email' });
// The above is equivalent to:
Story.find().populate({ path: 'fans', select: 'email' });
Query 条件与其他选项
如果要根据年龄来填充,只填充 name,并且,只返回最多 5 个数据,怎么做?
Story.
find(...).
populate({
path: 'fans',
match: { age: { $gte: 21 }},
// Explicitly exclude `_id`, see http://bit.ly/2aEfTdB
select: 'name -_id',
options: { limit: 5 }
}).
exec();
Refs 到 children
然而我们发现,用 author
对象没办法获取 story 列表,因为 author.stories
没有被 'pushed' 任何 story
对象。
于此有两方面,首先,我们希望 author
知道哪些 story 属于他们。通常, 你的 schema 应该通过在“多”的一方使用指向它们的父节点(parent pointer)解决一对多关系问题。 另一方面,如果你有充分理由得到指向子节点(child pointer)的数组, 你可以像下面代码一样把 document push()
到数组中。
author.stories.push(story1);
author.save(callback);
然后我们就能 find
和 populate
了:
Person.
findOne({ name: 'Ian Fleming' }).
populate('stories'). // only works if we pushed refs to children
exec(function (err, person) {
if (err) return handleError(err);
console.log(person);
});
如果父子节点互相指向,数据可能会在某一时刻失去同步。 为此我们可以不使用填充,直接 find()
我们需要的 story。
Story.
find({ author: author._id }).
exec(function (err, stories) {
if (err) return handleError(err);
console.log('The stories are an array: ', stories);
});
query 填充后返回的 document 功能齐全, 除非设置了 lean 选项,否则它就是可 remove
,可 save
的。 不要把它们和 sub docs 弄混了, 调用 remove 方法要小心,因为这样不只是从数组删除,还会从数据库删除它们。
填充现有 document
现有 document 同样可以被填充,mongoose 3.6 之后支持了 document#populate() 方法。
填充多个现有 document
如果要填充一个或多个 document 或是(像 mapReduce 输出的)对象, 我们可以使用 Model.populate() 方法,此方法适用版本同样需要大于 mongoose 3.6。 document#populate()
和 query#populate()
也是使用这个方法填充 document。
多级填充
假设 user schema 记录了 user 的 friends。
var userSchema = new Schema({
name: String,
friends: [{ type: ObjectId, ref: 'User' }]
});
你当然可以填充得到用户的 friends 列表,但是如果要再获得他们朋友的朋友呢? 指定 populate
选项就可以了:
User.
findOne({ name: 'Val' }).
populate({
path: 'friends',
// Get friends of friends - populate the 'friends' array for every friend
populate: { path: 'friends' }
});
跨数据库填充
假设现在有 event schema 和 conversation schema,每个 event 对应一个 conversation 线程。
var eventSchema = new Schema({
name: String,
// The id of the corresponding conversation
// 注意,这里没有 ref!
conversation: ObjectId
});
var conversationSchema = new Schema({
numMessages: Number
});
并且,event 和 conversation 保存在不同 MongoDB 实例。
var db1 = mongoose.createConnection('localhost:27000/db1');
var db2 = mongoose.createConnection('localhost:27001/db2');
var Event = db1.model('Event', eventSchema);
var Conversation = db2.model('Conversation', conversationSchema);
这个情况就不能直接使用 populate()
了,因为 populate()
不知道应该使用什么填充。 不过你可以显式指定一个 model。
Event.
find().
populate({ path: 'conversation', model: Conversation }).
exec(function(error, docs) { /* ... */ });
这就是“跨数据库填充”,这可以让你跨 mongodb 数据库甚至是跨 mongodb 实例填充。
动态引用
Mongoose 也可以同时从多个 collection 填充。 假设 user schema 有一系列 connection, 一个 user 可以连接到其他 user 或组织。
var userSchema = new Schema({
name: String,
connections: [{
kind: String,
item: { type: ObjectId, refPath: 'connections.kind' }
}]
});
var organizationSchema = new Schema({ name: String, kind: String });
var User = mongoose.model('User', userSchema);
var Organization = mongoose.model('Organization', organizationSchema);
上面的 refPath
属性意味着 mongoose 会查找 connections.kind
路径, 以此确定 populate()
使用的 model。换句话说,refPath
属性可以让你动态寻找 ref
。
// Say we have one organization:
// `{ _id: ObjectId('000000000000000000000001'), name: "Guns N' Roses", kind: 'Band' }`
// And two users:
// {
// _id: ObjectId('000000000000000000000002')
// name: 'Axl Rose',
// connections: [
// { kind: 'User', item: ObjectId('000000000000000000000003') },
// { kind: 'Organization', item: ObjectId('000000000000000000000001') }
// ]
// },
// {
// _id: ObjectId('000000000000000000000003')
// name: 'Slash',
// connections: []
// }
User.
findOne({ name: 'Axl Rose' }).
populate('connections.item').
exec(function(error, doc) {
// doc.connections[0].item is a User doc
// doc.connections[1].item is an Organization doc
});
虚拟值填充
4.5.0 新功能
目前为止你只能以 _id
为基础进行填充,然而经常会造成不便。 特别地, So far you've only populated based on the _id
field. However, that's sometimes not the right choice. In particular, arrays that grow without bound are a MongoDB anti-pattern. Using mongoose virtuals, you can define more sophisticated relationships between documents.
var PersonSchema = new Schema({
name: String,
band: String
});
var BandSchema = new Schema({
name: String
});
BandSchema.virtual('members', {
ref: 'Person', // The model to use
localField: 'name', // Find people where `localField`
foreignField: 'band', // is equal to `foreignField`
// If `justOne` is true, 'members' will be a single doc as opposed to
// an array. `justOne` is false by default.
justOne: false
});
var Person = mongoose.model('Person', PersonSchema);
var Band = mongoose.model('Band', BandSchema);
/**
* Suppose you have 2 bands: "Guns N' Roses" and "Motley Crue"
* And 4 people: "Axl Rose" and "Slash" with "Guns N' Roses", and
* "Vince Neil" and "Nikki Sixx" with "Motley Crue"
*/
Band.find({}).populate('members').exec(function(error, bands) {
/* `bands.members` is now an array of instances of `Person` */
});
要记得虚拟值默认不会被 toJSON()
输出。如果你需要填充的虚拟值显式在依赖 JSON.stringify()
的函数 (例如 Express 的 res.json()
function)中打印, 需要在 toJSON
中设置 virtuals: true
选项。
// Set `virtuals: true` so `res.json()` works
var BandSchema = new Schema({
name: String
}, { toJSON: { virtuals: true } });
如果你使用了填充保护,要确保 select 中包含了 foreignField
。
Band.
find({}).
populate({ path: 'members', select: 'name' }).
exec(function(error, bands) {
// Won't work, foreign field `band` is not selected in the projection
});
Band.
find({}).
populate({ path: 'members', select: 'name band' }).
exec(function(error, bands) {
// Works, foreign field `band` is selected
});
在中间件中使用填充
你可以在 pre 或 post 钩子中使用填充。 如果你总是需要填充某一字段,可以了解一下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 (let doc of docs) {
if (doc.isPublic) {
await doc.populate('user').execPopulate();
}
}
});
// `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').execPopulate(function() {
next();
});
});
下一步
现在我们介绍了 populate()
,接着看看 discriminators。