503

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:

  1. 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)
  2. I can't call update() on contact, since that method does not exist on a document
  3. 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.
  4. 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.

Kaspar Lee
  • 5,446
  • 4
  • 31
  • 54
Traveling Tech Guy
  • 27,194
  • 23
  • 111
  • 159

23 Answers23

562

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
Amarizky
  • 3
  • 3
Pascalius
  • 14,024
  • 4
  • 40
  • 38
211

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.

clint
  • 14,402
  • 12
  • 70
  • 79
  • 1
    Documentation seems poor at the moment. There is some in the API docs (search for "update" on the page. Looks like this: `MyModel.update({ age: { $gt: 18 } }, { oldEnough: true }, fn);` and `MyModel.update({ name: 'Tobi' }, { ferret: true }, { multi: true }, fn);` – CpILL Mar 15 '12 at 17:57
  • for the case document is not found, which _id is used? Mongoose generates it or the one that was queried against? – Haider Jun 13 '20 at 21:25
104

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){...})
Andreas Hultgren
  • 14,763
  • 4
  • 44
  • 48
chrixian
  • 2,783
  • 2
  • 15
  • 16
83

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.

Traveling Tech Guy
  • 27,194
  • 23
  • 111
  • 159
  • 2
    **This examples uses the interface added in MongoDB 2.2 to specify the multi and the upsert options in a document form. .. include:: /includes/fact-upsert-multi-options.rst** The documentation is stating this, don't know where to go from here. – Donald Derek Apr 21 '13 at 23:38
  • 1
    Although this should work, you are now running 2 operations (find, update) when only 1 (upsert) is needed. @chrixian shows the correct way to do this. – respectTheCode Apr 27 '13 at 12:45
  • 15
    It's worth noting that this is the only answer which allows Mongoose's validators to kick in. [As per the docs](http://mongoosejs.com/docs/api.html#model_Model.update), validation doesn't occur if you call update. – Tom Spencer May 06 '14 at 17:00
  • @fiznool looks like you can manually pass in the option `runValidators: true` during an update: [update docs](http://mongoosejs.com/docs/validation.html#update-validators) (however, update validators only run on `$set` and `$unset` operations) – Danny Dec 30 '16 at 03:20
  • See my answer based on this one if you need `.upsert()` to be available on all models. https://stackoverflow.com/a/50208331/1586406 – spondbob May 07 '18 at 06:37
  • This is not a recommended solution. The object can be created between the find and the save and you can end up with duplicates. You should do an upsert. This should not be marked as the accepted answer. – justin.m.chase Jan 24 '20 at 17:46
  • https://stackoverflow.com/questions/30002264/mongoose-upsert-not-working – Omar Zaoujal Jan 11 '22 at 02:43
79

I'm the maintainer of Mongoose. The more modern way to upsert a doc is to use the Model.updateOne() function.

await Contact.updateOne({
    phone: request.phone
}, { status: request.status }, { upsert: true });

If you need the upserted doc, you can use Model.findOneAndUpdate()

const doc = await Contact.findOneAndUpdate({
    phone: request.phone
}, { status: request.status }, { upsert: true, useFindAndModify: false });

The key takeaway is that you need to put the unique properties in the filter parameter to updateOne() or findOneAndUpdate(), and the other properties in the update parameter.

Here's a tutorial on upserting documents with Mongoose.

user1165759
  • 333
  • 3
  • 14
vkarpov15
  • 3,614
  • 24
  • 21
31

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);
    });
});
  • Why was this not upvoted? Seems like a great solution and very elegant – MadOgre Feb 05 '17 at 10:33
  • Brilliant solution, actually made me rethink how I approach promises. – lux Mar 07 '17 at 01:03
  • 4
    Even more elegant would be to rewrite `(model) => { return model.save(); }` as `model => model.save()`, and also `(err) => { res.send(err); }` as `err => res.send(err)` ;) – Jeremy Thille Jul 27 '17 at 09:42
18

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.

