首页 文章

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

提问于
浏览
159

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

但是,它仅更新每个文档中的 first 匹配的数组元素 . (这是$ - the positional operator的定义行为 . )

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

10 回答

  • 12

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

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

    • 单独更新每个项目(events.0.handled events.1.handled ...)或......

    • 阅读文档,手动编辑并保存以替换旧版本(如果要确保原子更新,请检查"Update if Current"

  • -1

    对我有用的是:

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

  • 63

    使用release of MongoDB 3.6(并在MongoDB 3.5.12的开发分支中可用),您现在可以在单个请求中更新多个数组元素 .

    这使用此版本中引入的filtered positional $[<identifier>] update运算符语法:

    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" 被期望使用"update multiple elements"但这不是,现在仍然不是这样 . 这里的用法适用于 "multiple documents" ,因为在现代API版本中一直是这种情况或现在另外指定为.updateMany()的强制设置 .

    注意有点讽刺的是,因为这是在.update()和类似方法的“options”参数中指定的,所以语法通常与所有最新的发行版驱动程序版本兼容 . 然而,mongo shell并不是这样,因为在那里实现该方法的方式(“具有讽刺意味的是为了向后兼容性”),arrayFilters参数不被内部方法识别和删除,该方法解析选项以提供“向后兼容性”使用以前的MongoDB服务器版本和“遗留”.update()API调用语法 . 因此,如果您想在mongo shell或其他“基于shell”的产品(特别是Robo 3T)中使用该命令,则需要3.6或更高版本的开发分支或 生产环境 版本中的最新版本 .

    另请参阅positional all $[],它也会更新"multiple array elements"但不应用于指定的条件,并应用于数组中所需操作的 all 元素 .

    另请参阅Updating a Nested Array with MongoDB了解这些新的位置运算符如何应用于"nested"数组结构,其中"arrays are within other arrays" .

    重要信息 - 以前版本的升级安装“可能”尚未启用MongoDB功能,这也可能导致语句失败 . 您应该确保升级过程完成,并提供索引升级等详细信息,然后运行db.adminCommand({setFeatureCompatibilityVersion:“3.6”})
    这启用了诸如新位置更新操作符等功能 . 您还可以查看:db.adminCommand({getParameter:1,featureCompatibilityVersion:1})
    要返回当前设置

  • 106

    这也可以通过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 匹配的子文档,并且所有其他文档具有较少的匹配子文档,则循环将执行三次 .

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

  • 41

    事实上,这确实涉及到http://jira.mongodb.org/browse/SERVER-1243的长期存在的问题,其中对于支持"all cases"的多种语言的明确语法实际上存在许多挑战找到数组匹配 . 实际上已经存在解决此问题的方法,例如Bulk Operations,这些已在此原始帖子之后实现 .

    在单个更新语句中仍然无法更新多个匹配的数组元素,因此即使使用“多”更新,您只能在该单个文档中为每个文档中的一个数据元素进行更新声明 .

    目前最好的解决方案是找到并循环所有匹配的文档并处理批量更新,这将至少允许在单个请求中以单一响应发送许多操作 . 您可以选择使用.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();
    

    当数组的标识符为"unique"时, .aggregate() 部分将起作用,或者每个元素的所有内容形成"unique"元素本身 . 这是因为$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,但由于管道中的数组扩展,使用该运算符使其成为效率最低的方法:

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

    如果您坚决决定进行“多”更新或认为最终比为每个匹配的文档处理多个更新更有效,那么您始终可以确定可能的数组匹配的最大数量,并且只执行许多“多”更新时间,直到基本上没有更新的文件 .

    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: 如果您认为在代码中更新整个数组内容可能更有效,那么每个文档中的整个数组只需 $set . 处理起来似乎更快,但无法保证阵列内容在读取和执行更新后没有发生变化 . 虽然 $set 仍然是原子操作符,但它只会使用"thinks"是正确的数据来更新数组,因此可能会覆盖读取和写入之间发生的任何更改 .

    • Do not calculate index values to update: 在类似于"one shot"方法的情况下,您只需计算出位置 0 和位置 2 (等等)是要更新的代码,并使用和最终语句对它们进行编码,如:

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

    同样,这里的问题是“推测”,即在读取文档时找到的那些索引值在更新时与第一个数组中的索引值相同 . 如果以更改顺序的方式将新项添加到数组中,则这些位置不再有效,并且实际上更新了错误的项 .

    因此,在确定允许在单个更新语句中处理多个匹配的数组元素的合理语法之前,基本方法是在单个语句中更新每个匹配的数组元素(理想情况下为Bulk)或基本计算出最大数组元素更新或继续更新,直到不再返回修改后的结果 . 无论如何,你应该"always"正在处理匹配的数组元素上的positional $更新,即使这只是每个语句更新一个元素 .

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

  • 18

    我很惊讶这仍然没有在mongo中得到解决 . 在处理子阵列时,整体mongo似乎不太好 . 例如,您无法统计子阵列 .

    我使用了哈维尔的第一个解决方案 . 将数组读入事件然后循环并构建集合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});
    

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

  • 0

    我've been looking for a solution to this using the newest driver for C# 3.6 and here'是我最终解决的问题 . 这里的关键是使用 "$[]" ,根据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});
    }
    

    有关更多上下文,请参阅我的原始帖子:Remove array element from ALL documents using MongoDB C# driver

  • 8

    实际上,save命令仅适用于Document类的实例 . 那有很多方法和属性 . 因此,您可以使用 lean() 函数来减少工作量 . 请参考这里 . https://hashnode.com/post/why-are-mongoose-mongodb-odm-lean-queries-faster-than-normal-queries-cillvawhq0062kj53asxoyn7j

    保存功能的另一个问题是,它会同时使多重保存冲突数据 . 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))
    })
    
  • 0

    我尝试了以下,它的工作正常 .

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

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

  • 3

    我只是想添加另一个对我有用的解决方案,而且非常简单 . 这里只是一个标签(字符串)数组,所以要将名为“test”的标签更新为“已更改”,只需执行以下操作:

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

相关问题