$从多个集合中查找,并嵌套输出

我有一个多个集合,我使用了单独的集合和外键方法,我想join这个集合来构build一个嵌套的集合。 这是我的collections架构:

const SurveySchema = new Schema({ _id:{ type: Schema.ObjectId, auto: true }, name: String, enabled: {type: Boolean, Default: true}, created_date:{type: Date, Default: Date.now}, company: {type: Schema.Types.ObjectId, ref: 'Company'},}); 

 const GroupSchema = new Schema({ _id:{ type: Schema.ObjectId, auto: true }, name: String, order: String, created_date:{type: Date, Default: Date.now}, questions: [{type: Schema.Types.ObjectId, ref: 'Question'}], survey: {type: Schema.Types.ObjectId, ref: 'Survey'} }); 

 const ResponseSchema = new Schema({ _id:{ type: Schema.ObjectId, auto: true }, response_text: String, order: String, created_date:{type: Date, Default: Date.now}, question:{type: Schema.Types.ObjectId, ref: 'Question'} }); 

这是我的代码来build立这个嵌套的对象:

 Survey.aggregate([ { $match: {} }, { $lookup: { from: 'groups', localField: '_id', foreignField: 'survey', as: 'groupsofquestions', }}, { $unwind: { path: "$groupsofquestions", preserveNullAndEmptyArrays: true }}, { $lookup: { from: 'questions', localField: 'groupsofquestions._id', foreignField: 'group', as: 'questionsofgroup', }}, { $lookup: { from: 'response', localField: 'questionsofgroup._id', foreignField: 'question', as: 'responses', }}, { $group: { _id: "$_id", name: {$first: "$name"}, groups: {$push: { id: "$groupsofquestions._id", name: "$groupsofquestions.name", questions: "$questionsofgroup", reponses: "$responses" }} }} ]) 

我想结构如下,(也有外部链接):

http://jsoneditoronline.org/?id=d7d1779b3b95e3acb28f8a2be0785423

 [ { "__v": 0, "_id": "59b6715725dcd2060da7f591", "company": "59b6715725dcd2060da7f58f", "created_date": "2017-09-11T11:19:51.709Z", "enabled": true, "name": "function String() { [native code] }", "groups": [ { "_id": "59b6715725dcd2060da7f592", "name": "groupe 1 des question", "order": "1", "created_date": "2017-09-11T11:19:51.709Z", "survey": "59b6715725dcd2060da7f591", "__v": 0, "questions": [ { "_id": "59b6715725dcd2060da7f594", "question_text": "question 1 group 1", "order": "1", "created_date": "2017-09-11T11:19:51.709Z", "group": "59b6715725dcd2060da7f592", "__v": 0, "responses": [ { "_id": "59b6715725dcd2060da7f598", "response_text": "reponse 1 question 1 group 1", "order": "1", "created_date": "2017-09-11T11:19:51.710Z", "question": "59b6715725dcd2060da7f594", "__v": 0 }, { "_id": "59b6715725dcd2060da7f599", "response_text": "reponse 2 question 1 group 1", "order": "2", "created_date": "2017-09-11T11:19:51.710Z", "question": "59b6715725dcd2060da7f594", "__v": 0 } ] }, { "_id": "59b6715725dcd2060da7f595", "question_text": "question 2 group 1", "order": "2", "created_date": "2017-09-11T11:19:51.710Z", "group": "59b6715725dcd2060da7f592", "__v": 0, "responses": [ { "_id": "59b6715725dcd2060da7f59a", "response_text": "reponse 1 question 2 group 1", "order": "1", "created_date": "2017-09-11T11:19:51.710Z", "question": "59b6715725dcd2060da7f595", "__v": 0 }, { "_id": "59b6715725dcd2060da7f59b", "response_text": "reponse 2 question 2 group 1", "order": "2", "created_date": "2017-09-11T11:19:51.710Z", "question": "59b6715725dcd2060da7f595", "__v": 0 } ] } ] }, { "_id": "59b6715725dcd2060da7f593", "name": "groupe 2 des question", "order": "2", "created_date": "2017-09-11T11:19:51.709Z", "survey": "59b6715725dcd2060da7f591", "__v": 0, "questions": [ { "_id": "59b6715725dcd2060da7f596", "question_text": "question 1 group 1", "order": "1", "created_date": "2017-09-11T11:19:51.710Z", "group": "59b6715725dcd2060da7f592", "__v": 0, "responses": [ { "_id": "59b6715725dcd2060da7f59c", "response_text": "reponse 1 question 1 group 2", "order": "1", "created_date": "2017-09-11T11:19:51.710Z", "question": "59b6715725dcd2060da7f596", "__v": 0 }, { "_id": "59b6715725dcd2060da7f59d", "response_text": "reponse 2 question 1 group 2", "order": "2", "created_date": "2017-09-11T11:19:51.710Z", "question": "59b6715725dcd2060da7f596", "__v": 0 } ] }, { "_id": "59b6715725dcd2060da7f597", "question_text": "question 2 group 1", "order": "2", "created_date": "2017-09-11T11:19:51.710Z", "group": "59b6715725dcd2060da7f592", "__v": 0, "responses": [ { "_id": "59b6715725dcd2060da7f59e", "response_text": "reponse 1 question 2 group 2", "order": "1", "created_date": "2017-09-11T11:19:51.710Z", "question": "59b6715725dcd2060da7f597", "__v": 0 }, { "_id": "59b6715725dcd2060da7f59f", "response_text": "reponse 2 question 2 group 2", "order": "2", "created_date": "2017-09-11T11:19:51.710Z", "question": "59b6715725dcd2060da7f597", "__v": 0 } ] } ] } ] } ] 

