从具有多级数组的 json 创建一个复杂的 mongodb 查询

create a complex mongodb query from a json with multi-level array

我有这个 json :

{
  "_id": "id",
  "field1": "value1",
  "field2": "value2",
  "field3": "value3",
  "field4": "value4",
  "field5": "value5",
  "field6": [
    {
      "field7": "value_a7_level1",
      "field8": "value_a8_level1",
      "field9": "value_a9_level1",
      "field10": [
        {
          "field11": "value_a11_level1",
          "field12": "value_a12_level1",
          "field13": "value_a13_level1",
          "field14": "value_a14_level1"
        },
        {
          "field11": "value_b11_level1",
          "field12": "value_b12_level1",
          "field13": "value_b13_level1",
          "field14": "value_b14_level1"
        }
      ],
      "field15": [
        {
          "field16": "zzz",
          "field17": "xxx",
          "field18": "value_a18_level1",
          "field19": "value_a19_level1"
        },
        {
          "field16": "xxx",
          "field17": "yyy",
          "field18": "value_b18_level1",
          "field19": "value_b19_level1"
        },
        {
          "field16": "xxx",
          "field17": "yyy",
          "field18": "value_c18_level1",
          "field19": "value_c19_level1"
        }
      ]
    },
    {
      "field7": "value_a7_level2",
      "field8": "value_a8_level2",
      "field9": "value_a9_level2",
      "field10": [
        {
          "field11": "value_a11_level2",
          "field12": "value_a12_level2",
          "field13": "value_a13_level2",
          "field14": "value_a14_level2"
        },
        {
          "field11": "value_b11_level2",
          "field12": "value_b12_level2",
          "field13": "value_b13_level2",
          "field14": "value_b14_level2"
        }
      ],
      "field15": [
        {
          "field16": "value_a16_level2",
          "field17": "value_a17_level2",
          "field18": "value_a18_level2",
          "field19": "value_a19_level2"
        },
        {
          "field16": "value_b16_level2",
          "field17": "value_b17_level2",
          "field18": "value_b18_level2",
          "field19": "value_b19_level2"
        },
        {
          "field16": "value_c16_level2",
          "field17": "value_c17_level2",
          "field18": "value_c18_level2",
          "field19": "value_c19_level2"
        }
      ]
    },
    {
      "field7": "value_a7_level3",
      "field8": "value_a8_level3",
      "field9": "value_a9_level3",
      "field10": [
        {
          "field11": "value_a11_level3",
          "field12": "value_a12_level3",
          "field13": "value_a13_level3",
          "field14": "value_a14_level3"
        },
        {
          "field11": "value_b11_level3",
          "field12": "value_b12_level3",
          "field13": "value_b13_level3",
          "field14": "value_b14_level3"
        }
      ],
      "field15": [
        {
          "field16": "xxx",
          "field17": "yyy",
          "field18": "value_a18_level3",
          "field19": "value_a19_level3"
        },
        {
          "field16": "value_b16_level3",
          "field17": "value_b17_level3",
          "field18": "value_b18_level3",
          "field19": "value_b19_level3"
        },
        {
          "field16": "value_c16_level3",
          "field17": "value_c17_level3",
          "field18": "value_c18_level3",
          "field19": "value_c19_level3"
        }
      ]
    }
  ],
  "field20": [
    {
      "field21": "value21_level1",
      "field22": "value22_level1",
      "field23": "value23_level1"
    },
    {
      "field21": "value21_level2",
      "field22": "value22_level2",
      "field23": "value23_level2"
    }
  ]
}

我想 return 根据过滤器和选择得到结果。 过滤器例如:

field16 和 field17 必须分别等于“XXX”和“YYY”。

我需要 return 一个 json 将包含所有字段,但 field15 将仅包含与过滤器对应的对象。此外,field6 将仅包含 chemp15 return 符合过滤器的对象。

在我的示例中,我将得到以下结果:

{
    "_id": "id",
    "field1": "value1",
    "field2": "value2",
    "field3": "value3",
    "field4": "value4",
    "field5": "value5",
    "field6": [
      {
        "field7": "value_a7_level1",
        "field8": "value_a8_level1",
        "field9": "value_a9_level1",
        "field10": [
          {
            "field11": "value_a11_level1",
            "field12": "value_a12_level1",
            "field13": "value_a13_level1",
            "field14": "value_a14_level1"
          },
          {
            "field11": "value_b11_level1",
            "field12": "value_b12_level1",
            "field13": "value_b13_level1",
            "field14": "value_b14_level1"
          }
        ],
        "field15": [
          {
            "field16": "xxx",
            "field17": "yyy",
            "field18": "value_b18_level1",
            "field19": "value_b19_level1"
          },
          {
            "field16": "xxx",
            "field17": "yyy",
            "field18": "value_c18_level1",
            "field19": "value_c19_level1"
          }
        ]
      },
      {
        "field7": "value_a7_level3",
        "field8": "value_a8_level3",
        "field9": "value_a9_level3",
        "field10": [
          {
            "field11": "value_a11_level3",
            "field12": "value_a12_level3",
            "field13": "value_a13_level3",
            "field14": "value_a14_level3"
          },
          {
            "field11": "value_b11_level3",
            "field12": "value_b12_level3",
            "field13": "value_b13_level3",
            "field14": "value_b14_level3"
          }
        ],
        "field15": [
          {
            "field16": "xxx",
            "field17": "yyy",
            "field18": "value_a18_level3",
            "field19": "value_a19_level3"
          }
        ]
      }
    ],
    "field20": [
      {
        "field21": "value21_level1",
        "field22": "value22_level1",
        "field23": "value23_level1"
      },
      {
        "field21": "value21_level2",
        "field22": "value22_level2",
        "field23": "value23_level2"
      }
    ]
  }

