¿Cómo actualizo / actualizo un documento en Mongoose?

369

Tal vez es el momento, tal vez soy yo ahogándome en una documentación escasa y no pudiendo entender el concepto de actualización en Mongoose :)

Aquí está el trato:

Tengo un esquema y modelo de contacto (propiedades acortadas):

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

Recibo una solicitud del cliente que contiene los campos que necesito y uso mi modelo de esta manera:

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

Y ahora llegamos al problema:

  1. Si llamo contact.save(function(err){...}), recibiré un error si el contacto con el mismo número de teléfono ya existe (como se esperaba, único)
  2. No puedo llamar update()al contacto, ya que ese método no existe en un documento
  3. Si llamo actualización en el modelo:
    Contact.update({phone:request.phone}, contact, {upsert: true}, function(err{...})
    entro en un bucle infinito de algún tipo, ya que la implementación de la actualización Mongoose claramente no quiere un objeto como segundo parámetro.
  4. Si hago lo mismo, pero en el segundo parámetro que pasar un array asociativo de las propiedades de la solicitud {status: request.status, phone: request.phone ...}funciona - pero luego no tengo ninguna referencia al contacto específico y no puede encontrar sus createdAty updatedAtpropiedades.

Entonces, en resumen, después de todo lo que intenté: dado un documento contact, ¿cómo lo actualizo si existe o lo agrego si no existe?

Gracias por tu tiempo.

Travelling Tech Guy
fuente
¿Qué hay de enganchar en el prepara save?
Shamoon

Respuestas:

429

Mongoose ahora admite esto de forma nativa con findOneAndUpdate (llama a MongoDB findAndModify ).

La opción upsert = true crea el objeto si no existe. Por defecto es falso .

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

En versiones anteriores, Mongoose no admite estos ganchos con este método:

  • valores predeterminados
  • setters
  • validadores
  • middleware
Pascalius
fuente
17
Esta debería ser la respuesta actualizada. La mayoría de los otros utiliza dos llamadas o (creo) recurrir al controlador mongodb nativo.
Huggie
10
El problema con findOneAndUpdate es que no se ejecutará el guardado previo.
a77icu5
2
¿Suena como un error en Mongoose o MongoDB?
Pascalius
8
De los documentos: "... cuando se utilizan los ayudantes findAndModify, no se aplica lo siguiente: valores predeterminados, configuradores, validadores, middleware" mongoosejs.com/docs/api.html#model_Model.findOneAndUpdate
kellen
2
@JamieHutber Esto no está configurado por defecto, es una propiedad personalizada
Pascalius
194

Acabo de quemar unas 3 horas sólidas tratando de resolver el mismo problema. Específicamente, quería "reemplazar" todo el documento si existe, o insertarlo de otra manera. Aquí está la solución:

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

Creé un problema en la página del proyecto Mongoose solicitando que se agregue información sobre esto a los documentos.

Clint Harris
fuente
1
La documentación parece pobre en este momento. Hay algunos en los documentos de la API (busque "actualizar" en la página. Se ve así: MyModel.update({ age: { $gt: 18 } }, { oldEnough: true }, fn);yMyModel.update({ name: 'Tobi' }, { ferret: true }, { multi: true }, fn);
CpILL
para el documento del caso no se encuentra, ¿qué _id se usa? ¿La mangosta lo genera o el que fue consultado?
Haider
91

Estabas cerca con

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

pero su segundo parámetro debe ser un objeto con un operador de modificación, por ejemplo

Contact.update({phone:request.phone}, {$set: { phone: request.phone }}, {upsert: true}, function(err){...})
chrixian
fuente
15
No creo que necesites la {$set: ... }parte aquí, ya que es mi forma automática de mi lectura
CpILL
55
Sí, mangosta dice que convierte todo en $ set
grantwparks
1
Esto era válido en el momento de su redacción, ya no uso MongoDB, así que no puedo hablar de los cambios en los últimos meses: D
chrixian
55
Sin embargo, no usar $ set podría ser un mal hábito si vas a usar el controlador nativo de vez en cuando.
UpTheCreek
Puede usar $ set y $ setOnInsert para establecer solo ciertos campos en el caso de un inserto
justin
73

Bueno, esperé lo suficiente y no obtuve respuesta. Finalmente abandoné todo el enfoque de actualización / actualización y seguí con:

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

¿Funciona? Sí. ¿Estoy contento con esto? Probablemente no. 2 llamadas DB en lugar de una.
Esperemos que una futura implementación de Mongoose tenga una Model.upsertfunción.

Travelling Tech Guy
fuente
2
Este ejemplo utiliza la interfaz agregada en MongoDB 2.2 para especificar las opciones múltiple y de inserción en un formulario de documento. .. include :: /includes/fact-upsert-multi-options.rst La documentación indica esto, no sé a dónde ir desde aquí.
Donald Derek
1
Aunque esto debería funcionar, ahora está ejecutando 2 operaciones (buscar, actualizar) cuando solo se necesita 1 (upsert). @chrixian muestra la forma correcta de hacer esto.
respectTheCode
12
Vale la pena señalar que esta es la única respuesta que permite que los validadores de Mongoose intervengan. Según los documentos , la validación no ocurre si llama a la actualización.
Tom Spencer,
Parece que @fiznool puede pasar manualmente la opción runValidators: truedurante una actualización: documentos de actualización (sin embargo, los validadores de actualización solo se ejecutan $sety $unsetfuncionan)
Danny
Vea mi respuesta basada en esta si necesita .upsert()estar disponible en todos los modelos. stackoverflow.com/a/50208331/1586406
spondbob
24

Solución muy elegante que puede lograr mediante el uso de la cadena de Promesas:

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
fuente
¿Por qué esto no fue votado? Parece una gran solución y muy elegante
MadOgre
Una solución brillante, en realidad me hizo repensar cómo abordo las promesas.
lux
44
Aún más elegante sería reescribir (model) => { return model.save(); }como model => model.save(), y también (err) => { res.send(err); }como err => res.send(err);)
Jeremy Thille
1
¿Dónde puedo obtener más información?
V0LT3RR4
18

Soy el mantenedor de Mongoose. La forma más moderna de insertar un documento es usar la Model.updateOne()función .

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

Si necesita el documento insertado, puede usar Model.findOneAndUpdate()

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

La conclusión clave es que necesita poner las propiedades únicas en el filterparámetro updateOne()ao findOneAndUpdate(), y las otras propiedades en el updateparámetro.

Aquí hay un tutorial sobre la inserción de documentos con Mongoose .

vkarpov15
fuente
15

Creé una cuenta StackOverflow SOLO para responder esta pregunta. Después de buscar infructuosamente las redes, escribí algo yo mismo. Así es como lo hice para que se pueda aplicar a cualquier modelo de mangosta. Importe esta función o agréguela directamente a su código donde está realizando la actualización.

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

Luego, para insertar un documento de mangosta, haga lo siguiente:

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

Esta solución puede requerir 2 llamadas de base de datos, sin embargo, usted obtiene el beneficio de

  • Validación de esquema contra su modelo porque está usando .save ()
  • Puede insertar objetos profundamente anidados sin enumeración manual en su llamada de actualización, por lo que si su modelo cambia, no tiene que preocuparse por actualizar su código

Solo recuerde que el objeto de destino siempre anulará la fuente, incluso si la fuente tiene un valor existente

Además, para las matrices, si el objeto existente tiene una matriz más larga que la que lo reemplaza, los valores al final de la matriz anterior permanecerán. Una manera fácil de aumentar la matriz completa es configurar la matriz antigua para que sea una matriz vacía antes de la matriz, si eso es lo que tiene la intención de hacer.

ACTUALIZACIÓN - 16/01/2016 Agregué una condición adicional porque si hay una matriz de valores primitivos, Mongoose no se da cuenta de que la matriz se actualiza sin usar la función "set".

Aaron Mast
fuente
2
+1 para crear acc solo por esto: P Ojalá pudiera dar otro + 1 solo por usar .save (), porque findOneAndUpate () nos impide usar validadores y pre, post, etc. Gracias, lo comprobaré también
user1576978
Lo sentimos, pero no funcionó aquí :( Se superó el tamaño de la pila de llamadas
user1576978
¿Qué versión de lodash estás usando? Estoy usando lodash versión 2.4.1 ¡Gracias!
Aaron Mast
Además, ¿qué tan complejo de objetos estás postulando? Si son demasiado grandes, el proceso de nodo puede no ser capaz de manejar la cantidad de llamadas recursivas necesarias para fusionar los objetos.
Aaron Mast
Usé esto, pero tuve que agregar if(_.isObject(value) && _.keys(value).length !== 0) {la condición de guardia para detener el desbordamiento de la pila. Lodash 4+ aquí, parece convertir valores no objeto en objetos en la keysllamada, por lo que la protección recursiva siempre fue cierta. Tal vez hay una mejor manera, pero sus casi trabajando para mí ahora ...
Richard G
12

Necesitaba actualizar / insertar un documento en una colección, lo que hice fue crear un nuevo objeto literal como este:

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

compuesto de datos que obtengo de otro lugar de mi base de datos y luego llamo actualización en el Modelo

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

Este es el resultado que obtengo después de ejecutar el script por primera vez:

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

Y esta es la salida cuando ejecuto el script por segunda vez:

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

Estoy usando la versión 3.6.16 de mangosta

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

        });
    });

