Populate(填充) - 图1mongoose

Populate(填充) - 图2mongoose

Populate(填充)

MongoDB 3.2 之后,也有像 sql 里 join 的聚合操作,那就是 $lookup 而 Mongoose,拥有更强大的 populate(),可以让你在别的 collection 中引用 document。

Population 可以自动替换 document 中的指定字段,替换内容从其他 collection 获取。 我们可以填充(populate)单个或多个 document、单个或多个纯对象,甚至是 query 返回的一切对象。 下面我们看看例子:

  1. var mongoose = require('mongoose');
  2. var Schema = mongoose.Schema;
  3. var personSchema = Schema({
  4. _id: Schema.Types.ObjectId,
  5. name: String,
  6. age: Number,
  7. stories: [{ type: Schema.Types.ObjectId, ref: 'Story' }]
  8. });
  9. var storySchema = Schema({
  10. author: { type: Schema.Types.ObjectId, ref: 'Person' },
  11. title: String,
  12. fans: [{ type: Schema.Types.ObjectId, ref: 'Person' }]
  13. });
  14. var Story = mongoose.model('Story', storySchema);
  15. var Person = mongoose.model('Person', personSchema);

现在我们创建了两个 ModelPerson model 的 stories 字段设为 ObjectId数组。 ref 选项告诉 Mongoose 在填充的时候使用哪个 model,本例中为 Story model。 所有储存在此的 _id 都必须是 Story model 中 document 的 _id

注意: ObjectIdNumberString 以及 Buffer 都可以作为 refs 使用。 但是最好还是使用 ObjectId,除非你是进阶玩家,并且有充分理由使用其他类型 作为 refs。

保存 refs

保存 refs 与保存普通属性一样,把 _id 的值赋给它就好了:

  1. var author = new Person({
  2. _id: new mongoose.Types.ObjectId(),
  3. name: 'Ian Fleming',
  4. age: 50
  5. });
  6. author.save(function (err) {
  7. if (err) return handleError(err);
  8. var story1 = new Story({
  9. title: 'Casino Royale',
  10. author: author._id // assign the _id from the person
  11. });
  12. story1.save(function (err) {
  13. if (err) return handleError(err);
  14. // thats it!
  15. });
  16. });

Population

至此我们做的东西还是跟平常差不多,只是创建了 PersonStory。 现在我们试试对 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:

  1. Story.
  2. findOne({ title: 'Casino Royale' }).
  3. populate('author').
  4. exec(function (err, story) {
  5. if (err) return handleError(err);
  6. console.log('The author is %s', story.author.name);
  7. // prints "The author is Ian Fleming"
  8. });

被填充的字段已经不是原来的 _id,而是被指定的 document 代替,这个 document 由另一条 query 从数据库返回。

refs 数组的原理也与此相似。对 query 对象调用 populate 方法, 就能返回装载对应 _id 的 document 数组。

设置被填充字段

Mongoose 4.0 之后,你可以手动填充一个字段。

  1. Story.findOne({ title: 'Casino Royale' }, function(error, story) {
  2. if (error) {
  3. return handleError(error);
  4. }
  5. story.author = author;
  6. console.log(story.author.name); // prints "Ian Fleming"
  7. });

字段选择

如果我们只需要填充的 document 其中一部分字段怎么办? 第二参数传入 field name syntax 就能实现。

  1. Story.
  2. findOne({ title: /casino royale/i }).
  3. populate('author', 'name'). // only return the Persons name
  4. exec(function (err, story) {
  5. if (err) return handleError(err);
  6. console.log('The author is %s', story.author.name);
  7. // prints "The author is Ian Fleming"
  8. console.log('The authors age is %s', story.author.age);
  9. // prints "The authors age is null'
  10. });

填充多个字段

要一次填充多个字段怎么办?

  1. Story.
  2. find(...).
  3. populate('fans').
  4. populate('author').
  5. exec();

如果对同一路径 populate() 两次,只有最后一次生效。

  1. // 第二个 `populate()` 覆盖了第一个,因为它们都填充 fans
  2. Story.
  3. find().
  4. populate({ path: 'fans', select: 'name' }).
  5. populate({ path: 'fans', select: 'email' });
  6. // The above is equivalent to:
  7. Story.find().populate({ path: 'fans', select: 'email' });

