将另一个集合中的相关项包含到结果集中

mat*_*fin 4 mongodb mongodb-query aggregation-framework

TLDR

如何使用MongoDB聚合来包含来自另一个通过一对多关系链接的集合中的相关文档?

本质上,我想要做的是能够获取问题列表并包括与该问题相关的所有标志.

更新(2016年11月11日):解决了下面发布的解决方案.

更新(05/07/2016):我已经设法通过使用组合$unwind, $lookup, $project等来获得带有相关标志的问题列表.更新的查询如下.

问题(05/07/2016):我只能获取具有嵌套标志的问题.即使他们没有任何标志,我也想获取所有问题.

我有两个集合,一个用于内容,一个用于内容标志,如下所示:

内容的架构(问题集)

{
    "_id" : ObjectId("..."),
    "slug" : "a-sample-title",
    "content" : "Some content.",
    "title" : "A Sample Title.",
    "kind" : "Question",
    "updated" : ISODate("2016-06-08T08:54:26.104Z"),
    "isPublished" : true,
    "isFeatured" : false,
    "flags" : [ 
        ObjectId("<id_of_flag_one>"), 
        ObjectId("<id_of_flag_two>")
    ],
    "answers" : [ 
        ObjectId("..."), 
        ObjectId("...")
    ],
    "related" : [],
    "isAnswered" : true,
    "__v" : 4
}
Run Code Online (Sandbox Code Playgroud)

标志的shcema(标志集合)

{
    "_id" : ObjectId("..."),
    "flaggedBy" : ObjectId("<a_users_id>"),
    "type" : "like",
    "__v" : 0
}
Run Code Online (Sandbox Code Playgroud)

在上面,一个问题可以有很多标志,一个标志只能有一个问题.我想要做的是在查询问题集时返回问题的所有标志.我尝试使用聚合运行这一点.

这是我正在使用的更新查询(05/07/2016)

fetchQuestions: (permission, params) => {
    return new Promise((resolve, reject) => {
        let query = Question.aggregate([
            {
                $lookup: {
                    from: 'users',
                    localField: 'author',
                    foreignField: '_id',
                    as: 'authorObject'
                }
            },
            {
                $unwind: '$authorObject'
            },
            {
                $unwind: '$flags'
            },
            {
                $lookup: {
                    from: 'flags',
                    localField: 'flags',
                    foreignField: '_id',
                    as: 'flagObjects'
                }
            },
            {
                $unwind: '$flagObjects'
            },
            {
                $group: {
                    _id: {
                        _id: '$_id',
                        title: '$title',
                        content: '$content',
                        updated: '$updated',
                        isPublished: '$isPublished',
                        isFeatured: '$isFeatured',
                        isAnswered: '$isAnswered',
                        answers: '$answers',
                        author: '$authorObject'
                    },
                    flags: {
                        $push: '$flags'
                    },
                    flagObjects: {
                        $push: '$flagObjects'
                    }
                }
            },
            {
                $project: {
                    _id: 0,
                    _id: '$_id._id',
                    title: '$_id.title',
                    content: '$_id.content',
                    updated: '$_id.updated',
                    isPublished: '$_id.isPublished',
                    isFeatured: '$_id.isFeatured',
                    author: {
                        fullname: '$_id.author.fullname',
                        username: '$_id.author.username'
                    },
                    flagCount: {
                        $size: '$flagObjects'
                    },
                    answersCount: {
                        $size: '$_id.answers'
                    },
                    flags: '$flagObjects',
                    wasFlagged: {
                        $cond: {
                            if: {
                                $gt: [
                                    {
                                        $size: '$flagObjects'
                                    },
                                    0
                                ]
                            },
                            then: true,
                            else: false
                        }
                    }
                }
            },
            {
                $sort: {
                    updated: 1
                }
            },
            {
                $skip: 0
            },
            // {
            //     $limit: 110
            // }
        ])
        .exec((error, result) => {
            if(error) reject(error);
            else resolve(result);
        });
    });
},
Run Code Online (Sandbox Code Playgroud)

