sql >> Base de Datos >  >> NoSQL >> MongoDB

Matriz de actualización masiva de subdocumento coincidente en Mongodb

En la respuesta más corta, es tanto "sí" como "no".

De hecho, hay una manera de hacer coincidir los elementos de matriz individuales y actualizarlos con valores separados en una sola declaración, ya que de hecho puede proporcionar "múltiples" arrayFilters condiciones y use esos identificadores en su declaración de actualización.

El problema con su muestra particular aquí es que una de las entradas en su "conjunto de cambios" (la última) en realidad no coincide con ningún miembro de la matriz que esté presente actualmente. La acción "presunta" aquí sería $push ese nuevo miembro no coincidente en la matriz donde no se encontró. Sin embargo, esa acción particular no puede hacerse en una "operación única" , pero puede usar bulkWrite() emitir declaraciones "múltiples" para cubrir ese caso.

Coincidencia de diferentes condiciones de matriz

Explicando que en puntos, considere los primeros dos elementos en su "conjunto de cambios". Puede solicitar un "único" declaración de actualización con múltiples arrayFilters así:

db.avail_rates_copy.updateOne(
  { "_id": 12345 },
  { 
    "$set": {
      "rates.$[one]": {
        "productId" : NumberInt(1234), 
        "rate" : 400.0, 
        "rateCardId": NumberInt(1),
        "month" : NumberInt(201801)
      },
      "rates.$[two]": {
        "productId" : NumberInt(1234), 
        "rate" : 500.0, 
        "rateCardId": NumberInt(1),
        "month" : NumberInt(201802)
      } 
    }
  },
  { 
    "arrayFilters": [
      {
        "one.productId": NumberInt(1234),
        "one.rateCardId": NumberInt(1),
        "one.month": NumberInt(201801)
      },
      {
        "two.productId": NumberInt(1234),
        "two.rateCardId": NumberInt(1),
        "two.month": NumberInt(201802)
      }
    ]
  }
)

Si ejecutó eso, verá que el documento modificado se convierte en:

{
        "_id" : 12345,
        "_class" : "com.example.ProductRates",
        "rates" : [
                {                             // Matched and changed this by one
                        "productId" : 1234,
                        "rate" : 400,
                        "rateCardId" : 1,
                        "month" : 201801
                },
                {                            // And this as two
                        "productId" : 1234,
                        "rate" : 500,
                        "rateCardId" : 1,
                        "month" : 201802
                },
                {
                        "productId" : 1234,
                        "rate" : 400,
                        "rateCardId" : 2,
                        "month" : 201803
                },
                {
                        "productId" : 1235,
                        "rate" : 500,
                        "rateCardId" : 1,
                        "month" : 201801
                },
                {
                        "productId" : 1235,
                        "rate" : 234,
                        "rateCardId" : 2,
                        "month" : 201803
                }
        ]
}

Tenga en cuenta aquí que especifica cada "identificador" dentro de la lista de arrayFilters con múltiples condiciones para hacer coincidir el elemento así:

  {
    "one.productId": NumberInt(1234),
    "one.rateCardId": NumberInt(1),
    "one.month": NumberInt(201801)
  },

Entonces, cada "condición" se mapea efectivamente como:

  <identifier>.<property>

Entonces sabe que está mirando las "rates" matriz por la declaración en el bloque de actualización por $[<indentifier>] :

 "rates.$[one]"

Y mira cada elemento de "rates" para que coincida con las condiciones. Entonces el "one" el identificador coincidiría con las condiciones prefijadas con "one" y del mismo modo para el otro conjunto de condiciones con el prefijo "two" , por lo tanto, la declaración de actualización real se aplica solo a aquellos que coinciden con las condiciones asignadas al identificador.

Si solo quisieras las "rates" propiedad en lugar de todo el objeto, entonces simplemente anote como:

{ "$set": { "rates.$[one].rate": 400, "rates.$[two].rate": 500 } }

