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

时间:2016-06-30 20:17:14

标签: 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
}

标志的shcema(标志集合)

{
    "_id" : ObjectId("..."),
    "flaggedBy" : ObjectId("<a_users_id>"),
    "type" : "like",
    "__v" : 0
}

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

以下是我正在使用的更新的查询(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);
        });
    });
},

我尝试使用其他聚合运算符,例如$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
},
...,
...,
...
]

1 个答案:

答案 0 :(得分:5)

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

我遇到的问题概述如下:

  • 我有一个Questions的集合,其中包含标题,内容,发布日期等各种字段,位于通常的ObjectID字段之上。

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

  • 当为Question发布标记时,ObjectID的{​​{1}}应添加到Flag附加到flags的数组字段中1}}文件。

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

我需要做的是从ObjectID集合中获取所有项目,并包含相关的标记

MongoDB聚合框架似乎是理想的解决方案,但是了解它可能有点棘手,特别是在处理Questions$group$lookup运算符时

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

这是解决问题的(相当大的)解决方案。

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