¿Cómo actualizo parcialmente un objeto en MongoDB para que el nuevo objeto se superponga / combine con el existente?

183

Dado este documento guardado en MongoDB

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

Es necesario guardar un objeto con nueva información sobre param2y param3desde el mundo exterior.

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

Quiero fusionar / superponer los nuevos campos sobre el estado existente del objeto para que no se elimine param1

Haciendo esto

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

Conducirá a que MongoDB esté haciendo exactamente lo que se le pidió, y establece some_key en ese valor. reemplazando el viejo.

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

¿Cuál es la forma de que MongoDB actualice solo los campos nuevos (sin indicarlos uno por uno explícitamente)? para obtener esto:

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

Estoy usando el cliente Java, pero cualquier ejemplo será apreciado

Eran Medan
fuente
2
vote por el tema de $ merge: jira.mongodb.org/browse/SERVER-21094
Ali

Respuestas:

107

Si entiendo la pregunta correctamente, desea actualizar un documento con el contenido de otro documento, pero solo los campos que aún no están presentes e ignorar por completo los campos que ya están configurados (incluso si tienen otro valor).

No hay forma de hacerlo en un solo comando.

Primero debe consultar el documento, averiguar qué desea $sety luego actualizarlo (utilizando los valores antiguos como un filtro coincidente para asegurarse de que no obtenga actualizaciones concurrentes en el medio).


Otra lectura de su pregunta sería con la que está satisfecho $set, pero no desea establecer explícitamente todos los campos. ¿Cómo pasarías los datos entonces?

Sabes que puedes hacer lo siguiente:

db.collection.update(  { _id:...} , { $set: someObjectWithNewData } 
Thilo
fuente
Si desea establecer todos los campos incondicionalmente, puede usar el parámetro $ multi , de esta manera: db.collection.update( { _id:...} , { $set: someObjectWithNewData, { $multi: true } } )
Villager
12
No hay $ multi parámetro. Hay una opción múltiple (que es a lo que se vinculó), pero eso no afecta la forma en que se actualizan los campos. Controla si actualiza un solo documento o todos los documentos que coinciden con el parámetro de consulta.
Bob Kerns
1
Esto no tiene ningún sentido en absoluto. Cuando se trata de mongo, se prefiere también que las actualizaciones tengan operaciones atómicas. La primera lectura causa una lectura sucia cada vez que alguien más cambia sus datos. Y dado que mongo no tiene transacciones, felizmente corromperá los datos por usted.
froginvasion
@froginvasion: puede hacerlo atómico utilizando los valores antiguos como un filtro coincidente en la actualización. De esa manera, su actualización fallará si hubo una actualización concurrente conflictiva mientras tanto. Luego puede detectar eso y actuar en consecuencia. Esto se llama bloqueo optimista. Pero sí, depende de que su aplicación lo implemente correctamente, Mongo en sí no tiene transacciones integradas.
Thilo
Creo que esta es una cuestión más general de fusionar dos objetos en javascript. IIRC la idea es simplemente asignar las propiedades del nuevo al viejo
information_interchange
110

Lo resolví con mi propia función. Si desea actualizar el campo especificado en el documento, debe abordarlo claramente.

Ejemplo:

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

Si solo desea actualizar param2, es incorrecto:

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

Debes usar:

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

Entonces escribí una función algo así:

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
fuente
11
Esta debería ser la respuesta aceptada. Para una mejor función flattenObject, consulte gist.github.com/penguinboy/762197
steph643
Gracias tu respuesta me ayudó a guiarme. Pude modificar el valor de una clave específica de un usuario en la colección de usuarios de la siguiente manera:db.users.update( { _id: ObjectId("594dbc3186bb5c84442949b1") }, { $set: { resume: { url: "http://i.imgur.com/UFX0yXs.jpg" } } } )
Luke Schoen
¿Es esta operación atómica y aislada? Es decir, si 2 subprocesos paralelos intentan establecer 2 campos diferentes del mismo documento, eso dará como resultado una condición de carrera y un resultado impredecible
so-random-dude
21

Puede usar la notación de puntos para acceder y establecer campos en lo profundo de los objetos, sin afectar las otras propiedades de esos objetos.

Dado el objeto que especificó anteriormente:

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

Podemos actualizar solo some_key.param2y 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"
    }
}

Puedes profundizar tanto como quieras. Esto también es útil para agregar nuevas propiedades a un objeto sin afectar las existentes.

