Come aggiorno / invio un documento in Mongoose?


369

Forse è il momento, forse sono io ad annegare nella scarsa documentazione e non essere in grado di avvolgere la testa attorno al concetto di aggiornamento in Mongoose :)

Ecco l'accordo:

Ho uno schema e un modello di contatto (proprietà abbreviate):

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

Ricevo una richiesta dal cliente, contenente i campi di cui ho bisogno e utilizzo così il mio modello:

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

E ora arriviamo al problema:

  1. Se chiamo contact.save(function(err){...})riceverò un errore se il contatto con lo stesso numero di telefono esiste già (come previsto - unico)
  2. Non riesco a contattare update()il contatto, poiché quel metodo non esiste su un documento
  3. Se chiamo aggiornamento sul modello:
    Contact.update({phone:request.phone}, contact, {upsert: true}, function(err{...})
    entro in un ciclo infinito di qualche tipo, poiché l'implementazione dell'aggiornamento Mongoose chiaramente non vuole un oggetto come secondo parametro.
  4. Se lo faccio lo stesso, ma nel secondo parametro che passare un array associativo delle proprietà delle richieste {status: request.status, phone: request.phone ...}funziona - ma poi non ho alcun riferimento al contatto specifico e non riesco a trovare le sue createdAte updatedAtle proprietà.

Quindi la linea di fondo, dopo tutto quello che ho provato: dato un documento contact, come posso aggiornarlo se esiste, o aggiungerlo se non lo fa?

Grazie per il tuo tempo.


Che dire di agganciarsi al preper save?
Shamoon,

Risposte:


429

Mongoose ora lo supporta nativamente con findOneAndUpdate (chiama MongoDB findAndModify ).

L'opzione upsert = true crea l'oggetto se non esiste. il valore predefinito è 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.');
});

Nelle versioni precedenti Mongoose non supporta questi hook con questo metodo:

  • default
  • setter
  • validatori
  • middleware

17
Questa dovrebbe essere la risposta aggiornata. La maggior parte degli altri utilizza due chiamate o (credo) ripiegare sul driver mongodb nativo.
Huggie,

10
il problema con findOneAndUpdate è che il pre-salvataggio non verrà eseguito.
a77icu5,

2
Sembra un bug in Mongoose o MongoDB?
Pascalio,

8
Dai documenti: "... quando si utilizzano gli helper findAndModify, non vengono applicati: impostazioni predefinite, setter, validatori, middleware" mongoosejs.com/docs/api.html#model_Model.findOneAndUpdate
kellen

2
@JamieHutber Questo non è impostato di default, è una proprietà personalizzata
Pascalius

194

Ho appena bruciato 3 ore solide cercando di risolvere lo stesso problema. In particolare, volevo "sostituire" l'intero documento, se esiste, o inserirlo in altro modo. Ecco la soluzione:

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

Ho creato un problema nella pagina del progetto Mongoose richiedendo che le informazioni al riguardo vengano aggiunte ai documenti.


1
La documentazione sembra scarsa al momento. Ce ne sono alcuni nei documenti API (cerca "aggiornamento" nella pagina. È simile al seguente: MyModel.update({ age: { $gt: 18 } }, { oldEnough: true }, fn);eMyModel.update({ name: 'Tobi' }, { ferret: true }, { multi: true }, fn);
CpILL

per il caso non viene trovato il documento, quale _id viene utilizzato? Mongoose lo genera o quello contro cui è stata interrogata?
Haider,

91

Eri vicino

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

ma il tuo secondo parametro dovrebbe essere ad esempio un oggetto con un operatore di modifica

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

15
Non credo che tu abbia bisogno della {$set: ... }parte qui come forma automatica della mia lettura
CpILL

5
Sì, la mangusta dice che trasforma tutto in $ set
grantwparks

1
Questo era valido al momento in cui è stato scritto, non uso più MongoDB, quindi non posso parlare di cambiamenti negli ultimi mesi: D
chrixian

5
Non utilizzare $ set potrebbe essere una cattiva abitudine da prendere se si utilizza il driver nativo di volta in volta.
UpTheCreek,

Puoi usare $ set e $ setOnInsert per impostare solo determinati campi nel caso di un inserimento
justin.m.chase

73

Bene, ho aspettato abbastanza a lungo e nessuna risposta. Alla fine ha rinunciato all'intero approccio di aggiornamento / upert e ha seguito:

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

Funziona? Sì. Sono contento di questo? Probabilmente no. 2 chiamate DB anziché una.
Speriamo che una futura implementazione di Mongoose abbia una Model.upsertfunzione.


2
Questo esempio utilizza l'interfaccia aggiunta in MongoDB 2.2 per specificare le opzioni multi e upsert in un modulo di documento. .. include :: /includes/fact-upsert-multi-options.rst La documentazione indica questo, non so dove andare da qui.
Donald Derek,

1
Anche se questo dovrebbe funzionare, ora stai eseguendo 2 operazioni (trova, aggiorna) quando è necessario solo 1 (upsert). @chrixian mostra il modo corretto per farlo.
rispettare Il codice

12
Vale la pena notare che questa è l'unica risposta che consente ai validatori di Mongoose di dare il via. Secondo i documenti , la convalida non si verifica se si chiama update.
Tom Spencer,

@fiznool sembra che tu possa passare manualmente l'opzione runValidators: truedurante un aggiornamento: aggiorna i documenti (tuttavia, i validatori dell'aggiornamento funzionano solo $sete le $unsetoperazioni)
Danny

