使用 MongoDB 更新嵌套数组

Updating a Nested Array with MongoDB

提问人:masanorinyo 提问时间:5/10/2014 最后编辑:Saurav Sahumasanorinyo 更新时间:7/8/2021 访问量:42520

问:

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

我的对象是这样的

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

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

在下面的示例中,我尝试将“success”字符串推送到“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);

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

如果你能在这里帮助我,那就太好了。我花了几个小时来弄清楚这一点。

先谢谢你!

javascript node.js mongodb mongoose mongodb-query

评论


答:

91赞 Neil Lunn 5/10/2014 #1

一般范围和解释

你在这里所做的事情有一些问题。首先是您的查询条件。您指的是几个不需要的值,其中至少有一个不在顶层。_id

为了进入一个“嵌套”值,并假定该值是唯一的,不会出现在任何其他文档中,您的查询表单应如下所示:_id

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

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

重要的读物在“嵌套数组”主题下的位置 $ 运算符的官方文档中。这说的是:

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

具体来说,这意味着将在位置占位符中匹配和返回的元素是第一个匹配数组中的索引值。这意味着在您的情况下,“顶级”级别数组上的匹配索引。

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

为了演示这一点,您可以将匹配值更改为“124”,结果将是带有“123”的元素的新条目,因为它们都在“array1”的零索引条目中,并且是返回给占位符的值。_id$push_id

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

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

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

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

或者,即使接受内部数组仅$push,并且从不更新:

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

这两者都适合在位置 $ 运算符的范围内进行原子更新


MongoDB 3.6 及更高版本

从 MongoDB 3.6 开始,有一些新功能可用于处理嵌套数组。它使用位置过滤的 $[<identifier>] 语法来匹配特定元素并在 update 语句中应用不同的条件: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" }] 
  }
)

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

由于该结构是“嵌套的”,因此我们实际上使用“多个过滤器”,如所示,使用过滤器定义的“数组”指定。标记的“identifier”用于与语句的更新块中实际使用的位置过滤的 $[<identifier>] 语法进行匹配。在本例中,是用于嵌套链指定的每个条件的标识符。innerouter

这种新的扩展使嵌套数组内容的更新成为可能,但它并没有真正帮助“查询”此类数据的实用性,因此如前所述,同样的警告也适用。

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

另请参阅如何在mongodb中更新多个数组元素,因为这些新的更新运算符实际上匹配并更新“多个数组元素”,而不仅仅是第一个,这是位置更新的先前操作。

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

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

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

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

评论

0赞 Nir 7/29/2018
谢谢,您对arrayFilters的解释比mongoDB站点文档要好得多。他们只是没有演示嵌套数组更新的工作原理。
0赞 Daggie Blanqx - Douglas Mwangi 6/5/2020
哇!你是个天才!你的解释是如此清晰明了。谢谢!
8赞 Jesper Pannerup 10/22/2014 #2

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

解决此问题的一种方法是使用子文档。这是通过在架构中嵌套架构来完成的

MainSchema = new mongoose.Schema({
   array1: [Array1Schema]
})

Array1Schema = new mongoose.Schema({
   array2: [Array2Schema]
})

Array2Schema = new mongoose.Schema({
   answeredBy": [...]
})

这样一来,对象将看起来像您显示的对象,但现在每个数组都填充了子文档。这样就可以点缀到您想要的子文档中。然后,您不使用 a,而是使用 or 来获取要更新的文档。.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()

评论

24赞 Neil Lunn 1/1/2015
我想值得补充一下以供参考和理解,这可能不是该问题的“更好”答案是有充分理由的。这里本质上发生的事情是从服务器“读取”文档,对数据结构进行修改,然后“写回”。尽管使用了帮助程序,但这在任何语言中都不难做到,但这通常是一个坏主意。您通常不希望这样做,因为您不能保证在此代码进行更改时,文档在服务器上没有更改。这就是为什么首选服务器“更新”操作的原因。