Samir Talwar
fuente
¿Puedo agregar una nueva clave ... como "alguna clave": {"param1": "val1", "param2": "val2_new", "param3": "val3_new", "param4": "val4_new",}
Urvashi Soni
17

La mejor solución es extraer propiedades del objeto y convertirlas en pares clave-valor de notación de puntos planos. Podría usar, por ejemplo, esta biblioteca:

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

Tiene .flatten función que le permite cambiar el objeto en un conjunto plano de propiedades que luego podrían asignarse al modificador $ set, sin preocuparse de que cualquier propiedad de su objeto DB existente se elimine / sobrescriba sin necesidad.

Tomado 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
  }
}
*/

Y luego forma un selector perfecto: actualizará SOLO las propiedades dadas. EDITAR: Me gusta ser arqueólogo algunas veces;)

SzybkiSasza
fuente
6

Tuve éxito al hacerlo de esta manera:

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

Tengo una función que maneja mis actualizaciones de perfil dinámicamente

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

Nota: 'perfil' es específico de mi implementación, es solo la cadena de la clave que le gustaría modificar.

Matt Smith
fuente
1

Parece que puede establecer isPartialObject que podría lograr lo que desea.

Patrick Tescher
fuente
lo intenté. no te permite guardar objetos parciales si no me equivoco ... pero gracias
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

Si tiene varios campos para actualizar

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

Al comenzar Mongo 4.2, db.collection.update()puede aceptar una canalización de agregación, que permite usar operadores de agregación como $addFields, que genera todos los campos existentes de los documentos de entrada y los campos recién agregados:

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" } }
  • La primera parte {}es la consulta de coincidencia, filtrando qué documentos actualizar (en este caso, todos los documentos).

  • La segunda parte [{ $addFields: { some_key: new_info } }]es la tubería de agregación de actualizaciones:

    • Tenga en cuenta los corchetes que significan el uso de una tubería de agregación.
    • Dado que esta es una tubería de agregación, podemos usar $addFields.
    • $addFields realiza exactamente lo que necesita: actualizar el objeto para que el nuevo objeto se superponga / combine con el existente:
    • En este caso, { param2: "val2_new", param3: "val3_new" }se fusionará con lo existente some_keyal mantenerse param1intacto y agregar o reemplazar ambos param2y param3.
  • No lo olvide { multi: true }, de lo contrario solo se actualizará el primer documento coincidente.

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

a

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

¡Espero que esto ayude!

nguyenthang338
fuente
1

Preferiría hacer una actualización, esta operación en MongoDB se utiliza para guardar el documento en la colección. Si el documento coincide con los criterios de consulta, realizará una operación de actualización; de lo contrario, insertará un nuevo documento en la colección.

algo similar como a continuación

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

use $ set haga este proceso

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

Haga un objeto de actualización con los nombres de las propiedades, incluida la ruta de puntos necesaria. ("somekey." + del ejemplo de OP), y luego use eso para hacer la actualización.

//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){...});
Andrés
fuente
0

Sí, la mejor manera es convertir la notación del objeto en una representación de cadena de valor clave clave, como se menciona en este comentario: https://stackoverflow.com/a/39357531/2529199

Quería resaltar un método alternativo usando esta biblioteca NPM: https://www.npmjs.com/package/dot-object que le permite manipular diferentes objetos usando la notación de puntos.

Usé este patrón para crear programáticamente una propiedad de objeto anidado al aceptar el valor-clave como una variable de función, de la siguiente manera:

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

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

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

Este patrón me permite usar propiedades anidadas y de un solo nivel indistintamente, e insertarlas limpiamente en Mongo.

Si necesita jugar con JS Objects más allá de Mongo, especialmente en el lado del cliente pero tiene consistencia al trabajar con Mongo, esta biblioteca le ofrece más opciones que el mongo-dot-notationmódulo NPM mencionado anteriormente .

PD: originalmente quería mencionar esto como un comentario, pero aparentemente mi representante de S / O no es lo suficientemente alto como para publicar un comentario. Entonces, sin tratar de entrometerse en el comentario de SzybkiSasza, solo quería destacar proporcionar un módulo alternativo.

Ashwin Shankar
fuente
0

Debería pensar en actualizar el objeto indistintamente y luego simplemente almacenar el objeto con los campos actualizados. Algo así como se hace a continuación

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

Tienes que usar documentos incrustados (stringfy el objeto de ruta)

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

Y así, en JavaScript puede usar Spread Operator (...) para actualizar

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