Як оновити / вставити документ у "Мангуста"?


367

Можливо, саме час, можливо, це я тону в розрідженій документації і не в змозі обернути голову навколо концепції оновлення в Мангузі :)

Ось угода:

У мене є контактна схема і модель (скорочені властивості):

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. Якщо я називаю оновлення на моделі:
    Contact.update({phone:request.phone}, contact, {upsert: true}, function(err{...})
    я потрапляю в нескінченний цикл певних типів, оскільки реалізація оновлення Mongoose явно не хоче об'єкт як другий параметр.
  4. Якщо я роблю те саме, але у другому параметрі я передаю асоціативний масив властивостей запиту, який {status: request.status, phone: request.phone ...}він працює - але я не маю посилання на конкретний контакт і не можу з’ясувати його createdAtта updatedAtвластивості.

Отже, підсумок, після всього, що я спробував: дав документ contact, як оновити його, якщо він існує, або додати його, якщо його немає?

Дякую за ваш час.


Як щодо підключення в preпротягом save?
Шамун

Відповіді:


427

Тепер Mongoose підтримує це за допомогою програми findOneAndUpdate (викликає MongoDB findAndModify ).

Параметр upsert = true створює об'єкт, якщо його не існує. за замовчуванням до 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.');
});

У старих версіях Mongoose не підтримує ці гачки цим методом:

  • за замовчуванням
  • сетери
  • валідатори
  • проміжне програмне забезпечення

17
Це має бути актуальна відповідь. Більшість інших використовує два дзвінки або (я вважаю) повернутися до рідного водія mongodb.
huggie

10
Проблема з FindOneAndUpdate полягає в тому, що попереднє збереження не буде виконано.
a77icu5

2
Звучить, як помилка в Монгуз чи MongoDB?
Паскалій

8
З документів: "... при використанні помічників findAndModify не застосовуються такі: за замовчуванням, сеттер, валідатори, середнє програмне забезпечення" mongoosejs.com/docs/api.html#model_Model.findOneAndUpdate
kellen

2
@JamieHutber Це не встановлено за замовчуванням, це спеціальна властивість
Pascalius

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

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 у формі документа. .. включити :: /includes/fact-upsert-multi-options.rst Документація це говорить, не знаю, куди піти звідси.
Дональд Дерек

1
Хоча це має працювати, зараз ви виконуєте 2 операції (знайдіть, оновіть), коли потрібно лише 1 (upsert). @chrixian показує правильний спосіб зробити це.
поважайтеКод

12
Варто зазначити, що це єдина відповідь, яка дозволяє валідаторам Mongoose запускати. Згідно з документами , перевірка не відбувається, якщо ви викликаєте оновлення.
Том Спенсер

@fiznool виглядає так, що ви можете вручну передавати параметр runValidators: trueпід час оновлення: оновити документи (однак, валідатори оновлення працюють лише $setта $unsetпрацюють)
Danny

Дивіться мою відповідь на основі цієї, якщо вам потрібно .upsert()бути доступною для всіх моделей. stackoverflow.com/a/50208331/1586406
spondbob

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

17

Я підтримувач Мангуста. Більш сучасний спосіб вставити документ - це використовувати 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параметрі.

Ось підручник із оформлення документів з мангустом .


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 для створення акаунта саме для цього: P Хочеться, щоб я міг дати ще один +1 лише для використання .save (), becaunse findOneAndUpate () не дає можливості використовувати валідатори та попередньо, публікувати тощо. Дякую, я теж перевірю це
користувач1576978

Вибачте, але це не спрацювало тут :( Я перевищив розмір стека викликів
user1576978

Яку версію lodash ви використовуєте? Я використовую lodash версії 2.4.1 Дякую!
Аарон Маст

Крім того, наскільки складні предмети ви наносите? Якщо вони занадто великі, процес вузла може не в змозі обробити кількість рекурсивних викликів, необхідних для об'єднання об'єктів.
Аарон Маст

Я використав це, але довелося додати if(_.isObject(value) && _.keys(value).length !== 0) {умову захисту, щоб зупинити переповнення стека. Лодаш 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


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

        });
    });

Ось кращий підхід до вирішення методу оновлення в мангусті, ви можете перевірити 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

Погляньте, він містить важливу інформацію

ОНОВЛЕНО:

Це не означає, що притвор не працює. Ось хороший приклад того, як його використовувати:

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

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

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

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

Після відповіді на подорож Tech Guy , яка вже приголомшлива, ми можемо створити плагін і приєднати його до мангуста, як тільки ми його ініціалізуємо, щоб .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 })коли завгодно.


2

Я просто через деякий час повернувся до цього питання і вирішив опублікувати плагін на основі відповіді Аарона Маста.

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

Використовуйте його як плагін мангуста. Він встановлює статичний метод, який буде рекурсивно зливати переданий об'єкт.

Model.upsert({unique: 'value'}, updateObject});

0

Цей coffeescript працює для мене з Node - хитрість полягає в тому, що _id отримує позбавлення від його обгортки ObjectID, коли він надсилається та повертається з клієнта, і це потрібно замінити для оновлень (коли не вказано _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

0

спиратися на те, що Мартин Кузьдович розмістив вище Я використовую наступне, щоб зробити оновлення за допомогою мангуста та глибокого злиття об’єктів json. Поряд з функцією model.save () в мангусті це дозволяє мангусту робити повну перевірку навіть тієї, яка спирається на інші значення в 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 ()
Кріс Делео

-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 недійсне, ми створюємо новий елемент. В іншому випадку використовуйте оновлення для оновлення, оскільки оновлення не створює новий елемент.


Якщо це два дзвінки до Монго, це насправді не розбещено?
huggie
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.