'How do I partially update an object in MongoDB so the new object will overlay / merge with the existing one

Given this document saved in MongoDB

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

An object with new information on param2 and param3 from the outside world needs to be saved

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

I want to merge / overlay the new fields over the existing state of the object so that param1 doesn't get removed

Doing this

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

Will lead to MongoDB is doing exactly as it was asked, and sets some_key to that value. replacing the old one.

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

What is the way to have MongoDB update only new fields (without stating them one by one explicitly)? to get this:

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

I'm using the Java client, but any example will be appreciated



Solution 1:[1]

I solved it with my own function. If you want to update specified field in document you need to address it clearly.

Example:

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

If you want to update param2 only, it's wrong to do:

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

You must use:

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

So i wrote a function something like that:

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

Solution 2:[2]

You can use dot-notation to access and set fields deep inside objects, without affecting the other properties of those objects.

Given the object you specified above:

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

We can update just some_key.param2 and 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"
    }
}

You can delve as deep as you like. This is also useful for adding new properties to an object without affecting the existing ones.

Solution 3:[3]

The best solution is to extract properties from object and make them flat dot-notation key-value pairs. You could use for example this library:

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

It has .flatten function that allows you to change object into flat set of properties that could be then given to $set modifier, without worries that any property of your existing DB object will be deleted/overwritten without need.

Taken from mongo-dot-notation docs:

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
  }
}
*/

And then it forms perfect selector - it will update ONLY given properties. EDIT: I like to be archeologist some times ;)

Solution 4:[4]

Mongo lets you update nested documents using a . convention. Take a look: Updating nested documents in mongodb. Here's another question from the past about a merge update, like the one you're looking for I believe: MongoDB atomic update via 'merge' document

Solution 5:[5]

Starting in Mongo 4.2, db.collection.updateMany() (or db.collection.update()) can accept an aggregation pipeline, which allows using aggregation operators such as $addFields, which outputs all existing fields from the input documents and newly added fields:

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.updateMany({}, [{ $addFields: { some_key: new_info } }])
// { some_key: { param1: "val1", param2: "val2_new", param3: "val3_new" } }
// { some_key: { param1: "val1", param2: "val2_new", param3: "val3_new" } }
  • The first part {} is the match query, filtering which documents to update (in this case all documents).

  • The second part [{ $addFields: { some_key: new_info } }] is the update aggregation pipeline:

    • Note the squared brackets signifying the use of an aggregation pipeline.
    • Since this is an aggregation pipeline, we can use $addFields.
    • $addFields performs exactly what you need: updating the object so that the new object will overlay / merge with the existing one:
    • In this case, { param2: "val2_new", param3: "val3_new" } will be merged into the existing some_key by keeping param1 untouched and either add or replace both param2 and param3.

Solution 6:[6]

I had success doing it this way:

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

I have a function that handles my profile updates dynamically

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

Note: 'profile' is specific to my implementation, it is just the string of the key that you would like to modify.

Solution 7:[7]

You could rather do a upsert, this operation in MongoDB is utilized to save document into collection. If document matches query criteria then it will perform update operation otherwise it will insert a new document into collection.

something similar as below

db.employees.update(
    {type:"FT"},
    {$set:{salary:200000}},
    {upsert : true}
 )

Solution 8:[8]

You can use $mergeObjects in the aggregation based update. Something like

db.collection.update(
   { _id:...},
   [{"$set":{
      "some_key":{
        "$mergeObjects":[
          "$some_key",
          new info or { param2 : "val2_new", param3 : "val3_new"}
       ]
      }
   }}]
)

More examples here

Solution 9:[9]

    // 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

If you have multiple fields to be updated

        Document doc = new Document();
        doc.put("param2","value2");
        doc.put("param3","value3");
        update.put("$set", doc);

Solution 10:[10]

It looks like you can set isPartialObject which might accomplish what you want.

Solution 11:[11]

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

to

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

Hope this help!

Solution 12:[12]

You have to use Embedded Documents (stringfy the path object)

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

And so, in JavaScript you can use Spread Operator (...) to update

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

Solution 13:[13]

Yeah, the best way is to convert the object notation to a flat key-value string representation, as mentioned in this comment: https://stackoverflow.com/a/39357531/2529199

I wanted to highlight an alternative method using this NPM library: https://www.npmjs.com/package/dot-object which lets you manipulate different objects using dot notation.

I used this pattern to programatically create a nested object property when accepting the key-value as a function variable, as follows:

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

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

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

This pattern lets me use nested as well as single-level properties interchangeably, and insert them cleanly into Mongo.

If you need to play around with JS Objects beyond just Mongo, especially on the client-side but have consistency when working with Mongo, this library gives you more options than the earlier mentioned mongo-dot-notation NPM module.

P.S I originally wanted to just mention this as a comment but apparently my S/O rep isn't high enough to post a comment. So, not trying to muscle in on SzybkiSasza's comment, just wanted to highlight providing an alternative module.

Solution 14:[14]

I tried findAndModify() to update a particular field in a pre-existing object.

https://docs.mongodb.com/manual/reference/method/db.collection.findAndModify/

Solution 15:[15]

use $set do this process

.update({"_id": args.dashboardId, "viewData._id": widgetId}, {$set: {"viewData.$.widgetData": widgetDoc.widgetData}})
.exec()
.then(dashboardDoc => {
    return {
        result: dashboardDoc
    };
}); 

Solution 16:[16]

Make an update object with the property names including the necessary dot path. ("somekey."+ from OP's example), and then use that do the update.

//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){...});

Solution 17:[17]

You should think about updating the object interchangeably and then simply store the object with the updated fields. Something like done below

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
}

Solution 18:[18]

If you want to update multiple fields of an object, you can try this:-

 let fieldsToUpdate = {};
 for (const key in allFields) {
 const fieldName = `flags.${key}`;   // define field as string literal
     fieldsToUpdate[fieldName] = allFields[key];
 }
 db.collection.updateOne(query, { $set: { ...fieldsToUpdate } } );