Comment mettre à jour / mettre à jour un document dans Mongoose?

369

C'est peut-être le moment, c'est peut-être moi qui me noie dans une documentation clairsemée et je ne peux pas me concentrer sur le concept de la mise à jour dans Mongoose :)

Voici l'affaire:

J'ai un schéma et un modèle de contact (propriétés raccourcies):

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

Je reçois une demande du client, contenant les champs dont j'ai besoin et utilise ainsi mon modèle:

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

Et maintenant nous arrivons au problème:

  1. Si j'appelle, contact.save(function(err){...})je recevrai une erreur si le contact avec le même numéro de téléphone existe déjà (comme prévu - unique)
  2. Je ne peux pas faire appel update()au contact, car cette méthode n'existe pas sur un document
  3. Si j'appelle mise à jour sur le modèle:
    Contact.update({phone:request.phone}, contact, {upsert: true}, function(err{...})
    j'entre dans une boucle infinie de certaines sortes, car l'implémentation de la mise à jour Mongoose ne veut clairement pas un objet comme deuxième paramètre.
  4. Si je fais de même, mais dans le deuxième paramètre, je passe un tableau associatif des propriétés de la demande, {status: request.status, phone: request.phone ...}cela fonctionne - mais alors je n'ai aucune référence au contact spécifique et ne peux pas trouver ses propriétés createdAtet updatedAt.

Donc, le résultat, après tout, j'ai essayé: étant donné un document contact, comment puis-je le mettre à jour s'il existe, ou l'ajouter s'il ne l'est pas?

Merci pour votre temps.

Voyager Tech Guy
la source
Qu'en est- accrochage dans le prepour save?
Shamoon

Réponses:

429

Mongoose prend désormais en charge cela nativement avec findOneAndUpdate (appelle MongoDB findAndModify ).

L'option upsert = true crée l'objet s'il n'existe pas. par défaut à 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.');
});

Dans les anciennes versions, Mongoose ne prend pas en charge ces crochets avec cette méthode:

  • par défaut
  • setters
  • validateurs
  • middleware
Pascalius
la source
17
Cela devrait être la réponse à jour. La plupart des autres utilisent deux appels ou (je crois) retombent sur le pilote natif mongodb.
huggie
10
le problème avec findOneAndUpdate est que la pré-sauvegarde ne sera pas exécutée.
a77icu5
2
Cela ressemble à un bug dans Mongoose ou MongoDB?
Pascalius
8
De la documentation: "... lorsque vous utilisez les aides findAndModify, les éléments suivants ne sont pas appliquées: par défaut, setters, validateurs, middleware" mongoosejs.com/docs/api.html#model_Model.findOneAndUpdate
Kellen
2
@JamieHutber Ce n'est pas défini par défaut, c'est une propriété personnalisée
Pascalius
194

Je viens de brûler un solide 3 heures en essayant de résoudre le même problème. Plus précisément, je voulais "remplacer" le document entier s'il existe, ou l'insérer autrement. Voici la solution:

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

J'ai créé un problème sur la page du projet Mongoose demandant que des informations à ce sujet soient ajoutées aux documents.

Clint Harris
la source
1
La documentation semble pour le moment médiocre. Il y en a dans la documentation de l'API (recherchez «mise à jour» sur la page. Cela ressemble à ceci: MyModel.update({ age: { $gt: 18 } }, { oldEnough: true }, fn);etMyModel.update({ name: 'Tobi' }, { ferret: true }, { multi: true }, fn);
CpILL
pour le document de cas introuvable, quel _id est utilisé? Mongoose le génère ou celui qui a été interrogé?
Haider
91

Tu étais proche avec

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

mais votre deuxième paramètre doit être un objet avec un opérateur de modification par exemple