Aquí hay un mejor enfoque para resolver el método de actualización en mangosta, puede consultar Scotch.io para obtener más detalles. ¡Esto definitivamente funcionó para mí!

Eyo Okon Eyo
fuente
55
Es un error pensar que esto hace lo mismo que la actualización de MongoDB. No es atómico.
Valentin Waeselynck
1
Quiero respaldar la respuesta de @ValentinWaeselynck. El código de Scotch está limpio, pero busca un documento y luego lo actualiza. En medio de ese proceso, el documento podría haber sido cambiado.
Nick Pineda
8

Hay un error introducido en 2.6, y afecta a 2.7 también

El upsert solía funcionar correctamente en 2.4

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

Echa un vistazo, contiene información importante

ACTUALIZADO:

No significa que upsert no funcione. Aquí hay un buen ejemplo de cómo usarlo:

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

Simplemente puede actualizar el registro con esto y obtener los datos actualizados en respuesta

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
fuente
6

Esto funcionó para mí.

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
fuente
Gracias. ¡Este fue el que finalmente funcionó para mí!
Luis Febro
4

Aquí está la forma más sencilla de crear / actualizar al mismo tiempo que llama al middleware y los validadores.

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
fuente
3

Para cualquiera que llegue aquí y siga buscando una buena solución para la "inserción" con soporte de ganchos, esto es lo que he probado y estoy trabajando. Todavía requiere 2 llamadas a base de datos, pero es mucho más estable que cualquier cosa que haya probado en una sola llamada.

