双嵌套外部字段上的$ lookup

时间:2018-05-03 01:05:53

标签: mongodb mongodb-query aggregation-framework

我有两个系列:

用户

{
    id: 1,
    name: "Michael", 
    starred: [1, 2]
}

学校

{
    id: 1,
    name: "Uni", 
    faculties: [{
        id:1000, 
        name: "faculty1", 
        subjects: [
            {id: 1, name: "sub1"},
            {id: 2, name: "sub2"},
            {id: 3, name: "sub3"}
        ]
    }]
}

现在,在我的用户集合中,我希望使用已加星标中的ID查找并收集每个主题对象。即。 starred: [1,2]包含我想要的id个主题。

所以最终结果应该返回

[{id: 1, name: sub1},{id: 2, name: sub2}]

我目前正在使用whis聚合管道

{$match: {name: 'Michael'}},
{$unwind: "$faculties"},
{$unwind: "$faculties.subjects"},
{$lookup:
  {
     from: 'schools',
     localField: 'starred',
     foreignField: 'faculties.subjects.id',
     as: 'starredSubjects'
   }
},
{$project: {starredSubjects: 1}}

但展开不起作用(我猜是因为我试图解开一个外国集合,而不是本地集合(即用户)。 foreignField: 'faculties.subjects.id也不会返回任何内容。我错过了什么?

(旁注:MongoExplorer webstorm插件的测试很棒)。

1 个答案:

答案 0 :(得分:3)

这真的不是一个很好的结构,并且有很好的理由。因此,在这里执行$lookup并不是一项简单的任务,因为“嵌套数组”存在各种含义

你基本上想要

db.users.aggregate([
   { "$match": { "name": "Michael" } },
   { "$lookup": {
     "from": "schools",
     "localField": "starred",
     "foreignField": "faculties.subjects.id",
     "as": "subjects"
   }},
   { "$addFields": {
     "subjects": {
       "$filter": {
         "input": {
           "$reduce": {
             "input": {
               "$reduce": {
                 "input": "$subjects.faculties.subjects",
                 "initialValue": [],
                 "in": { "$concatArrays": [ "$$value", "$$this" ] }
               }
             },
             "initialValue": [],
             "in": { "$concatArrays": [ "$$value", "$$this" ] }
           }
         },
         "cond": { "$in": ["$$this.id", "$starred"] }
       }
     }
   }}
])

或者使用MongoDB 3.6或更高版本:

db.users.aggregate([
  { "$match": { "name": "Michael" } },
  { "$lookup": {
    "from": "schools",
    "let": { "starred": "$starred" },
    "pipeline": [
      { "$match": {
        "$expr": {
          "$setIsSubset": [ 
            "$$starred",
            { "$reduce": {
              "input": "$faculties.subjects.id",
              "initialValue": [],
              "in": { "$concatArrays": [ "$$value", "$$this" ] }
            }}
          ]
        }
      }},
      { "$project": {
        "_id": 0,
        "subjects": {
          "$filter": {
            "input": {
              "$reduce": {
                "input":  "$faculties.subjects",
                "initialValue": [],
                "in": { "$concatArrays": [ "$$value", "$$this" ] }
              }
            },
            "cond": { "$in": [ "$$this.id", "$$starred" ] }
          }
        }
      }},
      { "$unwind": "$subjects" },
      { "$replaceRoot": { "newRoot": "$subjects" } }
    ],
    "as": "subjects"
  }}
])

这两种方法基本上都依赖于$reduce$concatArrays,以便将“嵌套数组”内容“扁平化”为可用于比较的形式。两者之间的主要区别在于MongoDB 3.6之前你基本上是从文档中提取所有“可能的”匹配,然后你可以做任何关于将内部数组条目“过滤”到只有那些匹配的内容。

使用$reduce$in运算符至少没有MongoDB 3.4,那么你实际上是在诉诸$unwind

db.users.aggregate([
   { "$match": { "name": "Michael" } },
   { "$lookup": {
     "from": "schools",
     "localField": "starred",
     "foreignField": "faculties.subjects.id",
     "as": "subjects"
   }},
   { "$unwind": "$subjects" },
   { "$unwind": "$subjects.faculties" },
   { "$unwind": "$subjects.faculties.subjects" },
   { "$redact": {
      "$cond": {
        "if": {
          "$setIsSubset": [
            ["$subjects.faculties.subjects.id"],
            "$starred"
          ]
        },
        "then": "$$KEEP",
        "else": "$$PRUNE"
      }
   }},
   { "$group": {
      "_id": "$_id",
      "id": { "$first": "$id" },
      "name": { "$first": "$name" },
      "starred": { "$first": "$starred" },
      "subjects": { "$push": "$subjects.faculties.subjects" }
   }}
])

当然使用$redact阶段来过滤逻辑比较,因为只有$expr与MongoDB 3.6和$setIsSubset进行比较才能与"starred"数组进行比较。

然后当然由于所有$unwind操作,您通常需要$group才能改造数组。

或者从另一个方向执行$lookup

db.schools.aggregate([
  { "$unwind": "$faculties" },
  { "$unwind": "$faculties.subjects" },
  { "$lookup": {
    "from": "users",
    "localField": "faculties.subjects.id",
    "foreignField": "starred",
    "as": "users"
  }},
  { "$unwind": "$users" },
  { "$match": { "users.name": "Michael" } },
  { "$group": {
    "_id": "$users._id",
    "id": { "$first": "$users.id" },
    "name": { "$first": "$users.name" },
    "starred": { "$first": "$users.starred" },
    "subjects": {
      "$push": "$faculties.subjects"
    }    
  }}
])

最后一种形式并不理想,因为在$lookup完成之后(或技术上讲“在”$lookup期间),您不会过滤“用户”。但无论如何,它首先需要与整个“学校”系列合作。

所有表单都返回相同的输出:

{
    "_id" : ObjectId("5aea649526a94676bb981df4"),
    "id" : 1,
    "name" : "Michael",
    "starred" : [
            1,
            2
    ],
    "subjects" : [
        {
                "id" : 1,
                "name" : "sub1"
        },
        {
                "id" : 2,
                "name" : "sub2"
        }

    ]
}

您只有来自相关文档的"subjects"内部数组的详细信息,该数组实际上与当前用户的"starred"值匹配。

所有这些都表明,使用MongoDB“嵌套数组”并不是一个好主意。在MongoDB 3.6之前,您甚至无法执行atomic updates of "nested arrays",即使有允许它的更改,它仍然“困难”最好进行任何查询操作,尤其是那些涉及连接和过滤的操作。

构建“嵌套数组”是一个常见的“新手”错误,因为你似乎认为你正在“更好地”组织“事物”。但事实上,它更像是一种“反模式”,你真的应该考虑一种“更平坦”的结构,例如:

{
    "_id" : ObjectId("5aea651326a94676bb981df5"),
    "id" : 1,
    "name" : "Uni",
    "subjects" : [
        {
                "id" : 1,
                "name" : "sub1",
                "facultyId": 1000,
                "facultyName": "faculty1"
        },
        {
                "id" : 2,
                "name" : "sub2",
                "facultyId": 1000,
                "facultyName": "faculty1"

        },
        {
                "id" : 3,
                "name" : "sub3",
                "facultyId": 1000,
                "facultyName": "faculty1"

        }
    ]
}

“很多”更容易使用,当然还可以根据需要执行“加入”。