Aaron Mast
  • 249
  • 3
  • 4
  • 2
    +1 for creating acc just for this :P Wish I could give another + 1 just for using .save(), becaunse findOneAndUpate() makes us unable to use validators and pre, post, etc stuff. Thanks, I'll check this too – user1576978 Jul 06 '15 at 14:42
  • Sorry, but it didn't work here :( I got call stack size exceeded – user1576978 Jul 07 '15 at 16:13
  • What version of lodash are you using? I am using lodash version 2.4.1 Thanks! – Aaron Mast Jul 08 '15 at 17:01
  • Also, how complex of objects are you upserting? If they are too large the node process may not be able to handle the number of recursive calls required to merge the objects. – Aaron Mast Jul 08 '15 at 17:07
  • I used this, but had to add `if(_.isObject(value) && _.keys(value).length !== 0) {` on the guard condition to stop the stack overflow. Lodash 4+ here, it seems to convert non object values into objects in the `keys` call so recursive guard was always true. Maybe there's a better way, but its nearly working for me now... – Richard G Jul 26 '16 at 13:23
  • Thanks Richard! I updated the code to reflect your findings. – Aaron Mast Jul 27 '16 at 14:20
13

I needed to update/upsert a document into one collection, what I did was to create a new object literal like this:

notificationObject = {
    user_id: user.user_id,
    feed: {
        feed_id: feed.feed_id,
        channel_id: feed.channel_id,
        feed_title: ''
    }
};

composed from data that I get from somewhere else in my database and then call update on the Model

Notification.update(notificationObject, notificationObject, {upsert: true}, function(err, num, n){
    if(err){
        throw err;
    }
    console.log(num, n);
});

this is the ouput that I get after running the script for the first time:

1 { updatedExisting: false,
    upserted: 5289267a861b659b6a00c638,
    n: 1,
    connectionId: 11,
    err: null,
    ok: 1 }

And this is the output when I run the script for the second time:

1 { updatedExisting: true, n: 1, connectionId: 18, err: null, ok: 1 }

I'm using mongoose version 3.6.16

Ionică Bizău
  • 109,027
  • 88
  • 289
  • 474
andres_gcarmona
  • 131
  • 1
  • 4
10
app.put('url', function(req, res) {

        // use our bear model to find the bear we want
        Bear.findById(req.params.bear_id, function(err, bear) {

            if (err)
                res.send(err);

            bear.name = req.body.name;  // update the bears info

            // save the bear
            bear.save(function(err) {
                if (err)
                    res.send(err);

                res.json({ message: 'Bear updated!' });
            });

        });
    });

Here is a better approach to solving the update method in mongoose, you can check Scotch.io for more details. This definitely worked for me!!!

Eyo Okon Eyo
  • 16,326
  • 2
  • 13
  • 17
  • 5
    It is a mistake to think this does the same thing as MongoDB's update. It's not atomic. – Valentin Waeselynck Oct 21 '15 at 16:10
  • 1
    I want to back up @ValentinWaeselynck answer. Scotch's code is clean - but you are fetching a document and then updating. In the middle of that process the document could of been changed. – Nick Pineda Mar 25 '16 at 17:27
8

There is a bug introduced in 2.6, and affects to 2.7 as well

The upsert used to work correctly on 2.4

https://groups.google.com/forum/#!topic/mongodb-user/UcKvx4p4hnY https://jira.mongodb.org/browse/SERVER-13843

Take a look, it contains some important info

UPDATED:

It doesnt mean upsert does not work. Here is a nice example of how to use it:

User.findByIdAndUpdate(userId, {online: true, $setOnInsert: {username: username, friends: []}}, {upsert: true})
    .populate('friends')
    .exec(function (err, user) {
        if (err) throw err;
        console.log(user);

        // Emit load event

        socket.emit('load', user);
    });
helpse
  • 1,518
  • 1
  • 18
  • 29
8

You can simply update the record with this and get the updated data in response

router.patch('/:id', (req, res, next) => {
    const id = req.params.id;
    Product.findByIdAndUpdate(id, req.body, {
            new: true
        },
        function(err, model) {
            if (!err) {
                res.status(201).json({
                    data: model
                });
            } else {
                res.status(500).json({
                    message: "not found any relative data"
                })
            }
        });
});
Andrew Au
  • 812
  • 7
  • 18
Muhammad Awais
  • 156
  • 1
  • 6
  • @Awais/@Andrew this piece of code is actually updating my id too, any idea on how we can get rid of the **id** getting updated. – srinivas Apr 12 '21 at 08:10
