$从多个集合中查找,并嵌套输出
我有一个多个集合,我使用了单独的集合和外键方法,我想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水线阶段实际上并不影响现有的订单。 所以发现的顺序总是保留下来。
一些技巧:
-
像
Group.collection.name
这样的Group.collection.name
实际上使用在mongoose模型上定义的属性来执行诸如“获取集合名称”之类的东西。 这样可以避免硬编码到$lookup
本身,并保持与代码运行时在模型上注册的内容一致。 -
如果你打算以某个数组的forms输出一个属性,或者在模式上有一个现有的“引用数组”,那么就应该保留这个名字。 为path创build临时名称实际上没有什么意义,除非在stream水线阶段专门为了在后一阶段对“字段的输出进行重新sorting”而进行。 否则,就像在任何情况下一样,使用你打算输出的名字。 这样阅读和解读意图要容易得多。
-
除非你是真正的意思,否则不要使用像
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(); } })();