首页 > 解决方案 > 重写嵌套数组中的属性

问题描述

我有这样的结构:

{
    "_id" : ObjectId("5af16cfdb508cf1a30ed0b38"),
    "messages" : [
        {
            "publicMessage" : {
                "message" : {
                    "includedMessages" : [
                        {
                            "image" : {
                                "url" : {
                                    "url" : "umT6Gsx6yO.jpg"
                                }
                            }
                        }
                    ]
                }
            }
        }
    ]
}

我犯了一个错误,将所有内容都存储在 image.url.url 而不是 image.url 中。如何将其移至图像根目录?当然有很多这样的文件,并不是每个人都有 image.url.url 所以需要用“where”更新所有文件。我试过这个:

db.test.aggregate(
[
    { "$addFields": { 
        "messages.publicMessage.message.includedMessages.image.url": "$messages.publicMessage.message.includedMessages.image.url.url" 
    }},
    { "$out": "test" }
    ]
)

但这是错误的并输出:

{
"_id" : ObjectId("5af16cfdb508cf1a30ed0b38"),
"messages" : [
    {
        "publicMessage" : {
            "message" : {
                "includedMessages" : [
                    {
                        "image" : {
                            "url" : [
                                [
                                    "umT6Gsx6yO.jpg"
                                ]
                            ]
                        }
                    }
                ]
            }
        }
    }
]

}

标签: mongodbaggregation-framework

解决方案


你想要$map在这里,因为它可以处理每个数组元素并重写内容。这也是一个真正的问题,它分为三个部分,尽管以最简单的形式,它只是由第一个列表处理。

映射数组

db.test.aggregate([
  { "$addFields": {
    "messages": {
      "$map": {
        "input": "$messages",
        "as": "m",
        "in": {
          "publicMessage": {
             "message": {
               "includedMessages": {
                 "$map": {
                   "input": "$$m.publicMessage.message.includedMessages",
                   "as": "i",
                   "in": {
                     "image": {
                       "url": "$$i.image.url.url"
                     }
                   }
                 }
               }
             }
           }
        }
      }
    }
  }},
  { "$out": "newtest" }
])

这会将文档返回为:

{
    "_id" : ObjectId("5af16cfdb508cf1a30ed0b38"),
    "messages" : [
        {
            "publicMessage" : {
                "message" : {
                    "includedMessages" : [
                        {
                            "image" : {
                                "url" : "umT6Gsx6yO.jpg"
                            }
                        }
                    ]
                }
            }
        }
    ]
}

简单投影只会作用于数组的最外层元素,并输出“作为数组”投影的所有内容。因此,您使用$map来处理每个数组。

合并嵌套对象

请注意,如果嵌套数组中的文档中实际上有更多字段,则需要在每个数组中“显式”指定这些字段,$map因为这实际上是用您指定的新内容重写每个数组成员。

如果您实际上有 MongoDB 3.6,那么您可以交替使用$mergeObjects运算符,而不是显式指定每个键和值。但是,您需要为“每个”嵌套级别执行此操作,因为您不能像使用 一样只使用“点字段路径” $addFields,而且“显式”指定每个键和值实际上可能要简单得多:

db.test.aggregate([
  { "$addFields": {
    "messages": {
      "$map": {
        "input": "$messages",
        "as": "m",
        "in": {
          "$mergeObjects": [
            "$$m",
            {
              "publicMessage": {
                "$mergeObjects": [
                  "$$m.publicMessage",
                  {
                    "message": {
                      "$mergeObjects": [
                        "$$m.publicMessage.message",
                        {
                          "includedMessages": {
                            "$map": {
                              "input": "$$m.publicMessage.message.includedMessages",
                              "as": "i",
                              "in": {
                                "$mergeObjects": [
                                  "$$i",
                                  { "image": { "url": "$$i.image.url.url" } }
                                ]
                              }
                            }
                          }
                        }
                      ]
                    }
                  }
                ]
              }
            }
          ]
        }
      }
    }
  }},
  { "$out": "newtest" }
])

写入输出

您可以添加$out到管道以写入新集合,因为您无法写入正在读取的“相同集合”,或者用于bulkWrite()重写现有集合的元素:

var batch = [];

