Как обновить несколько элементов массива в 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)
Однако он обновляет только первый соответствующий элемент массива в каждом документе. (Это определенное поведение для $ - позиционный оператор.)
Как я могу обновить все соответствующие элементы массива?
17 ответов
В данный момент невозможно использовать позиционный оператор для обновления всех элементов в массиве. См. JIRA http://jira.mongodb.org/browse/SERVER-1243
В качестве обходного пути вы можете:
- Обновлять каждый элемент отдельно (events.0.handled events.1.handled ...) или...
- Прочитайте документ, внесите изменения вручную и сохраните его, заменив старый (отметьте "Обновить, если текущий", если вы хотите обеспечить атомарные обновления)
С выпуском 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"
как дано в контексте вопроса, использовалось в ожидании, что это "обновит несколько элементов", но это не так и все еще не так. Его использование здесь применяется к "нескольким документам", как это всегда было, или теперь указано иным образом в качестве обязательной настройки .updateMany()
в современных версиях API.
ПРИМЕЧАНИЕ Несколько иронично, так как это указано в аргументе "options" для
.update()
и, как и методы, синтаксис обычно совместим со всеми последними версиями драйверов.Однако это не относится к
mongo
оболочки, так как способ реализации метода там ( "по иронии судьбы для обратной совместимости")arrayFilters
Аргумент не распознается и удаляется внутренним методом, который анализирует параметры для обеспечения "обратной совместимости" с предыдущими версиями сервера MongoDB и "устаревшими".update()
Синтаксис вызова API.Так что если вы хотите использовать команду в
mongo
shell или другие "основанные на оболочке" продукты (особенно Robo 3T), вам нужна последняя версия из ветки разработки или производственной версии начиная с версии 3.6 или выше.
Смотрите также positional all $[]
который также обновляет "множественные элементы массива", но без применения к указанным условиям и применяется ко всем элементам в массиве, где это желаемое действие.
Также смотрите Обновление вложенного массива с MongoDB, чтобы узнать, как эти новые позиционные операторы применяются к "вложенным" структурам массивов, где "массивы находятся в других массивах".
ВАЖНО! Обновленные установки из предыдущих версий "возможно" не включили функции MongoDB, что также может привести к сбою операторов. Вы должны убедиться, что процедура обновления завершена с такими деталями, как обновления индекса, а затем запустить
db.adminCommand( { setFeatureCompatibilityVersion: "3.6" } )
Это позволило использовать такие функции, как новые операторы позиционного обновления и другие. Вы также можете проверить с помощью:
db.adminCommand( { getParameter: 1, featureCompatibilityVersion: 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);
});
Я думаю, это понятнее для новичков Монго и всех, кто знаком с 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 и handled
не равно 0 встречаются в любом из документов в вашей коллекции. Таким образом, если у вас есть 100 документов в вашей коллекции, и один из них имеет три вложенных документа, которые соответствуют query
и у всех других документов меньше соответствующих поддокументов, цикл будет выполнен три раза.
Этот метод исключает опасность засорения других данных, которые могут быть обновлены другим процессом во время выполнения этого сценария. Это также сводит к минимуму объем данных, передаваемых между клиентом и сервером.
Это на самом деле относится к давней проблеме на http://jira.mongodb.org/browse/SERVER-1243 где на самом деле существует ряд проблем с ясным синтаксисом, который поддерживает "все случаи", когда сопоставления множественных массивов найденный. Фактически уже существуют методы, которые "помогают" в решении этой проблемы, такие как " Массовые операции", которые были реализованы после этой первоначальной публикации.
До сих пор невозможно обновить более одного сопоставленного элемента массива в одном операторе обновления, поэтому даже при "многократном" обновлении все, что вы когда-либо сможете обновить, - это только один согласованный элемент в массиве для каждого документа в этом отдельном документе. заявление.
Наилучшее возможное решение в настоящее время - это найти и зациклить все сопоставленные документы и обработать массовые обновления, которые по крайней мере позволят отправлять много операций в одном запросе с единичным ответом. Вы можете по желанию использовать .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()
часть там будет работать, когда есть "уникальный" идентификатор для массива или все содержимое каждого элемента образует сам "уникальный" элемент. Это связано с оператором "set" в $setDifference
используется для фильтрации любого false
значения, возвращаемые из $map
операция, используемая для обработки массива на совпадения.
Если содержимое вашего массива не имеет уникальных элементов, вы можете попробовать альтернативный подход с $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 })
}
В любом случае, есть некоторые вещи, которые вы не хотите делать в обновлении:
Не обновляйте массив "одним выстрелом": если, по вашему мнению, было бы более эффективно обновить весь массив содержимого в коде, а затем просто
$set
весь массив в каждом документе. Это может показаться быстрее для обработки, но нет никакой гарантии, что содержимое массива не изменилось с момента его чтения и выполнения обновления. Хоть$set
все еще является атомарным оператором, он будет обновлять массив только тем, что он "считает" правильными данными, и, следовательно, может перезаписывать любые изменения, происходящие между чтением и записью.Не рассчитывайте значения индекса для обновления: если вы похожи на подход "один выстрел", вы просто определяете эту позицию
0
и положение2
(и т. д.) - это элементы для обновления и кодирования их с помощью и конечного выражения, например:{ "$set": { "events.0.handled": 0, "events.2.handled": 0 }}
Опять же, проблема здесь заключается в "предположении", что значения индекса, найденные при чтении документа, являются одинаковыми значениями индекса в массиве во время обновления. Если новые элементы добавляются в массив способом, который изменяет порядок, то эти позиции больше не действительны, а неправильные элементы фактически обновляются.
Таким образом, до тех пор, пока не будет определен разумный синтаксис, позволяющий обрабатывать несколько совпадающих элементов массива в одном операторе обновления, тогда основной подход состоит в том, чтобы либо обновлять каждый совпадающий элемент массива в отдельном операторе (в идеале, в массе), либо по существу вырабатывать максимальное количество элементов массива. обновлять или продолжать обновлять до тех пор, пока больше не будут изменены результаты. В любом случае, вы должны "всегда" обрабатывать позиционно $
обновляет соответствующий элемент массива, даже если он обновляет только один элемент на оператор.
Массовые операции на самом деле являются "обобщенным" решением для обработки любых операций, которые работают как "множественные операции", и, поскольку для этого существует больше приложений, чем просто обновление нескольких элементов массива с тем же значением, то, конечно, оно было реализовано уже, и в настоящее время это лучший подход для решения этой проблемы.
Во-первых: ваш код не работал, потому что вы использовали позиционный оператор $
который только идентифицирует элемент для обновления в массиве, но даже не указывает явно его позицию в массиве.
Вам нужен отфильтрованный позиционный оператор $[<identifier>]
. Он обновит все элементы, соответствующие условию фильтра массива.
Решение:
db.collection.update({"events.profile":10}, { $set: { "events.$[elem].handled" : 0 } },
{
multi: true,
arrayFilters: [ { "elem.profile": 10 } ]
})
Посетите документ mongodb здесь
Что делает код:
{"events.profile":10}
фильтрует вашу коллекцию и возвращает документы, соответствующие фильтруВ
$set
оператор обновления: изменяет совпадающие поля документов, с которыми он действует.{multi:true}
Это делает.update()
изменяет все документы, соответствующие фильтру, следовательно, ведет себя какupdateMany()
{ "events.$[elem].handled" : 0 } and arrayFilters: [ { "elem.profile": 10 } ]
Этот метод предполагает использование отфильтрованного позиционного массива с arrayFilters. фильтрованный позиционный массив здесь$[elem]
действует как заполнитель для всех элементов в полях массива, которые соответствуют условиям, указанным в фильтре массива.
Вы можете обновить все элементы в MongoDB
db.collectioname.updateOne(
{ "key": /vikas/i },
{ $set: {
"arr.$[].status" : "completed"
} }
)
Он обновит все значение "status" до "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);
});
Я поражен, что это все еще не было обращено в Монго. В целом, монго выглядит не очень хорошо, когда имеешь дело с подмассивами. Вы не можете подсчитать подмассивы просто, например.
Я использовал первое решение Хавьера. Считайте массив в события, затем выполните цикл и создайте набор 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});
Это можно абстрагировать в функцию с помощью обратного вызова для условного теста
Тема очень старая, но я искал здесь ответ, поэтому предлагаю новое решение.
В 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
)
Надеюсь, это поможет кому-то вроде меня.
Я искал решение этой проблемы, используя новейший драйвер для C# 3.6, и вот исправление, на котором я в конце концов остановился. Ключ здесь использует "$[]", который, согласно MongoDB, является новым с версии 3.6. См. https://docs.mongodb.com/manual/reference/operator/update/positional-all/ 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});
}
Для получения дополнительной информации см. Мой оригинальный пост здесь: Удалите элемент массива из ВСЕХ документов, используя драйвер C# MongoDB.
Оператор $[] выбирает весь вложенный массив. Вы можете обновить все элементы массива с помощью '$[]'
.update({"events.profile":10},{$set:{"events.$[].handled":0}},false,true)
Имейте в виду, что некоторые ответы в этой теме, предлагающие использовать $[], НЕПРАВИЛЬНЫ.
db.collection.update(
{"events.profile":10},
{$set:{"events.$[].handled":0}},
{multi:true}
)
Приведенный выше код обновит значение "handled" до 0 для всех элементов в массиве "events", независимо от его значения "profile". Запрос{"events.profile":10}
предназначен только для фильтрации всего документа, а не документов в массиве. В этой ситуации необходимо использовать$[elem]
с arrayFilters
чтобы указать состояние элементов массива, чтобы ответ Нила Ланна был правильным.
Я попробовал следующее и все работает нормально.
.update({'events.profile': 10}, { '$set': {'events.$.handled': 0 }},{ safe: true, multi:true }, callback function);
// функция обратного вызова в случае nodejs
На самом деле, команда сохранения есть только в экземпляре класса Document. Это имеет много методов и атрибутов. Таким образом, вы можете использовать функцию lean(), чтобы уменьшить рабочую нагрузку. Обратитесь сюда. https://hashnode.com/post/why-are-mongoose-mongodb-odm-lean-queries-faster-than-normal-queries-cillvawhq0062kj53asxoyn7j
Еще одна проблема, связанная с функцией сохранения, которая в одно и то же время приводит к конфликту данных с несколькими сохранениями.Model.Update будет делать данные последовательно. Таким образом, чтобы обновить несколько элементов в массиве документа. Используйте свой знакомый язык программирования и попробуйте что-то вроде этого, я использую мангуст в этом:
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))
})
если вы хотите обновить массив внутри массива
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,
}
);
Обновить поле массива в нескольких документах в mongo db.
Используйте $pull или $push with 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."
});
}
});
Я просто хотел добавить другое решение, которое работает для меня и довольно просто. Здесь это просто массив тегов (строк), поэтому чтобы обновить тег с именем "test" до "change", просто сделайте это:
myDocuments.find({tags: "test" }, {fields: {_id: 1}}).forEach(function (doc) {
myDocuments.update(
{_id: doc._id, tags: "test"},
{$set:{'tags.$': "changed"}});
});