【问题标题】:Updating a Nested Array with MongoDB使用 MongoDB 更新嵌套数组
【发布时间】:2018-10-22 20:45:54
【问题描述】:

我正在尝试更新嵌套数组中的值,但无法使其正常工作。

我的对象是这样的

 {
    "_id": {
        "$oid": "1"
    },
    "array1": [
        {
            "_id": "12",
            "array2": [
                  {
                      "_id": "123",
                      "answeredBy": [],   // need to push "success" 
                  },
                  {
                      "_id": "124",
                      "answeredBy": [],
                  }
             ],
         }
     ]
 }

我需要将一个值推送到“answeredBy”数组。

在下面的示例中,我尝试将“成功”字符串推送到“123 _id”对象的“answeredBy”数组,但它不起作用。

callback = function(err,value){
     if(err){
         res.send(err);
     }else{
         res.send(value);
     }
};
conditions = {
    "_id": 1,
    "array1._id": 12,
    "array2._id": 123
  };
updates = {
   $push: {
     "array2.$.answeredBy": "success"
   }
};
options = {
  upsert: true
};
Model.update(conditions, updates, options, callback);

我找到了这个link,但它的回答只是说我应该使用类似结构的对象而不是数组。这不适用于我的情况。我真的需要我的对象嵌套在数组中

如果你能在这里帮助我,那就太好了。我一直在花费数小时来解决这个问题。

提前谢谢你!

