使用MongoDB更新嵌套数组

mas*_*nyo 30 javascript mongoose mongodb node.js mongodb-query

我试图更新嵌套数组中的值,但无法使其工作.

我的目标是这样的

 {
    "_id": {
        "$oid": "1"
    },
    "array1": [
        {
            "_id": "12",
            "array2": [
                  {
                      "_id": "123",
                      "answeredBy": [],
                  },
                  {
                      "_id": "124",
                      "answeredBy": [],
                  }
             ],
         }
     ]
 }
Run Code Online (Sandbox Code Playgroud)

我需要将值推送到"answersBy"数组.

在下面的例子中,我尝试将"success"字符串推送到"123 _id"对象的"answersBy"数组,但它不起作用.

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);
Run Code Online (Sandbox Code Playgroud)

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

如果你能在这里帮助我会很棒.我花了好几个小时来搞清楚这一点.

先感谢您!

Nei*_*unn 46

一般范围和解释

你在这里做的事情有些不对劲.首先是您的查询条件.您指的是_id您不应该需要的几个值,并且至少有一个值不在顶层.

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

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

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

重要的读物是在"嵌套阵列"主题下的位置$操作员的官方文档中.这说的是:

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

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

因此,如果你看一下如图所示的查询符号,我们就会"硬编码" 顶级数组中的第一个(或0个索引)位置,而"array2"中的匹配元素也是零索引条目.

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

这就是嵌套数组的一般问题.您可以删除其中一个级别,您仍然可以使用$push"顶部"数组中的正确元素,但仍然会有多个级别.

尝试避免嵌套数组,因为您将遇到更新问题,如图所示.

一般情况是"压扁"你"认为"是"水平"的东西,并在最后的细节项目上实际上做出"属性".例如,问题中结构的"扁平"形式应该是这样的:

 {
   "answers": [
     { "by": "success", "type2": "123", "type1": "12" }
   ]
 }
Run Code Online (Sandbox Code Playgroud)

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

 {
   "array": [
     { "type1": "12", "type2": "123", "answeredBy": ["success"] },
     { "type1": "12", "type2": "124", "answeredBy": [] }
   ]
 }
Run Code Online (Sandbox Code Playgroud)

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


MongoDB 3.6及以上版本

从MongoDB 3.6开始,有一些新功能可用于嵌套数组.这使用位置过滤$[<identifier>]语法来匹配特定元素,并arrayFilters在update语句中应用不同的条件:

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" }] 
  }
)
Run Code Online (Sandbox Code Playgroud)

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

因为结构是"嵌套的",所以我们实际使用"多个过滤器",如图所示,使用过滤器定义的"数组".标记的"标识符"用于匹配实际在语句的更新块中使用的位置过滤$[<identifier>]语法.在这种情况下inner,它outer是用于嵌套链指定的每个条件的标识符.

这种新的扩展使得嵌套数组内容的更新成为可能,但它并没有真正帮助"查询"这些数据的实用性,因此同样的警告适用于前面所解释的.

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

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

注意有点讽刺的是,因为这是在方法的"options"参数中指定的.update(),所以语法通常与所有最新的发行版驱动程序版本兼容.

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

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

另请参见positional all $[]哪些更新"多个数组元素"但不应用于指定的条件,并应用于数组中所需的操作所有元素.


Jes*_*sen 6

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

解决这个问题的方法是使用Sub-Documents.这是通过在模式中嵌套模式来完成的

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

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

Array2Schema = new mongoose.Schema({
   answeredBy": [...]
})
Run Code Online (Sandbox Code Playgroud)

这样,对象看起来就像您显示的那个,但现在每个数组都填充了子文档.这样就可以点到您想要的子文档中.而不是使用.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)
        });
    }
)
Run Code Online (Sandbox Code Playgroud)

没用过.push()函数这样我自己,所以语法可能是不对的,但我都用了.set().remove(),都运行完美.

  • 我认为值得添加以供参考和理解,有充分的理由说明为什么这可能不是解决问题的"更好"的答案.这里实质上发生的是从服务器"读取"文档,修改数据结构然后"写"回来.尽管使用了助手,但在任何语言中都不难做到,但这通常是一个坏主意.您通常不希望这样做,因为您无法保证在此代码进行更改时文档在服务器上未更改.这就是首选服务器"更新"操作的原因. (14认同)