Actualice el campo MongoDB usando el valor de otro campo

372

En MongoDB, ¿es posible actualizar el valor de un campo utilizando el valor de otro campo? El SQL equivalente sería algo como:

UPDATE Person SET Name = FirstName + ' ' + LastName

Y el pseudocódigo de MongoDB sería:

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

Respuestas:

260

La mejor manera de hacerlo es en la versión 4.2+, que permite utilizar la canalización de agregación en el documento de actualización y updateOne, updateManyoupdate método de recolección. Tenga en cuenta que este último ha quedado en desuso en la mayoría de los controladores de idiomas, si no en todos.

MongoDB 4.2+

La versión 4.2 también introdujo el $setoperador de etapa de canalización para el cual es un alias $addFields. Usaré $setaquí, ya que se mapea con lo que estamos tratando de lograr.

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

MongoDB 3.4+

En 3.4+ puedes usar $addFieldslos $outoperadores de canalización de agregación.

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

Tenga en cuenta que esto no actualiza su colección, sino que reemplaza la colección existente o crea una nueva. Además, para las operaciones de actualización que requieren "conversión de tipo", necesitará un procesamiento del lado del cliente y , según la operación, es posible que deba usar el find()método en lugar del .aggreate()método.

MongoDB 3.2 y 3.0

La forma en que hacemos esto es $projectintroduciendo nuestros documentos y utilizando el $concatoperador de agregación de cadenas para devolver la cadena concatenada. A partir de ahí, luego itera el cursor y utiliza el $setoperador de actualización para agregar el nuevo campo a sus documentos utilizando operaciones masivas para una máxima eficiencia.

Consulta de agregación:

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

MongoDB 3.2 o más reciente

a partir de esto, debe usar el 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 y 3.0

A partir de esta versión, debe utilizar la BulkAPI ahora en desuso y sus métodos asociados .

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
fuente
Creo que hay un problema con el código para "MongoDB 3.2 o posterior". Como forEach es asíncrono, normalmente nada se escribirá en el último bulkWrite.
Viktor Hedefalk
3
4.2+ no funciona. MongoError: el campo prefijado en dólares ($) '$ concat' en 'nombre. $ Concat' no es válido para el almacenamiento.
Josh Woodcock
@JoshWoodcock, creo que tuvo un error tipográfico en la consulta que está ejecutando. Te sugiero que verifiques dos veces.
styvane
@JoshWoodcock Funciona muy bien. Por favor pruebe esto usando el MongoDB Web Shell
styvane
2
Para aquellos que se encuentran con el mismo problema que describió @JoshWoodcock: preste atención a que la respuesta para 4.2+ describe una tubería de agregación , ¡así que no se pierda los corchetes en el segundo parámetro!
philsch
240

Deberías iterar. Para su caso específico:

db.person.find().snapshot().forEach(
    function (elem) {
        db.person.update(
            {
                _id: elem._id
            },
            {
                $set: {
                    name: elem.firstname + ' ' + elem.lastname
                }
            }
        );
    }
);
Carlos Barcelona
fuente
44
¿Qué sucede si otro usuario ha cambiado el documento entre su find () y su save ()?
UpTheCreek
3
Es cierto, pero copiar entre campos no debería requerir que las transacciones sean atómicas.
UpTheCreek
3
Es importante notar que save()reemplaza completamente el documento. Debería usar update()en su lugar.
Carlos
12
¿Qué taldb.person.update( { _id: elem._id }, { $set: { name: elem.firstname + ' ' + elem.lastname } } );
Philipp Jardas
1
Creé una función llamada create_guidque solo producía un guid único por documento al iterar forEachde esta manera (es decir, simplemente usarlo create_guiden una updatedeclaración mutli=truecausó que se generara el mismo guid para todos los documentos). Esta respuesta funcionó perfectamente para mí. +1
rmirabelle
103

Aparentemente hay una manera de hacer esto eficientemente desde MongoDB 3.4, vea la respuesta de styvane .


Respuesta obsoleta a continuación

No puede hacer referencia al documento en sí en una actualización (todavía). Tendrá que recorrer los documentos y actualizar cada documento utilizando una función. Vea esta respuesta para un ejemplo, o esta para el lado del servidor eval().