Query 条件与其他选项

如果要根据年龄来填充,只填充 name,并且,只返回最多 5 个数据,怎么做?

  1. Story.
  2. find(...).
  3. populate({
  4. path: 'fans',
  5. match: { age: { $gte: 21 }},
  6. // Explicitly exclude `_id`, see http://bit.ly/2aEfTdB
  7. select: 'name -_id',
  8. options: { limit: 5 }
  9. }).
  10. exec();

Refs 到 children

然而我们发现,用 author 对象没办法获取 story 列表,因为 author.stories 没有被 'pushed' 任何 story 对象。

于此有两方面,首先,我们希望 author 知道哪些 story 属于他们。通常, 你的 schema 应该通过在“多”的一方使用指向它们的父节点(parent pointer)解决一对多关系问题。 另一方面,如果你有充分理由得到指向子节点(child pointer)的数组, 你可以像下面代码一样把 document push() 到数组中。

  1. author.stories.push(story1);
  2. author.save(callback);

然后我们就能 findpopulate 了:

  1. Person.
  2. findOne({ name: 'Ian Fleming' }).
  3. populate('stories'). // only works if we pushed refs to children
  4. exec(function (err, person) {
  5. if (err) return handleError(err);
  6. console.log(person);
  7. });

如果父子节点互相指向,数据可能会在某一时刻失去同步。 为此我们可以不使用填充,直接 find() 我们需要的 story。

  1. Story.
  2. find({ author: author._id }).
  3. exec(function (err, stories) {
  4. if (err) return handleError(err);
  5. console.log('The stories are an array: ', stories);
  6. });

query 填充后返回的 document 功能齐全, 除非设置了 lean 选项,否则它就是可 remove,可 save 的。 不要把它们和 sub docs 弄混了, 调用 remove 方法要小心,因为这样不只是从数组删除,还会从数据库删除它们。

填充现有 document

现有 document 同样可以被填充,mongoose 3.6 之后支持了 document#populate() 方法。

填充多个现有 document

如果要填充一个或多个 document 或是(像 mapReduce 输出的)对象, 我们可以使用 Model.populate() 方法,此方法适用版本同样需要大于 mongoose 3.6document#populate()query#populate() 也是使用这个方法填充 document。

多级填充

假设 user schema 记录了 user 的 friends。

  1. var userSchema = new Schema({
  2. name: String,
  3. friends: [{ type: ObjectId, ref: 'User' }]
  4. });

你当然可以填充得到用户的 friends 列表,但是如果要再获得他们朋友的朋友呢? 指定 populate 选项就可以了:

  1. User.
  2. findOne({ name: 'Val' }).
  3. populate({
  4. path: 'friends',
  5. // Get friends of friends - populate the 'friends' array for every friend
  6. populate: { path: 'friends' }
  7. });

跨数据库填充

假设现在有 event schema 和 conversation schema,每个 event 对应一个 conversation 线程。

  1. var eventSchema = new Schema({
  2. name: String,
  3. // The id of the corresponding conversation
  4. // 注意,这里没有 ref!
  5. conversation: ObjectId
  6. });
  7. var conversationSchema = new Schema({
  8. numMessages: Number
  9. });

并且,event 和 conversation 保存在不同 MongoDB 实例。

  1. var db1 = mongoose.createConnection('localhost:27000/db1');
  2. var db2 = mongoose.createConnection('localhost:27001/db2');
  3. var Event = db1.model('Event', eventSchema);
  4. var Conversation = db2.model('Conversation', conversationSchema);

这个情况就不能直接使用 populate() 了,因为 populate() 不知道应该使用什么填充。 不过你可以显式指定一个 model

  1. Event.
  2. find().
  3. populate({ path: 'conversation', model: Conversation }).
  4. exec(function(error, docs) { /* ... */ });

这就是“跨数据库填充”,这可以让你跨 mongodb 数据库甚至是跨 mongodb 实例填充。

动态引用

Mongoose 也可以同时从多个 collection 填充。 假设 user schema 有一系列 connection, 一个 user 可以连接到其他 user 或组织。

  1. var userSchema = new Schema({
  2. name: String,
  3. connections: [{
  4. kind: String,
  5. item: { type: ObjectId, refPath: 'connections.kind' }
  6. }]
  7. });
  8. var organizationSchema = new Schema({ name: String, kind: String });
  9. var User = mongoose.model('User', userSchema);
  10. var Organization = mongoose.model('Organization', organizationSchema);

