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

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

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

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().


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/


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

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.


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

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)

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

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
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.