Mongoose: обнаружить, если вставленный документ является дубликатом, и если да, верните существующий документ

Это мой код:

    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);
        }
    });

Если я обнаруживаю, что дубликат пытается встать, я блокирую его. Однако, когда это произойдет, я хочу вернуть существующий документ. Как вы видите, я реализовал цепочку обратных вызовов, но это уродливо и непредсказуемо (то есть, как я узнаю, какой вызов будет вызван? Как я могу перейти вправо?). кто-нибудь знает, как решить эту проблему? Любая помощь была оценена.

Ответ 1

Пока ваш код не обрабатывает несколько ошибок и использует неправильную функцию find, общий поток типичен для работы, которую вы хотите сделать.

  • Если есть ошибки, отличные от дубликата, обратный вызов не вызывается, что, вероятно, вызовет проблемы с нисходящим потоком в вашем приложении NodeJs
  • используйте findOne, а не find, так как будет только один результат, если ключ уникален. В противном случае он вернет массив.
  • Если ваш обратный вызов ожидал традиционный error в качестве первого аргумента, вы могли бы напрямую передать обратный вызов функции findOne, а не вводить анонимную функцию.
  • Вы также можете посмотреть findOneAndUpdate в зависимости от того, какова будет ваша окончательная схема и логика.

Как уже упоминалось, вы можете использовать findOneAndUpdate, но с дополнительными затратами.

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);
}

Конечно, есть обратный вызов, но он снова напишет данные, если будет найден дубликат. Независимо от того, действительно ли эта проблема зависит от вариантов использования.

Я немного очистил ваш код... но это действительно очень просто, и обратный вызов должен быть ясным. Функция callback для функции всегда получает либо вновь сохраненный документ, либо тот, который был сопоставлен как дубликат. Ответственность функции, вызывающей saveNewValue, выполняет функция проверки ошибки и ее правильной обработки. Вы увидите, как я также убедился, что обратный вызов вызывается независимо от типа ошибки и всегда вызывается с результатом согласованным образом.

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);
    });
}

В качестве альтернативы вы можете использовать шаблон promise. В этом примере используется 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
});

Ответ 2

Мне действительно нравится ответ WiredPrairie, но реализация его обещания слишком сложна.

Итак, я решил добавить свою собственную реализацию обещания.

Mongoose 3.8.x

Если вы используете последний Mongoose 3.8.x, тогда нет необходимости использовать какой-либо другой модуль обещания, потому что, поскольку метод 3.8.0 model .create() возвращает обещание:

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() также возвращает обещание, поэтому здесь нет необходимости в обратных вызовах или любом дополнительном литье.

И если вы обычно не используете promises в своем коде, вот его обратная версия:

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);
}

Предыдущие версии Mongoose

Если вы используете версию Mongoose до 3.8.0, вам может понадобиться помощь от модуля when:

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;
        }
    });
}

Я использую вспомогательную функцию nodefn.call, чтобы превратить метод .save() в обратном вызове в обещание. Команда Mongoose обещала добавить поддержку promises в Mongoose 4.x.

Затем я использую .spread вспомогательный метод для извлечения первого аргумента из обратного вызова .save().