Como atualizo / upserti um documento no Mongoose?

367

Talvez seja a hora, talvez seja eu me afogando em documentação esparsa e não sendo capaz de entender o conceito de atualização no Mongoose :)

Aqui está o acordo:

Eu tenho um esquema e um modelo de contato (propriedades reduzidas):

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

Recebo uma solicitação do cliente, contendo os campos necessários e uso meu modelo da seguinte maneira:

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

E agora chegamos ao problema:

  1. Se eu ligar contact.save(function(err){...}), receberei um erro se o contato com o mesmo número de telefone já existir (conforme o esperado - único)
  2. Não consigo ligar update()para contato, pois esse método não existe em um documento
  3. Se eu chamar atualização no modelo:
    Contact.update({phone:request.phone}, contact, {upsert: true}, function(err{...})
    eu entro em um loop infinito de alguns tipos, já que a implementação da atualização do Mongoose claramente não quer um objeto como o segundo parâmetro.
  4. Se eu fizer o mesmo, mas no segundo parâmetro eu passar uma matriz associativa das propriedades do pedido {status: request.status, phone: request.phone ...}funciona - mas então eu não tenho nenhuma referência ao contacto específico e não pode descobrir suas createdAte updatedAtpropriedades.

Então, a questão de fundo, depois de tudo o que tentei: dado um documento contact, como atualizá-lo, se ele existir, ou adicioná-lo, se não existir?

Obrigado pelo seu tempo.

Travelling Tech Guy
fonte
Que tal enganchar no prepara save?
Shamoon

Respostas:

427

O Mongoose agora suporta isso nativamente com o findOneAndUpdate (chama o MongoDB findAndModify ).

A opção upsert = true cria o objeto se ele não existir. o padrão é 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.');
});

Nas versões anteriores, o Mongoose não suporta esses ganchos com este método:

  • padrões
  • setters
  • validadores
  • middleware
Pascalius
fonte
17
Essa deve ser a resposta atualizada. A maioria dos outros usa duas chamadas ou (acredito) voltar ao driver nativo do mongodb.
huggie
10
o problema com o findOneAndUpdate é que o pré-salvamento não será executado.
a77icu5
2
Parece um bug no Mongoose ou no MongoDB?
Pascalius 27/10/14
8
De docs: "... ao usar os ajudantes findAndModify, o seguinte não são aplicadas: defaults, setters, validadores, middleware" mongoosejs.com/docs/api.html#model_Model.findOneAndUpdate
Kellen
2
@JamieHutber Isto não é definida por padrão, é uma propriedade personalizada
Pascalius
194

Eu apenas queimei umas 3 horas sólidas tentando resolver o mesmo problema. Especificamente, eu queria "substituir" o documento inteiro, se existir, ou inseri-lo de outra forma. Aqui está a solução:

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

Criei um problema na página do projeto Mongoose solicitando que informações sobre isso fossem adicionadas aos documentos.

Clint Harris
fonte
11
A documentação parece ruim no momento. Há alguns documentos da API (pesquise "atualização" na página. É assim: MyModel.update({ age: { $gt: 18 } }, { oldEnough: true }, fn);eMyModel.update({ name: 'Tobi' }, { ferret: true }, { multi: true }, fn);
CpILL
para o documento do caso não encontrado, qual _id é usado? Mongoose gera ou o que foi consultado?
Haider
91

Você estava perto de

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

mas seu segundo parâmetro deve ser um objeto com um operador de modificação, por exemplo

Contact.update({phone:request.phone}, {$set: { phone: request.phone }}, {upsert: true}, function(err){...})
chrixian
fonte
15
Eu não acho que você precisa da {$set: ... }parte aqui como sua forma automática minha leitura
CpILL
5
Sim, mangusto diz que transforma tudo em $ set
grantwparks
11
Isso era válido no momento do que da escrita, eu não uso MongoDB mais, então não posso falar com alterações nos últimos meses: D
chrixian
5
Não usar $ set pode ser um mau hábito, se você estiver usando o driver nativo de vez em quando.
UpTheCreek
Você pode usar $ set e $ setOnInsert para definir apenas determinados campos no caso de uma inserção
justin.m.com
73

Bem, eu esperei o tempo suficiente e sem resposta. Finalmente desistiu de toda a abordagem de atualização / upsert e acompanhou:

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? Sim. Estou feliz com isso? Provavelmente não. 2 chamadas de banco de dados em vez de uma.
Esperamos que uma futura implementação do Mongoose venha com uma Model.upsertfunção.

Travelling Tech Guy
fonte
2
Este exemplo usa a interface adicionada no MongoDB 2.2 para especificar as opções multi e upsert em um formulário de documento. .. include :: /includes/fact-upsert-multi-options.rst A documentação está afirmando isso, não sei para onde ir a partir daqui.
Donald Derek
11
Embora isso deva funcionar, você está executando 2 operações (localização, atualização) quando apenas 1 (upsert) é necessária. @chrixian mostra a maneira correta de fazer isso.
respectTheCode
12
Vale ressaltar que esta é a única resposta que permite que os validadores do Mongoose entrem em ação. Conforme os documentos , a validação não ocorre se você chamar atualização.
Tom Spencer
olhares @fiznool como você pode passar manualmente na opção runValidators: truedurante uma atualização: atualização docs (no entanto, atualização validadores só correr em $sete $unsetoperações)
Danny
Veja minha resposta com base nesta caso você precise .upsert()estar disponível em todos os modelos. stackoverflow.com/a/50208331/1586406
spondbob
24

Solução muito elegante que você pode conseguir usando a cadeia de promessas:

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
fonte
Por que isso não foi votado? Parece ser uma grande solução e muito elegante
MadOgre
Solução brilhante, na verdade me fez repensar a maneira como abordo as promessas.
lux
4
Ainda mais elegante seria reescrever (model) => { return model.save(); }como model => model.save(), e também (err) => { res.send(err); }como err => res.send(err);)
Jeremy Thille
11
Onde posso obter mais informações?
V0LT3RR4
17

