使用 MongoDB 更新嵌套数组

Posted

技术标签:

【中文标题】使用 MongoDB 更新嵌套数组【英文标题】:Updating a Nested Array with MongoDB 【发布时间】:2020-05-05 14:32:51 【问题描述】:

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

我的对象是这样的

 
    "_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,但它的回答只是说我应该使用类似结构的对象而不是数组。这不适用于我的情况。我真的需要我的对象嵌套在数组中

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

提前谢谢你!

【问题讨论】:

【参考方案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,但这在任何语言中都不难做到,但这通常是个坏主意。您通常不希望这样做,因为您不能保证在此代码进行更改时文档在服务器上没有更改。这就是为什么首选服务器“更新”操作的原因。

以上是关于使用 MongoDB 更新嵌套数组的主要内容,如果未能解决你的问题,请参考以下文章

更新 mongodb 中的嵌套数组

更新数组mongodb内的嵌套数组[重复]

mongoDB:更新嵌套数组元素

如何在 mongodb/mongoose 的嵌套数组中检查是不是存在并更新对象?

MongoDB - 基于嵌套数组的字段值更新数组对象中的字段

Mongodb:UpdateMany不更新嵌套数组数据