如何在mongodb中更新多个数组元素


181

我有一个包含一系列元素的Mongo文档。

我想重置.handled其中.profile= XX 的数组中所有对象的属性。

该文件的格式如下:

{
    "_id": ObjectId("4d2d8deff4e6c1d71fc29a07"),
    "user_id": "714638ba-2e08-2168-2b99-00002f3d43c0",
    "events": [{
            "handled": 1,
            "profile": 10,
            "data": "....."
        } {
            "handled": 1,
            "profile": 10,
            "data": "....."
        } {
            "handled": 1,
            "profile": 20,
            "data": "....."
        }
        ...
    ]
}

因此,我尝试了以下方法:

.update({"events.profile":10},{$set:{"events.$.handled":0}},false,true)

但是,它仅更新每个文档中的第一个匹配数组元素。(这是$-位置运算符的定义行为。)

如何更新所有匹配的数组元素?


2
更新的子集或所有数组项已被添加到的MongoDB 3.6:docs.mongodb.com/manual/reference/operator/update/...
夏侯

确保检查出ar​​rayFilters并考虑使用哪个查询以使更新高效。查看尼尔·伦恩(Neil Lunn)的答案:stackoverflow.com/a/46054172/337401
Jaap

检查我的答案
Ucdemir

Answers:


111

目前,无法使用位置运算符更新数组中的所有项目。参见JIRA http://jira.mongodb.org/browse/SERVER-1243

作为一项工作,您可以:

  • 分别更新每个项目(events.0.handled events.1.handled ...)或...
  • 阅读文档,手动进行编辑,然后将其保存,以取代较旧的文档(如果要确保自动更新,请选中“如果最新,请更新”)

15
如果您有类似的问题,请为此问题投票-jira.mongodb.org/browse/SERVER-1243
LiorH 2011年

我实际上喜欢读取文档并保存的方法。但是我在Mongo之前使用了Couch,所以这种方法似乎更加自然,因为没有Couch的查询API,只有整个文档的REST api
亚当

1
这两种方法都需要大量的内存,对吗?如果有很多文档需要搜索,并且必须加载所有文档(或嵌套数组)以进行更新... +如果必须异步完成,则实现起来也会有些麻烦...
Ixx

12
除了所有技术上的困难,令人惊讶的是,该功能在MongoDB中不可用。此约束使自定义数据库架构的自由度大大降低。
成祖

5
Neil Lunn stackoverflow.com/a/46054172/337401针对3.6版回答了此问题。由于这是一个很普遍的问题,可能值得参考尼尔·伦恩的答案来更新此已接受的答案。
Jaap

71

随着MongoDB 3.6发布(可从MongoDB 3.5.12的开发分支中获得),您现在可以在单个请求中更新多个数组元素。

这使用了此版本中引入的过滤的位置$[<identifier>]更新运算符语法:

db.collection.update(
  { "events.profile":10 },
  { "$set": { "events.$[elem].handled": 0 } },
  { "arrayFilters": [{ "elem.profile": 10 }], "multi": true }
)

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

注意,在"multi"问题上下文中给定的,是希望这样做会“更新多个元素”,但事实并非如此,现在仍然不是。它的用法在这里适用于“多个文档”,就像往常一样或现在以其他方式将其指定为.updateMany()现代API版本中的强制性设置。

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

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

因此,如果要在mongoShell或其他“基于Shell的”产品(尤其是Robo 3T)中使用该命令,则需要从开发分支或生产版本开始的3.6或更高版本。

另请参见positional all $[]哪些还会更新“多个数组元素”,但不应用于指定条件,而是应用于数组中所有需要执行操作的元素。

另请参阅使用MongoDB更新嵌套数组,以了解这些新的位置运算符如何应用于“嵌套”数组结构,其中“数组在其他数组中”。

重要信息 -从早期版本升级的安装“可能”没有启用MongoDB功能,这也可能导致语句失败。您应确保升级过程已完成并包含索引升级等详细信息,然后运行

   db.adminCommand( { setFeatureCompatibilityVersion: "3.6" } )