Contact.update({phone:request.phone}, {$set: { phone: request.phone }}, {upsert: true}, function(err){...})
chrixian
la source
15
Je ne pense pas que vous ayez besoin de la {$set: ... }partie ici comme forme automatique de ma lecture
CpILL
5
Ouais, la mangouste dit qu'elle transforme tout en $ set
grantwparks
1
C'était valide au moment de la rédaction, je n'utilise plus MongoDB donc je ne peux pas parler des changements des derniers mois: D
chrixian
5
Cependant, ne pas utiliser $ set peut être une mauvaise habitude si vous utilisez de temps en temps le pilote natif.
UpTheCreek
Vous pouvez utiliser $ set et $ setOnInsert pour définir uniquement certains champs dans le cas d'une insertion
justin.m.chase
73

Eh bien, j'ai attendu assez longtemps et aucune réponse. A finalement renoncé à toute l'approche de mise à jour / upsert et est allé avec:

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

Est-ce que ça marche? Oui. Suis-je content de ça? Probablement pas. 2 appels DB au lieu d'un.
Espérons qu'une future implémentation de Mongoose proposera une Model.upsertfonction.

Voyager Tech Guy
la source
2
Cet exemple utilise l'interface ajoutée dans MongoDB 2.2 pour spécifier les options multi et upsert dans un formulaire de document. .. include :: /includes/fact-upsert-multi-options.rst La documentation le dit, je ne sais pas où aller à partir d'ici.
Donald Derek
1
Bien que cela devrait fonctionner, vous exécutez maintenant 2 opérations (rechercher, mettre à jour) alors qu'une seule (upsert) est nécessaire. @chrixian montre la bonne façon de procéder.
respectTheCode
12
Il convient de noter que c'est la seule réponse qui permet aux validateurs de Mongoose de démarrer. Selon les documents , la validation ne se produit pas si vous appelez update.
Tom Spencer
@fiznool semble que vous pouvez passer manuellement l'option runValidators: truelors d'une mise à jour: mise à jour des documents (cependant, les validateurs de mise à jour ne fonctionnent que sur $setet les $unsetopérations)
Danny
Voir ma réponse basée sur celle-ci si vous devez .upsert()être disponible sur tous les modèles. stackoverflow.com/a/50208331/1586406
spondbob
24

Solution très élégante que vous pouvez obtenir en utilisant la chaîne de promesses:

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);
    });
});
Martin Kuzdowicz
la source
Pourquoi cela n'a-t-il pas été voté? Semble être une excellente solution et très élégant
MadOgre
Une solution brillante m'a fait repenser la façon dont j'aborde les promesses.
lux
4
Encore plus élégant serait de réécrire en (model) => { return model.save(); }tant que model => model.save(), et aussi en (err) => { res.send(err); }tant que err => res.send(err);)
Jeremy Thille
1
Où puis-je obtenir plus d'informations?
V0LT3RR4
18

Je suis le mainteneur de Mongoose. La façon la plus moderne de mettre un document à jour est d'utiliser la Model.updateOne()fonction .

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

Si vous avez besoin du document inversé, vous pouvez utiliser Model.findOneAndUpdate()

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

Le point clé à retenir est que vous devez mettre les propriétés uniques dans le filterparamètre sur updateOne()ou findOneAndUpdate(), et les autres propriétés dans le updateparamètre.

Voici un tutoriel sur la mise à jour de documents avec Mongoose .

vkarpov15
la source
15

J'ai créé un compte StackOverflow JUSTE pour répondre à cette question. Après avoir vainement recherché les interwebs, j'ai juste écrit quelque chose moi-même. C'est ainsi que je l'ai fait pour qu'il puisse être appliqué à n'importe quel modèle de mangouste. Importez cette fonction ou ajoutez-la directement dans votre code où vous effectuez la mise à jour.

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

Ensuite, pour insérer un document mangouste, procédez comme suit,

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

Cette solution peut nécessiter 2 appels DB, mais vous bénéficiez de:

  • Validation de schéma par rapport à votre modèle car vous utilisez .save ()
  • Vous pouvez insérer des objets profondément imbriqués sans énumération manuelle dans votre appel de mise à jour, donc si votre modèle change, vous n'avez pas à vous soucier de la mise à jour de votre code

