Как обновить несколько элементов массива в mongodb - PullRequest
167 голосов
/ 12 января 2011

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

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

Как мне обновить все сопоставленных элементов массива?

Ответы [ 11 ]

109 голосов
/ 12 января 2011

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

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

  • Обновлять каждый элемент по отдельности (events.0.handled events.1.handled ...) или ...
  • Прочитайте документ, внесите изменения вручную и сохраните его, заменив более старый (отметьте "Обновить, если Текущий ", если вы хотите обеспечить атомные обновления)
63 голосов
/ 16 декабря 2013

Что сработало для меня, так это:

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

49 голосов
/ 05 сентября 2017

С выпуском 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 } )

Для возврата текущей настройки

18 голосов
/ 20 января 2016

Это также может быть выполнено с помощью цикла 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 голосов
/ 18 октября 2015

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

Если ограничение заключается в том, что если "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 поддерживает «курсор» из агрегата oно это просто вопрос выбора подхода и повторения результатов с тем же блоком кода, который показан для обработки операторов массового обновления.Массовые операции и «курсоры» из совокупного вывода представлены в одной и той же версии (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
    }}
    

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

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

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

8 голосов
/ 02 июня 2013

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

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

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

4 голосов
/ 06 июня 2018

Я искал решение этой проблемы, используя новейший драйвер для 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 #

1 голос
/ 19 июня 2019

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

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

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

1 голос
/ 04 сентября 2017

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

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

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

0 голосов
/ 29 июля 2017

На самом деле, команда сохранения есть только в экземпляре класса 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))
})
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...