或更高版本(适用于您安装的版本)。即"4.0"适用于第4版及更高版本。这启用了新的位置更新运算符等功能。您还可以通过以下方式进行检查:

   db.adminCommand( { getParameter: 1, featureCompatibilityVersion: 1 } )

返回当前设置


9
接受的答案应进行更新,并参考该答案。
Jaap

2
什么elem
user1063287

1
这是对的。请注意,RoboMongo尚不支持arrayFilters,因此通过CLI运行更新。stackoverflow.com/questions/48322834/…–
drlff

谢谢,尼尔,特别是对于重要部分,正是我所需要的
janfabian

此代码在pymongo中返回ERROR。错误是:引发TypeError(“%s必须为True或False”%(选项))TypeError:向上插入必须为True或False
Vagif

67

对我有用的是:

db.collection.find({ _id: ObjectId('4d2d8deff4e6c1d71fc29a07') })
  .forEach(function (doc) {
    doc.events.forEach(function (event) {
      if (event.profile === 10) {
        event.handled=0;
      }
    });
    db.collection.save(doc);
  });

我认为对于mongo新手以及熟悉JQuery和朋友的任何人来说都更加清楚。


我正在使用db.posts.find({ 'permalink':permalink }).forEach( function(doc) {...,正在获取Oops.. TypeError: Object # has no method 'forEach'
Squirrl 2013年

3
@Squirrl可能已经过时了mongodb版本?该文档明确说明了如何在游标上应用forEach函数,但未说明支持哪个版本。docs.mongodb.org/manual/reference/method/cursor.forEach
Daniel Cerecedo 2014年

@Squirrl尝试db.posts.find(...).toArray().forEach(...)
MARMOR

不使用就不能这样做Javascript吗?我想直接从mongo shell中执行此更新,而不使用Javascript API。
Meliodas

1
您能否在Java的mongodb驱动程序中或使用spring-data-mongodb编写此查询?谢谢,克里斯
-chiku

18

这也可以通过while循环来完成,该循环检查是否有剩余的文档仍然具有尚未更新的子文档。此方法保留了更新的原子性(此处的许多其他解决方案则没有)。

var query = {
    events: {
        $elemMatch: {
            profile: 10,
            handled: { $ne: 0 }
        }
    }
};

while (db.yourCollection.find(query).count() > 0) {
    db.yourCollection.update(
        query,
        { $set: { "events.$.handled": 0 } },
        { multi: true }
    );
}

循环执行的次数将等于集合中任何文档中出现子文档的最大次数,该子文档profile等于10 handled且不等于0。因此,如果您的集合中有100个文档,并且其中一个文档具有三个匹配的子文档,query而所有其他文档具有较少的匹配子文档,则循环将执行3次。

此方法避免了破坏脚本执行期间可能由另一个进程更新的其他数据的危险。它还最大程度地减少了客户端和服务器之间传输的数据量。


13

实际上,这确实与http://jira.mongodb.org/browse/SERVER-1243上的长期存在的问题有关,实际上,支持多数组匹配的“所有情况”的清晰语法存在许多挑战。找到了。实际上,已经有解决此问题的“方法”,例如在本原始帖子之后实施的批量操作

仍然不可能在单个update语句中更新多个匹配的数组元素,因此,即使使用“多”更新,您也只能在数组中为单个文档中的每个文档更新一个数学元素声明。

当前最好的解决方案是查找并循环所有匹配的文档并处理批量更新,这至少将允许在单个请求中以单个响应发送许多操作。您可以选择使用.aggregate()来将搜索结果中返回的数组内容减少为仅与更新选择条件匹配的内容:

db.collection.aggregate([
    { "$match": { "events.handled": 1 } },
    { "$project": {
        "events": {
            "$setDifference": [
               { "$map": {
                   "input": "$events",
                   "as": "event",
                   "in": {
                       "$cond": [
                           { "$eq": [ "$$event.handled", 1 ] },
                           "$$el",
                           false
                       ]
                   }
               }},
               [false]
            ]
        }
    }}
]).forEach(function(doc) {
    doc.events.forEach(function(event) {
        bulk.find({ "_id": doc._id, "events.handled": 1  }).updateOne({
            "$set": { "events.$.handled": 0 }
        });
        count++;

        if ( count % 1000 == 0 ) {
            bulk.execute();
            bulk = db.collection.initializeOrderedBulkOp();
        }
    });
});

if ( count % 1000 != 0 )
    bulk.execute();

.aggregate()当数组具有“唯一”标识符或每个元素的所有内容形成“唯一”元素本身时,将在该部分工作。这是由于使用了“ set”运算符$setDifference来过滤false$map用于处理数组以进行匹配的操作返回的任何值。

如果您的数组内容没有唯一元素,则可以尝试以下替代方法$redact

db.collection.aggregate([
    { "$match": { "events.handled": 1 } },
    { "$redact": {
        "$cond": {
            "if": {
                "$eq": [ { "$ifNull": [ "$handled", 1 ] }, 1 ]
            },
            "then": "$$DESCEND",
            "else": "$$PRUNE"
        }
    }}
])

局限性在于,如果“处理”实际上是要在其他文档级别存在的字段,那么您可能会得到意料之外的结果,但是如果该字段仅出现在一个文档位置并且是相等匹配项,那就很好了。

在撰写本文时,未来的发行版(3.1版之后的MongoDB)将具有一个$filter更简单的操作:

db.collection.aggregate([
    { "$match": { "events.handled": 1 } },
    { "$project": {
        "events": {
            "$filter": {
                "input": "$events",
                "as": "event",
                "cond": { "$eq": [ "$$event.handled", 1 ] }
            }
        }
    }}
])

并且所有支持的发行版都.aggregate()可以将以下方法与一起使用$unwind,但是由于管道中的数组扩展,该运算符的使用使其成为效率最低的方法:

db.collection.aggregate([
    { "$match": { "events.handled": 1 } },
    { "$unwind": "$events" },
    { "$match": { "events.handled": 1 } },
    { "$group": {
        "_id": "$_id",
        "events": { "$push": "$events" }
    }}        
])

在MongoDB版本支持聚合输出中的“游标”的所有情况下,这仅是选择一种方法并使用与显示用于处理批量更新语句的相同代码块对结果进行迭代的问题。批量操作和聚合输出中的“游标”在同一版本(MongoDB 2.6)中引入,因此通常会协同工作进行处理。

在甚至更早的版本中,最好只使用.find()返回游标,并过滤掉语句的执行,直到数组元素匹配.update()迭代的次数:

db.collection.find({ "events.handled": 1 }).forEach(function(doc){ 
    doc.events.filter(function(event){ return event.handled == 1 }).forEach(function(event){
        db.collection.update({ "_id": doc._id },{ "$set": { "events.$.handled": 0 }});
    });
});

如果您绝对地决定执行“多次”更新或认为最终比处理每个匹配文档的多次更新更有效,那么您始终可以确定可能的最大数组匹配数,而只需执行一次“多次”更新即可次,直到基本上没有更多文档可以更新。

适用于MongoDB 2.4和2.2版本的有效方法也可以.aggregate()用于查找此值:

var result = db.collection.aggregate([
    { "$match": { "events.handled": 1 } },
    { "$unwind": "$events" },
    { "$match": { "events.handled": 1 } },
    { "$group": {
        "_id": "$_id",
        "count": { "$sum": 1 }
    }},
    { "$group": {
        "_id": null,
        "count": { "$max": "$count" }
    }}
]);

var max = result.result[0].count;

while ( max-- ) {
    db.collection.update({ "events.handled": 1},{ "$set": { "events.$.handled": 0 }},{ "multi": true })
}

无论哪种情况,在更新中您都不想做某些事情:

  1. 不要“一枪”更新数组:如果您认为更新代码中的整个数组内容,然后更新$set每个文档中的整个数组可能更有效。这似乎可以更快地处理,但是不能保证自读取和执行更新以来,数组内容没有更改。尽管$set仍然是原子运算符,但它只会使用“认为”正确的数据来更新数组,因此可能会覆盖读写之间发生的任何更改。

  2. 不要计算要更新的索引值:与“一次性”方法类似,您只需确定位置0和位置2(等等)就是要进行更新和编码的元素,并最终声明如下:

    { "$set": {
        "events.0.handled": 0,
        "events.2.handled": 0
    }}

    同样,这里的问题是“推定”,即在读取文档时发现的那些索引值与更新时数组中的索引值相同。如果以更改顺序的方式将新项目添加到数组,则这些位置不再有效,并且实际上更新了错误的项目。

因此,在确定合理的语法以允许在单个update语句中处理多个匹配的数组元素之前,基本方法是要么更新单个语句中的每个匹配的数组元素(理想情况下为Bulk),要么实质上算出最大的数组元素更新或保持更新,直到没有更多修改结果返回为止。无论如何,您应该“始终” 对匹配的数组元素进行位置$更新,即使每个语句仅更新一个元素也是如此。

批量操作实际上是处理任何被计算为“多个操作”的操作的“通用”解决方案,并且由于有更多的应用程序,而不是仅更新具有相同值的多个数组元素,因此当然已经实现了已经是目前解决该问题的最佳方法。


8

我很惊讶这仍然没有在mongo中解决。在处理子数组时,整体mongo看起来并不好。例如,您不能简单地计算子数组。

我使用了哈维尔的第一个解决方案。将数组读入事件,然后循环遍历并构建set exp:

var set = {}, i, l;
for(i=0,l=events.length;i<l;i++) {
  if(events[i].profile == 10) {
    set['events.' + i + '.handled'] = 0;
  }
}

.update(objId, {$set:set});

可以使用条件测试的回调将其抽象为一个函数


谢谢你!不能相信此功能仍不受本地支持!用它来增加子数组的每一项,供其他阅读...更新每一项,只需删除if语句。
Zaheer 2014年

9
这不是一个安全的解决方案。如果在运行更新时添加了一条记录,则将破坏您的数据。
Merc 2014年

4

我一直在寻找针对C#3.6的最新驱动程序的解决方案,这是我最终确定的解决方案。这里的关键是使用“ $ []”,根据MongoDB,它是3.6版以来的新增功能。参见https://docs.mongodb.com/manual/reference/operator/update/positional-all/#up。S []了解更多信息。

这是代码:

{
   var filter = Builders<Scene>.Filter.Where(i => i.ID != null);
   var update = Builders<Scene>.Update.Unset("area.$[].discoveredBy");
   var result = collection.UpdateMany(filter, update, new UpdateOptions { IsUpsert = true});
}

有关更多上下文,请参见我的原始文章: 使用MongoDB C#驱动程序从所有文档中删除数组元素


4

线程很旧,但是我来这里寻求答案,因此提供了新的解决方案。

使用MongoDB 3.6+版时,现在可以使用位置运算符更新数组中的所有项目。请参阅此处的官方文档

以下查询将解决此处提出的问题。我还使用Java-MongoDB驱动程序进行了验证,并且可以成功运行。

.update(   // or updateMany directly, removing the flag for 'multi'
   {"events.profile":10},
   {$set:{"events.$[].handled":0}},  // notice the empty brackets after '$' opearor
   false,
   true
)

希望这可以帮助像我这样的人。



1

您可以更新MongoDB中的所有元素

db.collectioname.updateOne(
{ "key": /vikas/i },
{ $set: { 
 "arr.$[].status" : "completed"
} }
)

它将“ arr”数组中的所有“ status”值更新为“ completed”

如果只有一个文件

db.collectioname.updateOne(
 { key:"someunique", "arr.key": "myuniq" },
 { $set: { 
   "arr.$.status" : "completed", 
   "arr.$.msgs":  {
                "result" : ""
        }
   
 } }
)

但是,如果不是一个,并且您也不想更新数组中的所有文档,则需要遍历元素和if块内部

db.collectioname.find({findCriteria })
  .forEach(function (doc) {
    doc.arr.forEach(function (singlearr) {
      if (singlearr check) {
        singlearr.handled =0
      }
    });
    db.collection.save(doc);
  });

0

实际上,save命令仅在Document类的实例上。那有很多方法和属性。因此,您可以使用lean()函数来减少工作量。请参考这里。https://hashnode.com/post/why-are-mongoose-mongodb-odm-lean-queries-faster-than-normal-queries-cillvawhq0062kj53asxoyn7j

保存功能的另一个问题是,冲突数据将同时多次保存。 Model.Update将使数据一致。因此,要更新文档数组中的多个项目。使用您熟悉的编程语言并尝试类似的方法,我在其中使用猫鼬:

User.findOne({'_id': '4d2d8deff4e6c1d71fc29a07'}).lean().exec()
  .then(usr =>{
    if(!usr)  return
    usr.events.forEach( e => {
      if(e && e.profile==10 ) e.handled = 0
    })
    User.findOneAndUpdate(
      {'_id': '4d2d8deff4e6c1d71fc29a07'},
      {$set: {events: usr.events}},
      {new: true}
    ).lean().exec().then(updatedUsr => console.log(updatedUsr))
})

0

$ []运算符选择所有嵌套数组..您可以使用'$ []'更新所有数组项

.update({"events.profile":10},{$set:{"events.$[].handled":0}},false,true)

参考


您能否解释一下为什么在此末尾加上“ false,true”?我在文档中找不到它。
加森

错误回答所有位置运算符$[] 只会更新指定数组中的所有字段。起作用的是过滤后的位置运算符$[identifier],该运算符对符合指定条件的数组字段进行运算。应使用arrayFilters :Refrence docs.mongodb.com/manual/release-notes/3.6/#arrayfiltersdocs.mongodb.com/manual/reference/operator/update/...
伊格斯

0

请注意,此线程中的一些建议使用$ []的答案是错误的。

db.collection.update(
   {"events.profile":10},
   {$set:{"events.$[].handled":0}},
   {multi:true}
)

上面的代码会将“事件”数组中所有元素的“已处理”更新为0,无论其“配置文件”值如何。该查询{"events.profile":10}仅用于过滤整个文档,而不是数组中的文档。在这种情况下,必须使用$[elem]with arrayFilters来指定数组项的条件,因此Neil Lunn的答案是正确的。


0

更新mongo db中多个文档中的数组字段。

使用$ pull或$ push与update many查询来更新mongoDb中的数组元素。

Notification.updateMany(
    { "_id": { $in: req.body.notificationIds } },
    {
        $pull: { "receiversId": req.body.userId }
    }, function (err) {
        if (err) {
            res.status(500).json({ "msg": err });
        } else {
            res.status(200).json({
                "msg": "Notification Deleted Successfully."
            });
        }
    });

0

首先:您的代码无法正常工作,因为您使用的是位置运算符$,该运算符仅标识要在数组中更新的元素,甚至没有明确指定其在数组中的位置。

您需要的是过滤后的位置运算符$[<identifier>]。它将更新所有与数组过滤条件匹配的元素。

解:

db.collection.update({"events.profile":10}, { $set: { "events.$[elem].handled" : 0 } },
   {
     multi: true,
     arrayFilters: [ { "elem.profile": 10 } ]
})

在此处访问mongodb doc

代码的作用是:

  1. {"events.profile":10} 过滤您的收藏并返回与过滤器匹配的文档

  2. $set更新操作:修改匹配的文件领域它作用于。

  3. {multi:true}.update()修改与过滤器匹配的所有文档,因此行为如下updateMany()

  4. { "events.$[elem].handled" : 0 } and arrayFilters: [ { "elem.profile": 10 } ] 此技术涉及将已过滤的位置数组与arrayFilters一起使用。此处过滤后的位置数组将$[elem]充当与数组过滤器中指定的条件相匹配的数组字段中所有元素的占位符。

阵列过滤器

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.