N'oubliez pas que l'objet de destination remplacera toujours la source même si la source a une valeur existante

De plus, pour les tableaux, si l'objet existant a un tableau plus long que celui qui le remplace, les valeurs à la fin de l'ancien tableau resteront. Un moyen simple de mettre le tableau entier à l'envers consiste à définir l'ancien tableau comme un tableau vide avant le upsert si c'est ce que vous avez l'intention de faire.

MISE À JOUR - 16/01/2016 J'ai ajouté une condition supplémentaire car s'il y a un tableau de valeurs primitives, Mongoose ne se rend pas compte que le tableau est mis à jour sans utiliser la fonction "set".

Aaron Mast
la source
2
+1 pour créer un acc juste pour cela: P J'aimerais pouvoir donner un autre + 1 juste pour utiliser .save (), parce que findOneAndUpate () nous empêche d'utiliser des validateurs et des trucs pré, post, etc. Merci, je vais vérifier cela aussi
user1576978
Désolé, mais cela n'a pas fonctionné ici :( J'ai dépassé la taille de la pile d'appels
user1576978
Quelle version de lodash utilisez-vous? J'utilise la version 2.4.1 de lodash Merci!
Aaron Mast
De plus, à quel point les objets sont-ils complexes? S'ils sont trop volumineux, le processus de nœud peut ne pas être en mesure de gérer le nombre d'appels récursifs requis pour fusionner les objets.
Aaron Mast
J'ai utilisé cela, mais j'ai dû ajouter if(_.isObject(value) && _.keys(value).length !== 0) {la condition de garde pour arrêter le débordement de la pile. Lodash 4+ ici, il semble convertir des valeurs non-objets en objets dans l' keysappel, donc la garde récursive a toujours été vraie. Peut-être qu'il y a une meilleure façon, mais ça marche presque pour moi maintenant ...
Richard G
12

Je devais mettre à jour / mettre à jour un document dans une collection, ce que j'ai fait était de créer un nouvel objet littéral comme ceci:

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

composé de données que j'obtiens ailleurs dans ma base de données, puis appelle la mise à jour sur le modèle

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

c'est la sortie que j'obtiens après avoir exécuté le script pour la première fois:

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

Et voici la sortie lorsque j'exécute le script pour la deuxième fois:

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

J'utilise la version mangouste 3.6.16

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

        });
    });

Voici une meilleure approche pour résoudre la méthode de mise à jour en mangouste, vous pouvez consulter Scotch.io pour plus de détails. Cela a vraiment fonctionné pour moi !!!

Eyo Okon Eyo
la source
5
C'est une erreur de penser que cela fait la même chose que la mise à jour de MongoDB. Ce n'est pas atomique.
Valentin Waeselynck
1
Je veux sauvegarder la réponse @ValentinWaeselynck. Le code de Scotch est propre, mais vous récupérez un document, puis vous le mettez à jour. Au milieu de ce processus, le document pourrait être modifié.
Nick Pineda
8

Il y a un bogue introduit dans 2.6, et affecte aussi à 2.7

L'upsert fonctionnait correctement sur 2.4

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

Jetez un oeil, il contient des informations importantes

MISE À JOUR:

Cela ne signifie pas que upsert ne fonctionne pas. Voici un bel exemple de son utilisation:

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
la source
7

Vous pouvez simplement mettre à jour l'enregistrement avec cela et obtenir les données mises à jour en réponse

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"
                })
            }
        });
});
Muhammad Awais
la source
6

cela a fonctionné pour moi.

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

});

Emmanuel Ndukwe
la source
Merci. C'est celui qui a finalement fonctionné pour moi!
Luis Febro
4

Voici le moyen le plus simple de créer / mettre à jour tout en appelant le middleware et les valideurs.

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
la source
3