我尝试了几种方法来得到这个结果,但都没有成功。这是最后一个,但我一点都不满意,因为结果结构不合理:

db.requirements.aggregate([
    {
        $match: {
            "field6": {
                $elemMatch: {
                    "field15": {
                        $elemMatch: {
                            "field16": "xxx",
                            "field17": "yyy",
                        }
                    }
                }
            }
        }
    },
    {
        $addFields: {
            "field6": {
                $map: {
                    input: "$field6",
                    as: "f6",
                    in: {
                        $filter: {
                            input: "$$f6.field15",
                            as: "f15",
                            cond: {$and: [
                                {$eq: ["$$f15.f16", "xxx"]},
                                {$eq: ["$$f15.f17", "yyy"]}
                            ]}
                        }
                    }
                }
            }
        },
    }
]);

我也尝试使用 $unwind 和 $group,但它 return 不是我想要的 json。

谁能帮我找到解决办法?

预先感谢您的回答。

使用$reduce作为循环。

db.foo.aggregate([
    // In this strategy, we walk the field6 array with $reduce and                                                          
    // "rebuild it" with either a filtered field15 or no entry at all.                                                      
    {$addFields: {"field6": {$reduce: {
        input: "$field6",
        initialValue: [], // important: start rebuild with empty array                                                      
            in: {$let: {
                vars: {ee: {$filter: {input: "$$this.field15", as: "z",
                                      cond: {$and:[ {$eq:["$$z.field16","xxx"]},
                                                    {$eq:["$$z.field17","yyy"]}
                                                  ]}
                                     }}
                      },

                 in: {$cond: [
                     {$ne:[0,{$size: "$$ee"}]}, // IF ee is not size 0                                                      
                
                     // THEN append an entry with filter field15                                                            
                     // plus its peer fields.  Since we cannot directly                                                     
                     // say "$$this.field15 = $ee", we use $mergeObjects                                                    
                     // to overlay field15:$$ee onto the existing object                                                    
                     // with the peer fields field7, field8, field10, etc.                                                           
                     // $concatArrays wants arrays, not objects, so wrap                                                    
                     // it in [] to make an array of one:                                                                   
                     {$concatArrays: [ "$$value",
                         [ {$mergeObjects: [ "$$this", {field15: "$$ee"} ]} ]
                                     ]},

                     // ELSE no concat; just pass back the existing array:                                                  
                     "$$value"
                 ]}
        }}  
        }}
    }}

]);

或者,如果需要对 field6 应用更多条件,则可能更容易先在 field6$unwind 以隔离对该文档内字段的进一步操作。但是请注意,如果 field6 是一个冗长的数组,$unwind$group 可能会对性能产生影响。

db.foo.aggregate([
    // Get us down to dealing with only one array:
    {$unwind: "$field6"}

    // Overwrite field6.field15 with filtered version of same:
    ,{$addFields: {"field6.field15":
                   {$filter: {input: "$field6.field15",
                     as: "z2",
                     cond: {$and:[ {$eq:["$$z2.field16","xxx"]},
                                   {$eq:["$$z2.field17","yyy"]}
                                 ]}
                   }}
    }}
 
    // .. and eliminate those that have NO xxx/yyy in field16 and field17:
    ,{$match: {"field6.field15": {$ne:[]} }}

    // You might be good enough at this point, but if you really want to reform the
    // shape with an array for field6, use $group to put it back together.
    // Using $first on all the other peer fields to field6 is a bit ungainly, yes, but
    // it does produce the desired result:
    ,{$group: {_id:"$_id",
               "field1": {$first: "$field1"},
               "field2": {$first: "$field2"},
               "field3": {$first: "$field3"},
               "field4": {$first: "$field4"},
               "field5": {$first: "$field5"},
               "field6": {$push: "$field6"} // ah!  Rebuild array
          }}
]);

也许这就是您要找的:

     db.collection.aggregate([
         {
         "$addFields": {
            "field6": {
                     "$filter": {
                              "input": {
                                    "$map": {
                                     "input": "$field6",
                                     "as": "f6",
                                     "in": {
                                     "$cond": [
                                               true,
                                                       {
                                            "field7": "$$f6.field7",
                                            "field8": "$$f6.field8",
                                            "field9": "$$f6.field9",
                                            "field10": "$$f6.field10",
                                            "field15": {
                                                       "$filter": {
                                                       "input": "$$f6.field15",
                                                      "as": "f15",
                              "cond": {
                               $and: [
                              {
                                $eq: [
                                   "$$f15.field16",
                                      "xxx"
                                     ]
                               },
                                    {
                                $eq: [
                                     "$$f15.field17",
                                      "yyy"
                                     ]
                                 }
                             ]
                          }
                        }
                       }
                      },
                      false
                       ]
                     }
                  }
                 },
                 "as": "cls",
                 "cond": {
                           $ne: [
                          "$$cls.field15",
                           []
                           ]
                         }
         }
         }
       }
       }
      ])

解释:

  1. 使用新的 filed6 创建一个 $addFields 阶段,它将覆盖基于原始 field6 上的地图过滤的旧文件
  2. 根据关于 f16 和 f17 的第二个过滤器向地图提供 filed15
  3. 从输出中移除第一个过滤器中的 f15 空数组,因为它们是在映射中为未找到匹配项的 f15 生成的

(当然你可以用 $elemMatch 添加你的初始 $match 阶段,我刚刚删除它以节省一些 space )

playground