Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

mongoose: detect if document inserted is a duplicate and if so, return the existing document

This is my code:

    var thisValue = new models.Value({
        id:id,
        title:title //this is a unique value
    });

    console.log(thisValue);

    thisValue.save(function(err, product, numberAffected) {
        if (err) {
            if (err.code === 11000) { //error for dupes
                console.error('Duplicate blocked!');
                models.Value.find({title:title}, function(err, docs)
                   {
                       callback(docs) //this is ugly
                   });
            }
            return;
        }

        console.log('Value saved:', product);
        if (callback) {
            callback(product);
        }
    });

If I detect that a duplicate is trying to be inserted, i block it. However, when that happens, i want to return the existing document. As you can see I have implemented a string of callbacks, but this is ugly and its unpredictable (ie. how do i know which callback will be called? How do i pass in the right one?). Does anyone know how to solve this problem? Any help appreciated.

like image 613
dopatraman Avatar asked Feb 07 '14 22:02

dopatraman


2 Answers

While your code doesn't handle a few error cases, and uses the wrong find function, the general flow is typical giving the work you want to do.

  1. If there are errors other than the duplicate, the callback isn't called, which likely will cause downstream issues in your NodeJs application
  2. use findOne rather than find as there will be only one result given the key is unique. Otherwise, it will return an array.
  3. If your callback expected the traditional error as the first argument, you could directly pass the callback to the findOne function rather than introducing an anonymous function.
  4. You also might want to look at findOneAndUpdate eventually, depending on what your final schema and logic will be.

As mentioned, you might be able to use findOneAndUpdate, but with additional cost.

function save(id, title, callback) {
    Value.findOneAndUpdate(
       {id: id, title: title}, /* query */
       {id: id, title: title}, /* update */
       { upsert: true}, /* create if it doesn't exist */
       callback);
}

There's still a callback of course, but it will write the data again if the duplicate is found. Whether that's an issue is really dependent on use cases.

I've done a little clean-up of your code... but it's really quite simple and the callback should be clear. The callback to the function always receives either the newly saved document or the one that was matched as a duplicate. It's the responsibility of the function calling saveNewValue to check for an error and properly handle it. You'll see how I've also made certain that the callback is called regardless of type of error and is always called with the result in a consistent way.

function saveNewValue(id, title, callback) {
    if (!callback) { throw new Error("callback required"); }
    var thisValue = new models.Value({
        id:id,
        title:title //this is a unique value
    });

    thisValue.save(function(err, product) {
        if (err) {
            if (err.code === 11000) { //error for dupes
                return models.Value.findOne({title:title}, callback);
            }            
        }    
        callback(err, product);
    });
}

Alternatively, you could use the promise pattern. This example is using when.js.

var when = require('when');

function saveNewValue(id, title) {
    var deferred = when.defer();

    var thisValue = new models.Value({
        id:id,
        title:title //this is a unique value
    });

    thisValue.save(function(err, product) {
        if (err) {
            if (err.code === 11000) { //error for dupes
                return models.Value.findOne({title:title}, function(err, val) {
                    if (err) {
                        return deferred.reject(err);
                    }
                    return deferred.resolve(val);
                });
            }
            return deferred.reject(err);
        }
        return deferred.resolve(product);
    });

    return deferred.promise;
}

saveNewValue('123', 'my title').then(function(doc) {
    // success
}, function(err) {
    // failure
});
like image 138
WiredPrairie Avatar answered Oct 23 '22 18:10

WiredPrairie


I really like WiredPrairie's answer, but his promise implementation is way too complicated.

So, I decided to add my own promise implementation.

Mongoose 3.8.x

If you're using latest Mongoose 3.8.x then there is no need to use any other promise module, because since 3.8.0 model .create() method returns a promise:

function saveNewValue(id, title) {
    return models.Value.create({
        id:id,
        title:title //this is a unique value
    }).then(null, function(err) {
        if (err.code === 11000) {
            return models.Value.findOne({title:title}).exec()
        } else {
            throw err;
        }
    });
}

saveNewValue('123', 'my title').then(function(doc) {
    // success
    console.log('success', doc);
}, function(err) {
    // failure
    console.log('failure', err);
});

models.Value.findOne({title:title}).exec() also returns a promise, so there is no need for callbacks or any additional casting here.

And if you don't normally use promises in your code, here is callback version of it:

function saveNewValue(id, title, callback) {
    models.Value.create({
        id:id,
        title:title //this is a unique value
    }).then(null, function(err) {
        if (err.code === 11000) {
            return models.Value.findOne({title:title}).exec()
        } else {
            throw err;
        }
    }).onResolve(callback);
}

Previous versions of Mongoose

If you're using any Mongoose version prior to 3.8.0, then you may need some help from when module:

var when = require('when'),
    nodefn = require('when/node/function');

function saveNewValue(id, title) {
    var thisValue = new models.Value({
        id:id,
        title:title //this is a unique value
    });

    var promise = nodefn.call(thisValue.save.bind(thisValue));

    return promise.spread(function(product, numAffected) {
        return product;
    }).otherwise(function(err) {
        if (err.code === 11000) {
            return models.Value.findOne({title:title}).exec()
        } else {
            throw err;
        }
    });
}

I'm using nodefn.call helper function to turn callback-styled .save() method into a promise. Mongoose team promised to add promises support to it in Mongoose 4.x.

Then I'm using .spread helper method to extract the first argument from .save() callback.

like image 10
Leonid Beschastny Avatar answered Oct 23 '22 19:10

Leonid Beschastny