Pour ceux qui arrivent ici, toujours à la recherche d'une bonne solution pour "upserting" avec le support des crochets, c'est ce que j'ai testé et travaillé. Il nécessite toujours 2 appels DB mais est beaucoup plus stable que tout ce que j'ai essayé en un seul appel.

// 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);
    });
  });
}
Terry
la source
3

Si des générateurs sont disponibles, cela devient encore plus facile:

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
la source
3

Aucune autre solution n'a fonctionné pour moi. J'utilise une demande de publication et je mets à jour les données si elles sont trouvées, insérez-la également, _id est envoyé avec le corps de la demande qui doit être supprimé.

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

});
Priyanshu Chauhan
la source
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); 
  });
});


--
Ron Belson
la source
2
User.findByIdAndUpdate(req.param('userId'), req.body, (err, user) => {
    if(err) return res.json(err);

    res.json({ success: true });
});
Zeeshan Ahmad
la source
Bien que cet extrait de code puisse résoudre le problème, il n'explique pas pourquoi ni comment il répond à la question. Veuillez inclure une explication pour votre code , car cela aide vraiment à améliorer la qualité de votre message. N'oubliez pas que vous répondrez à la question pour les lecteurs à l'avenir, et ces personnes pourraient ne pas connaître les raisons de votre suggestion de code. Flaggers / relecteurs: pour les réponses en code uniquement comme celle-ci, downvote, ne supprimez pas!
Patrick
2

Suite à la réponse de Traveling Tech Guy , qui est déjà génial, nous pouvons créer un plugin et l'attacher à mangouste une fois que nous l'avons initialisé afin que .upsert() soit disponible sur tous les modèles.

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

Ensuite, vous pouvez faire quelque chose comme User.upsert({ _id: 1 }, { foo: 'bar' })ou YouModel.upsert({ bar: 'foo' }, { value: 1 })quand vous le souhaitez.

spondbob
la source
2

Je viens de revenir sur ce problème après un certain temps et j'ai décidé de publier un plugin basé sur la réponse d'Aaron Mast.

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

Utilisez-le comme un plugin mangouste. Il met en place une méthode statique qui fusionnera récursivement l'objet passé.

Model.upsert({unique: 'value'}, updateObject});
Richard G
la source
0

Ce coffeescript fonctionne pour moi avec Node - l'astuce est que le _id est débarrassé de son wrapper ObjectID lorsqu'il est envoyé et retourné par le client et donc cela doit être remplacé pour les mises à jour (quand aucun _id n'est fourni, save reviendra pour insérer et ajouter une).

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
Simon H
la source
0

pour s'appuyer sur ce que Martin Kuzdowicz a publié ci-dessus. J'utilise ce qui suit pour faire une mise à jour à l'aide de mangouste et une fusion profonde des objets json. Avec la fonction model.save () dans mangouste, cela permet à mangouste de faire une validation complète même en se basant sur d'autres valeurs dans le json. il nécessite le package deepmerge https://www.npmjs.com/package/deepmerge . Mais c'est un paquet très léger.

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);
    });
});
Chris Deleo
la source
1
Je vous déconseille d' utiliser req.bodytel quel , avant de tester l' injection NoSQL (voir owasp.org/index.php/Testing_for_NoSQL_injection ).
Traveling Tech Guy
1
@TravelingTechGuy Merci pour la prudence Je suis encore nouveau pour Node et Mongoose. Mon modèle mangouste avec validateurs ne serait-il pas suffisant pour attraper une tentative d'injection? pendant le model.save ()
Chris Deleo
-5

Après avoir lu les articles ci-dessus, j'ai décidé d'utiliser ce code:

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

si r est nul, nous créons un nouvel élément. Sinon, utilisez upsert dans la mise à jour car la mise à jour ne crée pas de nouvel élément.

Grant Li
la source
Si c'est deux appels à Mongo, ce n'est pas vraiment bouleversant n'est-ce pas?
huggie