【问题标题】:Include related items from another collection into a result set将另一个集合中的相关项包含到结果集中
【发布时间】:2017-05-25 17:21:25
【问题描述】:

TLDR

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

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

更新(2016 年 11 月 7 日):使用下面发布的解决方案解决。

更新(2016 年 5 月 7 日):通过使用 $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
},
...,
...,
...
]

【问题讨论】:

  • 如果您的 MongoDB 版本是 3.2 或更高版本,那么 $lookup
  • 嗯,我不明白。我在聚合查询中使用 $lookup。
  • 我还可以确认我使用的是 MongoDB 版本 3.2.x
  • 为什么你有两个$lookup 舞台?删除第一个 $lookup 阶段。还有$lookup does not play well with array field in 3.2.
  • 第一个 $lookup 用于获取问题的作者,以便稍后在我的投影中使用它。据我了解,这类似于在 Mongoose 中使用 populate() 函数。我使用您发布的链接中的建议修改了聚合查询,现在我返回了一个包含 5 个标志对象的列表。我的问题预测现在被忽略了。如何编写此代码,以便在查询的结果集中嵌套 flagObjects,而不更改查询的输出。使用聚合时我走的是正确的道路吗?

标签: mongodb mongodb-query aggregation-framework


【解决方案1】:

看来我已经找到了解决此问题的方法,将在下面发布。

我遇到的问题概述如下:

  • 我有一个 Questions 的集合,在通常的 ObjectID 字段之上包含各种字段,例如标题、内容、发布日期等。

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

  • 当为Question 发布标志时,应将FlagObjectID 添加到附加到Question 文档的名为flags 的数组字段中。

  • 简而言之,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);
        });
    });
},

这是返回的示例

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

【讨论】:

    猜你喜欢
    • 2014-12-18
    • 2016-05-16
    • 2012-01-22
    • 2010-09-13
    • 1970-01-01
    • 2012-08-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多