Cómo actualizar múltiples elementos de matriz en mongodb

183

Tengo un documento Mongo que contiene una gran variedad de elementos.

Me gustaría restablecer el .handledatributo de todos los objetos en la matriz donde .profile= XX.

El documento tiene la siguiente forma:

{
    "_id": ObjectId("4d2d8deff4e6c1d71fc29a07"),
    "user_id": "714638ba-2e08-2168-2b99-00002f3d43c0",
    "events": [{
            "handled": 1,
            "profile": 10,
            "data": "....."
        } {
            "handled": 1,
            "profile": 10,
            "data": "....."
        } {
            "handled": 1,
            "profile": 20,
            "data": "....."
        }
        ...
    ]
}

Entonces, probé lo siguiente:

.update({"events.profile":10},{$set:{"events.$.handled":0}},false,true)

Sin embargo, solo actualiza el primer elemento de matriz coincidente en cada documento. (Ese es el comportamiento definido para $: el operador posicional ).

¿Cómo puedo actualizar todos los elementos de matriz coincidentes?

LiorH
fuente
2
La actualización de un subconjunto o todos los elementos de la matriz se ha agregado a mongodb 3.6: docs.mongodb.com/manual/reference/operator/update/…
Jaap
asegúrese de revisar arrayFilters y considere qué consulta usar para que la actualización sea eficiente. Mira la respuesta de Neil Lunn: stackoverflow.com/a/46054172/337401
Jaap
compruebe mi respuesta
Ucdemir

Respuestas:

111

En este momento no es posible utilizar el operador posicional para actualizar todos los elementos de una matriz. Ver JIRA http://jira.mongodb.org/browse/SERVER-1243

Como solución, puede:

  • Actualice cada elemento individualmente (eventos.0. Eventos controlados.1. Manejados ...) o ...
  • Lea el documento, realice las ediciones manualmente y guárdelo reemplazando el anterior (marque "Actualizar si es actual" si desea garantizar actualizaciones atómicas)
Javier Ferrero
fuente
15
si tiene un problema similar, vote por este tema: jira.mongodb.org/browse/SERVER-1243
LiorH
De hecho, me gusta el documento leído y el enfoque de guardar. Pero utilicé Couch antes que Mongo, por lo que ese enfoque parece más natural, ya que no hay una API de consulta para Couch, solo una API REST para documentos completos
Adam
1
Ambos enfoques requieren una cantidad bastante alta de memoria, ¿verdad? Si hay muchos documentos que deben buscarse y deben cargarse todos (o las matrices anidadas) para actualizar ... + también es un poco problemático de implementar si esto tiene que hacerse de forma asíncrona ...
Ixx
13
Dejando a un lado todas las dificultades técnicas, es bastante sorprendente que esta característica no esté disponible en MongoDB. Esta restricción le quita mucha libertad de personalizar el esquema de la base de datos.
Sung Cho
55
Neil Lunn stackoverflow.com/a/46054172/337401 respondió esto para la versión 3.6. Como esta es una pregunta popular, podría valer la pena actualizar esta respuesta aceptada con una referencia a la respuesta de Neil Lunn.
Jaap
74

Con el lanzamiento de MongoDB 3.6 (y disponible en la rama de desarrollo de MongoDB 3.5.12) ahora puede actualizar múltiples elementos de matriz en una sola solicitud.

Esto utiliza la sintaxis de operador de actualización posicional filtrada$[<identifier>] introducida en esta versión:

db.collection.update(
  { "events.profile":10 },
  { "$set": { "events.$[elem].handled": 0 } },
  { "arrayFilters": [{ "elem.profile": 10 }], "multi": true }
)

El "arrayFilters"que pasó a las opciones para .update()o incluso .updateOne(), .updateMany(), .findOneAndUpdate()o .bulkWrite()método especifica las condiciones que coinciden con el identificador dado en la instrucción de actualización. Cualquier elemento que coincida con la condición dada será actualizado.

Observando que la información "multi"dada en el contexto de la pregunta se usó con la expectativa de que esto "actualizaría múltiples elementos", pero este no fue y sigue siendo el caso. Su uso aquí se aplica a "documentos múltiples" como siempre ha sido el caso o ahora se ha especificado como la configuración obligatoria .updateMany()en las versiones modernas de API.

