Как частично обновить объект в MongoDB, чтобы новый объект перекрывал / сливался с существующим

183

Учитывая этот документ сохранен в MongoDB

{
   _id : ...,
   some_key: { 
        param1 : "val1",
        param2 : "val2",
        param3 : "val3"
   }
}

Объект с новой информацией о param2и param3от внешнего мира , должен быть сохранен

var new_info = {
    param2 : "val2_new",
    param3 : "val3_new"
};

Я хочу объединить / наложить новые поля поверх существующего состояния объекта, чтобы param1 не был удален

Делая это

db.collection.update(  { _id:...} , { $set: { some_key : new_info  } } 

Это приведет к тому, что MongoDB будет работать именно так, как было задано, и установит some_key в это значение. замена старого.

{
   _id : ...,
   some_key: { 
      param2 : "val2_new",
      param3 : "val3_new"
   }
}

Каким образом MongoDB может обновлять только новые поля (без явного указания их по одному)? чтобы получить это:

{
   _id : ...,
   some_key: { 
        param1 : "val1",
        param2 : "val2_new",
        param3 : "val3_new"
   }
}

Я использую Java-клиент, но любой пример будет оценен

Эран Медан
источник
2
проголосуйте за вопрос слияния $: jira.mongodb.org/browse/SERVER-21094
Али

Ответы:

107

Если я правильно понимаю вопрос, вы хотите обновить документ содержимым другого документа, но только полями, которых еще нет, и полностью игнорировать поля, которые уже установлены (даже если для другого значения).

Нет способа сделать это в одной команде.

Сначала вы должны запросить документ, выяснить, что вы хотите, $setа затем обновить его (используя старые значения в качестве подходящего фильтра, чтобы убедиться, что вы не получаете одновременных обновлений между ними).


Еще одно прочтение вашего вопроса: вы довольны $set, но не хотите явно устанавливать все поля. Как бы вы передали данные тогда?

Вы знаете, что можете сделать следующее:

db.collection.update(  { _id:...} , { $set: someObjectWithNewData } 
Тило
источник
Если вы хотите установить все поля безоговорочно, вы можете использовать параметр $ multi , например так: db.collection.update( { _id:...} , { $set: someObjectWithNewData, { $multi: true } } )
Villager
12
Нет параметра $ multi. Существует несколько опций (это то, что вы связали), но это не влияет на то, как обновляются поля. Он управляет обновлением одного документа или всех документов, соответствующих параметру запроса.
Боб Кернс
1
Это не имеет никакого смысла вообще. При работе с Монго предпочтительно, чтобы обновления имели атомарные операции. Первое чтение вызывает грязное чтение, когда кто-то другой изменяет ваши данные. А поскольку у mongo нет транзакций, она с радостью испортит данные для вас.
froginvasion
@froginvasion: Вы можете сделать его атомарным, используя старые значения в качестве фильтра соответствия в обновлении. Таким образом, ваше обновление завершится неудачно, если в то же время произошло конфликтующее параллельное обновление. Вы можете обнаружить это и действовать соответственно. Это называется оптимистической блокировкой. Но да, это зависит от того, как ваше приложение правильно его реализует, в самом Mongo нет встроенных транзакций.
Тило
Я думаю, что это более общий вопрос объединения двух объектов в JavaScript. Идея IIRC состоит в том, чтобы просто присвоить свойства нового старому
information_interchange
110

Я решил это с моей собственной функцией. Если вы хотите обновить указанное поле в документе, вам необходимо четко указать его.

Пример:

{
    _id : ...,
    some_key: { 
        param1 : "val1",
        param2 : "val2",
        param3 : "val3"
    }
}

Если вы хотите обновить только param2, это неправильно:

db.collection.update(  { _id:...} , { $set: { some_key : new_info  } }  //WRONG

Вы должны использовать:

db.collection.update(  { _id:...} , { $set: { some_key.param2 : new_info  } } 

Итак, я написал функцию примерно так:

function _update($id, $data, $options=array()){

    $temp = array();
    foreach($data as $key => $value)
    {
        $temp["some_key.".$key] = $value;
    } 

    $collection->update(
        array('_id' => $id),
        array('$set' => $temp)
    );

}

_update('1', array('param2' => 'some data'));
mehmatrix
источник
11
Это должен быть принятый ответ. Для лучшей функции flattenObject см. Gist.github.com/penguinboy/762197
steph643
Спасибо, ваш ответ помог мне. Мне удалось изменить значение определенного ключа пользователя в коллекции пользователей следующим образом:db.users.update( { _id: ObjectId("594dbc3186bb5c84442949b1") }, { $set: { resume: { url: "http://i.imgur.com/UFX0yXs.jpg" } } } )
Люк Шоен
Является ли эта операция атомарной и изолированной? Это означает, что если 2 параллельных потока попытаются установить 2 разных поля одного и того же документа, это приведет к состоянию гонки и непредсказуемому результату
так-случайно-чувак
21

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

Учитывая объект, который вы указали выше:

> db.test.insert({"id": "test_object", "some_key": {"param1": "val1", "param2": "val2", "param3": "val3"}})
WriteResult({ "nInserted" : 1 })

Мы можем обновить только some_key.param2и some_key.param3:

> db.test.findAndModify({
... query: {"id": "test_object"},
... update: {"$set": {"some_key.param2": "val2_new", "some_key.param3": "val3_new"}},
... new: true
... })
{
    "_id" : ObjectId("56476e04e5f19d86ece5b81d"),
    "id" : "test_object",
    "some_key" : {
        "param1" : "val1",
        "param2" : "val2_new",
        "param3" : "val3_new"
    }
}

Вы можете копаться так глубоко, как вам нравится. Это также полезно для добавления новых свойств к объекту, не затрагивая существующие.

Самир Талвар
источник
Могу ли я добавить новый ключ ... как "некоторый ключ": {"param1": "val1", "param2": "val2_new", "param3": "val3_new", "param4": "val4_new",}
Урваши Сони
17

Лучшее решение - извлечь свойства из объекта и сделать их плоскими парами ключ-значение точка-нотация. Вы можете использовать, например, эту библиотеку:

https://www.npmjs.com/package/mongo-dot-notation

Оно имеет .flatten функцию, которая позволяет вам преобразовывать объект в плоский набор свойств, которые затем можно передать модификатору $ set, не беспокоясь о том, что любое свойство вашего существующего объекта БД будет удалено / перезаписано без необходимости.

Взято из mongo-dot-notationдокументов:

var person = {
  firstName: 'John',
  lastName: 'Doe',
  address: {
    city: 'NY',
    street: 'Eighth Avenu',
    number: 123
  }
};



var instructions = dot.flatten(person)
console.log(instructions);
/* 
{
  $set: {
    'firstName': 'John',
    'lastName': 'Doe',
    'address.city': 'NY',
    'address.street': 'Eighth Avenu',
    'address.number': 123
  }
}
*/

И тогда он образует идеальный селектор - он будет обновлять ТОЛЬКО данные свойства. РЕДАКТИРОВАТЬ: Мне нравится быть археологом несколько раз;)

SzybkiSasza
источник
10

Mongo позволяет обновлять вложенные документы, используя .соглашение. Посмотрите: Обновление вложенных документов в mongodb . Вот еще один вопрос из прошлого об обновлении слиянием, например, тот, который вы ищете, я полагаю: MongoDB atomic update через документ 'merge'

Павел Веллер
источник
6

Я имел успех, делая это так:

db.collection.update(  { _id:...} , { $set: { 'key.another_key' : new_info  } } );

У меня есть функция, которая обрабатывает обновления моего профиля динамически

function update(prop, newVal) {
  const str = `profile.${prop}`;
  db.collection.update( { _id:...}, { $set: { [str]: newVal } } );
}

Примечание: «профиль» специфичен для моей реализации, это просто строка ключа, которую вы хотите изменить.

Мэтт Смит
источник
2

Вы могли бы скорее сделать upsert, эта операция в MongoDB используется для сохранения документа в коллекцию. Если документ соответствует критериям запроса, он выполнит операцию обновления, в противном случае он вставит новый документ в коллекцию.

что-то похожее, как показано ниже

db.employees.update(
    {type:"FT"},
    {$set:{salary:200000}},
    {upsert : true}
 )
Pravin
источник
1

Похоже, вы можете установить isPartialObject, который может выполнить то, что вы хотите.

Патрик Тешер
источник
попробовал это. это не позволяет вам сохранить частичные объекты, если я не ошибаюсь ... но спасибо
Эран Медан
1
    // where clause DBObject
    DBObject query = new BasicDBObject("_id", new ObjectId(id));

    // modifications to be applied
    DBObject update = new BasicDBObject();

    // set new values
    update.put("$set", new BasicDBObject("param2","value2"));

   // update the document
    collection.update(query, update, true, false); //3rd param->upsertFlag, 4th param->updateMultiFlag

Если у вас есть несколько полей для обновления

        Document doc = new Document();
        doc.put("param2","value2");
        doc.put("param3","value3");
        update.put("$set", doc);
дешевое вино
источник
1

Запуск Mongo 4.2, db.collection.update()может принимать агрегации трубопровода, что позволяет использовать операторы агрегации , такие , как $addFields, который выводит все существующие поля из входных документов и вновь добавленных полей:

var new_info = { param2: "val2_new", param3: "val3_new" }

// { some_key: { param1: "val1", param2: "val2", param3: "val3" } }
// { some_key: { param1: "val1", param2: "val2"                 } }
db.collection.update({}, [{ $addFields: { some_key: new_info } }], { multi: true })
// { some_key: { param1: "val1", param2: "val2_new", param3: "val3_new" } }
// { some_key: { param1: "val1", param2: "val2_new", param3: "val3_new" } }
  • Первая часть {}- это запрос на совпадение, который фильтрует, какие документы обновлять (в данном случае все документы).

  • Вторая часть [{ $addFields: { some_key: new_info } }]- конвейер агрегации обновлений:

    • Обратите внимание на квадратные скобки, обозначающие использование конвейера агрегации.
    • Поскольку это конвейер агрегации, мы можем использовать $addFields .
    • $addFields выполняет именно то, что вам нужно: обновление объекта так, чтобы новый объект перекрывался / сливался с существующим:
    • В этом случае, { param2: "val2_new", param3: "val3_new" }будут объединены с существующими some_key, оставив param1нетронутыми и либо добавив, либо заменив оба param2и param3.
  • Не забывайте { multi: true }, иначе будет обновлен только первый соответствующий документ.

Ксавье Гихот
источник
1
db.collection.update(  { _id:...} , { $set: { some_key : new_info  } } 

в

db.collection.update( { _id: ..} , { $set: { some_key: { param1: newValue} } } ); 

Надеюсь, это поможет!

nguyenthang338
источник
0

использовать $ set сделать этот процесс

.update({"_id": args.dashboardId, "viewData._id": widgetId}, {$set: {"viewData.$.widgetData": widgetDoc.widgetData}})
.exec()
.then(dashboardDoc => {
    return {
        result: dashboardDoc
    };
}); 
KARTHIKEYAN.A
источник
0

Создайте объект обновления с именами свойств, включая необходимый точечный путь. («somekey.» + из примера OP), а затем используйте, чтобы выполнить обновление.

//the modification that's requested
updateReq = { 
   param2 : "val2_new",
   param3 : "val3_new"   
}

//build a renamed version of the update request
var update = {};
for(var field in updateReq){
    update["somekey."+field] = updateReq[field];
}

//apply the update without modifying fields not originally in the update
db.collection.update({._id:...},{$set:update},{upsert:true},function(err,result){...});
Андрей
источник
0

Да, лучший способ - преобразовать нотацию объекта в строковое представление ключ-значение, как упомянуто в этом комментарии: https://stackoverflow.com/a/39357531/2529199

Я хотел бы выделить альтернативный метод с использованием этой библиотеки NPM: https://www.npmjs.com/package/dot-object который позволяет вам манипулировать различными объектами, используя точечную запись.

Я использовал этот шаблон для программного создания свойства вложенного объекта при принятии значения ключа в качестве переменной функции следующим образом:

const dot = require('dot-object');

function(docid, varname, varvalue){
  let doc = dot.dot({
      [varname]: varvalue 
  });

  Mongo.update({_id:docid},{$set:doc});
}

Этот шаблон позволяет мне использовать как вложенные, так и одноуровневые свойства взаимозаменяемо и аккуратно вставлять их в Mongo.

Если вам нужно поиграть с объектами JS за пределами Mongo, особенно на стороне клиента, но иметь согласованность при работе с Mongo, эта библиотека предоставляет вам больше возможностей, чем упомянутый ранее mongo-dot-notationмодуль NPM.

PS Первоначально я хотел упомянуть это как комментарий, но, очевидно, мой представитель S / O недостаточно высок, чтобы оставить комментарий. Поэтому, не пытаясь вмешаться в комментарии SzybkiSasza, я просто хотел выделить альтернативный модуль.

Эшвин Шанкар
источник
0

Вы должны подумать об обновлении объекта взаимозаменяемо, а затем просто сохранить объект с обновленными полями. Нечто подобное сделано ниже

function update(_id) {
    return new Promise((resolve, reject) => {

        ObjModel.findOne({_id}).exec((err, obj) => {

            if(err) return reject(err)

            obj = updateObject(obj, { 
                some_key: {
                    param2 : "val2_new",
                    param3 : "val3_new"
                }
            })

            obj.save((err, obj) => {
                if(err) return reject(err)
                resolve(obj)
            })
        })
    })
}


function updateObject(obj, data) {

    let keys = Object.keys(data)

    keys.forEach(key => {

        if(!obj[key]) obj[key] = data[key]

        if(typeof data[key] == 'object') 
            obj[key] = updateObject(obj[key], data[key])
        else
            obj[key] = data[key]
    })

    return obj
}
Иван Браво Мендоса
источник
0

Вы должны использовать встроенные документы (строка объекта пути)

let update = {}
Object.getOwnPropertyNames(new_info).forEach(param => {
   update['some_key.' + param] = new_info[param]
})

Итак, в JavaScript вы можете использовать Spread Operator (...) для обновления

db.collection.update(  { _id:...} , { $set: { ...update  } } 
Guihgo
источник