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

MongodbMongodb Java

Mongodb Problem Overview


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

Mongodb Solutions


Solution 1 - Mongodb

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

If I understand the question correctly, you want to update a document with the contents of another document, but only the fields that are not already present, and completely ignore the fields that are already set (even if to another value).

There is no way to do that in a single command.

You have to query the document first, figure out what you want to $set and then update it (using the old values as a matching filter to make sure you don't get concurrent updates in between).


Another reading of your question would be that you are happy with $set, but do not want to explicitly set all fields. How would you pass in the data then?

You know you can do the following:

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

Solution 3 - Mongodb

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 4 - Mongodb

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 5 - Mongodb

Mongo lets you update nested documents using a . convention. Take a look: https://stackoverflow.com/questions/1145956/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: https://stackoverflow.com/questions/5965181/mongodb-atomic-update-via-merge-document

Solution 6 - Mongodb

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

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 8 - Mongodb

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 9 - Mongodb

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 10 - Mongodb

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

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

Solution 12 - Mongodb

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

to

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

Hope this help!

Solution 13 - Mongodb

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 14 - Mongodb

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 15 - Mongodb

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

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

Solution 16 - Mongodb

use $set do this process

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

Solution 17 - Mongodb

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 18 - Mongodb

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 19 - Mongodb

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionEran MedanView Question on Stackoverflow
Solution 1 - MongodbmehmatrixView Answer on Stackoverflow
Solution 2 - MongodbThiloView Answer on Stackoverflow
Solution 3 - MongodbSamir TalwarView Answer on Stackoverflow
Solution 4 - MongodbSzybkiSaszaView Answer on Stackoverflow
Solution 5 - MongodbPavel VellerView Answer on Stackoverflow
Solution 6 - MongodbXavier GuihotView Answer on Stackoverflow
Solution 7 - MongodbMatt SmithView Answer on Stackoverflow
Solution 8 - MongodbPravin BansalView Answer on Stackoverflow
Solution 9 - Mongodbs7vrView Answer on Stackoverflow
Solution 10 - MongodbVinoView Answer on Stackoverflow
Solution 11 - MongodbPatrick TescherView Answer on Stackoverflow
Solution 12 - Mongodbnguyenthang338View Answer on Stackoverflow
Solution 13 - MongodbGuihgoView Answer on Stackoverflow
Solution 14 - MongodbAshwin ShankarView Answer on Stackoverflow
Solution 15 - Mongodbh-raiView Answer on Stackoverflow
Solution 16 - MongodbKARTHIKEYAN.AView Answer on Stackoverflow
Solution 17 - MongodbAndrewView Answer on Stackoverflow
Solution 18 - MongodbIvan Bravo MendozaView Answer on Stackoverflow
Solution 19 - MongodbPavneet KaurView Answer on Stackoverflow