NOTA: Irónicamente, dado que esto se especifica en el argumento de "opciones" para .update()métodos similares y similares, la sintaxis generalmente es compatible con todas las versiones recientes del controlador de lanzamiento.

Sin embargo, esto no es cierto para el mongoshell, ya que la forma en que el método se implementa allí ("irónicamente para la compatibilidad con versiones anteriores") el arrayFiltersargumento no es reconocido y eliminado por un método interno que analiza las opciones para ofrecer "compatibilidad con versiones anteriores" Versiones del servidor MongoDB y una .update()sintaxis de llamada API "heredada" .

Por lo tanto, si desea utilizar el comando en el mongoshell u otros productos "basados ​​en el shell" (especialmente Robo 3T), necesita una versión más reciente de la rama de desarrollo o de la versión de producción a partir de 3.6 o superior.

Vea también positional all $[]qué también actualiza "elementos de matriz múltiple" pero sin aplicar a condiciones específicas y se aplica a todos los elementos en la matriz donde esa es la acción deseada.

Consulte también Actualización de una matriz anidada con MongoDB para ver cómo estos nuevos operadores posicionales se aplican a estructuras de matriz "anidadas", donde las "matrices están dentro de otras matrices".

IMPORTANTE : las instalaciones actualizadas de versiones anteriores "pueden" no haber habilitado las funciones de MongoDB, lo que también puede provocar que las declaraciones fallen. Debe asegurarse de que su procedimiento de actualización esté completo con detalles como actualizaciones de índice y luego ejecutar

   db.adminCommand( { setFeatureCompatibilityVersion: "3.6" } )

O una versión superior, según corresponda a su versión instalada. es decir, "4.0"para la versión 4 y posteriores en la actualidad. Esto habilitó características tales como los nuevos operadores de actualización posicional y otros. También puedes consultar con:

   db.adminCommand( { getParameter: 1, featureCompatibilityVersion: 1 } )

Para volver a la configuración actual

Neil Lunn
fuente
10
La respuesta aceptada debe actualizarse y consultar esta respuesta.
Jaap
3
¿Qué es elem?
user1063287
1
Esto es correcto. Tenga en cuenta que RoboMongo aún no es compatible arrayFilters, por lo que ejecuta la actualización a través de CLI. stackoverflow.com/questions/48322834/…
drlff
gracias, Neil, especialmente por la sección IMPORTANTE, exactamente lo que necesitaba
Janfabian
este código devuelve ERROR en pymongo. hay error: elevar TypeError ("% s debe ser verdadero o falso"% (opción,)) TypeError: upsert debe ser verdadero o falso
Vagif
67

Lo que funcionó para mí fue esto:

db.collection.find({ _id: ObjectId('4d2d8deff4e6c1d71fc29a07') })
  .forEach(function (doc) {
    doc.events.forEach(function (event) {
      if (event.profile === 10) {
        event.handled=0;
      }
    });
    db.collection.save(doc);
  });

Creo que es más claro para los novatos de Mongo y para cualquiera que esté familiarizado con JQuery y sus amigos.