6

this worked for me.

app.put('/student/:id', (req, res) => {
    Student.findByIdAndUpdate(req.params.id, req.body, (err, user) => {
        if (err) {
            return res
                .status(500)
                .send({error: "unsuccessful"})
        };
        res.send({success: "success"});
    });

});
Emmanuel Ndukwe
  • 326
  • 5
  • 7
  • Thanks. This was the one which finally worked for me! – Luis Febro Oct 14 '19 at 15:24
  • @Emmanuel this is working quite fine for me too, but if we observe carefully this is updating the id of the record, any idea how we can avoid updating the id? – srinivas Apr 12 '21 at 08:14
  • @srinivas you can add 'const {_id, ...data} = req.body;' before the Student.find.. line (line 2), and replace req.body on line 2 with data to remove the _id field. Note this is a shallow copy of req.body though. – James Jan 25 '22 at 22:54
4

Here's the simplest way to create/update while also calling the middleware and validators.

Contact.findOne({ phone: request.phone }, (err, doc) => {
    const contact = (doc) ? doc.set(request) : new Contact(request);

    contact.save((saveErr, savedContact) => {
        if (saveErr) throw saveErr;
        console.log(savedContact);
    });
})
Min
  • 99
  • 1
  • 2
  • 8
3

For anyone arriving here still looking for good a solution for "upserting" with hooks support, this is what I have tested and working. It still requires 2 DB calls but is much more stable than anything I've tried in a single call.

// Create or update a Person by unique email.
// @param person - a new or existing Person
function savePerson(person, done) {
  var fieldsToUpdate = ['name', 'phone', 'address'];

  Person.findOne({
    email: person.email
  }, function(err, toUpdate) {
    if (err) {
      done(err);
    }

    if (toUpdate) {
      // Mongoose object have extra properties, we can either omit those props
      // or specify which ones we want to update.  I chose to update the ones I know exist
      // to avoid breaking things if Mongoose objects change in the future.
      _.merge(toUpdate, _.pick(person, fieldsToUpdate));
    } else {      
      toUpdate = person;
    }

    toUpdate.save(function(err, updated, numberAffected) {
      if (err) {
        done(err);
      }

      done(null, updated, numberAffected);
    });
  });
}
Terry
  • 14,099
  • 9
  • 56
  • 84
3

If generators are available it becomes even more easier:

var query = {'username':this.req.user.username};
this.req.newData.username = this.req.user.username;
this.body = yield MyModel.findOneAndUpdate(query, this.req.newData).exec();
VuesomeDev
  • 4,095
  • 2
  • 34
  • 44
3

No other solution worked for me. I'm using a post request and updating data if found else insert it, also _id is sent with the request body that's needs to be removed.

router.post('/user/createOrUpdate', function(req,res){
    var request_data = req.body;
    var userModel = new User(request_data);
    var upsertData = userModel.toObject();
    delete upsertData._id;

    var currentUserId;
    if (request_data._id || request_data._id !== '') {
        currentUserId = new mongoose.mongo.ObjectId(request_data._id);
    } else {
        currentUserId = new mongoose.mongo.ObjectId();
    }

    User.update({_id: currentUserId}, upsertData, {upsert: true},
        function (err) {
            if (err) throw err;
        }
    );
    res.redirect('/home');

});
Priyanshu Chauhan
  • 5,297
  • 5
  • 35
  • 34
3

Following Traveling Tech Guy's answer, which already awesome, we can create a plugin and attach it to mongoose once we initialise it so that .upsert() will be available on all models.

plugins.js

export default (schema, options) => {
  schema.statics.upsert = async function(query, data) {
    let record = await this.findOne(query)
    if (!record) {
      record = new this(data)
    } else {
      Object.keys(data).forEach(k => {
        record[k] = data[k]
      })
    }
    return await record.save()
  }
}

db.js

import mongoose from 'mongoose'

import Plugins from './plugins'

mongoose.connect({ ... })
mongoose.plugin(Plugins)

export default mongoose

Then you can do something like User.upsert({ _id: 1 }, { foo: 'bar' }) or YouModel.upsert({ bar: 'foo' }, { value: 1 }) whenever you want.