db.test.aggregate([
  { "$addFields": {
    "messages": {
      "$map": {
        "input": "$messages",
        "as": "m",
        "in": {
          "publicMessage": {
             "message": {
               "includedMessages": {
                 "$map": {
                   "input": "$$m.publicMessage.message.includedMessages",
                   "as": "i",
                   "in": {
                     "image": {
                       "url": "$$i.image.url.url"
                     }
                   }
                 }
               }
             }
           }
        }
      }
    }
  }}
]).forEach(doc => {
  doc.messages.forEach((m,msgIdx) => {
    m.publicMessage.message.includedMessages.forEach((i,includeIdx) => {
      batch.push({
        "updateOne": {
          "filter": { "_id": doc._id },
          "update": {
            "$set": {
              [`messages.${msgIdx}.publicMessage.message.includedMessages.${includeIdx}.image.url`]:
                i.image.url
            }
          }
        }
      });
    });
    if (batch.length >= 1000) {
      db.test.bulkWrite(batch);
      batch = [];
    }
  });
});

if (batch.length >= 0) {
  db.test.bulkWrite(batch);
  batch = [];
}

请注意,当“编写”使用bulkWrite()时,不必担心诸如$mergeObjects或指定所有其他可能的嵌套键之类的事情,因为唯一改变集合的是实际的“更新”语句。事实上,由于聚合的唯一目的是将返回的数据“减少”为提供更新所需的形式,因此实际上希望返回“更少”而不是完整的文档。


关于“嵌套”数组的附录

嵌套数组并不是一个好主意,因为那里的写入表明,除了使用静态数组索引以便以原子方式将新条目写入数组而不覆盖所有其他现有内容之外,您实际上没有太多选择。

通常,您需要一个“更扁平”的结构,即使在最好的情况下,您仍然需要每个数组级别上的唯一标识符,如下所示:

{
    "_id" : ObjectId("5af16cfdb508cf1a30ed0b38"),
    "messages" : [
        {
            "_id" : ObjectId("5b11e6b3492daf3e5df114b0"),
            "publicMessage" : {
                "message" : {
                    "includedMessages" : [
                        {
                            "_id" : ObjectId("5b11e6b3492daf3e5df114b1"),
                            "image" : {
                                "url" : {
                                    "url" : "umT6Gsx6yO.jpg"
                                }
                            }
                        }
                    ]
                }
            }
        }
    ]
}

只要有一种方法可以唯一地匹配每个元素,那么您至少有机会执行不依赖于索引位置的原子更新,并假设数组内容没有随其他条目而改变:

var batch = [];

db.test.aggregate([
  { "$addFields": {
    "messages": {
      "$map": {
        "input": "$messages",
        "as": "m",
        "in": {
          "_id": "$$m._id",
          "publicMessage": {
             "message": {
               "includedMessages": {
                 "$map": {
                   "input": "$$m.publicMessage.message.includedMessages",
                   "as": "i",
                   "in": {
                     "_id": "$$i._id",
                     "image": {
                       "url": "$$i.image.url.url"
                     }
                   }
                 }
               }
             }
           }
        }
      }
    }
  }}
]).forEach(doc => {
  var $set = { };
  var arrayFilters = [];

  doc.messages.forEach((m,mIdx) => {

    arrayFilters.push({ [`m${mIdx}._id`]: m._id });

    m.publicMessage.message.includedMessages.forEach((i,iIdx) => {
      arrayFilters.push({ [`i${mIdx+iIdx}._id`]: i._id });
      $set[`messages.$[m${mIdx}].publicMessage.message.includedMessages.$[i${mIdx+iIdx}].image.url`]
        = i.image.url;
    });
  });

  batch.push({
    "updateOne": {
      "filter": { "_id": doc._id },
      "update": { $set },
      arrayFilters
    }
  });

  if (batch.length >= 1000) {
    db.test.bulkWrite(batch);
    batch = [];
  }

})

if (batch.length > 0) {
  db.test.bulkWrite(batch);
  batch = [];
}

如果有一种方法可以唯一地匹配每个数组项并且您拥有支持位置过滤$[<identifier>]更新的 MongoDB 3.6,那么这将是可能的。它的写入次数更少并且不依赖于固定的索引位置,因为这里的脚本仅使用数组索引来命名位置更新的唯一标识符,但更新本身并不依赖于该索引位置。

即使有这样的支持,尽管“嵌套数组”是出了名的难以查询。因此,您真正应该考虑的是简单地拥有“一个级别”,其中包含您的所有内容"includedMesages",并简单地重复嵌套在每个项目上的父级的细节。这似乎与您所学过的有关非规范化的内容有悖常理,但更新和查询的便利性往往超过了重复的程度。

典型的“查询”涉及 和 的类似组合,$map这种组合$filter可能变得非常复杂,如果不完全移动到单独的集合,只需简单地“展平”数组结构就可以轻松避免。


推荐阅读