Daniel Cerecedo
fuente
Estoy usando db.posts.find({ 'permalink':permalink }).forEach( function(doc) {...y Oops.. TypeError: Object # has no method 'forEach'
obtengo
3
@Squirrl podría estar desactualizado versión mongodb? El documento es claro sobre cómo solicitar cada función en un cursor, pero no indica desde qué versión es compatible. docs.mongodb.org/manual/reference/method/cursor.forEach
Daniel Cerecedo
@Squirrl trydb.posts.find(...).toArray().forEach(...)
marmor
¿No podemos hacer esto sin usar Javascript? Quiero realizar esta actualización directamente desde un shell mongo sin usar la API de Javascript.
Meliodas
1
¿Puede escribir esta consulta en el controlador mongodb para java o con spring-data-mongodb? Gracias, kris
chiku
18

Esto también se puede lograr con un ciclo while que verifica si quedan documentos que aún tengan subdocumentos que no se hayan actualizado. Este método conserva la atomicidad de sus actualizaciones (que muchas de las otras soluciones aquí no tienen).

var query = {
    events: {
        $elemMatch: {
            profile: 10,
            handled: { $ne: 0 }
        }
    }
};

while (db.yourCollection.find(query).count() > 0) {
    db.yourCollection.update(
        query,
        { $set: { "events.$.handled": 0 } },
        { multi: true }
    );
}

El número de veces que se ejecuta el bucle será igual al número máximo de veces que se produzcan subdocumentos con profile10 y handledno 0 en ninguno de los documentos de su colección. Entonces, si tiene 100 documentos en su colección y uno de ellos tiene tres subdocumentos que coinciden queryy todos los demás documentos tienen menos subdocumentos coincidentes, el bucle se ejecutará tres veces.

Este método evita el peligro de bloquear otros datos que pueden ser actualizados por otro proceso mientras se ejecuta este script. También minimiza la cantidad de datos que se transfieren entre el cliente y el servidor.

Sean
fuente
13

De hecho, esto se relaciona con el problema de larga data en http://jira.mongodb.org/browse/SERVER-1243, donde de hecho hay una serie de desafíos para una sintaxis clara que admita "todos los casos" donde las coincidencias de matrices múltiples encontró. De hecho, ya existen métodos que "ayudan" en la solución de este problema, como las operaciones masivas que se han implementado después de esta publicación original.

Todavía no es posible actualizar más de un único elemento de matriz coincidente en una sola declaración de actualización, por lo que incluso con una actualización "múltiple", todo lo que podrá actualizar es un solo elemento coincidente en la matriz para cada documento en ese único declaración.

La mejor solución posible en este momento es encontrar y hacer un bucle de todos los documentos coincidentes y procesar actualizaciones masivas que permitirán al menos enviar muchas operaciones en una sola solicitud con una respuesta singular. Opcionalmente, puede usar .aggregate()para reducir el contenido de la matriz devuelto en el resultado de la búsqueda a aquellos que coinciden con las condiciones para la selección de actualización:

db.collection.aggregate([
    { "$match": { "events.handled": 1 } },
    { "$project": {
        "events": {
            "$setDifference": [
               { "$map": {
                   "input": "$events",
                   "as": "event",
                   "in": {
                       "$cond": [
                           { "$eq": [ "$$event.handled", 1 ] },
                           "$$el",
                           false
                       ]
                   }
               }},
               [false]
            ]
        }
    }}
]).forEach(function(doc) {
    doc.events.forEach(function(event) {
        bulk.find({ "_id": doc._id, "events.handled": 1  }).updateOne({
            "$set": { "events.$.handled": 0 }
        });
        count++;

        if ( count % 1000 == 0 ) {
            bulk.execute();
            bulk = db.collection.initializeOrderedBulkOp();
        }
    });
});

if ( count % 1000 != 0 )
    bulk.execute();

La .aggregate()porción allí funcionará cuando haya un identificador "único" para la matriz o todo el contenido para cada elemento forme un elemento "único". Esto se debe a que el operador "set" se $setDifferenceutiliza para filtrar los falsevalores devueltos por la $mapoperación utilizada para procesar la matriz en busca de coincidencias.

Si su contenido de matriz no tiene elementos únicos, puede probar un enfoque alternativo con $redact:

db.collection.aggregate([
    { "$match": { "events.handled": 1 } },
    { "$redact": {
        "$cond": {
            "if": {
                "$eq": [ { "$ifNull": [ "$handled", 1 ] }, 1 ]
            },
            "then": "$$DESCEND",
            "else": "$$PRUNE"
        }
    }}
])

Donde está la limitación es que si "manejado" era de hecho un campo destinado a estar presente en otros niveles de documentos, entonces es probable que obtenga resultados inesperados, pero está bien donde ese campo aparece solo en una posición del documento y es una coincidencia de igualdad.

Las versiones futuras (posteriores a MongoDB 3.1) a partir de la escritura tendrán una $filteroperación más simple:

db.collection.aggregate([
    { "$match": { "events.handled": 1 } },
    { "$project": {
        "events": {
            "$filter": {
                "input": "$events",
                "as": "event",
                "cond": { "$eq": [ "$$event.handled", 1 ] }
            }
        }
    }}
])

Y todas las versiones que admiten .aggregate()pueden usar el siguiente enfoque $unwind, pero el uso de ese operador lo convierte en el enfoque menos eficiente debido a la expansión de la matriz en la tubería:

db.collection.aggregate([
    { "$match": { "events.handled": 1 } },
    { "$unwind": "$events" },
    { "$match": { "events.handled": 1 } },
    { "$group": {
        "_id": "$_id",
        "events": { "$push": "$events" }
    }}        
])

En todos los casos en que la versión de MongoDB admite un "cursor" de la salida agregada, entonces esto es solo una cuestión de elegir un enfoque e iterar los resultados con el mismo bloque de código que se muestra para procesar las declaraciones de actualización masiva. Las operaciones masivas y los "cursores" de la salida agregada se introducen en la misma versión (MongoDB 2.6) y, por lo tanto, generalmente funcionan de la mano para el procesamiento.

Incluso en versiones anteriores, entonces probablemente sea mejor usar solo .find()para devolver el cursor y filtrar la ejecución de las declaraciones solo la cantidad de veces que el elemento de matriz coincide para las .update()iteraciones:

db.collection.find({ "events.handled": 1 }).forEach(function(doc){ 
    doc.events.filter(function(event){ return event.handled == 1 }).forEach(function(event){
        db.collection.update({ "_id": doc._id },{ "$set": { "events.$.handled": 0 }});
    });
});

Si está decidido a realizar actualizaciones "múltiples" o considera que en última instancia es más eficiente que procesar múltiples actualizaciones para cada documento coincidente, entonces siempre puede determinar el número máximo de posibles coincidencias de matriz y simplemente ejecutar una actualización "múltiple". veces, hasta que básicamente no hay más documentos para actualizar.

Un enfoque válido para las versiones MongoDB 2.4 y 2.2 también podría usarse .aggregate()para encontrar este valor:

var result = db.collection.aggregate([
    { "$match": { "events.handled": 1 } },
    { "$unwind": "$events" },
    { "$match": { "events.handled": 1 } },
    { "$group": {
        "_id": "$_id",
        "count": { "$sum": 1 }
    }},
    { "$group": {
        "_id": null,
        "count": { "$max": "$count" }
    }}
]);

var max = result.result[0].count;

while ( max-- ) {
    db.collection.update({ "events.handled": 1},{ "$set": { "events.$.handled": 0 }},{ "multi": true })
}

Cualquiera sea el caso, hay ciertas cosas que no desea hacer dentro de la actualización:

  1. No actualice "una sola vez" la matriz: donde si cree que podría ser más eficiente actualizar todo el contenido de la matriz en código y luego solo $setla matriz completa en cada documento. Esto puede parecer más rápido de procesar, pero no hay garantía de que el contenido de la matriz no haya cambiado desde que se leyó y se realizó la actualización. Aunque $settodavía es un operador atómico, solo actualizará la matriz con lo que "cree" que son los datos correctos y, por lo tanto, es probable que sobrescriba cualquier cambio que ocurra entre lectura y escritura.

  2. No calcule los valores de índice para actualizar: cuando sea similar al enfoque de "un disparo", simplemente calcule que la posición 0y la posición 2(y así sucesivamente) son los elementos para actualizar y codificar esto con una declaración eventual como:

    { "$set": {
        "events.0.handled": 0,
        "events.2.handled": 0
    }}

    Nuevamente, el problema aquí es la "presunción" de que los valores de índice encontrados cuando se leyó el documento son los mismos valores de índice en la matriz al momento de la actualización. Si se agregan nuevos elementos a la matriz de una manera que cambia el orden, esas posiciones ya no son válidas y, de hecho, los elementos incorrectos se actualizan.

Entonces, hasta que haya una sintaxis razonable determinada para permitir que se procesen múltiples elementos de matriz coincidentes en una sola declaración de actualización, entonces el enfoque básico es actualizar cada elemento de matriz coincidente en una declaración individual (idealmente en Bulk) o esencialmente resolver los elementos de matriz máximos para actualizar o seguir actualizando hasta que no se devuelvan más resultados modificados. En cualquier caso, debería "siempre" estar procesando actualizaciones posicionales$ en el elemento de matriz coincidente, incluso si eso solo está actualizando un elemento por declaración.

Las operaciones masivas son, de hecho, la solución "generalizada" para procesar cualquier operación que resulte ser "operaciones múltiples", y dado que hay más aplicaciones para esto que la simple actualización de elementos de matriz múltiple con el mismo valor, por supuesto, se ha implementado ya, y actualmente es el mejor enfoque para resolver este problema.

Blakes Seven
fuente
8

Me sorprende que esto todavía no se haya abordado en mongo. En general, mongo no parece ser genial cuando se trata de sub-matrices. No puede contar sub-matrices simplemente por ejemplo.

Usé la primera solución de Javier. Lea la matriz en eventos, luego repita y cree el conjunto exp:

var set = {}, i, l;
for(i=0,l=events.length;i<l;i++) {
  if(events[i].profile == 10) {
    set['events.' + i + '.handled'] = 0;
  }
}

.update(objId, {$set:set});

Esto puede resumirse en una función utilizando una devolución de llamada para la prueba condicional

lukenofurther
fuente
¡Gracias por esto! ¡No puedo creer que esta característica aún no sea compatible de forma nativa! Lo usé para incrementar cada elemento de una submatriz, para que otros lean ... para actualizar cada elemento, simplemente elimine la instrucción if.
Zaheer
9
Esta no es una solución segura. Si se agrega un registro mientras ejecuta la actualización, dañará sus datos.
Merc
4

He estado buscando una solución para esto usando el controlador más nuevo para C # 3.6 y aquí está la solución que finalmente decidí. La clave aquí es usar "$ []", que según MongoDB es nuevo a partir de la versión 3.6. Ver https://docs.mongodb.com/manual/reference/operator/update/positional-all/#up. S [] para más información.

Aquí está el código:

{
   var filter = Builders<Scene>.Filter.Where(i => i.ID != null);
   var update = Builders<Scene>.Update.Unset("area.$[].discoveredBy");
   var result = collection.UpdateMany(filter, update, new UpdateOptions { IsUpsert = true});
}

Para obtener más contexto, vea mi publicación original aquí: elimine el elemento de matriz de TODOS los documentos usando el controlador MongoDB C #

C0d3 0n3
fuente
4

El hilo es muy antiguo, pero vine buscando una respuesta aquí, por lo tanto, proporcioné una nueva solución.

Con MongoDB versión 3.6+, ahora es posible usar el operador posicional para actualizar todos los elementos en una matriz. Ver documentación oficial aquí .

La siguiente consulta funcionaría para la pregunta que se hace aquí. También he verificado con el controlador Java-MongoDB y funciona correctamente.

.update(   // or updateMany directly, removing the flag for 'multi'
   {"events.profile":10},
   {$set:{"events.$[].handled":0}},  // notice the empty brackets after '$' opearor
   false,
   true
)

Espero que esto ayude a alguien como yo.

ersnh
fuente
1

Intenté lo siguiente y está funcionando bien.

.update({'events.profile': 10}, { '$set': {'events.$.handled': 0 }},{ safe: true, multi:true }, callback function);

// función de devolución de llamada en caso de nodejs

Pranay Saha
fuente
Este código solo actualiza el primer elemento coincidente en la matriz. Respuesta incorrecta.
Hamlett
Funciona para v2.6. ¿Puede estar en una versión anterior? Aquí está su doc. Docs.mongodb.com/manual/reference/method/db.collection.update/…
Jialin Zou
1

Puede actualizar todos los elementos en MongoDB

db.collectioname.updateOne(
{ "key": /vikas/i },
{ $set: { 
 "arr.$[].status" : "completed"
} }
)

Actualizará todo el valor de "estado" a "completado" en la matriz "arr"

Si solo un documento

db.collectioname.updateOne(
 { key:"someunique", "arr.key": "myuniq" },
 { $set: { 
   "arr.$.status" : "completed", 
   "arr.$.msgs":  {
                "result" : ""
        }
   
 } }
)

Pero si no es uno y tampoco desea que se actualicen todos los documentos de la matriz, debe recorrer el elemento y dentro del bloque if

db.collectioname.find({findCriteria })
  .forEach(function (doc) {
    doc.arr.forEach(function (singlearr) {
      if (singlearr check) {
        singlearr.handled =0
      }
    });
    db.collection.save(doc);
  });
VIKAS KOHLI
fuente
0

En realidad, el comando guardar es solo en la instancia de la clase Document. Que tienen muchos métodos y atributos. Entonces puede usar la función lean () para reducir la carga de trabajo. Consulte aquí https://hashnode.com/post/why-are-mongoose-mongodb-odm-lean-queries-faster-than-normal-queries-cillvawhq0062kj53asxoyn7j

Otro problema con la función de guardar, que hará que los datos de conflicto se guarden con múltiples guardados al mismo tiempo. Model.Update hará que los datos sean consistentes. Entonces, para actualizar varios elementos en una matriz de documentos. Usa tu lenguaje de programación familiar y prueba algo como esto, uso mangosta en eso:

User.findOne({'_id': '4d2d8deff4e6c1d71fc29a07'}).lean().exec()
  .then(usr =>{
    if(!usr)  return
    usr.events.forEach( e => {
      if(e && e.profile==10 ) e.handled = 0
    })
    User.findOneAndUpdate(
      {'_id': '4d2d8deff4e6c1d71fc29a07'},
      {$set: {events: usr.events}},
      {new: true}
    ).lean().exec().then(updatedUsr => console.log(updatedUsr))
})
usuario3176403
fuente
0

El operador $ [] selecciona toda la matriz anidada. Puede actualizar todos los elementos de la matriz con '$ []'

.update({"events.profile":10},{$set:{"events.$[].handled":0}},false,true)

Referencia

Ucdemir
fuente
¿Puede explicar por qué incluye el "falso, verdadero" al final aquí? No puedo encontrarlo en la documentación.
Garson
La respuesta incorrecta al operador posicional completo $[] solo actualiza todos los campos en la matriz especificada. Lo que funciona es el operador posicional filtrado $[identifier]que los operadores en campos de matriz que coinciden con las condiciones especificadas. Debe usarse con arrayFilters referencia: docs.mongodb.com/manual/release-notes/3.6/#arrayfilters y docs.mongodb.com/manual/reference/operator/update/…
Lawrence Eagles
0

Tenga en cuenta que algunas respuestas en este hilo sugieren que el uso $ [] es INCORRECTO.

db.collection.update(
   {"events.profile":10},
   {$set:{"events.$[].handled":0}},
   {multi:true}
)

El código anterior actualizará "manejado" a 0 para todos los elementos en la matriz de "eventos", independientemente de su valor de "perfil". La consulta {"events.profile":10}es solo para filtrar todo el documento, no los documentos en la matriz. En esta situación, es una necesidad para utilizar $[elem]con arrayFiltersespecificar la condición de elementos de matriz por lo que la respuesta de Neil Lunn es correcta.

Wenda Hu
fuente
0

Actualice el campo de matriz en varios documentos en mongo db.

Use $ pull o $ push con la actualización de muchas consultas para actualizar los elementos de la matriz en mongoDb.

Notification.updateMany(
    { "_id": { $in: req.body.notificationIds } },
    {
        $pull: { "receiversId": req.body.userId }
    }, function (err) {
        if (err) {
            res.status(500).json({ "msg": err });
        } else {
            res.status(200).json({
                "msg": "Notification Deleted Successfully."
            });
        }
    });
Irfan
fuente
0

Primero: su código no funcionó porque estaba usando el operador posicional $ que solo identifica un elemento para actualizar en una matriz pero ni siquiera especifica explícitamente su posición en la matriz.

Lo que necesita es el operador posicional filtrado $[<identifier>] . Actualizaría todos los elementos que coincidan con una condición de filtro de matriz.

Solución:

db.collection.update({"events.profile":10}, { $set: { "events.$[elem].handled" : 0 } },
   {
     multi: true,
     arrayFilters: [ { "elem.profile": 10 } ]
})

Visita mongodb doc aquí

Lo que hace el código:

  1. {"events.profile":10} filtra su colección y devuelve los documentos que coinciden con el filtro

  2. El $setoperador de actualización: modifica los campos coincidentes de los documentos sobre los que actúa.

  3. {multi:true}Hace que .update()modifica todos los documentos que coincidan con el filtro por lo tanto, comportándose comoupdateMany()

  4. { "events.$[elem].handled" : 0 } and arrayFilters: [ { "elem.profile": 10 } ] Esta técnica implica el uso de la matriz posicional filtrada con arrayFilters. la matriz posicional filtrada aquí $[elem]actúa como un marcador de posición para todos los elementos en los campos de la matriz que coinciden con las condiciones especificadas en el filtro de matriz.

Filtros de matriz

Lawrence Eagles
fuente