Agregar objetos no coincidentes

Entonces, la primera parte es relativamente simple de comprender, pero como se indicó, hacer un $push para el "elemento que no está allí" es un asunto diferente, ya que básicamente necesitamos una condición de consulta en el nivel de "documento" para determinar que un elemento de matriz "falta".

Lo que esto significa esencialmente es que debe emitir una actualización con el $push buscando cada elemento de la matriz para ver si existe o no. Cuando no está presente, el documento es una coincidencia y el $push se realiza.

Aquí es donde bulkWrite() entra en juego, y lo usa agregando una actualización adicional a nuestra primera operación anterior para cada elemento en el "conjunto de cambios":

db.avail_rates_copy.bulkWrite(
  [
    { "updateOne": {
      "filter": { "_id": 12345 },
      "update": {
        "$set": {
          "rates.$[one]": {
            "productId" : NumberInt(1234), 
            "rate" : 400.0, 
            "rateCardId": NumberInt(1),
            "month" : NumberInt(201801)
          },
          "rates.$[two]": {
            "productId" : NumberInt(1234), 
            "rate" : 500.0, 
            "rateCardId": NumberInt(1),
            "month" : NumberInt(201802)
          },
          "rates.$[three]": {
            "productId" : NumberInt(1235), 
            "rate" : 700.0, 
            "rateCardId": NumberInt(1),
            "month" : NumberInt(201802)
          }
        }
      },
      "arrayFilters": [
        {
          "one.productId": NumberInt(1234),
          "one.rateCardId": NumberInt(1),
          "one.month": NumberInt(201801)
        },
        {
          "two.productId": NumberInt(1234),
          "two.rateCardId": NumberInt(1),
          "two.month": NumberInt(201802)
        },
        {
          "three.productId": NumberInt(1235),
          "three.rateCardId": NumberInt(1),
          "three.month": NumberInt(201802)
        }
      ]    
    }},
    { "updateOne": {
      "filter": {
        "_id": 12345,
        "rates": {
          "$not": {
            "$elemMatch": {
              "productId" : NumberInt(1234), 
              "rateCardId": NumberInt(1),
              "month" : NumberInt(201801)
            }
          }
        }
      },
      "update": {
        "$push": {
          "rates": {
            "productId" : NumberInt(1234), 
            "rate" : 400.0, 
            "rateCardId": NumberInt(1),
            "month" : NumberInt(201801)
          }
        }
      }
    }},
    { "updateOne": {
      "filter": {
        "_id": 12345,
        "rates": {
          "$not": {
            "$elemMatch": {
              "productId" : NumberInt(1234), 
              "rateCardId": NumberInt(1),
              "month" : NumberInt(201802)
            }
          }
        }
      },
      "update": {
        "$push": {
          "rates": {
            "productId" : NumberInt(1234), 
            "rate" : 500.0, 
            "rateCardId": NumberInt(1),
            "month" : NumberInt(201802)
          }
        }
      }
    }},
    { "updateOne": {
      "filter": {
        "_id": 12345,
        "rates": {
          "$not": {
            "$elemMatch": {
              "productId" : NumberInt(1235),
              "rateCardId": NumberInt(1),
              "month" : NumberInt(201802)
            }
          }
        }
      },
      "update": {
        "$push": {
          "rates": {
            "productId" : NumberInt(1235),
            "rate" : 700.0, 
            "rateCardId": NumberInt(1),
            "month" : NumberInt(201802)
          }
        }
      }
    }}
  ],
  { "ordered": true }
)

Tenga en cuenta el $elemMatch dentro del filtro de consulta, ya que este es un requisito para hacer coincidir un elemento de matriz por "condiciones múltiples". No necesitábamos eso en arrayFilters entradas porque solo mire cada elemento de la matriz al que ya se aplicaron, pero como una "consulta", las condiciones requieren $elemMatch ya que la simple "notación de puntos" devolvería coincidencias incorrectas.

