- 英文原版
- 关于中文文档
- 快速上手
- 模式(Schemas)
- 模式类型(SchemaTypes)
- 连接(Connections)
- 模型(Models)
- 文档 (Documents)
- 子文档(Subdocuments)
- 查询 (queries)
- 验证 (validation)
- 中间件 (middleware)
- 填充 (Populate)
- 鉴别器 (Discriminators)
- 插件
- AWS Lambda
- API 文档
- Schema
- Connection
- Document
- Model
- Query
- Aggregate
- SchemaType
- VirtualType
- Error
- Version Compatibility
- FAQ
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 nameexec(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()` 覆盖了第一个,因为它们都填充 fansStory.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/2aEfTdBselect: '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 childrenexec(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 friendpopulate: { 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 uselocalField: '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()` worksvar 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()` callsMySchema.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 endpointMySchema.post('save', function(doc, next) {doc.populate('user').execPopulate(function() {next();});});
下一步
现在我们介绍了 populate(),接着看看 discriminators。
mongoose