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

247

我有一个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新增了更新子集或所有数组项的功能:https://docs.mongodb.com/manual/reference/operator/update/positional-all/#up._S_[] - Jaap
请务必查看arrayFilters并考虑使用哪种查询使更新更有效率。请查看Neil Lunn的答案:https://dev59.com/Rm445IYBdhLWcg3w1tui#46054172 - Jaap
16个回答

148

随着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"是指期望"更新多个元素",但这并不是现实情况。在此处使用适用于"多个文档"的情况,这一直是如此,或者现在在现代API版本中指定为.updateMany()的强制设置。

注意:有点讽刺的是,由于这是在“选项”参数中指定的,所以语法通常与所有最近发布的驱动程序版本兼容。但是,对于mongo shell来说并非如此,因为在那里实现该方法的方式(“具有向后兼容性”的讽刺)不识别arrayFilters参数,并通过解析选项的内部方法将其删除,以便与先前的MongoDB服务器版本保持“向后兼容性”和“遗留”的.update() API调用语法。因此,如果您想在mongo shell或其他“基于shell”的产品(特别是Robo 3T)中使用该命令,则需要从3.6或更高版本的开发分支或生产发布中获取最新版本。
另请参见 positional all $[],它也更新“多个数组元素”,但不适用于指定条件并适用于数组中的所有元素,其中这是期望的操作。
还请参阅使用MongoDB更新嵌套数组,了解这些新的位置运算符如何应用于“嵌套”的数组结构,其中“数组位于其他数组中”。

IMPORTANT - Upgraded installations from previous versions "may" have not enabled MongoDB features, which can also cause statements to fail. You should ensure your upgrade procedure is complete with details such as index upgrades and then run

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

Or higher version as is applicable to your installed version. i.e "4.0" for version 4 and onwards at present. This enabled such features as the new positional update operators and others. You can also check with:

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

To return the current setting


13
接受的回答应更新并参考此答案。 - Jaap
12
elem是什么? - user1063287
1
这是正确的。请注意,RoboMongo尚不支持arrayFilters,因此需要通过CLI运行更新。https://dev59.com/yqjja4cB1Zd3GeqP9FIz - drlff
谢谢你,尼尔,特别是那个重要的部分,正是我所需要的。 - janfabian
1
我有点困惑... arrayFilter 语句怎么知道 'elem' 是指 events 数组? - F.H.
显示剩余2条评论

120
更新:Mongo版本3.6起,此答案不再适用,因为提到的问题已经得到解决,现在有其他方法来实现此操作。请查看其他答案。

目前无法使用定位操作符更新数组中的所有项目。请参阅 JIRA http://jira.mongodb.org/browse/SERVER-1243