spondbob
  • 1,523
  • 2
  • 17
  • 34
2
//Here is my code to it... work like ninj

router.param('contractor', function(req, res, next, id) {
  var query = Contractors.findById(id);

  query.exec(function (err, contractor){
    if (err) { return next(err); }
    if (!contractor) { return next(new Error("can't find contractor")); }

    req.contractor = contractor;
    return next();
  });
});

router.get('/contractors/:contractor/save', function(req, res, next) {

    contractor = req.contractor ;
    contractor.update({'_id':contractor._id},{upsert: true},function(err,contractor){
       if(err){ 
            res.json(err);
            return next(); 
            }
    return res.json(contractor); 
  });
});


--
Ron Belson
  • 31
  • 1
  • 2
2

I just came back to this issue after a while, and decided to publish a plugin based on the answer by Aaron Mast.

https://www.npmjs.com/package/mongoose-recursive-upsert

Use it as a mongoose plugin. It sets up a static method which will recursively merge the object passed in.

Model.upsert({unique: 'value'}, updateObject});
Zargold
  • 1,892
  • 18
  • 24
Richard G
  • 5,243
  • 11
  • 53
  • 95
2
User.findByIdAndUpdate(req.param('userId'), req.body, (err, user) => {
    if(err) return res.json(err);

    res.json({ success: true });
});
ziishaned
  • 4,944
  • 3
  • 25
  • 32
  • While this code snippet may solve the problem, it doesn't explain why or how it answers the question. Please [include an explanation for your code](//meta.stackexchange.com/q/114762/269535), as that really helps to improve the quality of your post. Remember that you are answering the question for readers in the future, and those people might not know the reasons for your code suggestion. **Flaggers / reviewers:** [For code-only answers such as this one, downvote, don't delete!](//meta.stackoverflow.com/a/260413/2747593) – Patrick Aug 31 '17 at 08:25
0

This coffeescript works for me with Node - the trick is that the _id get's stripped of its ObjectID wrapper when sent and returned from the client and so this needs to be replaced for updates (when no _id is provided, save will revert to insert and add one).

app.post '/new', (req, res) ->
    # post data becomes .query
    data = req.query
    coll = db.collection 'restos'
    data._id = ObjectID(data._id) if data._id

    coll.save data, {safe:true}, (err, result) ->
        console.log("error: "+err) if err
        return res.send 500, err if err

        console.log(result)
        return res.send 200, JSON.stringify result
Simon H
  • 20,332
  • 14
  • 71
  • 128
0

to build on what Martin Kuzdowicz posted above. I use the following to do an update using mongoose and a deep merge of json objects. Along with the model.save() function in mongoose this allows mongoose to do a full validation even one that relies on other values in the json. it does require the deepmerge package https://www.npmjs.com/package/deepmerge. But that is a very light weight package.

var merge = require('deepmerge');

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

    const modelId = req.body.model_id;

    MyModel.findById(modelId).then((model) => {
        return Object.assign(model, merge(model.toObject(), req.body));
    }).then((model) => {
        return model.save();
    }).then((updatedModel) => {
        res.json({
            msg: 'model updated',
            updatedModel
        });
    }).catch((err) => {
        res.send(err);
    });
});
  • 1
    I'd caution against using `req.body` as-is, before testing for **NoSQL injection** (see https://www.owasp.org/index.php/Testing_for_NoSQL_injection). – Traveling Tech Guy Apr 18 '17 at 16:15
  • 1
    @TravelingTechGuy Thanks for the caution I'm still new to Node and Mongoose. Wouldn't my mongoose model with validators be enough to catch an injection attempt? during the model.save() – Chris Deleo Apr 19 '17 at 19:08
-6

After reading the posts above, I decided to use this code:

    itemModel.findOne({'pid':obj.pid},function(e,r){
        if(r!=null)
        {
             itemModel.update({'pid':obj.pid},obj,{upsert:true},cb);
        }
        else
        {
            var item=new itemModel(obj);
            item.save(cb);
        }
    });

if r is null, we create new item. Otherwise, use upsert in update because update does not create new item.

Grant Li
  • 973
  • 9
  • 8