How do I update/upsert a document in Mongoose?

How do I update/upsert a document in Mongoose?

Perhaps it’s the time, perhaps it’s me drowning in sparse documentation and not being able to wrap my head around the concept of updating in Mongoose 🙂
Here’s the deal:
I have a contact schema and model (shortened properties):
var mongoose = require(‘mongoose’),
Schema = mongoose.Schema;

var mongooseTypes = require(“mongoose-types”),
useTimestamps = mongooseTypes.useTimestamps;

var ContactSchema = new Schema({
phone: {
type: String,
index: {
unique: true,
dropDups: true
}
},
status: {
type: String,
lowercase: true,
trim: true,
default: ‘on’
}
});
ContactSchema.plugin(useTimestamps);
var Contact = mongoose.model(‘Contact’, ContactSchema);

I receive a request from the client, containing the fields I need and use my model thusly:
mongoose.connect(connectionString);
var contact = new Contact({
phone: request.phone,
status: request.status
});

And now we reach the problem:

If I call contact.save(function(err){…}) I’ll receive an error if the contact with the same phone number already exists (as expected – unique)
I can’t call update() on contact, since that method does not exist on a document
If I call update on the model:
Contact.update({phone:request.phone}, contact, {upsert: true}, function(err{…})
I get into an infinite loop of some sorts, since the Mongoose update implementation clearly doesn’t want an object as the second parameter.
If I do the same, but in the second parameter I pass an associative array of the request properties {status: request.status, phone: request.phone …} it works – but then I have no reference to the specific contact and cannot find out its createdAt and updatedAt properties.

So the bottom line, after all I tried: given a document contact, how do I update it if it exists, or add it if it doesn’t?
Thanks for your time.

Solutions/Answers:

Solution 1:

Mongoose now supports this natively with findOneAndUpdate (calls MongoDB findAndModify).

The upsert = true option creates the object if it doesn’t exist. defaults to false.

var query = {'username':req.user.username};
req.newData.username = req.user.username;
MyModel.findOneAndUpdate(query, req.newData, {upsert:true}, function(err, doc){
    if (err) return res.send(500, { error: err });
    return res.send("succesfully saved");
});

In older versions Mongoose does not support these hooks with this method:

  • defaults
  • setters
  • validators
  • middleware

Solution 2:

I just burned a solid 3 hours trying to solve the same problem. Specifically, I wanted to “replace” the entire document if it exists, or insert it otherwise. Here’s the solution:

var contact = new Contact({
  phone: request.phone,
  status: request.status
});

// Convert the Model instance to a simple object using Model's 'toObject' function
// to prevent weirdness like infinite looping...
var upsertData = contact.toObject();

// Delete the _id property, otherwise Mongo will return a "Mod on _id not allowed" error
delete upsertData._id;

// Do the upsert, which works like this: If no Contact document exists with 
// _id = contact.id, then create a new doc using upsertData.
// Otherwise, update the existing doc with upsertData
Contact.update({_id: contact.id}, upsertData, {upsert: true}, function(err{...});

I created an issue on the Mongoose project page requesting that info about this be added to the docs.

Solution 3:

You were close with

Contact.update({phone:request.phone}, contact, {upsert: true}, function(err){...})

but your second parameter should be an object with a modification operator for example

Contact.update({phone:request.phone}, {$set: { phone: request.phone }}, {upsert: true}, function(err){...})

Solution 4:

Well, I waited long enough and no answer. Finally gave up the whole update/upsert approach and went with:

ContactSchema.findOne({phone: request.phone}, function(err, contact) {
    if(!err) {
        if(!contact) {
            contact = new ContactSchema();
            contact.phone = request.phone;
        }
        contact.status = request.status;
        contact.save(function(err) {
            if(!err) {
                console.log("contact " + contact.phone + " created at " + contact.createdAt + " updated at " + contact.updatedAt);
            }
            else {
                console.log("Error: could not save contact " + contact.phone);
            }
        });
    }
});

Does it work? Yep. Am I happy with this? Probably not. 2 DB calls instead of one.
Hopefully a future Mongoose implementation would come up with a Model.upsert function.

Solution 5:

Very elegant solution you can achieve by using chain of Promises:

app.put('url', (req, res) => {

    const modelId = req.body.model_id;
    const newName = req.body.name;

    MyModel.findById(modelId).then((model) => {
        return Object.assign(model, {name: newName});
    }).then((model) => {
        return model.save();
    }).then((updatedModel) => {
        res.json({
            msg: 'model updated',
            updatedModel
        });
    }).catch((err) => {
        res.send(err);
    });
});

Solution 6:

I created a StackOverflow account JUST to answer this question. After fruitlessly searching the interwebs I just wrote something myself. This is how I did it so it can be applied to any mongoose model. Either import this function or add it directly into your code where you are doing the updating.

function upsertObject (src, dest) {

  function recursiveFunc (src, dest) {
    _.forOwn(src, function (value, key) {
      if(_.isObject(value) && _.keys(value).length !== 0) {
        dest[key] = dest[key] || {};
        recursiveFunc(src[key], dest[key])
      } else if (_.isArray(src) && !_.isObject(src[key])) {
          dest.set(key, value);
      } else {
        dest[key] = value;
      }
    });
  }

  recursiveFunc(src, dest);

  return dest;
}

Then to upsert a mongoose document do the following,

YourModel.upsert = function (id, newData, callBack) {
  this.findById(id, function (err, oldData) {
    if(err) {
      callBack(err);
    } else {
      upsertObject(newData, oldData).save(callBack);
    }
  });
};

This solution may require 2 DB calls however you do get the benefit of,

  • Schema validation against your model because you are using .save()
  • You can upsert deeply nested objects without manual enumeration in your update call, so if your model changes you do not have to worry about updating your code

Just remember that the destination object will always override the source even if the source has an existing value

Also, for arrays, if the existing object has a longer array than the one replacing it then the values at the end of the old array will remain. An easy way to upsert the entire array is to set the old array to be an empty array before the upsert if that is what you are intending on doing.

UPDATE – 01/16/2016
I added an extra condition for if there is an array of primitive values, Mongoose does not realize the array becomes updated without using the “set” function.