事实上,这与在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 })
}
无论如何,更新过程中有一些事情是您不想做的:
不要使用“一次性”更新数组:如果您认为在代码中更新整个数组内容,然后只需在每个文档中使用$set
整个数组可能更有效率。虽然这似乎更快速处理,但并不能保证在读取数组内容后没有发生更改,并且执行更新操作。尽管$set
仍然是一个原子操作,它只会使用它“认为”正确的数据来更新数组,因此很可能会覆盖在读和写之间发生的任何更改。
不要计算索引值进行更新:与“一次性”方法类似,在这种情况下,您只需确定要更新的位置0
和位置2
(等等),并将其编码到最终语句中:
{ "$set": {
"events.0.handled": 0,
"events.2.handled": 0
}}
问题在于假设读取文档时找到的索引值与更新时数组中的相同索引值。如果以改变顺序的方式向数组添加新项,则这些位置不再有效,实际上将更新错误的项。
因此,在确定允许单个更新语句处理多个匹配数组元素的合理语法之前,基本方法是要么在单个语句中逐个更新每个匹配的数组元素(最好批量处理),要么计算要更新的最大数组元素,或者持续更新,直到没有返回更改后结果为止。
无论如何,您应该始终在匹配的数组元素上处理占位符$
更新,即使每个语句仅更新一个元素。
批量操作实际上是处理任何工作为“多个操作”的操作的“通用”解决方案,由于其应用远不止于仅使用相同值更新多个数组元素,因此已经实现了该方案,它目前是解决此问题的最佳方法。