// 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
fuente
3

Si hay generadores disponibles, se vuelve aún más fácil:

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
fuente
3

Ninguna otra solución funcionó para mí. Estoy usando una solicitud de publicación y actualizando datos si se encuentra, insértela, también _id se envía con el cuerpo de la solicitud que debe eliminarse.

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
fuente
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
fuente
2
User.findByIdAndUpdate(req.param('userId'), req.body, (err, user) => {
    if(err) return res.json(err);

    res.json({ success: true });
});
Zeeshan Ahmad
fuente
Si bien este fragmento de código puede resolver el problema, no explica por qué o cómo responde la pregunta. Por favor incluya una explicación para su código , como que realmente ayuda a mejorar la calidad de su puesto. Recuerde que está respondiendo la pregunta para los lectores en el futuro, y que esas personas podrían no conocer los motivos de su sugerencia de código. Señaladores / revisores: para respuestas de solo código como esta, ¡voto negativo, no elimine!
Patrick
2

Después de la respuesta de Travelling Tech Guy , que ya es increíble, podemos crear un complemento y adjuntarlo a la mangosta una vez que lo inicializamos para que .upsert()esté disponible en todos los modelos.

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

Entonces puedes hacer algo como User.upsert({ _id: 1 }, { foo: 'bar' })o YouModel.upsert({ bar: 'foo' }, { value: 1 })cuando quieras.

spondbob
fuente
2

Acabo de regresar a este problema después de un tiempo y decidí publicar un complemento basado en la respuesta de Aaron Mast.

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

Úselo como un complemento de mangosta. Establece un método estático que fusionará recursivamente el objeto pasado.

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

Este coffeescript funciona para mí con Node: el truco es que el _id se despoja de su contenedor ObjectID cuando se envía y devuelve desde el cliente, por lo que debe reemplazarse para las actualizaciones (cuando no se proporciona _id, guardar volverá a insertarse y agregarse uno).

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
fuente
0

para construir sobre lo que Martin Kuzdowicz publicó anteriormente. Utilizo lo siguiente para hacer una actualización usando mangosta y una fusión profunda de objetos json. Junto con la función model.save () en mongoose, esto permite que la mangosta realice una validación completa, incluso una que se base en otros valores en el json. requiere el paquete deepmerge https://www.npmjs.com/package/deepmerge . Pero ese es un paquete muy ligero.

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
fuente
1
Advierto contra el uso req.bodycomo está, antes de probar la inyección NoSQL (ver owasp.org/index.php/Testing_for_NoSQL_injection ).
Travelling Tech Guy
1
@TravelingTechGuy Gracias por la precaución Todavía soy nuevo en Node and Mongoose. ¿No sería suficiente mi modelo de mangosta con validadores para atrapar un intento de inyección? durante la model.save ()
Chris Deleo
-5

Después de leer las publicaciones anteriores, decidí usar este código:

    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 es nulo, creamos un nuevo elemento. De lo contrario, use upsert en la actualización porque la actualización no crea un nuevo elemento.

Grant Li
fuente
Si son dos llamadas a Mongo, no es realmente firme, ¿verdad?
Huggie