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

369

Возможно, пришло время, возможно, это я тону в редких документах и ​​не могу обернуться вокруг концепции обновления в 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);
var Contact = mongoose.model('Contact', ContactSchema);

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

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

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

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

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

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

Путешествующий техник
источник
А как насчет зацепки preдля save?
Shamoon

Ответы:

429

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 не поддерживает эти ловушки с помощью этого метода:

  • по умолчанию
  • сеттеры
  • валидаторы
  • промежуточный слой
Pascalius
источник
17
Это должен быть актуальный ответ. Большинство других использует два вызова или (я полагаю) возвращается к родному драйверу mongodb.
Хагги
10
проблема с findOneAndUpdate заключается в том, что предварительное сохранение не будет выполнено.
a77icu5
2
Похоже, ошибка в Mongoose или MongoDB?
Паскалиус
8
Из документов: "... при использовании помощников findAndModify не применяются следующие параметры: значения по умолчанию, установщики, валидаторы, промежуточное программное обеспечение" mongoosejs.com/docs/api.html#model_Model.findOneAndUpdate
kellen
2
@JamieHutber Это не установлено по умолчанию, это пользовательское свойство
Паскалиус
194

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

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

Я создал проблему на странице проекта Mongoose с просьбой добавить информацию об этом в документы.

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

Вы были близки с

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

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

Contact.update({phone:request.phone}, {$set: { phone: request.phone }}, {upsert: true}, function(err){...})
chrixian
источник
15
Я не думаю, что вам нужна эта {$set: ... }часть, так как она автоматически формируется во время моего чтения
CpILL
5
Да, Мангуста говорит, что все превращается в $ set
grantwparks
1
Это было действительно на момент написания статьи, я больше не использую MongoDB, поэтому не могу говорить об изменениях за последние месяцы: D
chrixian
5
Не использовать $ set может быть плохой привычкой, если вы будете время от времени использовать собственный драйвер.
UpTheCreek
Вы можете использовать $ set и $ setOnInsert для установки только определенных полей в случае вставки
justin.m.chase
73

Я ждала достаточно долго и ответа не было. В конце концов отказались от всего подхода обновления / вставки и пошли с:

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 показывает правильный способ сделать это.
respectTheCode
12
Стоит отметить, что это единственный ответ, который позволяет использовать валидаторы Mongoose. Согласно документам , проверка не происходит, если вы вызываете update.
Том Спенсер
@fiznool выглядит , как вы можете вручную передать в опции runValidators: trueво время обновления: обновление документы (однако, обновление валидаторы работать только на $setи $unsetопераций)
Дэнни
Смотрите мой ответ на основе этого, если вам нужно .upsert()быть доступным на всех моделях. stackoverflow.com/a/50208331/1586406
Спондбоб
24

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

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);
    });
});
Мартин Куздович
источник
Почему за это не проголосовали? Похоже, отличное решение и очень элегантный
MadOgre
Блестящее решение, фактически заставило меня переосмыслить то, как я подхожу к обещаниям.
люкс
4
Еще более элегантно было бы переписать (model) => { return model.save(); }как model => model.save(), а также (err) => { res.send(err); }как err => res.send(err);)
Джереми Тилле
1
Где я могу получить больше информации?
V0LT3RR4
18

Я хранитель Мангуста. Более современный способ использовать документ - использовать Model.updateOne()функцию .

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

Если вам нужен загруженный документ, вы можете использовать Model.findOneAndUpdate()

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

Главное, что вы должны поместить уникальные свойства в filterпараметре в updateOne()или findOneAndUpdate(), а другие свойства в updateпараметре.

Вот учебник по загрузке документов с Mongoose .

vkarpov15
источник
15

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

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

Это решение может потребовать 2 вызова БД, однако вы получаете

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

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

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

ОБНОВЛЕНИЕ - 16.01.2016 Я добавил дополнительное условие: если существует массив примитивных значений, Mongoose не понимает, что массив обновляется без использования функции «set».

Аарон Маст
источник
2
+1 за создание acc только для этого: P Хотелось бы дать еще +1 для использования .save (), так как findOneAndUpate () делает нас неспособными использовать валидаторы и pre, post и т.д. Спасибо, я тоже это
проверю
Извините, но это не сработало :( Я получил размер стека вызовов, превышенный
user1576978
Какую версию lodash вы используете? Я использую lodash версии 2.4.1 Спасибо!
Аарон Маст
Кроме того, какой сложный объект вы огорчаете? Если они слишком велики, процесс узла может не справиться с количеством рекурсивных вызовов, необходимых для объединения объектов.
Аарон Маст
Я использовал это, но должен был добавить if(_.isObject(value) && _.keys(value).length !== 0) {условие охраны, чтобы остановить переполнение стека. Lodash 4+ здесь, кажется, преобразует не объектные значения в объекты в keysвызове, поэтому рекурсивная защита всегда была истинной. Может быть, есть лучший способ, но сейчас он почти работает для меня ...
Ричард Г.
12

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

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

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

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

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

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

И это вывод, когда я запускаю скрипт во второй раз:

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

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

andres_gcarmona
источник
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!' });
            });

        });
    });

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

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

В версии 2.6 появилась ошибка, которая также влияет на версию 2.7.

Упсерт корректно работал на 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);
    });
helpse
источник
7

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

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"
                })
            }
        });
});
Мухаммед Авайс
источник
6

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

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

});

Эммануэль Ндукве
источник
Спасибо. Это был тот, который, наконец, работал для меня!
Луис Фебро
4

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

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
источник
3

Для тех, кто прибывает сюда, все еще ищущих хорошее решение для «апперсинга» с поддержкой хуков, это то, что я протестировал и работал. Это все еще требует 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);
    });
  });
}
Терри
источник
3

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

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();
Blacksonic
источник
3

Никакое другое решение не помогло мне. Я использую почтовый запрос и обновляю данные, если они найдены, либо вставьте их, а также _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');

});
Прияншу Чаухан
источник
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); 
  });
});


--
Рон Белсон
источник
2
User.findByIdAndUpdate(req.param('userId'), req.body, (err, user) => {
    if(err) return res.json(err);

    res.json({ success: true });
});
Зеешан Ахмад
источник
Хотя этот фрагмент кода может решить проблему, он не объясняет, почему и как он отвечает на вопрос. Пожалуйста, включите объяснение вашего кода , так как это действительно помогает улучшить качество вашего сообщения. Помните, что вы отвечаете на вопрос читателей в будущем, и эти люди могут не знать причин, по которым вы предлагаете код. Флаггеры / рецензенты: для ответов только с кодом, таких как этот, downvote, не удаляйте!
Патрик
2

Следуя ответу 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' })или YouModel.upsert({ bar: 'foo' }, { value: 1 })когда захотите.

spondbob
источник
2

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

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

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

Model.upsert({unique: 'value'}, updateObject});
Ричард Дж
источник
0

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

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
Саймон Х
источник
0

основываться на том, что Мартин Куздович написал выше. Я использую следующее для обновления с помощью mongoose и глубокого слияния объектов json. Наряду с функцией model.save () в mongoose это позволяет mongoose выполнить полную проверку, даже если она использует другие значения в json. для этого требуется пакет Deepmerge 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 ()
Крис Deleo
-5

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

    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 равно нулю, мы создаем новый элемент. В противном случае используйте upsert в обновлении, потому что обновление не создает новый элемент.

Грант Ли
источник
Если это два звонка в Монго, не правда ли, это правда?
Хагги