Atualizar o campo MongoDB usando o valor de outro campo

372

No MongoDB, é possível atualizar o valor de um campo usando o valor de outro campo? O SQL equivalente seria algo como:

UPDATE Person SET Name = FirstName + ' ' + LastName

E o pseudocódigo do MongoDB seria:

db.person.update( {}, { $set : { name : firstName + ' ' + lastName } );
Chris Fulstow
fonte

Respostas:

259

A melhor maneira de fazer isso é na versão 4.2+ que permite a utilização do gasoduto agregação no documento de atualização e o updateOne, updateManyou updatemétodo de coleta. Observe que o último foi preterido na maioria dos drivers, se não em todos os idiomas.

MongoDB 4.2+

A versão 4.2 também introduziu o $setoperador de estágio de pipeline, que é um alias para $addFields. Vou usar $setaqui como ele mapeia com o que estamos tentando alcançar.

db.collection.<update method>(
    {},
    [
        {"$set": {"name": { "$concat": ["$firstName", " ", "$lastName"]}}}
    ]
)

MongoDB 3.4+

No 3.4+, você pode usar $addFieldse os $outoperadores de pipeline de agregação.

db.collection.aggregate(
    [
        { "$addFields": { 
            "name": { "$concat": [ "$firstName", " ", "$lastName" ] } 
        }},
        { "$out": "collection" }
    ]
)

Observe que isso não atualiza sua coleção, mas substitui a coleção existente ou cria uma nova. Também para operações de atualização que requerem "conversão de tipo", você precisará de processamento no lado do cliente e , dependendo da operação, poderá precisar usar o find()método em vez do .aggreate()método.

MongoDB 3.2 e 3.0

A maneira como fazemos isso é usando $projectnossos documentos e usando o $concatoperador de agregação de string para retornar a string concatenada. A partir daí, você itera o cursor e usa o $setoperador de atualização para adicionar o novo campo aos seus documentos usando operações em massa para obter a máxima eficiência.

Consulta de agregação:

var cursor = db.collection.aggregate([ 
    { "$project":  { 
        "name": { "$concat": [ "$firstName", " ", "$lastName" ] } 
    }}
])

MongoDB 3.2 ou mais recente

disso, você precisa usar o bulkWritemétodo

var requests = [];
cursor.forEach(document => { 
    requests.push( { 
        'updateOne': {
            'filter': { '_id': document._id },
            'update': { '$set': { 'name': document.name } }
        }
    });
    if (requests.length === 500) {
        //Execute per 500 operations and re-init
        db.collection.bulkWrite(requests);
        requests = [];
    }
});

if(requests.length > 0) {
     db.collection.bulkWrite(requests);
}

MongoDB 2.6 e 3.0

Nesta versão, você precisa usar a BulkAPI agora obsoleta e seus métodos associados .

var bulk = db.collection.initializeUnorderedBulkOp();
var count = 0;

cursor.snapshot().forEach(function(document) { 
    bulk.find({ '_id': document._id }).updateOne( {
        '$set': { 'name': document.name }
    });
    count++;
    if(count%500 === 0) {
        // Excecute per 500 operations and re-init
        bulk.execute();
        bulk = db.collection.initializeUnorderedBulkOp();
    }
})

// clean up queues
if(count > 0) {
    bulk.execute();
}

MongoDB 2.4

cursor["result"].forEach(function(document) {
    db.collection.update(
        { "_id": document._id }, 
        { "$set": { "name": document.name } }
    );
})
styvane
fonte
Eu acho que há um problema com o código para "MongoDB 3.2 ou mais recente". Como o forEach é assíncrono, nada será gravado no último bulkWrite.
Viktor Hedefalk 04/10/19
3
4.2+ Não funciona. MongoError: O campo prefixado em dólar ($) '$ concat' em 'name. $ Concat' não é válido para armazenamento.
Josh Woodcock
@ JoshWoodcock, acho que você teve um erro de digitação na consulta que está executando. Eu sugiro que você verifique.
styvane
@JoshWoodcock Funciona lindamente. Teste isso usando o MongoDB Web Shell
styvane
2
Para aqueles que enfrentam o mesmo problema, o @JoshWoodcock descreveu: preste atenção que a resposta para 4.2+ descreve um pipeline de agregação ; portanto, não perca os colchetes no segundo parâmetro!
philsch 7/01
240

Você deve percorrer. Para o seu caso específico:

db.person.find().snapshot().forEach(
    function (elem) {
        db.person.update(
            {
                _id: elem._id
            },
            {
                $set: {
                    name: elem.firstname + ' ' + elem.lastname
                }
            }
        );
    }
);
Carlos Barcelona
fonte
4
O que acontece se outro usuário alterou o documento entre seu find () e seu save ()?
UpTheCreek 15/02/2019
3
É verdade, mas a cópia entre campos não deve exigir que as transações sejam atômicas.
UpTheCreek
3
É importante notar que save()substitui completamente o documento. Deve usar em seu update()lugar.
Carlos
12
Como sobredb.person.update( { _id: elem._id }, { $set: { name: elem.firstname + ' ' + elem.lastname } } );
Philipp Jardas
11
Eu criei uma função chamada create_guidque produzia apenas um guia único por documento ao iterar forEachdessa maneira (ou seja, o simples uso create_guidde uma updateinstrução com mutli=truecausou o mesmo guia para todos os documentos). Essa resposta funcionou perfeitamente para mim. 1
rmirabelle
103

Aparentemente, existe uma maneira de fazer isso com eficiência desde o MongoDB 3.4, veja a resposta do styvane .


Resposta obsoleta abaixo

Você não pode se referir ao próprio documento em uma atualização (ainda). Você precisará percorrer os documentos e atualizar cada documento usando uma função. Veja esta resposta para um exemplo, ou esta para o servidor eval().

Niels van der Rest
fonte
31
Isso ainda é válido hoje?
Christian Engel
3
@ChristianEngel: Parece que sim. Não consegui encontrar nada nos documentos do MongoDB que mencionasse uma referência ao documento atual em uma updateoperação. Essa solicitação de recurso relacionada ainda não foi resolvida.
Niels van der Rest
4
Ainda é válido em abril de 2017? Ou já existem novos recursos que podem fazer isso?
26717 Kim
11
@ Kim Parece que ainda é válido. Além disso, a solicitação de recurso que @ niels-van-der-rest apontou em 2013 ainda está em OPEN.
Danziger
8
isto não é uma resposta válida mais, ter um olhar para resposta @styvane
aitchkhan
45

Para um banco de dados com alta atividade, você pode encontrar problemas nos quais suas atualizações afetam a alteração ativa dos registros e, por esse motivo, recomendo o uso do snapshot ()

db.person.find().snapshot().forEach( function (hombre) {
    hombre.name = hombre.firstName + ' ' + hombre.lastName; 
    db.person.save(hombre); 
});

http://docs.mongodb.org/manual/reference/method/cursor.snapshot/

Eric Kigathi
fonte
2
O que acontece se outro usuário editou a pessoa entre find () e save ()? Eu tenho um caso em que várias chamadas podem ser feitas para o mesmo objeto, alterando-as com base em seus valores atuais. O segundo usuário deve esperar pela leitura até que o primeiro seja concluído com o salvamento. Isso consegue isso?
Marco #
4
Sobre snapshot(): Deprecated in the mongo Shell since v3.2. Starting in v3.2, the $snapshot operator is deprecated in the mongo shell. In the mongo shell, use cursor.snapshot() instead. link
ppython
10

Em relação a esta resposta , a função de captura instantânea foi preterida na versão 3.6, de acordo com esta atualização . Portanto, na versão 3.6 e superior, é possível executar a operação desta maneira:

db.person.find().forEach(
    function (elem) {
        db.person.update(
            {
                _id: elem._id
            },
            {
                $set: {
                    name: elem.firstname + ' ' + elem.lastname
                }
            }
        );
    }
);
Aldo
fonte
9

Iniciando Mongo 4.2, db.collection.update()pode aceitar um pipeline de agregação, finalmente permitindo a atualização / criação de um campo com base em outro campo:

// { firstName: "Hello", lastName: "World" }
db.collection.update(
  {},
  [{ $set: { name: { $concat: [ "$firstName", " ", "$lastName" ] } } }],
  { multi: true }
)
// { "firstName" : "Hello", "lastName" : "World", "name" : "Hello World" }
  • A primeira parte {}é a consulta de correspondência, filtrando quais documentos serão atualizados (no nosso caso, todos os documentos).

  • A segunda parte [{ $set: { name: { ... } }]é o pipeline de agregação de atualização (observe os colchetes ao quadrado significando o uso de um pipeline de agregação). $seté um novo operador de agregação e um alias de $addFields.

  • Não se esqueça { multi: true }, caso contrário, apenas o primeiro documento correspondente será atualizado.

Xavier Guihot
fonte
8

Tentei a solução acima, mas achei inadequada para grandes quantidades de dados. Descobri o recurso de fluxo:

MongoClient.connect("...", function(err, db){
    var c = db.collection('yourCollection');
    var s = c.find({/* your query */}).stream();
    s.on('data', function(doc){
        c.update({_id: doc._id}, {$set: {name : doc.firstName + ' ' + doc.lastName}}, function(err, result) { /* result == true? */} }
    });
    s.on('end', function(){
        // stream can end before all your updates do if you have a lot
    })
})
Chris Gibb
fonte
11
Como isso é diferente? O vapor será acelerado pela atividade de atualização? Você tem alguma referência a isso? Os documentos do Mongo são bastante pobres.
Nico
2

Aqui está o que criamos para copiar um campo para outro para ~ 150_000 registros. Demorou cerca de 6 minutos, mas ainda é significativamente menos intensivo em recursos do que seria para instanciar e iterar sobre o mesmo número de objetos ruby.

js_query = %({
  $or : [
    {
      'settings.mobile_notifications' : { $exists : false },
      'settings.mobile_admin_notifications' : { $exists : false }
    }
  ]
})

js_for_each = %(function(user) {
  if (!user.settings.hasOwnProperty('mobile_notifications')) {
    user.settings.mobile_notifications = user.settings.email_notifications;
  }
  if (!user.settings.hasOwnProperty('mobile_admin_notifications')) {
    user.settings.mobile_admin_notifications = user.settings.email_admin_notifications;
  }
  db.users.save(user);
})

js = "db.users.find(#{js_query}).forEach(#{js_for_each});"
Mongoid::Sessions.default.command('$eval' => js)
Chris Bloom
fonte
1

Com o MongoDB versão 4.2 ou posterior , as atualizações são mais flexíveis, pois permitem o uso do pipeline de agregação em seu update, updateOnee updateMany. Agora você pode transformar seus documentos usando os operadores de agregação e atualizar sem a necessidade de explicitar o $setcomando (em vez disso, usamos$replaceRoot: {newRoot: "$$ROOT"} )

Aqui, usamos a consulta agregada para extrair o registro de data e hora do campo ObjectID "_id" do MongoDB e atualizar os documentos (eu não sou especialista em SQL, mas acho que o SQL não fornece nenhum ObjectID gerado automaticamente com registro de data e hora, você precisa criar automaticamente essa data)

var collection = "person"

agg_query = [
    {
        "$addFields" : {
            "_last_updated" : {
                "$toDate" : "$_id"
            }
        }
    },
    {
        $replaceRoot: {
            newRoot: "$$ROOT"
        } 
    }
]

db.getCollection(collection).updateMany({}, agg_query, {upsert: true})
Yi Xiang Chong
fonte
Você não precisa { $replaceRoot: { newRoot: "$$ROOT" } }; significa substituir o documento por si só, o que é inútil. Se você substituir $addFieldspor seu alias $sete updateManyqual for um dos aliases update, obterá exatamente a mesma resposta que esta acima.
Xavier Guihot 15/04