Vedi la mia risposta basata su questo se devi .upsert()essere disponibile su tutti i modelli. stackoverflow.com/a/50208331/1586406
spondbob

24

Soluzione molto elegante che puoi ottenere utilizzando la catena di promesse:

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

Perché questo non è stato votato? Sembra un'ottima soluzione e molto elegante
MadOgre,

Soluzione geniale, in realtà mi ha fatto ripensare il modo in cui approccio alle promesse.
lux

4
Ancora più elegante sarebbe riscrivere (model) => { return model.save(); }come model => model.save(), e anche (err) => { res.send(err); }come err => res.send(err);)
Jeremy Thille,

1
Dove posso ottenere maggiori informazioni?
V0LT3RR4,

18

Sono il manutentore di Mongoose. Il modo più moderno di far valere un documento è usare la Model.updateOne()funzione .

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

Se hai bisogno di un documento modificato, puoi usarlo Model.findOneAndUpdate()

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

La chiave da asporto è che è necessario mettere le proprietà univoche nel filterparametro in updateOne()oe findOneAndUpdate()le altre proprietà nel updateparametro.

Ecco un tutorial su come far valere documenti con Mongoose .


15

Ho creato un account StackOverflow SOLO per rispondere a questa domanda. Dopo aver cercato inutilmente nelle interwebs ho appena scritto qualcosa da solo. È così che l'ho fatto in modo che possa essere applicato a qualsiasi modello di mangusta. Importa questa funzione o aggiungila direttamente nel tuo codice in cui stai effettuando l'aggiornamento.

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

Quindi, per inserire un documento di mangusta, procedi come segue,

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

Questa soluzione potrebbe richiedere 2 chiamate DB, tuttavia si ottiene il vantaggio di,

  • Convalida dello schema sul tuo modello perché stai usando .save ()
  • Puoi inserire oggetti profondamente annidati senza enumerazione manuale nella tua chiamata di aggiornamento, quindi se il tuo modello cambia non devi preoccuparti di aggiornare il tuo codice

Ricorda solo che l'oggetto di destinazione sovrascriverà sempre l'origine anche se l'origine ha un valore esistente

Inoltre, per gli array, se l'oggetto esistente ha un array più lungo di quello che lo sostituisce, i valori alla fine del vecchio array rimarranno. Un modo semplice per potenziare l'intero array è quello di impostare il vecchio array come un array vuoto prima dell'upert se questo è ciò che si intende fare.

AGGIORNAMENTO - 16/01/2016 Ho aggiunto una condizione aggiuntiva per se esiste un array di valori primitivi, Mongoose non si rende conto che l'array viene aggiornato senza usare la funzione "set".


2
+1 per la creazione di acc solo per questo: P Vorrei poter dare un altro + 1 solo per l'uso di .save (), perché findOneAndUpate () ci rende incapaci di usare validatori e pre, post, ecc. Grazie, controllerò anche questo
user1576978

Siamo spiacenti, ma non ha funzionato qui :( Ho superato la dimensione dello stack delle chiamate
user1576978

Quale versione di lodash stai usando? Sto usando lodash versione 2.4.1 Grazie!
Aaron Mast

Inoltre, quanto complesso di oggetti stai aumentando? Se sono troppo grandi, il processo del nodo potrebbe non essere in grado di gestire il numero di chiamate ricorsive richieste per unire gli oggetti.
Aaron Mast

L'ho usato, ma ho dovuto aggiungere if(_.isObject(value) && _.keys(value).length !== 0) {la condizione di guardia per fermare lo straripamento dello stack. Lodash 4+ qui, sembra convertire i valori di non oggetto in oggetti nella keyschiamata, quindi la protezione ricorsiva era sempre vera. Forse c'è un modo migliore, ma ora funziona quasi per me ...
Richard G,

12

Avevo bisogno di aggiornare / inserire un documento in una raccolta, quello che ho fatto è stato creare un nuovo oggetto letterale come questo:

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

composto da dati che ottengo da qualche altra parte nel mio database e quindi chiamo aggiornamento sul Modello

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

questo è il risultato che ottengo dopo aver eseguito lo script per la prima volta:

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

E questo è l'output quando eseguo lo script per la seconda volta:

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

Sto usando la versione mongoose 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!' });
            });

        });
    });

