Como atualizo parcialmente um objeto no MongoDB para que o novo objeto seja sobreposto / mesclado com o existente

183

Dado este documento salvo no MongoDB

{
   _id : ...,
   some_key: { 
        param1 : "val1",
        param2 : "val2",
        param3 : "val3"
   }
}

Um objeto com novas informações dentro param2e param3fora do mundo precisa ser salvo

var new_info = {
    param2 : "val2_new",
    param3 : "val3_new"
};

Desejo mesclar / sobrepor os novos campos sobre o estado existente do objeto para que o param1 não seja removido

Fazendo isso

db.collection.update(  { _id:...} , { $set: { some_key : new_info  } } 

Isso levará o MongoDB a fazer exatamente o que foi solicitado e define some_key para esse valor. substituindo o antigo.

{
   _id : ...,
   some_key: { 
      param2 : "val2_new",
      param3 : "val3_new"
   }
}

Como é que o MongoDB atualiza apenas novos campos (sem declará-los um por um explicitamente)? para conseguir esta:

{
   _id : ...,
   some_key: { 
        param1 : "val1",
        param2 : "val2_new",
        param3 : "val3_new"
   }
}

Estou usando o cliente Java, mas qualquer exemplo será apreciado

Eran Medan
fonte
2
vote na questão $ merge: jira.mongodb.org/browse/SERVER-21094
Ali

Respostas:

107

Se entendi a pergunta corretamente, você deseja atualizar um documento com o conteúdo de outro documento, mas apenas os campos que ainda não estão presentes, e ignora completamente os campos que já estão definidos (mesmo que com outro valor).

Não há como fazer isso em um único comando.

Você deve consultar o documento primeiro, descobrir o que deseja $sete depois atualizá-lo (usando os valores antigos como um filtro correspondente para garantir que você não receba atualizações simultâneas).


Outra leitura da sua pergunta seria a sua satisfação $set, mas não deseja definir explicitamente todos os campos. Como você passaria os dados então?

Você sabe que pode fazer o seguinte:

db.collection.update(  { _id:...} , { $set: someObjectWithNewData } 
Thilo
fonte
Se você deseja definir todos os campos incondicionalmente, pode usar o parâmetro $ multi , assim: db.collection.update( { _id:...} , { $set: someObjectWithNewData, { $multi: true } } )
Villager
12
Não há parâmetro $ multi. Há uma opção múltipla (à qual você vinculou), mas isso não afeta a atualização dos campos. Ele controla se você atualiza um único documento ou todos os documentos que correspondem ao parâmetro de consulta.
Bob Kerns
1
Isso não faz nenhum sentido. Ao lidar com o mongo, é preferível também que as atualizações tenham operações atômicas. A primeira leitura causa uma leitura suja sempre que alguém altera seus dados. E como o mongo não tem transações, ele corromperá os dados para você.
precisa saber é o seguinte
@froginvasion: você pode torná-lo atômico usando os valores antigos como um filtro correspondente na atualização. Dessa forma, sua atualização falhará se houver uma atualização simultânea conflitante nesse meio tempo. Você pode detectar isso e agir em conformidade. Isso é chamado de bloqueio otimista. Mas sim, depende do aplicativo ser implementado corretamente, o próprio Mongo não possui transações internas.
Thilo 26/01
Penso que esta é uma questão mais geral de mesclar dois objetos em javascript. IIRC a idéia é simplesmente atribuir as propriedades do novo com o velho
information_interchange
110

Eu resolvi isso com minha própria função. Se você deseja atualizar o campo especificado no documento, precisa tratá-lo claramente.

Exemplo:

{
    _id : ...,
    some_key: { 
        param1 : "val1",
        param2 : "val2",
        param3 : "val3"
    }
}

Se você deseja atualizar apenas o param2, é errado fazer:

db.collection.update(  { _id:...} , { $set: { some_key : new_info  } }  //WRONG

Você deve usar:

db.collection.update(  { _id:...} , { $set: { some_key.param2 : new_info  } } 

Então, eu escrevi uma função algo como isso:

function _update($id, $data, $options=array()){

    $temp = array();
    foreach($data as $key => $value)
    {
        $temp["some_key.".$key] = $value;
    } 

    $collection->update(
        array('_id' => $id),
        array('$set' => $temp)
    );

}

_update('1', array('param2' => 'some data'));
mehmatrix
fonte
11
Essa deve ser a resposta aceita. Para uma melhor função flattenObject, consulte gist.github.com/penguinboy/762197
steph643
Obrigado sua resposta ajudou a me guiar. Eu fui capaz de modificar o valor de uma chave específica de um usuário na coleção de usuários da seguinte maneira:db.users.update( { _id: ObjectId("594dbc3186bb5c84442949b1") }, { $set: { resume: { url: "http://i.imgur.com/UFX0yXs.jpg" } } } )
Luke Schoen
Esta operação é atômica e isolada? Ou seja, se 2 threads paralelos tentarem definir 2 campos diferentes do mesmo documento, isso resultará em condição de corrida e resultado imprevisível
so-random-dude
21

Você pode usar a notação de ponto para acessar e definir campos profundamente nos objetos, sem afetar as outras propriedades desses objetos.

Dado o objeto que você especificou acima:

> db.test.insert({"id": "test_object", "some_key": {"param1": "val1", "param2": "val2", "param3": "val3"}})
WriteResult({ "nInserted" : 1 })

Podemos atualizar apenas some_key.param2e some_key.param3:

> db.test.findAndModify({
... query: {"id": "test_object"},
... update: {"$set": {"some_key.param2": "val2_new", "some_key.param3": "val3_new"}},
... new: true
... })
{
    "_id" : ObjectId("56476e04e5f19d86ece5b81d"),
    "id" : "test_object",
    "some_key" : {
        "param1" : "val1",
        "param2" : "val2_new",
        "param3" : "val3_new"
    }
}

Você pode se aprofundar o quanto quiser. Isso também é útil para adicionar novas propriedades a um objeto sem afetar as existentes.

Samir Talwar
fonte
Posso adicionar uma nova chave ... como "alguma chave": {"param1": "val1", "param2": "val2_new", "param3": "val3_new", "param4": "val4_new",}
Urvashi Soni 18/04
17

A melhor solução é extrair propriedades do objeto e torná-las pares de valor-chave de notação de ponto simples. Você pode usar, por exemplo, esta biblioteca:

https://www.npmjs.com/package/mongo-dot-notation

Tem .flatten função que permite alterar o objeto em um conjunto plano de propriedades que podem ser atribuídas ao modificador $ set, sem preocupações de que qualquer propriedade do seu objeto de banco de dados existente seja excluída / substituída sem necessidade.

Extraído de mongo-dot-notationdocumentos:

var person = {
  firstName: 'John',
  lastName: 'Doe',
  address: {
    city: 'NY',
    street: 'Eighth Avenu',
    number: 123
  }
};



var instructions = dot.flatten(person)
console.log(instructions);
/* 
{
  $set: {
    'firstName': 'John',
    'lastName': 'Doe',
    'address.city': 'NY',
    'address.street': 'Eighth Avenu',
    'address.number': 123
  }
}
*/

E então forma o seletor perfeito - ele atualizará SOMENTE as propriedades dadas. EDIT: Eu gosto de ser arqueólogo algumas vezes;)

SzybkiSasza
fonte
6

Eu tive sucesso fazendo assim:

db.collection.update(  { _id:...} , { $set: { 'key.another_key' : new_info  } } );

Eu tenho uma função que lida com minhas atualizações de perfil dinamicamente

function update(prop, newVal) {
  const str = `profile.${prop}`;
  db.collection.update( { _id:...}, { $set: { [str]: newVal } } );
}

Nota: 'profile' é específico da minha implementação, é apenas a string da chave que você deseja modificar.

Matt smith
fonte
1

Parece que você pode definir isPartialObject que pode realizar o que você deseja.

Patrick Tescher
fonte
tentei. ele doens't deixar você guardar objetos parciais, se não me engano ... mas graças
Eran Medan
1
    // where clause DBObject
    DBObject query = new BasicDBObject("_id", new ObjectId(id));

    // modifications to be applied
    DBObject update = new BasicDBObject();

    // set new values
    update.put("$set", new BasicDBObject("param2","value2"));

   // update the document
    collection.update(query, update, true, false); //3rd param->upsertFlag, 4th param->updateMultiFlag

Se você tiver vários campos a serem atualizados

        Document doc = new Document();
        doc.put("param2","value2");
        doc.put("param3","value3");
        update.put("$set", doc);
Vino
fonte
1

Iniciando Mongo 4.2, db.collection.update()pode aceitar um pipeline de agregação, que permite o uso de operadores de agregação, como $addFields, que gera todos os campos existentes nos documentos de entrada e nos campos adicionados recentemente:

var new_info = { param2: "val2_new", param3: "val3_new" }

// { some_key: { param1: "val1", param2: "val2", param3: "val3" } }
// { some_key: { param1: "val1", param2: "val2"                 } }
db.collection.update({}, [{ $addFields: { some_key: new_info } }], { multi: true })
// { some_key: { param1: "val1", param2: "val2_new", param3: "val3_new" } }
// { some_key: { param1: "val1", param2: "val2_new", param3: "val3_new" } }
  • A primeira parte {}é a consulta de correspondência, filtrando quais documentos atualizar (neste caso, todos os documentos).

  • A segunda parte [{ $addFields: { some_key: new_info } }]é o pipeline de agregação de atualização:

    • Observe os colchetes ao quadrado, significando o uso de um pipeline de agregação.
    • Como esse é um pipeline de agregação, podemos usá-lo $addFields.
    • $addFields executa exatamente o que você precisa: atualizando o objeto para que o novo objeto se sobreponha / mescle com o existente:
    • Nesse caso, { param2: "val2_new", param3: "val3_new" }será mesclado ao existente some_keymantendo-se param1intocado e adicione ou substitua ambos param2e param3.
  • Não se esqueça { multi: true }, caso contrário, apenas o primeiro documento correspondente será atualizado.

Xavier Guihot
fonte
1
db.collection.update(  { _id:...} , { $set: { some_key : new_info  } } 

para

db.collection.update( { _id: ..} , { $set: { some_key: { param1: newValue} } } ); 

Espero que esta ajuda!

nguyenthang338
fonte
1

Você pode preferir fazer um upsert, essa operação no MongoDB é utilizada para salvar o documento na coleção. Se o documento corresponder aos critérios de consulta, ele executará a operação de atualização, caso contrário, ele inserirá um novo documento na coleção.

algo semelhante como abaixo

db.employees.update(
    {type:"FT"},
    {$set:{salary:200000}},
    {upsert : true}
 )
Pravin
fonte
0

use $ set faça este processo

.update({"_id": args.dashboardId, "viewData._id": widgetId}, {$set: {"viewData.$.widgetData": widgetDoc.widgetData}})
.exec()
.then(dashboardDoc => {
    return {
        result: dashboardDoc
    };
}); 
KARTHIKEYAN.A
fonte
0

Faça um objeto de atualização com os nomes das propriedades, incluindo o caminho de ponto necessário. ("somekey." + do exemplo do OP) e use isso para fazer a atualização.

//the modification that's requested
updateReq = { 
   param2 : "val2_new",
   param3 : "val3_new"   
}

//build a renamed version of the update request
var update = {};
for(var field in updateReq){
    update["somekey."+field] = updateReq[field];
}

//apply the update without modifying fields not originally in the update
db.collection.update({._id:...},{$set:update},{upsert:true},function(err,result){...});
Andrew
fonte
0

Sim, a melhor maneira é converter a notação de objeto em uma representação simples de cadeia de valor-chave, conforme mencionado neste comentário: https://stackoverflow.com/a/39357531/2529199

Eu queria destacar um método alternativo usando esta biblioteca do NPM: https://www.npmjs.com/package/dot-object, que permite manipular objetos diferentes usando a notação de ponto.

Usei esse padrão para criar programaticamente uma propriedade de objeto aninhado ao aceitar o valor-chave como variável de função, da seguinte maneira:

const dot = require('dot-object');

function(docid, varname, varvalue){
  let doc = dot.dot({
      [varname]: varvalue 
  });

  Mongo.update({_id:docid},{$set:doc});
}

Esse padrão me permite usar propriedades aninhadas e de nível único de forma intercambiável e inseri-las corretamente no Mongo.

Se você precisar brincar com objetos JS além do Mongo, especialmente no lado do cliente, mas tiver consistência ao trabalhar com o Mongo, esta biblioteca oferece mais opções do que o mongo-dot-notationmódulo NPM mencionado anteriormente .

PS: Eu originalmente queria mencionar isso como um comentário, mas aparentemente meu representante de S / O não é alto o suficiente para postar um comentário. Portanto, não tentando usar o comentário de SzybkiSasza, só queria destacar o fornecimento de um módulo alternativo.

Ashwin Shankar
fonte
0

Você deve pensar em atualizar o objeto de forma intercambiável e, em seguida, simplesmente armazenar o objeto com os campos atualizados. Algo como feito abaixo

function update(_id) {
    return new Promise((resolve, reject) => {

        ObjModel.findOne({_id}).exec((err, obj) => {

            if(err) return reject(err)

            obj = updateObject(obj, { 
                some_key: {
                    param2 : "val2_new",
                    param3 : "val3_new"
                }
            })

            obj.save((err, obj) => {
                if(err) return reject(err)
                resolve(obj)
            })
        })
    })
}


function updateObject(obj, data) {

    let keys = Object.keys(data)

    keys.forEach(key => {

        if(!obj[key]) obj[key] = data[key]

        if(typeof data[key] == 'object') 
            obj[key] = updateObject(obj[key], data[key])
        else
            obj[key] = data[key]
    })

    return obj
}
Ivan Bravo Mendoza
fonte
0

Você precisa usar Documentos Incorporados (stringfy o objeto path)

let update = {}
Object.getOwnPropertyNames(new_info).forEach(param => {
   update['some_key.' + param] = new_info[param]
})

E assim, em JavaScript, você pode usar o Spread Operator (...) para atualizar

db.collection.update(  { _id:...} , { $set: { ...update  } } 
Guihgo
fonte