作为一种解决方法,您可以:

  • 单独更新每个项目 (events.0.handled events.1.handled ...) 或...
  • 读取文档,手动编辑并保存以替换旧文档(如果要确保原子性更新,请查看"当前更新"

19
如果您遇到类似的问题,请为此问题投票 - http://jira.mongodb.org/browse/SERVER-1243 - LiorH
1
这两种方法都需要相当高的内存,对吧?如果有很多文档需要搜索,并且必须加载所有文档(或嵌套数组)以进行更新...而且如果必须异步完成此操作,则实现起来也有点麻烦... - User
13
除了技术难题之外,令人惊讶的是这个功能在MongoDB中不可用。这个限制大大剥夺了自定义数据库架构的自由。 - mc9
@javier-ferrero MongoDB 3.6新增了更新子集或所有数组项的功能:https://docs.mongodb.com/manual/reference/operator/update/positional-all/#up._S_[] - Jaap
6
Neil Lunn在Stack Overflow网站上回答了这个问题的Python 3.6版本。由于这是一个常见问题,更新此已接受答案并引用Neil Lunn的答案可能是值得的。 - Jaap
显示剩余2条评论

76

对我有效的方法是这样的:

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

我认为对于MongoDB的新手以及熟悉jQuery和相关技术的人来说,这更加清晰易懂。


我正在使用 db.posts.find({ 'permalink':permalink }).forEach( function(doc) {...,但是出现了以下错误: 糟糕.. TypeError: Object # has no method 'forEach' - Squirrl
3
可能是@Squirrl使用了过时的MongoDB版本?该文档清楚地说明了如何在游标上应用forEach函数,但没有指明支持哪个版本。http://docs.mongodb.org/manual/reference/method/cursor.forEach/ - Daniel Cerecedo
@Squirrl 尝试使用 db.posts.find(...).toArray().forEach(...) - marmor
我们能否不使用Javascript完成这个操作?我想在mongo shell中直接执行这个更新操作,而不使用Javascript API。 - Meliodas
@DazzleR 那段JS代码可以在Mongo shell中运行。只需复制/粘贴即可。 - Daniel Cerecedo
1
你能否用Java的MongoDB驱动程序或Spring-Data-MongoDB写出这个查询语句?谢谢,Kris。 - 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 匹配的子文档,而其他所有文档都有更少的匹配子文档,则循环将执行三次。

该方法避免了可能会在脚本执行期间被另一个进程更新的其他数据被覆盖的危险。它还将客户端和服务器之间传输的数据量最小化。


15

事实上,这与在http://jira.mongodb.org/browse/SERVER-1243长期存在的问题有关,其中存在一些挑战,以支持找到“所有情况”的多个数组匹配的清晰语法。实际上已经有一些方法可以帮助解决此问题,例如批量操作,这是在最初发布后实施的。

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

目前最好的解决方案是查找并循环所有匹配的文档,然后处理批量更新,这将允许在单个请求中发送许多操作,并获得单个响应。您还可以选择使用.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()只有当数组中存在“唯一”标识符或每个元素的所有内容形成一个“唯一”元素本身时才有效。这是因为在$setDifference中使用了“set”运算符来过滤从$map操作返回的任何false值,该操作用于处理匹配项的数组。

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

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

在这种情况下,其局限性在于如果“handled”实际上是一个应该存在于其他文档级别的字段,那么您可能会得到意外的结果,但是如果该字段仅出现在一个文档位置并且是一个相等匹配,则是可以接受的。

撰写本文时,未来版本(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 }});
    });
});
如果您坚决要进行"multi"更新,或者认为这比为每个匹配的文档处理多个更新更有效率,那么您可以始终确定可能的最大数组匹配数,并执行相应次数的"multi"更新,直到基本上没有需要更新的文档。
对于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
    }}
    

    问题在于假设读取文档时找到的索引值与更新时数组中的相同索引值。如果以改变顺序的方式向数组添加新项,则这些位置不再有效,实际上将更新错误的项。

    因此,在确定允许单个更新语句处理多个匹配数组元素的合理语法之前,基本方法是要么在单个语句中逐个更新每个匹配的数组元素(最好批量处理),要么计算要更新的最大数组元素,或者持续更新,直到没有返回更改后结果为止。

    无论如何,您应该始终在匹配的数组元素上处理占位符$更新,即使每个语句仅更新一个元素。

    批量操作实际上是处理任何工作为“多个操作”的操作的“通用”解决方案,由于其应用远不止于仅使用相同值更新多个数组元素,因此已经实现了该方案,它目前是解决此问题的最佳方法。


14

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

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

解决方案:

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

访问mongodb文档

代码的作用:

  1. {"events.profile":10} 过滤集合并返回与筛选器匹配的文档。

  2. $set 更新操作符:修改其所作用的文档中匹配字段。

  3. {multi:true} 使 .update() 修改所有匹配筛选器的文档,因此表现类似于 updateMany()

  4. { "events.$[elem].handled" : 0 } and arrayFilters: [ { "elem.profile": 10 } ]该技术涉及使用带有arrayFilters的过滤位置数组。此处的过滤位置数组$[elem]作为占位符,表示数组字段中满足数组筛选器指定条件的所有元素。

数组过滤器


11

你可以更新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);
  });

$[] 工作正常。谢谢。 - Azahar

8

我惊讶于Mongo仍未解决这个问题。总的来说,Mongo在处理子数组时似乎并不是很出色。例如,你无法简单地计算子数组的数量。

我使用了Javier的第一个解决方案。将数组读入“events”,然后循环并构建集合表达式:

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
9
这不是一个安全的解决方案。如果在你运行更新时添加了一条记录,将会破坏你的数据。 - Merc

7

这个帖子非常老,但是我在这里寻找答案,因此提供新的解决方案。

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

希望这对像我一样的人有所帮助。


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#驱动程序从所有文档中删除数组元素


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接