我已经尝试过使用其他聚合运算符$unwind,$group但是结果集返回了五个或更少的项目,我发现很难理解这些应该如何协同工作以获得我需要的东西.

这是我得到的回应,这正是我所需要的.唯一的问题是,如上所述,我只会得到带有旗帜而不是所有问题的问题.

"questions": [
{
  "_id": "5757dd42d0c2ae292f76f11a",
  "flags": [
    {
      "_id": "5774e0a81f2874821f71ace8",
      "flaggedBy": "57569d02d0c2ae292f76f0f5",
      "type": "concern",
      "__v": 0
    },
    {
      "_id": "577a0f5414b834372a6ac772",
      "flaggedBy": "5756aa79d0c2ae292f76f0f8",
      "type": "concern",
      "__v": 0
    }
  ],
  "title": "A question for the landing page.",
  "content": "This is a question that will appear on the landing page.",
  "updated": "2016-06-08T08:54:26.104Z",
  "isPublished": true,
  "isFeatured": false,
  "author": {
    "fullname": "Matt Finucane",
    "username": "matfin-386829"
  },
  "flagCount": 2,
  "answersCount": 2,
  "wasFlagged": true
},
...,
...,
...
]
Run Code Online (Sandbox Code Playgroud)

mat*_*fin 6

看起来我已经找到了这个问题的解决方案,将在下面发布.

我遇到的问题概述如下:

  • 我有一系列的Questions各种领域,如标题,内容,发布日期等,在通常的ObjectID领域之上.

  • 我有一个Flags与问题相关的单独集合.

  • 当标记被写入到用于在Question,所述ObjectID的那Flag应添加到称为阵列字段flags附加到Question文档中.

  • 简而言之,Flags不直接存储在Question文档中.对它的引用Flag存储为ObjectID.

我需要做的是从Questions集合中获取所有项目并包含相关的标志.

MongoDB的聚合框架似乎为这个理想的解决方案,而是让你的头围绕它可以是一个有点棘手,尤其是在处理时$group,$lookup$unwind运营商.

我还应该指出我正在使用NodeJS v6.x.x和Mongoose 4.4.x.

这是该问题的(相当大的)评论解决方案.