Consulte también el $not El operador se usa aquí para "negar" el $elemMatch , ya que nuestras verdaderas condiciones son solo hacer coincidir un documento que "no tiene un elemento de matriz coincidente" a las condiciones proporcionadas, y eso es lo que justifica la selección para agregar un nuevo elemento.

Y esa declaración única emitida al servidor esencialmente intenta cuatro operaciones de actualización como una para intentar actualizar elementos de matriz coincidentes y otra para cada uno de los tres "cambiar conjuntos" intentando $push donde se encontró que el documento no coincidía con las condiciones del elemento de matriz en el "conjunto de cambios".

Por lo tanto, el resultado es el esperado:

{
        "_id" : 12345,
        "_class" : "com.example.ProductRates",
        "rates" : [
                {                               // matched and updated
                        "productId" : 1234,
                        "rate" : 400,
                        "rateCardId" : 1,
                        "month" : 201801
                },
                {                               // matched and updated
                        "productId" : 1234,
                        "rate" : 500,
                        "rateCardId" : 1,
                        "month" : 201802
                },
                {
                        "productId" : 1234,
                        "rate" : 400,
                        "rateCardId" : 2,
                        "month" : 201803
                },
                {
                        "productId" : 1235,
                        "rate" : 500,
                        "rateCardId" : 1,
                        "month" : 201801
                },
                {
                        "productId" : 1235,
                        "rate" : 234,
                        "rateCardId" : 2,
                        "month" : 201803
                },
                {                              // This was appended
                        "productId" : 1235,
                        "rate" : 700,
                        "rateCardId" : 1,
                        "month" : 201802
                }
        ]
}

Dependiendo de cuántos elementos realmente no coincidieron con bulkWrite() La respuesta informará sobre cuántas de esas declaraciones realmente coincidieron y afectaron un documento. En este caso es 2 emparejado y modificado, ya que la "primera" operación de actualización coincide con las entradas de matriz existentes, y la "última" actualización de cambio coincide con que el documento no contiene la entrada de matriz y realiza el $push para modificar.

Conclusión

Ahí tienes el enfoque combinado, donde:

  • La primera parte de "actualizar" su pregunta es muy fácil y se puede hacer en una sola declaración , como se demuestra en la primera sección.

  • La segunda parte donde hay un elemento de matriz que "no existe actualmente" dentro de la matriz de documentos actual, esto realmente requiere que use bulkWrite() para emitir operaciones "múltiples" en una sola solicitud.

Por lo tanto actualizar , es "SÍ" a una sola operación. Pero agregando diferencia significa múltiples operaciones. Pero puede combinar los dos enfoques tal como se demuestra aquí.

Hay muchas formas "extravagantes" en las que puede construir estas declaraciones basadas en el contenido de la matriz "conjunto de cambios" con código, por lo que no necesita "codificar" cada miembro.

Como un caso básico para JavaScript y compatible con la versión actual de mongo shell (que, de manera un tanto molesta, no admite operadores de propagación de objetos):

db.getCollection('avail_rates_copy').drop();
db.getCollection('avail_rates_copy').insert(
  {
    "_id" : 12345,
    "_class" : "com.example.ProductRates",
    "rates" : [
      {
        "productId" : 1234,
        "rate" : 100,
        "rateCardId" : 1,
        "month" : 201801
      },
      {
        "productId" : 1234,
        "rate" : 200,
        "rateCardId" : 1,
        "month" : 201802
      },
      {
        "productId" : 1234,
        "rate" : 400,
        "rateCardId" : 2,
        "month" : 201803
      },
      {
        "productId" : 1235,
        "rate" : 500,
        "rateCardId" : 1,
        "month" : 201801
      },
      {
        "productId" : 1235,
        "rate" : 234,
        "rateCardId" : 2,
        "month" : 201803
      }
    ]
  }
);

