Как обновить несколько элементов массива в mongodb

184

У меня есть документ 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)

Однако он обновляет только первый соответствующий элемент массива в каждом документе. (Это определенное поведение для $ - позиционный оператор .)

Как я могу обновить все соответствующие элементы массива?

LiorH
источник
2
Обновление подмножества или всех элементов массива было добавлено в mongodb 3.6: docs.mongodb.com/manual/reference/operator/update/…
Jaap
Обязательно посмотрите arrayFilters и подумайте, какой запрос использовать, чтобы сделать обновление эффективным. Проверьте ответ Нила Ланна: stackoverflow.com/a/46054172/337401
Jaap
проверьте мой ответ
Ucdemir

Ответы:

112

В данный момент невозможно использовать позиционный оператор для обновления всех элементов в массиве. См. JIRA http://jira.mongodb.org/browse/SERVER-1243

В качестве обходного пути вы можете:

  • Обновите каждый элемент отдельно (events.0.handled events.1.handled ...) или ...
  • Прочитайте документ, внесите изменения вручную и сохраните его, заменив старый (отметьте «Обновить, если текущий», если вы хотите обеспечить атомарные обновления)
Хавьер Ферреро
источник
15
если у вас есть похожая проблема, проголосуйте за эту проблему - jira.mongodb.org/browse/SERVER-1243
LiorH
Мне действительно нравится читать документ и сохранить подход. Но я использовал Couch до Mongo, поэтому такой подход кажется более естественным, поскольку для Couch нет API запросов, а просто REST API для целых документов
Адам,
1
Оба этих подхода требуют достаточно большого объема памяти, верно? Если есть много документов, которые нужно искать и загружать их все (или вложенные массивы) для обновления ... + также немного проблематично реализовать, если это нужно делать асинхронно ...
13
13
За исключением всех технических трудностей, довольно удивительно, что эта функция недоступна в MongoDB. Это ограничение отнимает много свободы в настройке схемы базы данных.
Сон Чо
5
Neil Lunn stackoverflow.com/a/46054172/337401 ответил на это для версии 3.6. Поскольку это популярный вопрос, возможно, стоит обновить этот принятый ответ ссылкой на ответ Нила Ланна.
Яап
75

С выпуском 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оболочке или других «основанных на оболочке» продуктах (в частности, Robo 3T), вам нужна последняя версия из ветки разработки или производственной версии начиная с версии 3.6 или выше.

Смотрите также, positional all $[]который также обновляет «несколько элементов массива», но без применения к указанным условиям и применяется ко всем элементам в массиве, где это желаемое действие.

Также смотрите Обновление вложенного массива с помощью MongoDB, чтобы узнать, как эти новые позиционные операторы применяются к «вложенным» структурам массивов, где «массивы находятся в других массивах».

ВАЖНО! Обновленные установки из предыдущих версий «возможно» не включили функции MongoDB, что также может привести к сбою операторов. Вы должны убедиться, что процедура обновления завершена с такими деталями, как обновления индекса, а затем запустить

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

Или более высокая версия в зависимости от установленной версии. то есть "4.0"для версии 4 и далее в настоящее время. Это позволило использовать такие функции, как новые операторы позиционного обновления и другие. Вы также можете проверить с помощью:

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

Чтобы вернуть текущую настройку

Нил Ланн
источник
10
Принятый ответ следует обновить и обратиться к этому ответу.
Яап
3
Что такое elem?
user1063287
1
Это верно. Обратите внимание, что RoboMongo arrayFiltersпока не поддерживает , поэтому запускайте обновление через CLI. stackoverflow.com/questions/48322834/…
drlff
спасибо, Нил, специально для ВАЖНОГО раздела, именно то, что мне было нужно
janfabian
этот код возвращает ошибку в pymongo. есть ошибка: поднять TypeError («% s должен быть True или False»% (option,)) TypeError: upsert должен быть True или False
Vagif
67

То, что работало для меня, было этим:

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 и друзьями.

Даниэль Сереседо
источник
Я использую db.posts.find({ 'permalink':permalink }).forEach( function(doc) {...и получаюOops.. TypeError: Object # has no method 'forEach'
Squirrl
3
@Squirrl может быть устаревшей версией mongodb? Документ ясно о том, как применить функцию forEach к курсору, но не указывает, какая версия поддерживается. docs.mongodb.org/manual/reference/method/cursor.forEach
Даниэль Сереседо,
@Squirrl попробуйdb.posts.find(...).toArray().forEach(...)
март
Разве мы не можем сделать это без использования Javascript? Я хочу выполнить это обновление непосредственно из оболочки Монго без использования Javascript API.
Мелиодас
1
Можете ли вы написать этот запрос в драйвере Mongodb для Java или с Spring-Data-Mongodb? Спасибо, крис
Тику
18

Это также может быть выполнено с помощью цикла 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а во всех других документах меньше соответствующих вложенных документов, цикл будет выполнен три раза.

Этот метод исключает опасность засорения других данных, которые могут быть обновлены другим процессом во время выполнения этого сценария. Это также сводит к минимуму объем данных, передаваемых между клиентом и сервером.

Шон
источник
13

На самом деле это относится к давней проблеме на 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 })
}