Niels van der Rest
fuente
31
¿Sigue siendo válido hoy?
Christian Engel
3
@ChristianEngel: Parece que sí. No pude encontrar nada en los documentos de MongoDB que mencione una referencia al documento actual en una updateoperación. Esta solicitud de función relacionada aún no se ha resuelto.
Niels van der Rest
44
¿Sigue siendo válido en abril de 2017? ¿O ya hay nuevas características que pueden hacer esto?
Kim
1
@ Kim Parece que todavía es válido. Además, la solicitud de función que @ niels-van-der-rest señaló en 2013 todavía está vigente OPEN.
Danziger
8
esta ya no es una respuesta válida, eche un vistazo a la respuesta de
@styvane
45

Para una base de datos con alta actividad, puede encontrarse con problemas en los que sus actualizaciones afectan a registros que cambian activamente y, por esta razón, recomiendo usar 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
fuente
2
¿Qué sucede si otro usuario editó a la persona entre find () y save ()? Tengo un caso en el que se pueden hacer varias llamadas al mismo objeto cambiándolas en función de sus valores actuales. El segundo usuario debería esperar con la lectura hasta que el primero termine de guardar. ¿Esto logra eso?
Marco
44
Sobre el 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. enlace
ppython
10

Con respecto a esta respuesta , la función de instantánea está en desuso en la versión 3.6, de acuerdo con esta actualización . Entonces, en la versión 3.6 y superior, es posible realizar la operación de esta manera:

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

Al comenzar Mongo 4.2, db.collection.update()puede aceptar una canalización de agregación, finalmente permitiendo la actualización / creación de un campo basado en otro campo:

// { firstName: "Hello", lastName: "World" }
db.collection.update(
  {},
  [{ $set: { name: { $concat: [ "$firstName", " ", "$lastName" ] } } }],
  { multi: true }
)
// { "firstName" : "Hello", "lastName" : "World", "name" : "Hello World" }
  • La primera parte {}es la consulta de coincidencia, filtrando qué documentos actualizar (en nuestro caso, todos los documentos).

  • La segunda parte [{ $set: { name: { ... } }]es la tubería de agregación de actualización (tenga en cuenta los corchetes que significan el uso de una tubería de agregación). $setes un nuevo operador de agregación y un alias de $addFields.

  • No lo olvide { multi: true }, de lo contrario solo se actualizará el primer documento coincidente.

Xavier Guihot
fuente
8

Probé la solución anterior pero me pareció inadecuada para grandes cantidades de datos. Luego descubrí la función de transmisión:

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
fuente
1
¿Cómo es esto diferente? ¿La actividad de actualización acelerará el vapor? ¿Tienes alguna referencia al respecto? Los documentos de Mongo son bastante pobres.
Nico
2

Esto es lo que se nos ocurrió para copiar un campo a otro para ~ 150_000 registros. Tomó alrededor de 6 minutos, pero aún requiere mucho menos recursos de lo que hubiera sido crear instancias e iterar sobre la misma cantidad de objetos rubí.

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
fuente
1

Con MongoDB versión 4.2+ , las actualizaciones son más flexibles, ya que permiten el uso de la canalización de agregación en su update, updateOney updateMany. Ahora puede transformar sus documentos utilizando los operadores de agregación y luego actualizarlos sin la necesidad de indicar explícitamente el $setcomando (en su lugar usamos $replaceRoot: {newRoot: "$$ROOT"})

Aquí usamos la consulta agregada para extraer la marca de tiempo del campo ObjectID "_id" de MongoDB y actualizar los documentos (no soy un experto en SQL, pero creo que SQL no proporciona ningún ObjectID generado automáticamente que tenga una marca de tiempo, debería crear automáticamente esa fecha)

var collection = "person"

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

db.getCollection(collection).updateMany({}, agg_query, {upsert: true})
Yi Xiang Chong
fuente
Usted no necesita { $replaceRoot: { newRoot: "$$ROOT" } }; significa reemplazar el documento por sí mismo, lo cual no tiene sentido. Si reemplaza $addFieldspor su alias $sety updateManycuál es uno de los alias update, entonces obtiene exactamente la misma respuesta que esta anterior.
Xavier Guihot