Eu sou o mantenedor do Mongoose. A maneira mais moderna para Upsert um doc é usar a Model.updateOne()função .

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

Se você precisar do documento com upsert, poderá usar Model.findOneAndUpdate()

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

A principal dica é que você precisa colocar as propriedades exclusivas no filterparâmetro para updateOne()ou findOneAndUpdate(), e as outras propriedades no updateparâmetro.

Aqui está um tutorial sobre como aumentar documentos com o Mongoose .

vkarpov15
fonte
15

Eu criei uma conta StackOverflow APENAS para responder a esta pergunta. Depois de procurar infrutuosamente as interwebs, acabei de escrever algo. Foi assim que fiz, para que possa ser aplicado a qualquer modelo de mangusto. Importe essa função ou adicione-a diretamente ao seu código onde você está fazendo a atualização.

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

Em seguida, para converter um documento em mangusto, faça o seguinte,

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

Esta solução pode exigir 2 chamadas de banco de dados, mas você obtém o benefício de,

  • Validação de esquema no seu modelo porque você está usando .save ()
  • Você pode alterar objetos profundamente aninhados sem enumeração manual em sua chamada de atualização; portanto, se o modelo mudar, não será necessário se preocupar em atualizar seu código.

Lembre-se de que o objeto de destino sempre substituirá a fonte, mesmo se a fonte tiver um valor existente

Além disso, para matrizes, se o objeto existente tiver uma matriz mais longa que a que está sendo substituída, os valores no final da matriz antiga permanecerão. Uma maneira fácil de alterar a matriz inteira é definir a matriz antiga como uma matriz vazia antes da upsert, se é isso que você pretende fazer.

ATUALIZAÇÃO - 16/01/2016 Adicionei uma condição extra para que, se houver uma matriz de valores primitivos, o Mongoose não perceba que a matriz é atualizada sem usar a função "set".

Aaron Mast
fonte
2
+1 por criar acc apenas para isso: P Queria poder dar outro +1 apenas por usar .save (), porque findOneAndUpate () nos torna incapazes de usar validadores e pré, postar, etc. Obrigado, eu vou verificar isso também #
user1576978
Desculpe, mas não funcionou aqui :( Eu tenho tamanho pilha de chamadas excedeu
user1576978
Qual versão do lodash você está usando? Estou usando o lodash versão 2.4.1 Obrigado!
Aaron Mast
Além disso, como você é complexo de objetos? Se eles forem muito grandes, o processo do nó poderá não conseguir lidar com o número de chamadas recursivas necessárias para mesclar os objetos.
Aaron Mast
Eu usei isso, mas tive que adicionar if(_.isObject(value) && _.keys(value).length !== 0) {a condição de guarda para parar o estouro da pilha. Lodash 4+ aqui, parece converter valores não-objetos em objetos na keyschamada, portanto a proteção recursiva sempre foi verdadeira. Talvez haja uma maneira melhor, mas a sua quase trabalhando para mim agora ...
Richard G
12

Eu precisava atualizar / upsert um documento em uma coleção, o que fiz foi criar um novo objeto literal como este:

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

composto de dados que eu recebo de outro lugar no meu banco de dados e depois chamo de atualização no Model

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

esta é a saída que eu recebo depois de executar o script pela primeira vez:

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

E esta é a saída quando executo o script pela segunda vez:

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

Estou usando o mangusto versão 3.6.16

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

        });
    });

