我有一个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)

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

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


当前回答

这实际上与http://jira.mongodb.org/browse/SERVER-1243上长期存在的问题有关,在http://jira.mongodb.org/browse/SERVER-1243上,对于支持找到多个数组匹配的“所有情况”的清晰语法,实际上存在许多挑战。事实上,已经有一些“帮助”解决这个问题的方法,比如在这篇原始文章之后实施的批量操作。

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

目前可能的最佳解决方案是查找并循环所有匹配的文档,并处理Bulk更新,这至少将允许在单个请求中以单个响应发送多个操作。您可以选择使用.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操作返回的任何错误值。

如果你的数组内容没有唯一的元素,你可以用$ react尝试另一种方法:

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版本支持来自汇总输出的“游标”的所有情况下,这只是选择一种方法,并使用显示用于处理Bulk更新语句的相同代码块迭代结果的问题。在同一个版本(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 })
}

无论哪种情况,在更新中有一些事情是你不想做的:

Do not "one shot" update the array: Where if you think it might be more efficient to update the whole array content in code and then just $set the whole array in each document. This might seem faster to process, but there is no guarantee that the array content has not changed since it was read and the update is performed. Though $set is still an atomic operator, it will only update the array with what it "thinks" is the correct data, and thus is likely to overwrite any changes occurring between read and write. Do not calculate index values to update: Where similar to the "one shot" approach you just work out that position 0 and position 2 ( and so on ) are the elements to update and code these in with and eventual statement like: { "$set": { "events.0.handled": 0, "events.2.handled": 0 }} Again the problem here is the "presumption" that those index values found when the document was read are the same index values in th array at the time of update. If new items are added to the array in a way that changes the order then those positions are not longer valid and the wrong items are in fact updated.

So until there is a reasonable syntax determined for allowing multiple matched array elements to be processed in single update statement then the basic approach is to either update each matched array element in an indvidual statement ( ideally in Bulk ) or essentially work out the maximum array elements to update or keep updating until no more modified results are returned. At any rate, you should "always" be processing positional $ updates on the matched array element, even if that is only updating one element per statement.

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

其他回答

这实际上与http://jira.mongodb.org/browse/SERVER-1243上长期存在的问题有关,在http://jira.mongodb.org/browse/SERVER-1243上,对于支持找到多个数组匹配的“所有情况”的清晰语法,实际上存在许多挑战。事实上,已经有一些“帮助”解决这个问题的方法,比如在这篇原始文章之后实施的批量操作。

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

目前可能的最佳解决方案是查找并循环所有匹配的文档,并处理Bulk更新,这至少将允许在单个请求中以单个响应发送多个操作。您可以选择使用.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操作返回的任何错误值。

如果你的数组内容没有唯一的元素,你可以用$ react尝试另一种方法:

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版本支持来自汇总输出的“游标”的所有情况下,这只是选择一种方法,并使用显示用于处理Bulk更新语句的相同代码块迭代结果的问题。在同一个版本(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 })
}

无论哪种情况,在更新中有一些事情是你不想做的:

Do not "one shot" update the array: Where if you think it might be more efficient to update the whole array content in code and then just $set the whole array in each document. This might seem faster to process, but there is no guarantee that the array content has not changed since it was read and the update is performed. Though $set is still an atomic operator, it will only update the array with what it "thinks" is the correct data, and thus is likely to overwrite any changes occurring between read and write. Do not calculate index values to update: Where similar to the "one shot" approach you just work out that position 0 and position 2 ( and so on ) are the elements to update and code these in with and eventual statement like: { "$set": { "events.0.handled": 0, "events.2.handled": 0 }} Again the problem here is the "presumption" that those index values found when the document was read are the same index values in th array at the time of update. If new items are added to the array in a way that changes the order then those positions are not longer valid and the wrong items are in fact updated.

So until there is a reasonable syntax determined for allowing multiple matched array elements to be processed in single update statement then the basic approach is to either update each matched array element in an indvidual statement ( ideally in Bulk ) or essentially work out the maximum array elements to update or keep updating until no more modified results are returned. At any rate, you should "always" be processing positional $ updates on the matched array element, even if that is only updating one element per statement.

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

对我有用的是:

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循环来完成,该循环检查是否有任何文档仍然有未更新的子文档。这种方法保留了更新的原子性(这里的许多其他解决方案都没有做到这一点)。

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且处理不等于0的子文档在集合中的任何文档中出现的最大次数。因此,如果您的集合中有100个文档,其中一个文档有三个匹配查询的子文档,而所有其他文档的匹配子文档更少,则循环将执行三次。

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

这个帖子很老了,但我来这里寻找答案,因此提供了新的解决方案。

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

希望这能帮助到像我这样的人。

请注意,在这个帖子中建议使用$[]的一些答案是错误的。

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

上面的代码将“events”数组中的所有元素的“handled”更新为0,不管它的“profile”值是多少。查询{"events. events. "Profile ":10}仅用于过滤整个文档,而不是数组中的文档。在这种情况下,必须使用$[elem]和arrayFilters来指定数组项的条件,以便Neil Lunn的答案是正确的。