有人可以帮助我构build样本中显示的响应吗?

大多数情况下,您需要使用$group来在使用$unwind处理之后“重新构build”,以便再次嵌套数组输出。 还有一些技巧:

  Survey.aggregate([ { "$lookup": { "from": Group.collection.name, "localField": "_id", "foreignField": "survey", "as": "groups" }}, { "$unwind": "$groups" }, { "$lookup": { "from": Question.collection.name, "localField": "groups.questions", "foreignField": "_id", "as": "groups.questions" }}, { "$unwind": "$groups.questions" }, { "$lookup": { "from": Response.collection.name, "localField": "groups.questions._id", "foreignField": "question", "as": "groups.questions.responses" }}, { "$group": { "_id": { "_id": "$_id", "company": "$company", "created_date": "$created_date", "enabled": "$enabled", "name": "$name", "groups": { "_id": "$groups._id", "name": "$groups.name", "order": "$groups.order", "created_date": "$groups.created_date", "survey": "$groups.survey" } }, "questions": { "$push": "$groups.questions" } }}, { "$sort": { "_id": 1 } }, { "$group": { "_id": "$_id._id", "company": { "$first": "$_id.company" }, "created_date": { "$first": "$_id.created_date" }, "enabled": { "$first": "$_id.enabled" }, "name": { "$first": "$_id.name" }, "groups": { "$push": { "_id": "$_id.groups._id", "name": "$_id.groups.name", "order": "$_id.groups.order", "created_date": "$_id.groups.created_date", "survey": "$_id.groups.survey", "questions": "$questions" } } }}, { "$sort": { "_id": 1 } } ]); 

所以这就是重buildarrays的方法,你只需要一步一步地完成,而不是一劳永逸。 这可能是通常理解的最困难的概念,但是“stream水线”意味着事实上你可以“多次”做事,将一个行动链接到另一个行动的输出。

因此,第一个$group是在“组”级别完成的,因为你想要$push items "questions"数组,这是$unwind最后的“解构”。 请注意, "responses"仍然是最后一个$lookup阶段的结果。 但是除了数组内容之外,其他所有内容都在_id “分组键”中。

在“第二个” $group您实际上使用像$first这样的运算符来构buildSurvey级别的特定字段属性。 "groups"数组是用$push重新构造的,而前一个阶段的“分组键”中的每个属性都以_id为前缀,这就是它们在这里被引用的方式。

另外,从技术的angular度来看,如果您有预期的订单,则应该在每次调用$group后总是进行$sort 。 分组键上的集合不保证任何特定的顺序(尽pipe通常是反向堆栈顺序)。 如果你期望一个订单,然后指定它,特别是当应用$push来重build$group后面的数组。

在初始$group 之前没有$sort的原因是因为前面的stream水线阶段实际上并不影响现有的订单。 所以发现的顺序总是保留下来。

一些技巧:

  1. Group.collection.name这样的Group.collection.name实际上使用在mongoose模型上定义的属性来执行诸如“获取集合名称”之类的东西。 这样可以避免硬编码到$lookup本身,并保持与代码运行时在模型上注册的内容一致。

  2. 如果你打算以某个数组的forms输出一个属性,或者在模式上有一个现有的“引用数组”,那么就应该保留这个名字。 为path创build临时名称实际上没有什么意义,除非在stream水线阶段专门为了在后一阶段对“字段的输出进行重新sorting”而进行。 否则,就像在任何情况下一样,使用你打算输出的名字。 这样阅读和解读意图要容易得多。

  3. 除非你是真正的意思,否则不要使用像preserveNullAndEmptyArrays这样的选项。 有一种“特殊的方式”, $lookup + $unwind的组合实际上是处理的,真正在“单一阶段”执行,而不是在“展开”之前检索所有结果。 你可以在聚合pipe道的“explain”输出中看到这个。 总之,如果你总是有关系匹配,那么不要使用这个选项。 最好不要。


