Как обновить несколько элементов массива в 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)

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

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

11 ответов


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

Как обойти можно:

  • обновление каждого элемента в отдельности (события.0.обрабатывать события.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 & friends.


С выпуск 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() метод задает условия для соответствия идентификатору, указанному в инструкции update. Все элементы, соответствующие данному условию, будут обновлены.

отметить, что "multi" как указано в контексте вопроса, было использовано в ожидании, что это "обновит несколько элементов", но это не было и до сих пор не так. Это использование здесь относится к "несколько документов" как всегда case или теперь иначе указано как обязательное значение .updateMany() в современных версиях API.

Примечание несколько иронично, так как это указано в аргументе "options" для .update() и, как и методы, синтаксис, как правило, совместим со всеми последними версиями драйверов выпуска.

однако это не относится к mongo shell, так как способ реализации метода там ("по иронии судьбы для обратного совместимость") the arrayFilters аргумент не распознается и удаляется внутренним методом, который анализирует параметры для обеспечения " обратной совместимости "с предыдущими версиями сервера MongoDB и" устаревшим".update() синтаксис вызова API.

так что если вы хотите использовать команду mongo shell или другие продукты на основе shell ( в частности, Robo 3T ) вам нужна последняя версия из отрасли разработки или выпуска продукции с 3.6 или более значительный.

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

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

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

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

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

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

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


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

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


это на самом деле относится к давней проблеме в http://jira.mongodb.org/browse/SERVER-1243 где на самом деле существует ряд проблем с четким синтаксисом, который поддерживает "все случаи", когда найдены совпадения массива mutiple. На самом деле уже существуют методы, которые "помогают" в решении этой проблемы, такие как Массовые Операции, которые были реализованы после этого оригинальный пост.

по-прежнему невозможно обновить больше чем один согласованный элемент массива в одном операторе обновления, поэтому даже с" мульти " обновлением все, что вы когда-либо сможете обновить, - это только один математический элемент в массиве для каждого документа в этом одном операторе.

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

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

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

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


Я поражен, что это все еще не было рассмотрено в монго. В целом 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});

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


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


я попробовал следующее и его работа в порядке.

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

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


на самом деле команда save находится только в экземпляре класса Document. У этого есть много методов и атрибутов. Таким образом, вы можете использовать lean () функция для уменьшения рабочей нагрузки. Смотрите здесь. https://hashnode.com/post/why-are-mongoose-mongodb-odm-lean-queries-faster-than-normal-queries-cillvawhq0062kj53asxoyn7j

еще одна проблема с функцией сохранения, которая сделает данные конфликта с несколькими сохранениями одновременно. модель.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))
})

используя Mongo 3.6, данные SpringBoot с Mongo Reactive. У меня была проблема, когда мой запрос обновлял только один элемент в массиве. Заполнитель " $ " относится только к одному элементу. Для ссылки на многократное использование" $[]".

@Autowired private ReactiveMongoTemplate template;

public Mono<RoomData> findAndUnsetUserFromRemovedTeamId(String roomId, String teamId){
    Query query = new Query();
    query.addCriteria(
        Criteria.where("id").is(roomId).and("userRoomDatas").elemMatch(Criteria.where("teamId").is(teamId)));
    Update update = new Update();
    update.set("userRoomDatas.$[].teamId", roomId);
    update.set("userRoomDatas.$[].isSpectator", true);
    update.set("userRoomDatas.$[].isReady", false);

    FindAndModifyOptions options = FindAndModifyOptions.options();
    options.returnNew(true);
    return template.findAndModify(query, update, options, RoomData.class);
}

поэтому в моем коде мне нужно было заменить значения UserData, если их teamId равен удаляемому. Запрос запрашивает все userRoomData, значение teamId которого равно удаляемому. Обновление устанавливает новые значения для teamId / isSpectator / isReady. И FindAndModifyOptions используется для возврата нового значения вместо старого.

таким образом, общий ключ здесь - "$ [] "вместо"$". Теперь говорят, что у вас несколько RoomDatas, что у каждого есть несколько UserDatas, что нужно иметь ценности изменились. Вы должны использовать метод updateMulti из ReactiveMongoTemplate и передать правильные parms.


Я просто хотел добавить еще одно решение, которое сработало для меня и довольно просто. Здесь это просто массив тегов (строк), поэтому, чтобы обновить тег под названием "test" до "changed", просто сделайте это:

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