Raz Buchnik
Raz Buchnik

Reputation: 8411

MongoDB update only changed fields in an object instead of replacing the whole object

I want to update a document contains object in MongoDB. Since the updated object is given from the user, I don't know what exactly he was about to change.

For example I have this document in users collection:

{
  _id: 1,
  date: new Date(),
  info: {
    first_name: "a",
    last_name: "b",
    phone: "c"
  }
}

The user is able to edit first_name and last_name but not phone.

This is the req.body (the input I receive from the user):

{
  info: {
    first_name: "aa"
  }
}

Now in order to update only "info.first_name" and preserve the other fields that are stored in the info ojbect, I have to set an update query like this:

db.users.update({}, {$set: {"info.first_name": req.body.info.first_name}}, cb);

But the input is can be dynamic and changeable. I don't know if it's always be first name that was updated it can be last name or even other sub object like address: "info.address.city" (for example)

For that reason I handle those updates like this:

$set: req.body

And that is it. It works but I have an issue with this. It replaces the whole info ojbect and doesn't preserve other keys that were stored in it - phone is getting deleted and last name is getting deleted.

Is there a way to tell MongoDB to not replace the whole object but just update those keys that are mentioned?

Upvotes: 7

Views: 10391

Answers (3)

Tyler Dane
Tyler Dane

Reputation: 1069

Using $or and $ne, you can indeed create one update operation that:

  • compares fields
  • only modifies the changed fields
  • doesn't do anything if nothing changed

Here's the basic idea:

const new_info = {
    first_name: "aa"
}

db.collection.updateOne(
{ 
  _id : ObjectId("1"),
  $or: [ 
      { info.first_name: { $ne: new_info.first_name }},
      { info.last_name: { $ne: new_info.last_name }},
      { info.phone: { $ne: new_info.phone }},
  ]
},
{
  $set: {
      info.first_name: new_info.first_name,
      info.last_name: new_info.last_name,
      info.phone: new_info.phone,
  }
}
)

You'd likely have to modify the above to only check for the other fields if they're present in the req.body. I'm also not certain if querying the nested documents like that is exactly right. However, I hope that gives you the general idea.

All credit goes to Prasad_Saya, who shared this exact approach on this MongoDB forum post

Upvotes: 0

user10135683
user10135683

Reputation: 51

would it not be easier to read the current entry in the DB and spread the req.body over the current entry?

// read current entry in database
let currentObject = await user.findOne({_id});

// merge current entry with changed updated key:values
let newObject = ( currentObject, ...req.body );

// write update to database
let user.findOneAndUpdate({_id}, {$set: newObject}, )

Upvotes: 4

audiochick
audiochick

Reputation: 184

I also looked into it, and you can't set mongo to only update specific fields. What you can do instead is dynamically build your query based on input.

I usually looped through the input, and then validated what was allowed to be modified, and built a dynamic query based on that:

var updateQuery = {};
for(key of req.body.info) {
    if (req.body.info.hasOwnProperty(key) && canBeModified(key)) //this would be a function to validate what can be updated
        updateQuery['info.' + key] = req.body.info[key]};
}
db.users.update({}, {$set: updateQuery}, cb);

Upvotes: 0

Related Questions