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

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

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

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

171176 次浏览

< >强更新: 从Mongo 3.6版开始,这个答案不再有效,因为上面提到的问题已经修复,有办法实现这一点。 . .


此时,不可能使用位置操作符来更新数组中的所有项。参见JIRA http://jira.mongodb.org/browse/SERVER-1243

作为一份工作,你可以:

  • 单独更新每一项 (events.0。处理events.1.handled 李)或…< / >
  • 阅读文档,进行编辑 手动保存并替换 旧的(检查" Current"如果您想确保 李原子更新)< / >

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

我用了哈维尔的第一个方法。将数组读入事件,然后循环并构建set 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});

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

对我有用的是:

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的人来说,这一点更清楚。朋友。

这实际上与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操作返回的任何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版本支持来自汇总输出的“游标”的所有情况下,这只是选择一种方法,并使用显示用于处理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 })
}

无论哪种情况,你做想要在更新中做的某些事情:

  1. 如果你认为在代码中更新整个数组内容可能更有效,然后在每个文档中只$set整个数组。这看起来处理起来更快,但是不能保证数组内容在读取和执行更新后没有改变。虽然$set仍然是一个原子操作符,但它只会用它“认为”正确的数据更新数组,因此很可能会覆盖读写之间发生的任何更改。

  2. 与“一枪”方法类似,你只需要计算出位置0和位置2(等等)是更新和编码这些元素的元素,最终语句如下:

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

    同样,这里的问题是“假设”读取文档时找到的那些索引值与更新时th数组中的索引值相同。如果新项以改变顺序的方式添加到数组中,那么这些位置不再有效,实际上更新了错误的项

因此,在确定允许在一个update语句中处理多个匹配的数组元素的合理语法之前,基本方法是在单个语句中更新每个匹配的数组元素(理想情况下是Bulk),或者基本上计算出要更新的最大数组元素,或者一直更新,直到没有更多修改的结果返回。无论如何,你应该“总是”在处理匹配数组元素的位置$更新,即使每条语句只更新一个元素。

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

这也可以用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的子文档,而所有其他文档匹配的子文档更少,循环将执行三次。

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

实际上,save命令只在Document类的实例上。 有很多方法和属性。所以你可以使用精益()函数来减少工作负载。 请参考这里。https://hashnode.com/post/why-are-mongoose-mongodb-odm-lean-queries-faster-than-normal-queries-cillvawhq0062kj53asxoyn7j < / p > save函数的另一个问题,这将使冲突数据同时与多个save。 模型。更新将使数据一致。 因此要更新文档数组中的多个项目。使用您熟悉的编程语言并尝试这样做,我在其中使用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))
})

我尝试了以下,它的工作良好。

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

// nodejs情况下的回调函数

使用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"被用于期望这将“更新多个元素”;但过去不是,现在也不是。它在这里的用法适用于“多个documents",就像一直以来的情况一样,或者现在在现代API版本中指定为.updateMany()的强制设置。

请注意有点讽刺的是,既然这是在"options".update()和类似方法的参数,语法通常与所有最新发布的驱动程序版本兼容。

然而,在mongo shell中却不是这样,因为方法在那里实现的方式(&;讽刺的是向后兼容性&;)arrayFilters参数不被内部方法识别和删除,该方法解析选项以提供“向后兼容性”。与以前的MongoDB服务器版本和“遗留”;.update() API调用语法。

因此,如果你想在mongo shell或其他基于shell的"产品(特别是Robo 3T),您需要来自开发分支或生产版本的3.6或更高版本的最新版本。

另见positional all $[],它也更新“多个数组元素”;但不应用于指定的条件,并应用于数组中的所有元素,这是所需的操作。

也请参见用MongoDB更新嵌套数组了解这些新的位置操作符如何应用于"nested"数组结构,其中“数组”在其他数组中。

重要的 -升级安装从以前的版本"可能"没有启用MongoDB特性,这也会导致语句失败。您应该确保您的升级过程已经完成,包括索引升级等详细信息,然后运行

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

或更高版本,适用于您的安装版本。即"4.0"版本4及以上目前。这使得诸如新的位置更新操作符等特性成为可能。你也可以查询:

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

返回当前设置

我一直在使用c# 3.6的最新驱动程序寻找解决方案,下面是我最终确定的解决方案。这里的关键是使用“[]美元”,根据MongoDB的3.6版本。更多信息请参见https://docs.mongodb.com/manual/reference/operator/update/positional-all/。< em > S < / em > []

代码如下:

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

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

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

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

$[]操作符选择所有嵌套数组。使用'$[]'可以更新所有数组项

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

参考

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

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

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

在mongodb中更新多个文档中的数组字段。

使用$pull或$push和update many query来更新mongoDb中的数组元素。

Notification.updateMany(
{ "_id": { $in: req.body.notificationIds } },
{
$pull: { "receiversId": req.body.userId }
}, function (err) {
if (err) {
res.status(500).json({ "msg": err });
} else {
res.status(200).json({
"msg": "Notification Deleted Successfully."
});
}
});

首先:你的代码没有工作,因为你使用了位置操作符$,它只标识数组中要更新的元素,但甚至没有显式地指定它在数组中的位置。

你需要的是经过筛选的位置操作符$[<identifier>]。它将更新所有匹配数组筛选条件的元素。

解决方案:

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

访问mongodb doc这里

代码的作用:

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

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

  3. {multi:true}它使.update()修改所有匹配过滤器的文档,因此行为类似updateMany()

  4. <李> < p > { "events.$[elem].handled" : 0 } and arrayFilters: [ { "elem.profile": 10 } ] 该技术涉及到使用arrayFilters过滤位置数组。这里过滤的位置数组$[elem]充当数组字段中与数组过滤器中指定的条件匹配的所有元素的占位符

Array filters .

您可以更新MongoDB中的所有元素

db.collectioname.updateOne(
{ "key": /vikas/i },
{ $set: {
"arr.$[].status" : "completed"
} }
)

它将更新所有的“状态”;值为“completed”;在&;arr"数组

如果只有一份文件

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

如果你想在数组中更新数组

    await Booking.updateOne(
{
userId: req.currentUser?.id,
cart: {
$elemMatch: {
id: cartId,
date: date,
//timeSlots: {
//$elemMatch: {
//id: timeSlotId,
//},
//},
},
},
},
{
$set: {
version: booking.version + 1,
'cart.$[i].timeSlots.$[j].spots': spots,
},
},
{
arrayFilters: [
{
'i.id': cartId,
},
{
'j.id': timeSlotId,
},
],
new: true,
}
);