Як оновити кілька елементів масиву в mongodb


183

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

Однак він оновлює лише перший відповідний елемент масиву в кожному документі. (Це визначена поведінка для $ - позиційного оператора .)

Як я можу оновити всі відповідні елементи масиву?


2
Оновлення підмножини або всіх елементів масиву додано до mongodb 3.6: docs.mongodb.com/manual/reference/operator/update/…
Jaap

не забудьте перевірити arrayFilters і врахуйте, який запит використовувати, щоб зробити оновлення ефективним. Ознайомтеся з відповіддю Ніла Ланна: stackoverflow.com/a/46054172/337401
Jaap

перевірити мою відповідь
Ucdemir

Відповіді:


111

На даний момент неможливо використовувати оператор позиції для оновлення всіх елементів масиву. Дивіться JIRA http://jira.mongodb.org/browse/SERVER-1243

Як робота навколо вас можна:

  • Оновіть кожен елемент окремо (події.0. Події, що обробляються 1. Обробляються ...) або ...
  • Прочитайте документ, виконайте правки вручну та збережіть його, замінивши старіший (поставте прапорець "Оновити, якщо поточний", якщо ви хочете забезпечити атомне оновлення)

15
якщо у вас є подібні проблеми, проголосуйте за це питання - jira.mongodb.org/browse/SERVER-1243
LiorH

Мені справді подобається прочитаний документ і зберегти підхід. Але я використовував Couch перед Mongo, так що такий підхід здається більш природним, оскільки для Couch немає API запиту, просто REST api для цілих документів
adam

1
Обидва ці підходи потребують досить великої кількості пам'яті, правда? Якщо є багато документів, які потрібно шукати і повинні завантажувати всі (або вкладені масиви), щоб оновити ... + також трохи клопітно реалізувати, якщо це потрібно зробити асинхронно ...
Ixx

13
Всі технічні труднощі осторонь, дивно, що ця функція недоступна в MongoDB. Це обмеження забирає багато свободи від налаштування схеми бази даних.
Sung Cho

5
Ніл Lunn stackoverflow.com/a/46054172/337401 відповів на це у версії 3.6. Оскільки це популярне запитання, можливо, варто буде оновити цю прийняту відповідь з посиланням на відповідь Ніла Ланна.
Jaap

74

З випуском 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 "застарілого" 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
Прийняту відповідь слід оновити і посилатися на цю відповідь.
Jaap

3
Що таке elem?
користувач1063287

1
Це вірно. Зауважте, що RoboMongo ще не підтримує arrayFilters, тому виконується оновлення через CLI. stackoverflow.com/questions/48322834 / ...
drlff

дякую, Ніл, особливо для
ВАЖЛИВОГО

цей код повертає ПОМИЛКУ в pymongo. tehre - помилка: підняти TypeError ("% s має бути істинним або помилковим"% (опція,)) TypeError: upsert повинен бути правдивим або помилковим
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(...)
MARMOR

Чи не можемо ми цього зробити без використання Javascript? Я хочу виконати це оновлення безпосередньо з оболонки mongo без використання Javascript API.
Меліодас

1
Чи можете ви, будь ласка, написати цей запит у драйвері mongodb для java чи за допомогою data-mongodb? Дякую, kris
chiku

18

Це також може бути здійснено за допомогою циклу, який перевіряє, чи залишаються документи, які ще мають піддокументи, які не були оновлені. Цей метод зберігає атомність ваших оновлень (чого багато інших рішень тут не мають).

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});

Це можна абстрагувати функцією, використовуючи зворотний виклик для умовного тесту


Дякую за це! Не можу повірити, що ця функція все ще не підтримується споконвічно! Використовуйте це для збільшення кожного елемента підмасива, для інших, хто читає ... для оновлення кожного елемента просто видаліть оператор if.
Захер

9
Це не безпечне рішення. Якщо запис додається під час запуску оновлення, ви пошкодите свої дані.
Merc

4

Я шукав рішення для цього, використовуючи найновіший драйвер для C # 3.6, і ось виправлення, на якому я врешті вирішив. Ключовим тут є використання "$ []", яке, згідно з монгоДБ, є новим версії 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 #


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
)

Сподіваюся, це допомагає комусь, як я.


1

Я спробував наступне і його добре працює.

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

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


Цей код просто оновлює перший відповідний елемент у масиві. Неправильну відповідь.
Гамлет

Він працює для v2.6. Ви можете бути в більш старій версії? Ось його doc docs.mongodb.com/manual/reference/method/db.collection.update/…
Zou

1

Ви можете оновити всі елементи в MongoDB

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

Він оновить усе значення "статус" до "завершено" в масиві "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

Власне, команда 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}},false,true)

Довідково


Чи можете ви пояснити, чому ви включаєте "помилкове, справжнє" в кінці тут? Я не можу знайти його в документації.
Гарсон

Неправильна відповідь на весь позиційний оператор $[] просто оновлює всі поля у вказаному масиві. Що працює - це відфільтрований позиційний оператор, $[identifier]який працює в полях масиву, що відповідають заданим умовам. Слід використовувати з arrayFilters Refrence: docs.mongodb.com/manual/release-notes/3.6/#arrayfilters та docs.mongodb.com/manual/reference/operator/update/…
Lawrence Eagles

0

Будь ласка, майте на увазі, що деякі відповіді в цій темі, що пропонують використовувати $ [], є WRONG.

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

Вищевказаний код оновить "оброблюваний" до 0 для всіх елементів масиву "події", незалежно від значення "профілю". Запит {"events.profile":10}полягає лише у відфільтруванні всього документа, а не документів у масиві. У цій ситуації це необхідно використовувати $[elem]з , arrayFiltersщоб визначити стан елементів масиву так відповідь Нілу Ланн є правильним.


0

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

Використовуйте $ pull або $ push для оновлення багатьох запитів для оновлення елементів масиву в 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]виконує функцію заповнення всіх елементів у полях масиву, які відповідають умовам, визначеним у фільтрі масиву.

Фільтри масиву

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.