来自多个集合的$ lookup和嵌套输出

时间:2017-09-12 01:36:23

标签: javascript node.js mongodb mongoose aggregation-framework

我有多个系列,我使用了单独的系列&外键方法,我想加入这个集合来构建嵌套集合。 这是我的收藏模式:

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'}
});

这是构建这个嵌套对象的代码:

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
              }
            ]
          }
        ]
      }
    ]
  }
]

有人可以帮我构建响应,如示例所示吗?

1 个答案:

答案 0 :(得分:0)

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

   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 } }
    ]);

这就是重建阵列的方法,你可以一步一步地完成,而不是一次性完成所有这些。它可能是最难理解的概念,但“管道”意味着你实际上可以“多次”做事,将一个动作链接到另一个的输出。

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

在“第二个”$lookup上,您实际使用$group之类的运算符来构建"groups"级别的特定字段属性。 _id数组再次使用$first构造,因此前一阶段“分组键”中的每个属性都以Group.collection.name为前缀,因此这里引用它们的方式

此外,作为技术立场,如果您有预期的订单,则在每次调用$push后应始终$sort。分组键上的集合不能以任何特定顺序保证(尽管通常是反向堆栈顺序)。如果您需要订单,请指定订单,特别是在应用$group重建$push后的数组时。

原始$group之前没有$sort 的原因是因为前面的管道阶段实际上对现有订单没有任何影响。因此始终保留发现的顺序。

一些提示:

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

  2. 如果您打算将属性输出为数组,或者甚至在模式上通过某个名称拥有现有的“引用数组”,则“保留该名称”。为路径制作临时名称确实没有多大意义,除非您在管道阶段专门为了在以后阶段“重新排序”字段输出的目的而这样做。否则,只需使用您想要输出的名称,就像在所有情况下一样。通过这种方式阅读和解释意图要容易得多。

  3. 除非你的意思是真的,否则不要使用像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(); } })(); 这样的选项。有一种“特殊方式”,实际处理$lookup + $lookup的组合,并且真正在“单一阶段”执行,而不是在“展开”之前检索所有结果。您可以在聚合管道的“explain”输出中看到这一点。简而言之,如果您总是有关系匹配,那么请不要使用该选项。最好不要。

  4. 示范

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

    .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();
      }
    
    
    })();
    

    您可以看到架构更改(仅添加连接的虚拟字段)以及修订列表中的代码:

    {{1}}