2011-01-12 4 views
128

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

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

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

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

+0

Обновление подмножества или всех элементов массива было добавлено в mongodb 3.6: https://docs.mongodb.com/manual/reference/operator/update/positional-all/#up._S_ [] – Jaap

+0

не забудьте проверить out arrayFilters и рассмотрим, какой запрос использовать, чтобы сделать обновление эффективным. Обратите внимание на ответ Нила Лунна: https://stackoverflow.com/a/46054172/337401 – Jaap

ответ

86

С августа 2017 года теперь можно обновлять несколько элементов массива. Подробную информацию см. В JIRA http://jira.mongodb.org/browse/SERVER-1243.

db.coll.update({}, {$set: {“a.$[].b”: 2}}) 
Input: {a: [{b: 0}, {b: 1}]} 
Output: {a: [{b: 2}, {b: 2}]} 

Другие обходные включают:

  • Обновление каждый элемент по отдельности (events.0.handled events.1.handled ...) или ...
  • Прочитайте документ , делать изменения вручную и сохранить его замены старше один (проверить "Update if Current", если вы хотите, чтобы обеспечить атомные обновления)
+14

, если у вас есть аналогичная проблема, проголосуйте за эту проблему - http://jira.mongodb.org/browse/SERVER-1243 – LiorH

+0

I на самом деле нравится чтение документа и сохранение подхода. Но я использовал Couch перед Mongo, так что подход кажется более естественным, поскольку API запросов для Couch отсутствует, просто REST api для целых документов – adam

+1

Оба этих подхода требуют довольно большого объема памяти, не так ли? Если есть много документов, которые нужно искать, и их нужно загрузить (или вложенные массивы) для обновления ...+ также немного затруднительно реализовать, если это нужно сделать асинхронно ... – Ixx

7

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

Я использовал первое решение Хавьера. Прочитайте массив в событиях затем перебрать и построить множество ехр:

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. – Zaheer

+9

Это не безопасное решение. Если запись будет добавлена ​​во время обновления, вы испортите свои данные. – Merc

55

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

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 # не имеет метода 'forEach'' – Squirrl

+2

@Squirrl может быть устаревшей версией mongodb? В документе ясно, как применять функцию forEach на курсоре, но не указывается, какая версия поддерживается. http://docs.mongodb.org/manual/reference/ метод/cursor.forEach/ –

+2

@dcerecedo отлично работает для меня – datasci

-1

Я просто хотел добавить еще одно решение, которое сработало для меня и довольно просто. Вот это просто массив тегов (строки) так, чтобы обновить тег под названием «тест» на «изменился», просто сделать это:

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

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

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

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

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

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

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

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

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

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

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

0

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

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

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

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

14

С release of MongoDB 3.6 (и имеющихся в отрасли разработки от MongoDB 3.5.12) теперь вы можете обновить несколько элементов массива в одном запросе.

Это использует filtered positional $[<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.

NOTE Somewhat ironically, since this is specified in the "options" argument for .update() and like methods, the syntax is generally compatible with all recent release driver versions.

However this is not true of the mongo shell, since the way the method is implemented there ("ironically for backward compatibility") the arrayFilters argument is not recognized and removed by an internal method that parses the options in order to deliver "backward compatibility" with prior MongoDB server versions and a "legacy" .update() API call syntax.

So if you want to use the command in the mongo shell or other "shell based" products (notably Robo 3T) you need a latest version from either the development branch or production release as of 3.6 or greater.

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

Также см. Updating a Nested Array with MongoDB, как эти новые позиционные операторы применяются к «вложенным» структурам массивов, где «массивы находятся внутри других массивов».

+2

Принятый ответ следует обновить и обратиться к этому ответу. – Jaap