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

我有一个Mongo文件,其中包含一系列元素。

我想重置数组中.profile = XX的所有对象的.handled属性。 该文件的格式如下:

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

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

我怎样才能更新所有匹配的数组元素?

此时不能使用位置运算符来更新数组中的所有项目。 参见JIRA http://jira.mongodb.org/browse/SERVER-1243

作为一项工作,你可以:

  • 分别更新每个项目(events.0.handled events.1.handled …)或…
  • 阅读文档,手动进行编辑并保存replace旧文档(如果要确保primefaces更新,请选中“如果当前更新”)

这对我有效:

 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和朋友的人都更清楚。

这也可以通过一个while循环来完成,该循环检查是否有任何仍然有未被更新的子文档的文档。 这种方法保留了更新的primefaces性(这里的许多其他解决scheme都没有)。

 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 } ); } 

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

这个方法避免了在脚本执行时可能会被其他进程更新的其他数据的危险。 它还最大限度地减less了在客户端和服务器之间传输的数据量。

事实上,这个问题与http://jira.mongodb.org/browse/SERVER-1243上的长期问题有关,在这个问题中,对于支持“所有情况”的清晰语法而言,实际上存在许多挑战,其中多个数组匹配是find。 实际上已经有了一些方法来“解决”这个问题,比如在这个原来的岗位之后实施的批量操作 。

在单个更新语句中更新多个匹配的数组元素仍然是不可能的,所以即使对于“多”更新,您所能够更新的只是数组中的每个文档中的一个元素声明。

目前最好的解决scheme是查找并循环所有匹配的文档,并处理批量更新,这将至less允许在单个请求中发送多个操作,并带有单一响应。 您可以select使用.aggregate()将search结果中返回的数组内容减less为与更新select条件匹配的数组内容。

 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()部分将工作,或者每个元素的所有内容本身形成一个“唯一”元素。 这是由于$setDifference的“set”操作符,用于过滤用于处理匹配数组的$map操作返回的任何false值。

如果你的数组内容没有独特的元素,你可以尝试使用$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使用以下方法,但由于pipe道中的数组扩展,该运算符的使用使其成为效率最低的方法:

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

在所有MongoDB版本支持聚合输出的“游标”的情况下,这只是select一种方法,并用显示的相同代码块迭代结果来处理批量更新语句的问题。 批量操作和聚集输出中的“游标”是在相同版本(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仍然是一个primefaces操作符,但是它只会更新数组,使其“认为”是正确的数据,因此可能会覆盖读写之间发生的任何更改。

  2. 不要计算要更新的索引值:在类似于“one shot”方法的地方,只需要找出位置0和位置2 (依此类推)就是更新的元素,并用下面的语句进行编码:

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

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

因此,直到有一个合理的语法确定允许在单个更新语句中处理多个匹配的数组元素,那么基本的方法是更新每个匹配的数组元素在一个单独的语句中(理想情况下是批量)或基本上计算出最大数组元素更新或保持更新,直到没有更多的修改结果被返回。 无论如何,即使只是每个语句更新一个元素,也应该“始终”处理匹配的数组元素上的位置$更新。

批量操作实际上是处理任何被认为是“多个操作”的操作的“一般化”解决scheme,并且因为对于这个操作有更多的应用程序而不是仅更新具有相同值的多个数组元素,所以当然已经实现了已经是现在解决这个问题的最好方法了。

我很惊讶,这在Mongo中还没有得到解决。 在处理子数组时,整个mongo似乎不是很好。 你不能简单地计数子数组。

我用哈维尔的第一个解决scheme。 将数组读入事件,然后循环并构buildset 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}); 

这可以抽象成一个函数,使用callback条件testing

随着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 } ) 

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

注意到在这个问题的背景下提出的"multi"是用于期望这将“更新多元素”,但事实并非如此,事实也并非如此。 这里的用法适用于“多个文档”,因为它一直是现在的情况,或者现在被指定为现代API版本中的.updateMany()的强制设置。

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

然而,对于mongo shell来说,这是不正确的,因为在那里实现方法的方式(“为了向后兼容性而讽刺地”), arrayFilters参数不能被内部方法识别和移除,parsing选项以提供“向后兼容性”与之前的MongoDB服务器版本和“legacy” .update() API调用语法。

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

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

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

我只是想添加另一个解决scheme,为我工作,是非常简单的。 这里只是一个标签(string)的数组,所以更新一个名为“test”的标签为“changed”,只需要这样做:

 myDocuments.find({tags: "test" }, {fields: {_id: 1}}).forEach(function (doc) { myDocuments.update( {_id: doc._id, tags: "test"}, {$set:{'tags.$': "changed"}}); }); 

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

保存function还有一个问题,那就是在同一时间多次保存冲突数据。 Model.Update将使数据保持一致。 所以要更新文档数组中的多个项目。 使用你熟悉的编程语言,尝试这样的事情,我使用mongoose:

 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)) }) 

我试了下面,它的工作正常。

 .update({'events.profile': 10}, { '$set': {'events.$.handled': 0 }},{ safe: true, multi:true }, callback function); 

//在nodejs的情况下callback函数