var changeSet = [
  {
      "productId" : 1234, 
      "rate" : 400.0, 
      "rateCardId": 1,
      "month" : 201801
  }, 
  {
      "productId" : 1234, 
      "rate" : 500.0, 
      "rateCardId": 1,
      "month" : 201802
  }, 
  {

      "productId" : 1235, 
      "rate" : 700.0, 
      "rateCardId": 1,
      "month" : 201802
  }
];

var arrayFilters = changeSet.map((obj,i) => 
  Object.keys(obj).filter(k => k != 'rate' )
    .reduce((o,k) => Object.assign(o, { [`u${i}.${k}`]: obj[k] }) ,{})
);

var $set = changeSet.reduce((o,r,i) =>
  Object.assign(o, { [`rates.$[u${i}].rate`]: r.rate }), {});

var updates = [
  { "updateOne": {
    "filter": { "_id": 12345 },
    "update": { $set },
    arrayFilters
  }},
  ...changeSet.map(obj => (
    { "updateOne": {
      "filter": {
        "_id": 12345,
        "rates": {
          "$not": {
            "$elemMatch": Object.keys(obj).filter(k => k != 'rate')
              .reduce((o,k) => Object.assign(o, { [k]: obj[k] }),{})
          }
        }
      },
      "update": {
        "$push": {
          "rates": obj
        }
      }
    }}
  ))
];

db.getCollection('avail_rates_copy').bulkWrite(updates,{ ordered: true });

Esto creará dinámicamente una lista de operaciones de actualización "en masa" que se vería así:

[
  {
    "updateOne": {
      "filter": {
        "_id": 12345
      },
      "update": {
        "$set": {
          "rates.$[u0].rate": 400,
          "rates.$[u1].rate": 500,
          "rates.$[u2].rate": 700
        }
      },
      "arrayFilters": [
        {
          "u0.productId": 1234,
          "u0.rateCardId": 1,
          "u0.month": 201801
        },
        {
          "u1.productId": 1234,
          "u1.rateCardId": 1,
          "u1.month": 201802
        },
        {
          "u2.productId": 1235,
          "u2.rateCardId": 1,
          "u2.month": 201802
        }
      ]
    }
  },
  {
    "updateOne": {
      "filter": {
        "_id": 12345,
        "rates": {
          "$not": {
            "$elemMatch": {
              "productId": 1234,
              "rateCardId": 1,
              "month": 201801
            }
          }
        }
      },
      "update": {
        "$push": {
          "rates": {
            "productId": 1234,
            "rate": 400,
            "rateCardId": 1,
            "month": 201801
          }
        }
      }
    }
  },
  {
    "updateOne": {
      "filter": {
        "_id": 12345,
        "rates": {
          "$not": {
            "$elemMatch": {
              "productId": 1234,
              "rateCardId": 1,
              "month": 201802
            }
          }
        }
      },
      "update": {
        "$push": {
          "rates": {
            "productId": 1234,
            "rate": 500,
            "rateCardId": 1,
            "month": 201802
          }
        }
      }
    }
  },
  {
    "updateOne": {
      "filter": {
        "_id": 12345,
        "rates": {
          "$not": {
            "$elemMatch": {
              "productId": 1235,
              "rateCardId": 1,
              "month": 201802
            }
          }
        }
      },
      "update": {
        "$push": {
          "rates": {
            "productId": 1235,
            "rate": 700,
            "rateCardId": 1,
            "month": 201802
          }
        }
      }
    }
  }
]

Tal como se describió en la "forma larga" de la respuesta general, pero, por supuesto, simplemente usa el contenido de la "matriz" de entrada para construir todas esas declaraciones.

Puede realizar dicha construcción de objetos dinámicos en cualquier idioma, y ​​todos los controladores de MongoDB aceptan la entrada de algún tipo de estructura que puede "manipular", que luego se transforma en BSON antes de que se envíe al servidor para su ejecución.