上面的 refPath 属性意味着 mongoose 会查找 connections.kind 路径, 以此确定 populate() 使用的 model。换句话说,refPath 属性可以让你动态寻找 ref

  1. // Say we have one organization:
  2. // `{ _id: ObjectId('000000000000000000000001'), name: "Guns N' Roses", kind: 'Band' }`
  3. // And two users:
  4. // {
  5. // _id: ObjectId('000000000000000000000002')
  6. // name: 'Axl Rose',
  7. // connections: [
  8. // { kind: 'User', item: ObjectId('000000000000000000000003') },
  9. // { kind: 'Organization', item: ObjectId('000000000000000000000001') }
  10. // ]
  11. // },
  12. // {
  13. // _id: ObjectId('000000000000000000000003')
  14. // name: 'Slash',
  15. // connections: []
  16. // }
  17. User.
  18. findOne({ name: 'Axl Rose' }).
  19. populate('connections.item').
  20. exec(function(error, doc) {
  21. // doc.connections[0].item is a User doc
  22. // doc.connections[1].item is an Organization doc
  23. });

虚拟值填充

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.

  1. var PersonSchema = new Schema({
  2. name: String,
  3. band: String
  4. });
  5. var BandSchema = new Schema({
  6. name: String
  7. });
  8. BandSchema.virtual('members', {
  9. ref: 'Person', // The model to use
  10. localField: 'name', // Find people where `localField`
  11. foreignField: 'band', // is equal to `foreignField`
  12. // If `justOne` is true, 'members' will be a single doc as opposed to
  13. // an array. `justOne` is false by default.
  14. justOne: false
  15. });
  16. var Person = mongoose.model('Person', PersonSchema);
  17. var Band = mongoose.model('Band', BandSchema);
  18. /**
  19. * Suppose you have 2 bands: "Guns N' Roses" and "Motley Crue"
  20. * And 4 people: "Axl Rose" and "Slash" with "Guns N' Roses", and
  21. * "Vince Neil" and "Nikki Sixx" with "Motley Crue"
  22. */
  23. Band.find({}).populate('members').exec(function(error, bands) {
  24. /* `bands.members` is now an array of instances of `Person` */
  25. });

要记得虚拟值默认不会toJSON() 输出。如果你需要填充的虚拟值显式在依赖 JSON.stringify() 的函数 (例如 Express 的 res.json() function)中打印, 需要在 toJSON 中设置 virtuals: true 选项。

  1. // Set `virtuals: true` so `res.json()` works
  2. var BandSchema = new Schema({
  3. name: String
  4. }, { toJSON: { virtuals: true } });

如果你使用了填充保护,要确保 select 中包含了 foreignField

  1. Band.
  2. find({}).
  3. populate({ path: 'members', select: 'name' }).
  4. exec(function(error, bands) {
  5. // Won't work, foreign field `band` is not selected in the projection
  6. });
  7. Band.
  8. find({}).
  9. populate({ path: 'members', select: 'name band' }).
  10. exec(function(error, bands) {
  11. // Works, foreign field `band` is selected
  12. });

在中间件中使用填充

你可以在 pre 或 post 钩子中使用填充。 如果你总是需要填充某一字段,可以了解一下mongoose-autopopulate 插件

  1. // Always attach `populate()` to `find()` calls
  2. MySchema.pre('find', function() {
  3. this.populate('user');
  4. });
  1. // Always `populate()` after `find()` calls. Useful if you want to selectively populate
  2. // based on the docs found.
  3. MySchema.post('find', async function(docs) {
  4. for (let doc of docs) {
  5. if (doc.isPublic) {
  6. await doc.populate('user').execPopulate();
  7. }
  8. }
  9. });
  1. // `populate()` after saving. Useful for sending populated data back to the client in an
  2. // update API endpoint
  3. MySchema.post('save', function(doc, next) {
  4. doc.populate('user').execPopulate(function() {
  5. next();
  6. });
  7. });

下一步

现在我们介绍了 populate(),接着看看 discriminators