MongoDB - Actualizar objetos en la matriz de un documento (actualización anidada)

204

Supongamos que tenemos la siguiente colección, sobre la que tengo algunas preguntas:

{
    "_id" : ObjectId("4faaba123412d654fe83hg876"),
    "user_id" : 123456,
    "total" : 100,
    "items" : [
            {
                    "item_name" : "my_item_one",
                    "price" : 20
            },
            {
                    "item_name" : "my_item_two",
                    "price" : 50
            },
            {
                    "item_name" : "my_item_three",
                    "price" : 30
            }
    ]
}

1 - Quiero aumentar el precio de "item_name": "my_item_two" y, si no existe , debería adjuntarse a la matriz "items".

2 - ¿Cómo puedo actualizar dos campos al mismo tiempo? Por ejemplo, aumente el precio de "my_item_three" y al mismo tiempo aumente el "total" (con el mismo valor).

Prefiero hacer esto en el lado de MongoDB, de lo contrario tengo que cargar el documento en el lado del cliente (Python) y construir el documento actualizado y reemplazarlo con el existente en MongoDB.

ACTUALIZAR Esto es lo que he intentado y funciona bien SI EL Objeto existe :

db.test_invoice.update({user_id : 123456 , "items.item_name":"my_item_one"} , {$inc: {"items.$.price": 10}})

Pero si la clave no existe, no hace nada. Además, solo actualiza el objeto anidado. Con este comando no hay manera de actualizar el campo "total" también.

Majid
fuente
1
Creo que no puedes hacer esto en mongo, excepto quizás con mucho dolor usando la evaluación. Mongo es muy limitado en operaciones de datos.
Antti Haapala
3
@Haapala: mongodb tiene $ inc y actualiza con upsert
jdi
1
@jdi sí sí, pero no ayuda mucho aquí, pero lo que necesita es $ incs múltiples, condicionalmente, y si el elemento no existe, entonces se necesita un $ push.
Antti Haapala

Respuestas:

237

Para la pregunta # 1, dividámoslo en dos partes. Primero, incremente cualquier documento que tenga "items.item_name" igual a "my_item_two". Para ello, deberá utilizar el operador posicional "$". Algo como:

 db.bar.update( {user_id : 123456 , "items.item_name" : "my_item_two" } , 
                {$inc : {"items.$.price" : 1} } , 
                false , 
                true);

Tenga en cuenta que esto solo incrementará el primer subdocumento coincidente en cualquier matriz (por lo tanto, si tiene otro documento en la matriz con "item_name" igual a "my_item_two", no se incrementará). Pero esto podría ser lo que quieres.

La segunda parte es más complicada. Podemos insertar un nuevo elemento en una matriz sin un "my_item_two" de la siguiente manera:

 db.bar.update( {user_id : 123456, "items.item_name" : {$ne : "my_item_two" }} , 
                {$addToSet : {"items" : {'item_name' : "my_item_two" , 'price' : 1 }} } ,
                false , 
                true);

Para su pregunta # 2, la respuesta es más fácil. Para incrementar el total y el precio de item_three en cualquier documento que contenga "my_item_three", puede usar el operador $ inc en varios campos al mismo tiempo. Algo como:

db.bar.update( {"items.item_name" : {$ne : "my_item_three" }} ,
               {$inc : {total : 1 , "items.$.price" : 1}} ,
               false ,
               true);
matulef
fuente
Gracias por la respuesta. La respuesta para la Pregunta # 2 es perfecta y funciona bien :) Pero para la Pregunta # 1, el problema es que debe buscar el documento por "user_id" y no por "items.item_name". En este caso, no puedo usar el operador posicional, ya que no he especificado la matriz en la consulta de búsqueda. También quiero que ambas partes se hagan de una sola vez. (si es posible)
Majid
Buenos ejemplos. Sentí que estaba haciendo evidente esta dirección a partir de los enlaces en mi respuesta, pero seguía obteniendo resistencia diciendo que no era lo que estaba buscando. Supongo que el OP solo necesitaba ver ejemplos sobre cómo hacer $ inc múltiples.
jdi
Para la pregunta 1, aún debe poder hacerlo en dos partes agregando el user_id al bit de consulta de cada una de las actualizaciones (modificaré la respuesta en consecuencia). Tendrá que pensar más sobre si es posible hacerlo de una sola vez.
matulef
66
¿Cuáles son los parámetros tercero y cuarto en el método de actualización? ¿y por qué?
skmahawar
55
@skmahawar con respecto a los parámetros tercero y cuarto, docs.mongodb.com/manual/reference/method/db.collection.update indica que estas opciones son para "upsert" y "multi" respectivamente. Para upsert, si se establece en verdadero, crea un nuevo documento cuando ningún documento coincide con los criterios de consulta. El valor predeterminado es falso, que no inserta un nuevo documento cuando no se encuentra ninguna coincidencia ". Para múltiples, si se establece en verdadero, actualiza varios documentos que cumplen con los criterios de consulta. Si se establece en falso, actualiza un documento. falsa.
Mike Strand
24

No hay forma de hacer esto en una sola consulta. Tienes que buscar el documento en la primera consulta:

Si el documento existe:

db.bar.update( {user_id : 123456 , "items.item_name" : "my_item_two" } , 
                {$inc : {"items.$.price" : 1} } , 
                false , 
                true);

Más

db.bar.update( {user_id : 123456 } , 
                {$addToSet : {"items" : {'item_name' : "my_item_two" , 'price' : 1 }} } ,
                false , 
                true);

No es necesario agregar condición {$ne : "my_item_two" }.

También en entornos multiproceso, debe tener cuidado de que solo un hilo pueda ejecutar el segundo (insertar caso, si no se encontró el documento) a la vez, de lo contrario se insertarán documentos incrustados duplicados.

Udit
fuente
1
no, hay una manera de hacer esto en una sola consulta, ver arriba
Martijn Scheffer
1
@MartijnScheffer, no veo una manera de hacer esto como una sola consulta en ninguna parte de este hilo. Incluso la "respuesta" está utilizando 2 consultas igual que en esta respuesta. ¿Me estoy perdiendo de algo? También espero que haya una manera de hacer esto en una consulta para evitar problemas de subprocesos múltiples
dferraro
@Udit, ¿cómo puedo usar los artículos. $. Price dentro de la condición? cuando intento agregar una condición como "incrementar solo si el precio es mayor que 0", no funciona, básicamente no se actualiza nada. ¿Puedo usar esto en la condición where o me falta algo? items. $. price: {$ gt: 0}
dferraro
algo debe haber desaparecido :)
Martijn Scheffer
3

Podemos usar el $setoperador para actualizar la matriz anidada dentro del objeto archivado, actualizar el valor

db.getCollection('geolocations').update( 
   {
       "_id" : ObjectId("5bd3013ac714ea4959f80115"), 
       "geolocation.country" : "United States of America"
   }, 
   { $set: 
       {
           "geolocation.$.country" : "USA"
       } 
    }, 
   false,
   true
);
KARTHIKEYAN.A
fuente