使用 MongoDB 更新嵌套数组

IT技术 javascript node.js mongodb mongoose mongodb-query
2021-01-26 01:04:22

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

我的对象是这样的

 {
    "_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);

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

如果你能在这里帮助我,那就太好了。我已经花了几个小时来解决这个问题。

先感谢您!

2个回答

一般范围和说明

你在这里做的事情有一些问题。首先是您的查询条件。您指的是几个_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”中的匹配元素也是零索引条目。

为了证明这一点,您可以将匹配_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": [] }
   ]
 }

两者都适用于位置运算符范围内的原子更新$


MongoDB 3.6 及以上

从 MongoDB 3.6 开始,有新功能可用于处理嵌套数组。这使用位置过滤$[<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()方法指定的条件匹配的更新语句中给出的标识符。任何符合给定条件的元素都将被更新。

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

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

你通常真的“刻意”表达为“属性”,即使你的大脑最初认为“嵌套”,它通常只是对你如何相信“先前的相关部分”聚集在一起的react。实际上,您确实需要更多的非规范化。

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

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

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

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

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

哇!你是个天才!你的解释是如此清晰和直截了当。谢谢!
2021-03-27 01:04:22
谢谢,您对 arrayFilters 的解释比 mongoDB 站点文档要好得多。他们只是没有演示嵌套数组更新是如何工作的。
2021-04-06 01:04:22

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

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

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

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

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

这样,对象将看起来像您显示的对象,但现在每个数组都填充了子文档。这使得您可以将自己的方式添加到您想要的子文档中。而不是使用 a.update然后使用 a .findor.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(),并且两者都可以正常工作。

我认为值得添加以供参考和理解,有充分的理由说明为什么这可能不是问题的“更好”答案。这里发生的主要事情是从服务器“读取”文档,修改数据结构,然后“写”回。尽管使用了助手,但这在任何语言中都不难做到,但这通常是一个坏主意。您通常不想这样做,因为您无法保证在此代码进行更改时服务器上的文档没有更改。这就是为什么首选服务器“更新”操作的原因。
2021-03-21 01:04:22