fetchQuestions: (permission, params) => {
    return new Promise((resolve, reject) => {
        let query = Question.aggregate([
            /**
             *  We need to perform a lookup on the author 
             *  so we can include the user details for the 
             *  question. This lookup is quite easy to handle 
             *  because a question should only have one author.
             */
            {
                $lookup: {
                    from: 'users',
                    localField: 'author',
                    foreignField: '_id',
                    as: 'authorObject'
                }
            },
            /**
             *  We need this so that the lookup on the author
             *  object pulls out an author object and not an
             *  array containing one author. This simplifies
             *  the process of $project below.
             */
            {
                $unwind: '$authorObject'
            },
            /**
             *  We need to unwind the flags field, which is an 
             *  array of ObjectIDs. At this stage of the aggregation 
             *  pipeline, questions will be repeated so for example 
             *  if there are two questions and one of them has two 
             *  flags and the other has four flags, the result set 
             *  will have six items and the questions will be repeated
             *  the same number of times as the flags they contain.
             *  The $group function later on will take care of this 
             *  and return only unique questions.
             *
             *  It is important to point out how the $unwind function 
             *  is used here. If we did not specify the preserveNullAndEmptyArrays
             *  parameter then the only questions returned would be those
             *  that have flags. Those without would be skipped.
             */
            {
                $unwind: {
                    path: '$flags',
                    preserveNullAndEmptyArrays: true
                }
            },
            /**
             *  Now that we have the ObjectIDs for the flags from the 
             *  $unwind operation above, we need to perform a lookup on
             *  the flags collection to get our flags. We return these 
             *  with the variable name 'flagObjects' we can use later.
             */
            {
                $lookup: {
                    from: 'flags',
                    localField: 'flags',
                    foreignField: '_id',
                    as: 'flagObjects'
                }
            },
            /**
             *  We then need to perform another unwind on the 'flagObjects' 
             *  and pass them into the next $group function
             */
            {
                $unwind: {
                    path: '$flagObjects',
                    preserveNullAndEmptyArrays: true
                }
            },
            /**
             *  The next stage of the aggregation pipeline takes all 
             *  the duplicated questions with their flags and the flagObjects
             *  and normalises the data. The $group aggregator requires an _id
             *  property to describe how a question should be unique. It also sets
             *  up some variables that can be used when it comes to the $project
             *  stage of the aggregation pipeline.
             *  the flagObjects property calls on the $push function to add a collection
             *  of flagObjects that were pulled from the $lookup above.
             */
            {
                $group: {
                    _id: {
                        _id: '$_id',
                        title: '$title',
                        content: '$content',
                        updated: '$updated',
                        isPublished: '$isPublished',
                        isFeatured: '$isFeatured',
                        isAnswered: '$isAnswered',
                        answers: '$answers',
                        author: '$authorObject'
                    },
                    flagObjects: {
                        $push: '$flagObjects'
                    }
                }
            },
            /**
             *  The $project stage of the pipeline then puts together what the final 
             *  result set should look like when the query is executed. Here we can use
             *  various Mongo functions to reshape the data and create new attributes.
             */
            {
                $project: {
                    _id: 0,
                    _id: '$_id._id',
                    title: '$_id.title',
                    updated: '$_id.updated',
                    isPublished: '$_id.isPublished',
                    isFeatured: '$_id.isFeatured',
                    author: {
                        fullname: '$_id.author.fullname',
                        username: '$_id.author.username'
                    },
                    flagCount: {
                        $size: '$flagObjects'
                    },
                    answersCount: {
                        $size: '$_id.answers'
                    },
                    flags: '$flagObjects',
                    wasFlagged: {
                        $cond: {
                            if: {
                                $gt: [
                                    {
                                        $size: '$flagObjects'
                                    },
                                    0
                                ]
                            },
                            then: true,
                            else: false
                        }
                    }
                }
            },
            /**
             *  Then we can sort, skip and limit if needs be.
             */
            {
                $sort: {
                    updated: -1
                }
            },
            {
                $skip: 0
            },
            // {
            //     $limit: 110
            // }
        ]);

        query.exec((error, result) => {
            if(error) reject(error);
            else resolve(result);
        });
    });
},
Run Code Online (Sandbox Code Playgroud)

这是返回的样本

"questions": [
    {
      "_id": "576a85d68c4333a017083fca",
      "title": "How do I do this?",
      "updated": "2016-06-22T12:34:30.919Z",
      "isPublished": false,
      "isFeatured": false,
      "author": {
        "fullname": "Matt Finucane",
        "username": "matfin-386829"
      },
      "flagCount": 1,
      "answersCount": 0,
      "flags": [
        {
          "_id": "5776541a2e38844428696615",
          "flaggedBy": "5756aa79d0c2ae292f76f0f8",
          "type": "concern",
          "__v": 0
        }
      ],
      "wasFlagged": true
    },
    {
      "_id": "576a85d68c4333a017083fc9",
      "title": "Is this a question?",
      "updated": "2016-06-22T12:34:30.918Z",
      "isPublished": true,
      "isFeatured": false,
      "author": {
        "fullname": "Matt Finucane",
        "username": "matfin-386829"
      },
      "flagCount": 2,
      "answersCount": 0,
      "flags": [
        {
          "_id": "5773ce4ea363e5161ae69e7f",
          "flaggedBy": "5756aa79d0c2ae292f76f0f8",
          "type": "concern",
          "__v": 0
        },
        {
          "_id": "577654382e3884442869661d",
          "flaggedBy": "57569d02d0c2ae292f76f0f5",
          "type": "concern",
          "__v": 0
        }
      ],
      "wasFlagged": true
    }
]
Run Code Online (Sandbox Code Playgroud)