Aqui está uma abordagem melhor para resolver o método de atualização no mangusto, você pode verificar o Scotch.io para obter mais detalhes. Definitivamente funcionou para mim !!!

Eyo Okon Eyo
fonte
5
É um erro pensar que isso faz o mesmo que a atualização do MongoDB. Não é atômico.
Valentin Waeselynck
11
Quero fazer backup da resposta @ValentinWaeselynck. O código do Scotch está limpo - mas você está buscando um documento e depois atualizando. No meio desse processo, o documento poderia ter sido alterado.
22416 Nick Pineda
8

Existe um bug introduzido na 2.6 e afeta também a 2.7

O upsert costumava funcionar corretamente no 2.4

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

Dê uma olhada, ele contém algumas informações importantes

ATUALIZADA:

Isso não significa que upsert não funciona. Aqui está um bom exemplo de como usá-lo:

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

Você pode simplesmente atualizar o registro com isso e obter os dados atualizados em resposta

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

isso funcionou para mim.

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
fonte
Obrigado. Este foi o que finalmente funcionou para mim!
22619 Luis Febro
4

Aqui está a maneira mais simples de criar / atualizar enquanto também chama o middleware e os 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);
    });
})
Mín.
fonte
3

Para quem chega aqui ainda está procurando uma solução boa para "upserting" com suporte a ganchos, é isso que testei e trabalho. Ele ainda requer 2 chamadas de banco de dados, mas é muito mais estável do que qualquer coisa que eu tentei em uma única chamada.

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

Se houver geradores disponíveis, fica ainda mais 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
fonte
3

Nenhuma outra solução funcionou para mim. Estou usando uma solicitação de postagem e atualizando os dados, se encontrado, insira-os, também _id é enviado com o corpo da solicitação que precisa ser removido.

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

    res.json({ success: true });
});
Zeeshan Ahmad
fonte
Embora esse trecho de código possa resolver o problema, ele não explica por que ou como responde à pergunta. Por favor incluir uma explicação para o seu código , como o que realmente ajuda a melhorar a qualidade do seu post. Lembre-se de que você está respondendo à pergunta dos leitores no futuro e essas pessoas podem não saber os motivos da sua sugestão de código. Sinalizadores / revisores: para respostas somente de código como esta, com voto negativo, não exclua!
Patrick
2

Seguindo a resposta do Traveling Tech Guy , que já é incrível, podemos criar um plug-in e anexá-lo ao mangusto assim que o inicializarmos, para que .upsert()fique disponível em todos os 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

Então você pode fazer algo como User.upsert({ _id: 1 }, { foo: 'bar' })ou YouModel.upsert({ bar: 'foo' }, { value: 1 })quando quiser.

spondbob
fonte
2

Acabei de voltar para esse problema depois de um tempo e decidi publicar um plug-in com base na resposta de Aaron Mast.

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

Use-o como um plugin mangusto. Ele configura um método estático que mesclará recursivamente o objeto passado.

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

Esse coffeescript funciona para mim com o Node - o truque é que os _id são removidos do seu wrapper ObjectID quando enviados e retornados do cliente e, portanto, precisam ser substituídos por atualizações (quando nenhum _id for fornecido, save será revertido para inserir e adicionar 1).

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

para construir sobre o que Martin Kuzdowicz postou acima. Eu uso o seguinte para fazer uma atualização usando mangusto e uma mesclagem profunda de objetos json. Juntamente com a função model.save () no mangusto, isso permite que o mangusto faça uma validação completa, mesmo que confie em outros valores no json. requer o pacote deepmerge https://www.npmjs.com/package/deepmerge . Mas esse é um pacote muito leve.

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
fonte
11
Eu recomendaria não usar req.bodycomo está antes de testar a injeção de NoSQL (consulte owasp.org/index.php/Testing_for_NoSQL_injection ).
Traveling Tech Guy
11
@TravelingTechGuy Obrigado pela cautela, ainda sou novo no Node e no Mongoose. Meu modelo de mangusto com validadores não seria suficiente para pegar uma tentativa de injeção? durante o model.save ()
Chris Deleo
-5

Depois de ler as postagens acima, decidi 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);
        }
    });

se r for nulo, criamos um novo item. Caso contrário, use upsert na atualização porque a atualização não cria um novo item.

Grant Li
fonte
Se são duas chamadas para o Mongo, não é realmente exagerado, é?
huggie