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

How to Update Multiple Array Elements in mongodb

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

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

文件格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
   "_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":"....."
        }
        ...
    ]
}

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

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

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

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


此时,无法使用位置运算符更新数组中的所有项。见jira http://jira.mongodb.org/browse/server-1243

作为一项工作,你可以:

  • 单独更新每个项目(事件0.已处理事件1.已处理……或…
  • 阅读文档,进行编辑手动保存,替换旧的(如果如果你想确保原子更新


对我有用的是:

1
2
3
4
5
6
7
8
9
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);
  });

我认为这对蒙古新手和任何熟悉jquery&friends的人都更清楚。


随着MongoDB3.6的发布(可从MongoDB3.5.12的开发分支中获得),现在您可以在一个请求中更新多个数组元素。

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

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

转交给.update()或甚至.updateOne().updateMany().findOneAndUpdate().bulkWrite()方法指定了与更新语句中给出的标识符匹配的条件。任何符合给定条件的元素都将被更新。

注意到在问题上下文中给出的"multi"用于预期这将"更新多个元素",但事实并非如此。这里的用法适用于"多文档",就像以前的情况一样,或者现在在现代API版本中指定为.updateMany()的强制设置。

NOTE Somewhat ironically, since this is specified in the"options" argument for .update() and like methods, the syntax is generally compatible with all recent release driver versions.

However this is not true of the mongo shell, since the way the method is implemented there ("ironically for backward compatibility" ) the arrayFilters argument is not recognized and removed by an internal method that parses the options in order to deliver"backward compatibility" with prior MongoDB server versions and a"legacy" .update() API call syntax.

So if you want to use the command in the mongo shell or other"shell based" products ( notably Robo 3T ) you need a latest version from either the development branch or production release as of 3.6 or greater.

另请参见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

1
   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:

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

To return the current setting


This can also be accomplished with a while loop which checks to see if any documents remain that still have subdocuments that have not been updated. This method preserves the atomicity of your updates (which many of the other solutions here do not).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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匹配,而所有其他文档的匹配子文档都较少,则循环将执行三次。

此方法避免了在执行此脚本时可能被其他进程更新的其他数据被删除的危险。它还最小化了客户机和服务器之间传输的数据量。


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

在单个update语句中,仍然不可能更新多个匹配的数组元素,因此即使使用"multi"更新,您将能够更新的所有元素也只是数组中该单个语句中每个文档的一个数学元素。好的。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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尝试另一种方法:好的。

1
2
3
4
5
6
7
8
9
10
11
12
db.collection.aggregate([
    {"$match": {"events.handled": 1 } },
    {"$redact": {
       "$cond": {
           "if": {
               "$eq": [ {"$ifNull": ["$handled", 1 ] }, 1 ]
            },
           "then":"$$DESCEND",
           "else":"$$PRUNE"
        }
    }}
])

其中的限制是,如果"已处理"实际上是一个要出现在其他文档级别的字段,那么您很可能会得到未执行的结果,但如果该字段只出现在一个文档位置并且是相等匹配的,则很好。好的。

将来的版本(3.1版MongoDB之后)在编写时将有一个更简单的$filter操作:好的。

1
2
3
4
5
6
7
8
9
10
11
12
db.collection.aggregate([
    {"$match": {"events.handled": 1 } },
    {"$project": {
       "events": {
           "$filter": {
               "input":"$events",
               "as":"event",
               "cond": {"$eq": ["$$event.handled", 1 ] }
            }
        }
    }}
])

支持.aggregate()的所有版本都可以对$unwind使用以下方法,但由于在管道中进行了阵列扩展,使用该运算符使其成为效率最低的方法:好的。

1
2
3
4
5
6
7
8
9
db.collection.aggregate([
    {"$match": {"events.handled": 1 } },
    {"$unwind":"$events" },
    {"$match": {"events.handled": 1 } },
    {"$group": {
       "_id":"$_id",
       "events": {"$push":"$events" }
    }}        
])

在所有情况下,MongoDB版本都支持来自聚合输出的"cursor",那么这只是选择一种方法并使用显示用于处理批量更新语句的相同代码块迭代结果的问题。聚合输出中的批量操作和"光标"在同一版本(MongoDB 2.6)中引入,因此通常是手工处理的。好的。

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

1
2
3
4
5
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()来查找该值:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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仍然是一个原子运算符,但它只会用它"认为"的正确数据更新数组,因此很可能覆盖读写之间发生的任何更改。好的。

  • 不要计算要更新的索引值:如果与"一次性"方法类似,您只需计算出位置0和位置2等是要用和最终语句更新和编码这些内容的元素,例如:好的。

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

    这里的问题是"假定",即在读取文档时找到的索引值与更新时数组中的索引值相同。如果以更改顺序的方式将新项添加到数组中,那么这些位置将不再有效,并且实际上会更新错误的项。好的。

  • 因此,在确定了允许在单个update语句中处理多个匹配数组元素的合理语法之前,基本方法是更新Individual语句中的每个匹配数组元素(最好是批量更新),或者基本上计算出要更新的最大数组元素,或者一直更新直到不再修改返回确认的结果。无论如何,您应该"始终"在匹配的数组元素上处理位置$更新,即使每个语句只更新一个元素。好的。

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


    我很惊讶这个问题在蒙古人还没有被解决。总的来说,Mongo在处理子数组时似乎不是很好。例如,不能简单地计算子数组。

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

    1
    2
    3
    4
    5
    6
    7
    8
    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});

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


    我一直在寻找一个解决方案,使用C 3.6的最新驱动程序,这里是我最终解决的问题。这里的关键是使用"$[]",根据MongoDB的说法,它是3.6版以后的新版本。有关详细信息,请参阅https://docs.mongodb.com/manual/reference/operator/update/positional all/up.s[]。

    代码如下:

    1
    2
    3
    4
    5
    {
       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驱动程序从所有文档中删除数组元素


    我试过以下方法,效果很好。

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

    //在nodejs的情况下回调函数


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

    save函数的另一个问题是,它将使冲突数据同时与多个save发生冲突。model.update将使数据一致。所以要更新文档数组中的多个项。使用您熟悉的编程语言并尝试类似的操作,我在其中使用mongoose:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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))
    })

    我只是想添加另一个对我有效的解决方案,而且非常简单。这里只是一个标签(字符串)数组,因此要将名为"test"的标签更新为"changed",只需执行以下操作:

    [cc]myDocuments.find({tags:"test" }, {fields: {_id: 1}}).forEach(function (doc) {
    myDocuments.update(
    {_id: doc._id, tags:"test