示范

作为一个完整的列表和概念validation,我们可以加载源JSON,将其存储在数据库中的不同集合中,然后使用聚合语句来检索和重构所需的结构:

 const fs = require('fs'), mongoose = require('mongoose'), Schema = mongoose.Schema; mongoose.Promise = global.Promise; mongoose.set('debug',true); const uri = 'mongodb://localhost/nested', options = { useMongoClient: true }; const responseSchema = new Schema({ response_text: String, order: String, created_date: Date, question: { type: Schema.Types.ObjectId, ref: 'Question' } }); const questionSchema = new Schema({ question_text: String, order: String, created_date: Date, group: { type: Schema.Types.ObjectId, ref: 'Group' } }); const groupSchema = new Schema({ name: String, order: String, created_date: Date, survey: { type: Schema.Types.ObjectId, ref: 'Survey' }, questions: [{ type: Schema.Types.ObjectId, ref: 'Question' }] }); const surveySchema = new Schema({ company: { type: Schema.Types.ObjectId, ref: 'Company' }, created_date: Date, enabled: Boolean, name: String }); const companySchema = new Schema({ }); const Company = mongoose.model('Company', companySchema); const Survey = mongoose.model('Survey', surveySchema); const Group = mongoose.model('Group', groupSchema); const Question = mongoose.model('Question', questionSchema); const Response = mongoose.model('Response', responseSchema); function log(data) { console.log(JSON.stringify(data,undefined,2)) } (async function() { try { const conn = await mongoose.connect(uri,options); await Promise.all( Object.keys(conn.models).map( m => conn.models[m].remove() ) ); // Initialize data let content = JSON.parse(fs.readFileSync('./jsonSurveys.json')); //log(content); for ( let item of content ) { let survey = await Survey.create(item); let company = await Company.create({ _id: survey.company }); for ( let group of item.groups ) { await Group.create(group); for ( let question of group.questions ) { await Question.create(question); for ( let response of question.responses ) { await Response.create(response); } } } } // Run aggregation let results = await Survey.aggregate([ { "$lookup": { "from": Group.collection.name, "localField": "_id", "foreignField": "survey", "as": "groups" }}, { "$unwind": "$groups" }, { "$lookup": { "from": Question.collection.name, "localField": "groups.questions", "foreignField": "_id", "as": "groups.questions" }}, { "$unwind": "$groups.questions" }, { "$lookup": { "from": Response.collection.name, "localField": "groups.questions._id", "foreignField": "question", "as": "groups.questions.responses" }}, { "$group": { "_id": { "_id": "$_id", "company": "$company", "created_date": "$created_date", "enabled": "$enabled", "name": "$name", "groups": { "_id": "$groups._id", "name": "$groups.name", "order": "$groups.order", "created_date": "$groups.created_date", "survey": "$groups.survey" } }, "questions": { "$push": "$groups.questions" } }}, { "$sort": { "_id": 1 } }, { "$group": { "_id": "$_id._id", "company": { "$first": "$_id.company" }, "created_date": { "$first": "$_id.created_date" }, "enabled": { "$first": "$_id.enabled" }, "name": { "$first": "$_id.name" }, "groups": { "$push": { "_id": "$_id.groups._id", "name": "$_id.groups.name", "order": "$_id.groups.order", "created_date": "$_id.groups.created_date", "survey": "$_id.groups.survey", "questions": "$questions" } } }}, { "$sort": { "_id": 1 } } ]); log(results); } catch(e) { console.error(e); } finally { mongoose.disconnect(); } })(); 

替代案例

另外值得注意的是,通过一些小的模式更改,使用.populate()嵌套调用可以实现相同的结果:

  let alternate = await Survey.find().populate({ path: 'groups', populate: { path: 'questions', populate: { path: 'responses' } } }); 

虽然看起来更加简单,但是实际上会引入更多的负载,这是因为这会向数据库发出多个查询以检索数据,而不是一次调用:

 Mongoose: groups.find({ survey: { '$in': [ ObjectId("59b6715725dcd2060da7f591") ] } }, { fields: {} }) Mongoose: questions.find({ _id: { '$in': [ ObjectId("59b6715725dcd2060da7f594"), ObjectId("59b6715725dcd2060da7f595"), ObjectId("59b6715725dcd2060da7f596"), ObjectId("59b6715725dcd2060da7f597") ] } }, { fields: {} }) Mongoose: responses.find({ question: { '$in': [ ObjectId("59b6715725dcd2060da7f594"), ObjectId("59b6715725dcd2060da7f595"), ObjectId("59b6715725dcd2060da7f596"), ObjectId("59b6715725dcd2060da7f597") ] } }, { fields: {} }) 

您可以看到模式更改(只是添加了连接的虚拟字段)以及修订列表中的实际代码:

 const fs = require('fs'), mongoose = require('mongoose'), Schema = mongoose.Schema; mongoose.Promise = global.Promise; mongoose.set('debug',true); const uri = 'mongodb://localhost/nested', options = { useMongoClient: true }; const responseSchema = new Schema({ response_text: String, order: String, created_date: Date, question: { type: Schema.Types.ObjectId, ref: 'Question' } }); const questionSchema = new Schema({ question_text: String, order: String, created_date: Date, group: { type: Schema.Types.ObjectId, ref: 'Group' } },{ toJSON: { virtuals: true, transform: function(doc,obj) { delete obj.id; return obj; } } }); questionSchema.virtual('responses',{ ref: 'Response', localField: '_id', foreignField: 'question' }); const groupSchema = new Schema({ name: String, order: String, created_date: Date, survey: { type: Schema.Types.ObjectId, ref: 'Survey' }, questions: [{ type: Schema.Types.ObjectId, ref: 'Question' }] }); const surveySchema = new Schema({ company: { type: Schema.Types.ObjectId, ref: 'Company' }, created_date: Date, enabled: Boolean, name: String },{ toJSON: { virtuals: true, transform: function(doc,obj) { delete obj.id; return obj; } } }); surveySchema.virtual('groups',{ ref: 'Group', localField: '_id', foreignField: 'survey' }); const companySchema = new Schema({ }); const Company = mongoose.model('Company', companySchema); const Survey = mongoose.model('Survey', surveySchema); const Group = mongoose.model('Group', groupSchema); const Question = mongoose.model('Question', questionSchema); const Response = mongoose.model('Response', responseSchema); function log(data) { console.log(JSON.stringify(data,undefined,2)) } (async function() { try { const conn = await mongoose.connect(uri,options); await Promise.all( Object.keys(conn.models).map( m => conn.models[m].remove() ) ); // Initialize data let content = JSON.parse(fs.readFileSync('./jsonSurveys.json')); //log(content); for ( let item of content ) { let survey = await Survey.create(item); let company = await Company.create({ _id: survey.company }); for ( let group of item.groups ) { await Group.create(group); for ( let question of group.questions ) { await Question.create(question); for ( let response of question.responses ) { await Response.create(response); } } } } // Run aggregation let results = await Survey.aggregate([ { "$lookup": { "from": Group.collection.name, "localField": "_id", "foreignField": "survey", "as": "groups" }}, { "$unwind": "$groups" }, { "$lookup": { "from": Question.collection.name, "localField": "groups.questions", "foreignField": "_id", "as": "groups.questions" }}, { "$unwind": "$groups.questions" }, { "$lookup": { "from": Response.collection.name, "localField": "groups.questions._id", "foreignField": "question", "as": "groups.questions.responses" }}, { "$group": { "_id": { "_id": "$_id", "company": "$company", "created_date": "$created_date", "enabled": "$enabled", "name": "$name", "groups": { "_id": "$groups._id", "name": "$groups.name", "order": "$groups.order", "created_date": "$groups.created_date", "survey": "$groups.survey" } }, "questions": { "$push": "$groups.questions" } }}, { "$sort": { "_id": 1 } }, { "$group": { "_id": "$_id._id", "company": { "$first": "$_id.company" }, "created_date": { "$first": "$_id.created_date" }, "enabled": { "$first": "$_id.enabled" }, "name": { "$first": "$_id.name" }, "groups": { "$push": { "_id": "$_id.groups._id", "name": "$_id.groups.name", "order": "$_id.groups.order", "created_date": "$_id.groups.created_date", "survey": "$_id.groups.survey", "questions": "$questions" } } }}, { "$sort": { "_id": 1 } } ]); log(results); let alternate = await Survey.find().populate({ path: 'groups', populate: { path: 'questions', populate: { path: 'responses' } } }); log(alternate); } catch(e) { console.error(e); } finally { mongoose.disconnect(); } })();