Ecco un approccio migliore per risolvere il metodo di aggiornamento in mangusta, è possibile controllare Scotch.io per maggiori dettagli. Questo ha sicuramente funzionato per me !!!


5
È un errore pensare che questo faccia la stessa cosa dell'aggiornamento di MongoDB. Non è atomico.
Valentin Waeselynck,

1
Voglio eseguire il backup della risposta @ValentinWaeselynck. Il codice di Scotch è pulito, ma stai recuperando un documento e quindi aggiornandolo. Nel mezzo di quel processo il documento potrebbe essere cambiato.
Nick Pineda,

8

È stato introdotto un bug in 2.6 e riguarda anche 2.7

Il upsert funzionava correttamente su 2.4

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

Dai un'occhiata, contiene alcune informazioni importanti

AGGIORNAMENTO:

Non significa che upsert non funziona. Ecco un bell'esempio di come 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);
    });

7

Puoi semplicemente aggiornare il record con questo e ottenere i dati aggiornati in risposta

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

questo ha funzionato per me.

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

});


Grazie. Questo è stato quello che alla fine ha funzionato per me!
Luis Febro,

4

Ecco il modo più semplice per creare / aggiornare chiamando anche middleware e validatori.

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

Per chi arriva qui ancora alla ricerca di una buona soluzione per "sostenere" con il supporto dei ganci, questo è ciò che ho testato e funzionante. Richiede ancora 2 chiamate DB ma è molto più stabile di qualsiasi cosa abbia provato in una singola chiamata.

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

Se sono disponibili generatori, diventa ancora più semplice:

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

Nessun'altra soluzione ha funzionato per me. Sto usando una richiesta di post e aggiornando i dati se trovato altrimenti inseriscilo, anche _id viene inviato con il corpo della richiesta che deve essere rimosso.

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

Sebbene questo frammento di codice possa risolvere il problema, non spiega perché o come risponde alla domanda. Includi una spiegazione per il tuo codice , in quanto ciò aiuta davvero a migliorare la qualità del tuo post. Ricorda che stai rispondendo alla domanda per i lettori in futuro e che queste persone potrebbero non conoscere i motivi del tuo suggerimento sul codice. Flaggers / recensori: per le risposte di solo codice come questo, downvote, non eliminare!
Patrick,

2

Seguendo la risposta di Traveling Tech Guy , che è già fantastica, possiamo creare un plug-in e collegarlo a mongoose una volta inizializzato in modo che .upsert() sia disponibile su tutti i modelli.

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

Quindi puoi fare qualcosa del genere User.upsert({ _id: 1 }, { foo: 'bar' })o YouModel.upsert({ bar: 'foo' }, { value: 1 })quando vuoi.



0

Questo coffeescript funziona per me con Node: il trucco è che _id viene rimosso dal suo wrapper ObjectID quando viene inviato e restituito dal client e quindi questo deve essere sostituito per gli aggiornamenti (quando non viene fornito alcun _id, il salvataggio tornerà a inserire e aggiungere 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

0

basarsi su ciò che Martin Kuzdowicz ha pubblicato sopra. Uso quanto segue per fare un aggiornamento usando mangusta e una fusione profonda di oggetti json. Insieme alla funzione model.save () in mongoose questo consente a mongoose di fare una validazione completa anche quella che si basa su altri valori nel json. richiede il pacchetto deepmerge https://www.npmjs.com/package/deepmerge . Ma questo è un pacchetto molto leggero.

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
Vorrei req.bodymettere in guardia dall'uso così com'è, prima di testare l' iniezione NoSQL (vedi owasp.org/index.php/Testing_for_NoSQL_injection ).
Traveling Tech Guy,

1
@TravelingTechGuy Grazie per l'attenzione sono ancora nuovo su Node e Mongoose. Il mio modello di mangusta con validatori non sarebbe abbastanza per catturare un tentativo di iniezione? durante il model.save ()
Chris Deleo,

-5

Dopo aver letto i post sopra, ho deciso di utilizzare questo codice:

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

se r è nullo, creiamo un nuovo elemento. In caso contrario, utilizzare upsert in update perché update non crea un nuovo elemento.


Se si tratta di due chiamate a Mongo, non è davvero positivo, vero?
Huggie,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.