MongoDB
 sql >> Base de Dados >  >> NoSQL >> MongoDB

Matriz de atualização em massa do subdocumento correspondente no Mongodb


Na resposta mais curta, é "sim" e "não".

De fato, existe uma maneira de combinar elementos de matriz individuais e atualizá-los com valores separados em uma única instrução, pois você pode fornecer "vários" arrayFilters condições e use esses identificadores em sua declaração de atualização.

O problema com seu exemplo específico aqui é que uma das entradas em seu "conjunto de alterações" (o último) não corresponde a nenhum membro da matriz que esteja presente no momento. A ação "presumida" aqui seria $push esse novo membro não correspondido na matriz onde não foi encontrado. No entanto, essa ação específica não pode ser feito em uma "única operação" , mas você pode usar bulkWrite() emitir "múltiplas" declarações para cobrir esse caso.

Correspondência de diferentes condições de matriz


Explicando isso em pontos, considere os dois primeiros itens do seu "conjunto de mudanças". Você pode aplicar um "single" instrução de atualização com vários arrayFilters assim:
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)
      }
    ]
  }
)

Se você executar isso, verá que o documento modificado se torna:
{
        "_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
                }
        ]
}

Observe aqui que você especifica cada "identfier" na lista de arrayFilters com várias condições para corresponder ao elemento assim:
  {
    "one.productId": NumberInt(1234),
    "one.rateCardId": NumberInt(1),
    "one.month": NumberInt(201801)
  },

Portanto, cada "condição" mapeia efetivamente como:
  <identifier>.<property>

Então ele sabe que está olhando para as "taxas" array pela instrução no bloco de atualização pelo $[] :
 "rates.$[one]"

E analisa cada elemento de "rates" para corresponder às condições. Portanto, o "um" identificador corresponderia às condições prefixadas com "one" e da mesma forma para o outro conjunto de condições prefixadas com "two" , portanto, a instrução de atualização real se aplica apenas àqueles que correspondem às condições atribuídas ao identificador.

Se você só queria as "tarifas" propriedade em oposição ao objeto inteiro, então você apenas notará como:
{ "$set": { "rates.$[one].rate": 400, "rates.$[two].rate": 500 } }

Adicionando objetos sem correspondência


Portanto, a primeira parte é relativamente simples de compreender, mas, como afirmado, fazendo um $push para o "elemento que não está lá" é uma questão diferente, pois basicamente precisamos de uma condição de consulta no nível "documento" para determinar que um elemento do array está "faltando".

O que isso significa essencialmente é que você precisa emitir uma atualização com o $push procurando cada elemento do array para ver se ele existe ou não. Quando não estiver presente, o documento é uma correspondência e o $push é desempenhado.

É aqui que bulkWrite() entra em jogo, e você o usa adicionando uma atualização adicional à nossa primeira operação acima para cada elemento no "conjunto de alterações":
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 }
)

Observe o $elemMatch dentro do filtro de consulta, pois este é um requisito para corresponder a um elemento de matriz por "múltiplas condições". Não precisávamos disso no arrayFilters entradas porque elas somente observe cada item da matriz a que já se aplicam, mas como uma "consulta", as condições exigem $elemMatch como simples "notação de ponto" retornaria correspondências incorretas.

Veja também o $not operador é usado aqui para "negar" o $elemMatch , pois nossas verdadeiras condições são apenas corresponder a um documento que "não tenha um elemento de matriz correspondente" às condições fornecidas, e é isso que justifica a seleção para anexar um novo elemento.

E essa única declaração emitida para o servidor basicamente tenta quatro operações de atualização como uma para tentar atualizar os elementos de matriz correspondentes e outra para cada um dos três "alterar conjuntos" tentando $push onde o documento foi encontrado para não corresponder às condições para o elemento da matriz no "conjunto de alterações".

O resultado é, portanto, o 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
                }
        ]
}

Dependendo de quantos elementos realmente não correspondiam ao bulkWrite() A resposta informará quantas dessas declarações realmente corresponderam e afetaram um documento. Neste caso é 2 correspondido e modificado, uma vez que a "primeira" operação de atualização corresponde a entradas de matriz existentes e a "última" atualização de alteração corresponde que o documento não contém a entrada de matriz e executa o $push para modificar.

Conclusão


Então, você tem a abordagem combinada, onde:

  • A primeira parte da "atualização" em sua pergunta é muito fácil e pode ser feita em uma instrução única , como demonstrado na primeira seção.

  • A segunda parte onde há um elemento de array que "não existe atualmente" dentro da matriz de documentos atual, isso exige que você use bulkWrite() para emitir "múltiplas" operações em uma única solicitação.

Portanto atualize , é "SIM" para uma única operação. Mas adicionando diferença significa múltiplas operações. Mas você pode combinar as duas abordagens exatamente como é demonstrado aqui.

Existem muitas maneiras "fantasiosas" nas quais você pode construir essas instruções com base no conteúdo da matriz "conjunto de alterações" com código, para que você não precise "codificar" cada membro.

Como um caso básico para JavaScript e compatível com a versão atual do shell mongo (que um tanto irritantemente não suporta operadores de propagação 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 });

Isso construirá dinamicamente uma lista de operações de atualização "em massa" que se pareceriam com:
[
  {
    "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
          }
        }
      }
    }
  }
]

Assim como foi descrito na "forma longa" da resposta geral, mas é claro que simplesmente usa o conteúdo "array" de entrada para construir todas essas declarações.

Você pode fazer essa construção de objetos dinâmicos em qualquer linguagem, e todos os drivers do MongoDB aceitam a entrada de algum tipo de estrutura que você tem permissão para "manipular", que é então transformada em BSON antes de ser realmente enviada ao servidor para execução.