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

131

У меня есть документ Mongo, который содержит массив элементов.

Я хотел бы reset атрибут .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)

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

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

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

9 ответов

85
Лучший ответ

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

Как работа вокруг, вы можете:

  • Обновление каждого элемента индивидуально (события .0.handled events.1.handled ...) или...
  • Прочитайте документ, внесите изменения вручную и сохраните его, заменив более старый (проверьте " Обновить, если Текущий ", если вы хотите обеспечить атомные обновления)
  • 14
    если у вас есть похожая проблема, проголосуйте за эту проблему - jira.mongodb.org/browse/SERVER-1243
  • 0
    Мне действительно нравится читать документ и сохранить подход. Но я использовал Couch до Mongo, поэтому такой подход кажется более естественным, поскольку для Couch не существует API запросов, а просто REST API для целых документов.
Показать ещё 5 комментариев
52

Что для меня работало:

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

  • 0
    Я использую db.posts.find({ 'permalink':permalink }).forEach( function(doc) {... и получаю Oops.. TypeError: Object # has no method 'forEach'
  • 3
    @Squirrl может быть устаревшей версией mongodb? Документ ясно о том, как применить функцию forEach к курсору, но не указывает, какая версия поддерживается. docs.mongodb.org/manual/reference/method/cursor.forEach
Показать ещё 6 комментариев
13

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

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

  • 1
    это хорошо работает для тех, кто застрял <3,6
10

С выпуском 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, как эти новые позиционные операторы применяются к "вложенным" структурам массивов, где "массивы находятся внутри других массивов".

  • 4
    Принятый ответ необходимо обновить и обратиться к этому ответу.
  • 0
    Что такое elem ?
Показать ещё 2 комментария
10

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

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

В настоящее время наилучшим решением является поиск и объединение всех согласованных документов и обработка массовых обновлений, которые по меньшей мере позволят отправлять многие операции в одном запросе с сингулярным ответом. Вы можете по желанию использовать .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 поддерживает "курсор" из совокупного вывода, это просто вопрос выбора подхода и повторения результатов с тем же блоком кода, который показан для обработки операторов Bulk update. Массовые операции и "курсоры" из совокупной производительности вводятся в той же версии (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", обновите это много раз, пока в основном не будет больше документов для обновления.

Допустимый подход для версий 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
    }}
    

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

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

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

7

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

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

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

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

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

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

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

  • 0
    Этот код просто обновляет первый соответствующий элемент в массиве. Неправильный ответ.
0

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

Еще одна проблема с функцией сохранения, которая сделает данные конфликта одновременно с мультисохранением. Model.Update будет делать данные последовательно. Таким образом, для обновления нескольких элементов в массиве документа. Используйте свой знакомый язык программирования и попробуйте что-то вроде этого, я использую 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))
})
0

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

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню