Как мне обновить / сохранить документ в Mongoose?

289

Возможно, это время, возможно, это я утонул в разреженной документации и не смог окунуться в концепцию обновления в Mongoose:)

Здесь сделка:

У меня есть схема контактов и модель (укороченные свойства):

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);
mongoose.model('Contact', ContactSchema); //is this line superflous??
var Contact = mongoose.model('Contact', ContactSchema);

Я получаю запрос от клиента, содержащий нужные мне поля и использую мою модель:

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

И теперь мы достигаем проблемы:

  • Если я вызываю contact.save(function(err){...}), я получаю сообщение об ошибке, если контакт с тем же номером телефона уже существует (как и ожидалось - уникальным)
  • Я не могу вызвать update() при контакте, так как этот метод не существует в документе
  • Если я вызываю обновление модели:
    Contact.update({phone:request.phone}, contact, {upsert: true}, function(err{...})
    Я попадаю в бесконечный цикл, потому что реализация обновления Mongoose явно не хочет, чтобы объект был вторым параметром.
  • Если я делаю то же самое, но во втором параметре передаю ассоциативный массив свойств запроса {status: request.status, phone: request.phone ...}, он работает, но тогда я не имею ссылки на конкретный контакт и не могу узнать его createdAt и updatedAt свойства.

Итак, нижняя строка, после всего, что я пробовал: дал документ contact, как мне его обновить, если он существует, или добавить его, если это не так?

Спасибо за ваше время.

  • 0
    Как насчет перехвата в pre для save ?
Теги:
mongoose

23 ответа

65
Лучший ответ

Хорошо, я ждал достаточно долго и не отвечал. Наконец, отказался от всего подхода update/upsert и пошел с:

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

Это работает? Ага. Я доволен этим? Возможно нет. 2 вызова БД вместо одного.
Надеемся, что в будущей реализации Mongoose появится функция Model.upsert.

  • 2
    В этом примере используется интерфейс, добавленный в MongoDB 2.2, для указания параметров multi и upsert в форме документа. .. include :: /include/fact-upsert-multi-options.rst В документации говорится об этом, не знаю, куда идти дальше.
  • 1
    Хотя это должно работать, теперь вы выполняете 2 операции (поиск, обновление), когда требуется только 1 (upsert). @chrixian показывает правильный способ сделать это.
Показать ещё 4 комментария
315

Mongoose теперь поддерживает это с помощью findOneAndUpdate (вызывает MongoDB findAndModify).

Опция upsert = true создает объект, если он не существует. по умолчанию ложно.

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

В старых версиях Mongoose не поддерживает эти ловушки с помощью этого метода:

  • по умолчанию
  • сеттеры
  • валидаторы
  • промежуточный слой
  • 13
    Это должен быть актуальный ответ. Большинство других использует два вызова или (я полагаю) возвращается к родному драйверу mongodb.
  • 8
    проблема с findOneAndUpdate заключается в том, что предварительное сохранение не будет выполнено.
Показать ещё 9 комментариев
175

Я просто сожгла твёрдую 3 часа, пытаясь решить ту же проблему. В частности, я хотел "заменить" весь документ, если он существует, или вставить его иначе. Здесь решение:

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

// Convert the Model instance to a simple object using Model '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{...});

Я создал проблему на странице проекта Mongoose, указав, что информация об этом будет добавлена ​​в документы.

  • 1
    Документация кажется плохой на данный момент. В документации API есть некоторые (ищите «обновление» на странице. Похоже на это: MyModel.update({ age: { $gt: 18 } }, { oldEnough: true }, fn); и MyModel.update({ name: 'Tobi' }, { ferret: true }, { multi: true }, fn);
  • 1
    Это прекрасно работает, именно то, что я искал
91

Вы были рядом с

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

но ваш второй параметр должен быть объектом с оператором изменения, например

Contact.update({phone:request.phone}, {$set: { phone: request.phone }}, {upsert: true}, function(err){...})
  • 14
    Я не думаю, что вам нужна часть {$set: ... } здесь, поскольку она автоматически формирует мое чтение
  • 5
    Да, Мангуст говорит, что все превращается в $ set
Показать ещё 2 комментария
22

Очень элегантное решение, которое вы можете достичь, используя цепочку 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);
    });
});
  • 0
    Почему за это не проголосовали? Похоже, отличное решение и очень элегантный
  • 0
    Блестящее решение, на самом деле заставило меня переосмыслить то, как я подхожу к обещаниям.
Показать ещё 3 комментария
14

Я создал учетную запись StackOverflow JUST, чтобы ответить на этот вопрос. После бесплодного поиска в переплетках я сам что-то написал. Так я сделал это, чтобы его можно было применить к любой модели мангуста. Либо импортируйте эту функцию, либо добавьте ее прямо в свой код, где вы делаете обновление.

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

Затем, чтобы обновить документ мангуста, выполните следующие действия:

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

Для этого решения могут потребоваться два вызова БД, но вы получаете преимущество,

  • Проверка схемы против вашей модели, поскольку вы используете .save()
  • Вы можете взломать глубоко вложенные объекты без ручного перечисления в своем вызове обновления, поэтому, если ваша модель изменена, вам не нужно беспокоиться об обновлении кода.

Просто помните, что целевой объект всегда будет переопределять исходный код, даже если источник имеет существующее значение

Кроме того, для массивов, если существующий объект имеет более длинный массив, чем тот, который его заменяет, тогда значения в конце старого массива останутся. Простой способ взломать весь массив состоит в том, чтобы установить старый массив как пустой массив перед обновлением, если это то, что вы намереваетесь делать.

ОБНОВЛЕНИЕ - 01/16/2016 Я добавил дополнительное условие, если имеется массив примитивных значений, Mongoose не понимает, что массив обновляется без использования функции "set".

  • 2
    +1 для создания acc только для этого: P Хотелось бы дать еще +1 для использования .save (), так как findOneAndUpate () делает нас неспособными использовать валидаторы и pre, post и т.д. Спасибо тоже проверю
  • 0
    Извините, но это не сработало :( Я получил размер стека вызовов, превышенный
Показать ещё 4 комментария
12

Мне нужно было обновить/загрузить документ в одну коллекцию, я сделал это, чтобы создать новый литерал объекта следующим образом:

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

состоит из данных, которые я получаю из другого места в моей базе данных, а затем вызываю обновление в Model

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

это вывод, который я получил после запуска script в первый раз:

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

И это результат, когда я запускаю script во второй раз:

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

Я использую версию мангуста 3.6.16

9
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!' });
            });

        });
    });

Вот лучший подход к решению метода обновления в мангусте, вы можете проверить Scotch.io для получения более подробной информации. Это определенно сработало для меня!

  • 5
    Ошибочно думать, что это то же самое, что и обновление MongoDB. Это не атомно.
  • 1
    Я хочу поддержать ответ @ValentinWaeselynck. Код Скотча чистый, но вы получаете документ, а затем обновляете его. В середине этого процесса документ мог быть изменен.
8

Существует ошибка, введенная в 2.6, а также влияет на 2.7, а

Upsert используется для корректной работы 2.4.

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

Взгляните, он содержит важную информацию

ОБНОВЛЕНО:

Это не значит, что upsert не работает. Вот хороший пример того, как его использовать:

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

Вы можете просто обновить запись с этим и получить обновленные данные в ответ

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

это сработало для меня.

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

});
2

Если генераторы доступны, становится еще проще:

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


--
  • 0
    Вы не предоставляете данные обновления ... ОБНОВЛЕНИЕ: отредактировано.
2

Для тех, кто прибывает сюда, ища хорошее решение для "upserting" с поддержкой крючков, это то, что я тестировал и работал. Он по-прежнему требует 2 вызова БД, но намного более стабилен, чем все, что я пробовал за один раз.

// 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);
    });
  });
}
2
ContactSchema.connection.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);
            }
        });
    }
});

  • 0
    Комментарии используют мини-форматирование Markdown
  • 3
    Не уверен, что ты здесь сделал ...
Показать ещё 1 комментарий
1

Никакое другое решение не работало для меня. Я использую почтовый запрос и обновляю данные, если найденный else вставляет его, также _id отправляется с телом запроса, который необходимо удалить.

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

});
1
User.findByIdAndUpdate(req.param('userId'), req.body, (err, user) => {
    if(err) return res.json(err);

    res.json({ success: true });
});
1

Здесь самый простой способ создания/обновления, а также вызов промежуточного программного обеспечения и валидаторов.

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

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

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

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

Model.upsert({unique: 'value'}, updateObject});
  • 0
    Спаси мой день: D много, много спасибо.
0

Следуя ответу Traveling Tech Guy, который уже великолепен, мы можем создать плагин и прикрепить его к mongoose после его инициализации, чтобы .upsert() был доступен на всех моделях.

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

Затем вы можете в User.upsert({ _id: 1 }, { foo: 'bar' }) сделать что-нибудь вроде User.upsert({ _id: 1 }, { foo: 'bar' }) или YouModel.upsert({ bar: 'foo' }, { value: 1 }).

0

чтобы опираться на то, что Мартин Куздович опубликовал выше. Я использую следующее, чтобы сделать обновление, используя mongoose и глубокое слияние json-объектов. Наряду с функцией model.save() в мангусте это позволяет мангусте выполнять полную проверку даже на той, которая полагается на другие значения в json. для этого требуется пакет глубины https://www.npmjs.com/package/deepmerge. Но это очень легкий вес.

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
    Я бы предостерег от использования req.body как есть, перед тестированием на внедрение NoSQL (см. Owasp.org/index.php/Testing_for_NoSQL_injection ).
  • 1
    @TravelingTechGuy Спасибо за осторожность, я все еще плохо знаком с Node и Mongoose. Разве моей модели мангуста с валидаторами не хватит, чтобы поймать попытку инъекции? во время model.save ()
0

Этот файл coffeescript работает для меня с Node - трюк заключается в том, что _id лишился своей оболочки ObjectID при отправке и возврате от клиента, и поэтому это нужно заменить для обновлений (когда no_id не предоставляется, сохранение будет верните, чтобы вставить и добавить один).

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
-2

Прочитав сообщения выше, я решил использовать этот код:

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

если r равно null, мы создаем новый элемент. В противном случае используйте upsert в обновлении, потому что обновление не создает новый элемент.

  • 0
    Если это два звонка в Монго, не правда ли, это правда?
  • 1
    Вы должны обрабатывать любые ошибки из FineOne

Ещё вопросы

Сообщество Overcoder
Наверх
Меню