【问题讨论】:

    标签: javascript node.js mongodb mongoose mongodb-query


    【解决方案1】:

    一般范围和说明

    您在此处执行的操作存在一些问题。首先是您的查询条件。您指的是几个您不需要的 _id 值,并且至少其中一个不在顶层。

    为了进入“嵌套”值并假设_id 值是唯一的并且不会出现在任何其他文档中,您的查询表单应该是这样的:

    Model.update(
        { "array1.array2._id": "123" },
        { "$push": { "array1.0.array2.$.answeredBy": "success" } },
        function(err,numAffected) {
           // something with the result in here
        }
    );
    

    现在这实际上会起作用,但实际上它只是一个侥幸,因为有很好的理由说明它不适合你。

    重要的阅读是在“嵌套数组”主题下的positional $ 运算符的官方文档中。这是什么意思:

    位置 $ 运算符不能用于遍历多个数组的查询,例如遍历嵌套在其他数组中的数组的查询,因为 $ 占位符的替换是单个值

    具体而言,这意味着将在位置占位符中匹配并返回的元素是 first 匹配数组中的索引值。这意味着在您的情况下,“顶级”数组上的匹配索引。

    因此,如果您查看所示的查询符号,我们已经“硬编码”了顶级数组中的 first (或 0 索引)位置,恰好匹配的元素在“array2”也是零索引条目。

    为了证明这一点,您可以将匹配的_id 值更改为“124”,结果将$push 一个新条目添加到具有_id“123”的元素上,因为它们都在“”的零索引条目中array1",这是返回给占位符的值。

    这就是嵌套数组的普遍问题。您可以删除其中一个级别,您仍然可以 $push 到“顶部”数组中的正确元素,但仍然会有多个级别。

    尽量避免嵌套数组,因为你会遇到更新问题,如图所示。

    一般情况是把你“认为”的东西“扁平化”为“层次”,实际上使这些“属性”在最终的细节项目上。例如,问题中结构的“扁平化”形式应该是这样的:

     {
       "answers": [
         { "by": "success", "type2": "123", "type1": "12" }
       ]
     }
    

    或者即使接受内部数组只是$push,并且永远不会更新:

     {
       "array": [
         { "type1": "12", "type2": "123", "answeredBy": ["success"] },
         { "type1": "12", "type2": "124", "answeredBy": [] }
       ]
     }
    

    这两者都适合positional $ operator范围内的原子更新


    MongoDB 3.6 及以上版本

    从 MongoDB 3.6 开始,有一些新功能可用于处理嵌套数组。这使用positional filtered $[<identifier>] 语法来匹配特定元素并通过更新语句中的arrayFilters 应用不同的条件:

    Model.update(
      {
        "_id": 1,
        "array1": {
          "$elemMatch": {
            "_id": "12","array2._id": "123"
          }
        }
      },
      {
        "$push": { "array1.$[outer].array2.$[inner].answeredBy": "success" }
      },
      {
        "arrayFilters": [{ "outer._id": "12" },{ "inner._id": "123" }] 
      }
    )
    

    "arrayFilters" 传递给.update() 的选项,甚至 .updateOne().updateMany().findOneAndUpdate().bulkWrite() 方法指定匹配更新语句中给出的标识符的条件。任何符合给定条件的元素都会被更新。

    因为结构是“嵌套的”,所以我们实际上使用“多个过滤器”,正如所示的过滤器定义的“数组”所指定的那样。标记的“标识符”用于匹配语句更新块中实际使用的positional filtered $[<identifier>] 语法。在这种情况下,innerouter 是用于嵌套链中指定的每个条件的标识符。

    这个新的扩展使得嵌套数组内容的更新成为可能,但它对“查询”此类数据的实用性并没有真正的帮助,因此与前面解释的相同的警告也适用。

    你通常真的“意思”来表达“属性”,即使你的大脑最初认为是“嵌套”,它通常只是对你认为“以前的关系部分”如何组合在一起的反应。实际上,您确实需要更多的非规范化。

    另请参阅How to Update Multiple Array Elements in mongodb,因为这些新的更新运算符实际上匹配和更新“多个数组元素”,而不仅仅是第一个,后者是之前的位置更新操作。

    注意有点讽刺的是,由于这是在 .update() 和类似方法的“选项”参数中指定的,因此语法通常与所有最新发布的驱动程序版本兼容。

    但是,mongo shell 并非如此,因为该方法在那里实现的方式(“具有讽刺意味的是,为了向后兼容”)arrayFilters 参数无法被解析选项的内部方法识别和删除为了提供与先前 MongoDB 服务器版本的“向后兼容性”和“遗留”.update() API 调用语法。

    因此,如果您想在 mongo shell 或其他“基于 shell”的产品(尤其是 Robo 3T)中使用该命令,您需要开发分支或生产版本中 3.6 或更高版本的最新版本。

    另请参阅positional all $[],它也更新“多个数组元素”,但不应用于指定条件,并应用于数组中所需操作的所有元素。

    【讨论】:

    • 谢谢,您对 arrayFilters 的解释比 mongoDB 站点文档要好得多。他们只是没有演示嵌套数组更新是如何工作的。
    • 哇!你是个天才!你的解释是如此清晰和直截了当。谢谢!
    【解决方案2】:

    我知道这是一个非常古老的问题,但我自己只是在努力解决这个问题,并找到了我认为的更好的答案。

    解决此问题的一种方法是使用Sub-Documents。这是通过在您的架构中嵌套架构来完成的

    MainSchema = new mongoose.Schema({
       array1: [Array1Schema]
    })
    
    Array1Schema = new mongoose.Schema({
       array2: [Array2Schema]
    })
    
    Array2Schema = new mongoose.Schema({
       answeredBy": [...]
    })
    

    这样,对象将看起来像您显示的对象,但现在每个数组都填充了子文档。这使得您可以将自己的方式添加到您想要的子文档中。而不是使用.update,而是使用.find.findOne 来获取要更新的文档。

    Main.findOne((
        {
            _id: 1
        }
    )
    .exec(
        function(err, result){
            result.array1.id(12).array2.id(123).answeredBy.push('success')
            result.save(function(err){
                console.log(result)
            });
        }
    )
    

    我自己没有使用过.push() 函数,所以语法可能不正确,但我同时使用了.set().remove(),两者都工作得很好。

    【讨论】:

    • 我认为值得添加以供参考和理解,有充分的理由说明这可能不是问题的“更好”答案。这里本质上发生的是从服务器“读取”文档,对数据结构进行修改,然后“写”回。尽管使用了 helper,但这在任何语言中都不难做到,但这通常是个坏主意。您通常不希望这样做,因为您不能保证在此代码进行更改时文档在服务器上没有更改。这就是为什么首选服务器“更新”操作的原因。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多