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.
pre
for save
?
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
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.
MyModel.update({ age: { $gt: 18 } }, { oldEnough: true }, fn);
and MyModel.update({ name: 'Tobi' }, { ferret: true }, { multi: true }, fn);
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){...})
{$set: ... }
part here as its automatic form my reading
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.
runValidators: true
during an update: update docs (however, update validators only run on $set
and $unset
operations)
.upsert()
to be available on all models. stackoverflow.com/a/50208331/1586406
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.
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);
});
});
(model) => { return model.save(); }
as model => model.save()
, and also (err) => { res.send(err); }
as err => res.send(err)
;)
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.
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...
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
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!!!
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);
});
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"
})
}
});
});
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"}); }); });
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);
});
})
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);
});
});
}
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();
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');
});
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.
//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);
});
});
--
User.findByIdAndUpdate(req.param('userId'), req.body, (err, user) => {
if(err) return res.json(err);
res.json({ success: true });
});
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});
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
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);
});
});
req.body
as-is, before testing for NoSQL injection (see owasp.org/index.php/Testing_for_NoSQL_injection).
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.
Success story sharing
newData
:(