В любом случае, есть некоторые вещи, которые вы не хотите делать в обновлении:

  1. Не обновляйте массив «одним выстрелом»: если, по вашему мнению, может быть более эффективно обновить весь контент массива в коде, а затем только $setвесь массив в каждом документе. Это может показаться быстрее для обработки, но нет никакой гарантии, что содержимое массива не изменилось с момента его чтения и выполнения обновления. Хотя $setон все еще является атомарным оператором, он будет обновлять массив только тем, что, по его мнению, является правильными данными, и, таким образом, может перезаписывать любые изменения, происходящие между чтением и записью.

  2. Не рассчитывайте значения индекса для обновления: если вы похожи на подход «одним выстрелом», вы просто определяете, что позиция 0и позиция 2(и т. Д.) Являются элементами для обновления и кодирования их с помощью и конечного выражения, например:

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

    Опять проблема здесь заключается в «предположении», что эти значения индекса, найденные при чтении документа, являются одинаковыми значениями индекса в массиве во время обновления. Если новые элементы добавляются в массив таким образом, что это меняет порядок, то эти позиции больше не действительны, а неправильные элементы фактически обновляются.

Таким образом, до тех пор, пока не будет определен разумный синтаксис, позволяющий обрабатывать несколько совпадающих элементов массива в одном операторе обновления, тогда основной подход состоит в том, чтобы либо обновлять каждый совпадающий элемент массива в отдельном операторе (в идеале, в массе), либо по существу вырабатывать максимальное количество элементов массива. обновлять или продолжать обновлять до тех пор, пока больше не будут изменены результаты. В любом случае, вы должны «всегда» обрабатывать позиционные$ обновления для элемента соответствующего массива, даже если это обновляет только один элемент на оператор.

Массовые операции на самом деле являются «обобщенным» решением для обработки любых операций, которые работают как «множественные операции», и, поскольку для этого существует больше приложений, чем просто обновление нескольких элементов массива с тем же значением, то, конечно, оно было реализовано уже, и в настоящее время это лучший подход для решения этой проблемы.

Блейкс Семь
источник
8

Я поражен, что это все еще не было обращено в Монго. В целом, монго выглядит не очень хорошо при работе с подмассивами. Вы не можете сосчитать подмассивы просто, например.

Я использовал первое решение Хавьера. Считайте массив в события, затем выполните цикл и создайте набор 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});

Это можно абстрагировать в функцию с помощью обратного вызова для условного теста

lukenofurther
источник
Спасибо за это! Не могу поверить, что эта функция по-прежнему не поддерживается изначально! Используйте это, чтобы увеличить каждый элемент подмассива, чтобы другие читали ... чтобы обновить каждый элемент, просто удалите оператор if.
Захир
9
Это не безопасное решение. Если запись будет добавлена ​​во время выполнения обновления, вы повредите свои данные.
Merc
4

Я искал решение этой проблемы, используя новейший драйвер для C # 3.6, и вот исправление, на котором я в конце концов остановился. Ключ здесь использует "$ []", который согласно 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});
}

Для получения дополнительной информации см. Мой оригинальный пост здесь: Удалите элемент массива из ВСЕХ документов с помощью драйвера MongoDB C #

C0d3 0n3
источник
4

Тема очень старая, но я пришел сюда, чтобы найти ответ, и поэтому нашел новое решение.

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

Надеюсь, это поможет кому-то вроде меня.

ersnh
источник
1

Я попробовал следующее и все работает нормально.

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

// функция обратного вызова в случае nodejs

Пранай Саха
источник
Этот код просто обновляет первый соответствующий элемент в массиве. Неправильный ответ.
Гамлет
Работает на v2.6. Вы можете быть на более старой версии? Вот его документация docs.mongodb.com/manual/reference/method/db.collection.update/…
Jialin Zou
1

Вы можете обновить все элементы в MongoDB

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

Это обновит все значения «status» до «complete» в массиве «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);
  });
ВИКАС КОЛИ
источник
0

На самом деле, команда сохранения есть только в экземпляре класса 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))
})
user3176403
источник
0

Оператор $ [] выбирает весь вложенный массив. Вы можете обновить все элементы массива с помощью $ []

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

Ссылка

Ucdemir
источник
Не могли бы вы объяснить, почему вы включаете «ложь, правда» в конце здесь? Я не могу найти это в документации.
Гарсон
Неправильный ответ позиционный оператор all $[] просто обновляет все поля в указанном массиве. Работает фильтрованный позиционный оператор, $[identifier]который работает с полями массива, соответствующими указанным условиям. Следует использовать с arrayFilters Refrence: docs.mongodb.com/manual/release-notes/3.6/#arrayfilters и docs.mongodb.com/manual/reference/operator/update/…
Лоуренс Иглс
0

Помните, что некоторые ответы в этой теме, предлагающие использовать $ [], НЕПРАВИЛЬНЫ.

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

Приведенный выше код обновит «handled» до 0 для всех элементов в массиве «events», независимо от его значения «profile». Запрос {"events.profile":10}предназначен только для фильтрации всего документа, а не документов в массиве. В этой ситуации необходимо использовать $[elem]с, arrayFiltersчтобы указать условие элементов массива, чтобы ответ Нила Ланна был правильным.

Венда Ху
источник
0

Обновление поля массива в нескольких документах в mongo db.

Используйте $ pull или $ push с запросом update many для обновления элементов массива в 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."
            });
        }
    });
Ирфан
источник
0

Во-первых: ваш код не работал, потому что вы использовали позиционный оператор, $который только идентифицирует элемент для обновления в массиве, но даже не указывает явно его позицию в массиве.

Вам нужен отфильтрованный позиционный оператор $[<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. { "events.$[elem].handled" : 0 } and arrayFilters: [ { "elem.profile": 10 } ] Этот метод предполагает использование отфильтрованного позиционного массива с arrayFilters. отфильтрованный позиционный массив здесь $[elem]действует как заполнитель для всех элементов в полях массива, которые соответствуют условиям, указанным в фильтре массива.

Фильтры массива

Лоуренс Иглз
источник