在mongoose中没有join但有时我们仍然想引用其他collection的document,population由此而来。
population是自动将document中指定path替换为其他collection的document的过程。我们能迁移document、多个document、简单对象、多个简单对象或者是查询返回的所有对象。
var mongoose = require('mongoose') , Schema = mongoose.Schema var personSchema = Schema({ _id : Number, name : String, age : Number, stories : [{ type: Schema.Types.ObjectId, ref: 'Story' }] }); var storySchema = Schema({ _creator : { type: Number, ref: 'Person' }, title : String, fans : [{ type: Number, ref: 'Person' }] }); var Story = mongoose.model('Story', storySchema); var Person = mongoose.model('Person', personSchema);
到目前为止我们已经创建了2个Model。Person model的stories域已经设置到一个ObjectIds数组。ref选项告诉mongoose在population的时候使用哪个Model,在我们的例子是Story model。我们存储在这里的所有 _id 必须是来自 Story Model 的document _id。我们同时还声明了 Story _creator 属性为Number,和在personSchema里的_id的类型一样。匹配_id和ref的类型非常重要。
注意: ObjectId, Number, String, 和 Buffer也可以用来作为 ref。
保存对于其他document的ref的工作方式和保存属性相同,只是分配_id值。
var aaron = new Person({ _id: 0, name: 'Aaron', age: 100 }); aaron.save(function (err) { if (err) return handleError(err); var story1 = new Story({ title: "Once upon a timex.", _creator: aaron._id // assign the _id from the person }); story1.save(function (err) { if (err) return handleError(err); // thats it! }); });
到目前为止,我们还没有做什么太大的不同。我们只是创造了一个Person,一个Story。现在,让我们来看看使用查询生成器填充我们的story’s_creator:
Story .findOne({ title: 'Once upon a timex.' }) .populate('_creator') .exec(function (err, story) { if (err) return handleError(err); console.log('The creator is %s', story._creator.name); // prints "The creator is Aaron" });
填充的路径不再设置到原来的_id,在返回结果之前通过单独查询它们的值将会被从数据库返回的mongoose document取代。
ref数组的工作方式相同。在query上调用populate方法就会返回一个document数组取代原来的_ids。
mongoose >= 3.6 exposes the original _ids used during population through the document#populated() method.
在Mongoose >= 4.0,你也可以手动填充一个域。
Story.findOne({ title: 'Once upon a timex.' }, function(error, story) { if (error) { return handleError(error); } story._creator = aaron; console.log(story._creator.name); // prints "Aaron" });
注意这只对单个ref有效。目前不能手动填充ref数组。
如果我们只想要一些填充document的特定的字段?这能通过传递一般的字段名语法作为第二个参数给populate方法来完成。
Story .findOne({ title: /timex/i }) .populate('_creator', 'name') // only return the Persons name .exec(function (err, story) { if (err) return handleError(err); console.log('The creator is %s', story._creator.name); // prints "The creator is Aaron" console.log('The creators age is %s', story._creator.age); // prints "The creators age is null' })
如果我们想同时填充多个path怎么办?
Story .find(...) .populate('fans _creator') // 用空格分隔 path 名 .exec()
在 mongoose >= 3.6,我们能传递一个由空格分隔的字符串组成的path名给populate。在3.6之前你必须多次执行populate() 方法。
Story .find(...) .populate('fans') .populate('_creator') .exec()
如果我们想根据年龄填充fans数组,只选择他们的名字并返回最多5个人呢?
Story .find(...) .populate({ path: 'fans', match: { age: { $gte: 21 }}, select: 'name -_id', options: { limit: 5 } }) .exec()
如果我们使用aaron对象,我们不能获得stories列表。这是因为还没有story对象被‘压栈’到 aaron.stories。
这里有两种观点。首先,让aaron知道哪个stories是他的就好了。
aaron.stories.push(story1);
aaron.save(callback);
这使我们能够执行一个查找和填充组合:
Person .findOne({ name: 'Aaron' }) .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({ _creator: aaron._id }) .exec(function (err, stories) { if (err) return handleError(err); console.log('The stories are an array: ', stories); })
现在,我们有一个story的_creator不正确。我们可以就像其它mongoose属性设置一样修改refs:
var guille = new Person({ name: 'Guillermo' }); guille.save(function (err) { if (err) return handleError(err); story._creator = guille; console.log(story._creator.name); // prints "Guillermo" in mongoose >= 3.6 // see https://github.com/Automattic/mongoose/wiki/3.6-release-notes story.save(function (err) { if (err) return handleError(err); Story .findOne({ title: /timex/i }) .populate({ path: '_creator', select: 'name' }) .exec(function (err, story) { if (err) return handleError(err); console.log('The creator is %s', story._creator.name) // prints "The creator is Guillermo" }) }) })
从query population返回的document变为功能齐全,可删除的、可保存的document,除非lean选项被指定。要将它们和子文档搞混了。当调用它的删除方法时要小心,因为你将从数据库中删除它,而不仅仅是数组。
如果我们有一个现有的mongoose document并想填充它的一些path,mongoose >= 3.6 支持document#populate()方法。
如果我们有一个或多个文件,甚至是简单的对象(像mapReduce 输出),我们可以在 mongoose >= 3.6 使用Model.populate()方法来填充它们。document#populate() 和 query#populate() 就是用这个方法更新document。
你有一个记录着用户好友的user schema。
var userSchema = new Schema({ name: String, friends: [{ type: ObjectId, ref: 'User' }] });
填充让你获得用户的好友列表,但如果你想要用户的朋友的朋友呢?指定populate选项来告诉mongoose用用户所有的朋友来填充friend数组:
User. findOne({ name: 'Val' }). populate({ path: 'friends', // Get friends of friends - populate the 'friends' array for every friend populate: { path: 'friends' } });
比如说你有一个代表event的shema和一个代表conversation的shema。每一个event都有一个对应的conversation线程。
var eventSchema = new Schema({ name: String, // The id of the corresponding conversation // Notice there's no ref here! 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,conversation 域永远是空。可是,你能确指定该mode。
Event. find(). populate({ path: 'conversation', model: Conversation }). exec(function(error, docs) { /* ... */ });
这可被称为“跨数据库填充”,因为它使你能够通过MongoDB数据库甚至是通过MongoDB实例填充。