MongoDB
 sql >> Database >  >> NoSQL >> MongoDB

Matrice di aggiornamento in blocco del sottodocumento corrispondente in Mongodb

Nella risposta più breve, è sia "sì" che "no".

C'è davvero un modo per abbinare i singoli elementi dell'array e aggiornarli con valori separati in una singola istruzione, poiché puoi in effetti fornire arrayFilters "multipli" condizioni e utilizza tali identificatori nella tua dichiarazione di aggiornamento.

Il problema con il tuo esempio particolare qui è che una delle voci nel tuo "set di modifiche" (l'ultimo) non corrisponde effettivamente a nessun membro dell'array attualmente presente. L'azione "presunta" qui sarebbe $push quel nuovo membro non corrispondente nell'array in cui non è stato trovato. Tuttavia quella particolare azione non può essere eseguita in una "operazione singola" , ma puoi utilizzare bulkWrite() rilasciare dichiarazioni "multiple" per coprire quel caso.

Corrispondenza a condizioni di array differenti

Spiegando che in punti, considera i primi due elementi nel tuo "set di modifiche". Puoi applicare un "singolo" istruzione di aggiornamento con più arrayFilters così:

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 lo esegui, vedresti che il documento modificato diventa:

{
        "_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
                }
        ]
}

Nota qui che specifichi ogni "identificatore" nell'elenco di arrayFilters con più condizioni per abbinare l'elemento in questo modo:

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

Quindi ogni "condizione" viene effettivamente mappata come:

  <identifier>.<property>

Quindi sa di guardare le "rates" array dall'istruzione nel blocco di aggiornamento da $[<indentifier>] :

 "rates.$[one]"

E guarda ogni elemento di "rates" per soddisfare le condizioni. Quindi il "one" identificatore corrisponderebbe alle condizioni precedute da "one" e allo stesso modo per l'altro insieme di condizioni precedute da "two" , pertanto la dichiarazione di aggiornamento effettiva si applica solo a quelle che soddisfano le condizioni assegnate all'identificatore.

Se volevi solo le "rates" proprietà rispetto all'intero oggetto, quindi annoti semplicemente come:

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

Aggiunta di oggetti non corrispondenti

Quindi la prima parte è relativamente semplice da comprendere, ma come affermato facendo un $push per l'"elemento che non c'è" è una questione diversa, poiché fondamentalmente abbiamo bisogno di una condizione di query a livello di "documento" per determinare che un elemento dell'array è "mancante".

Ciò significa essenzialmente che è necessario inviare un aggiornamento con $push cercando ogni elemento dell'array per vedere se esiste o meno. Quando non è presente, il documento corrisponde e il $push viene eseguita.

Qui è dove bulkWrite() entra in gioco e lo usi aggiungendo un aggiornamento aggiuntivo alla nostra prima operazione sopra per ogni elemento nel "set di modifiche":

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 }
)

Nota il $elemMatch all'interno del filtro di query, poiché questo è un requisito per abbinare un elemento dell'array in base a "condizioni multiple". Non ne avevamo bisogno su arrayFilters voci perché solo guarda ogni elemento dell'array a cui sono già applicati, ma come "query" le condizioni richiedono $elemMatch poiché la semplice "notazione del punto" restituirebbe corrispondenze errate.

Vedi anche $not operatore viene utilizzato qui per "negare" il $elemMatch , poiché le nostre vere condizioni devono corrispondere solo a un documento che "ha un elemento array non corrispondente" alle condizioni fornite, ed è ciò che giustifica la selezione per aggiungere un nuovo elemento.

E quella singola dichiarazione rilasciata al server essenzialmente ne tenta quattro operazioni di aggiornamento come una per tentare di aggiornare gli elementi dell'array corrispondenti e un'altra per ciascuno dei tre "modifica set" tentando di $push dove è stato riscontrato che il documento non corrisponde alle condizioni per l'elemento dell'array nel "set di modifiche".

Il risultato è quindi quello atteso:

{
        "_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
                }
        ]
}

A seconda di quanti elementi non corrispondono a bulkWrite() la risposta riporterà su quante di queste affermazioni corrispondono effettivamente e influiscono su un documento. In questo caso è 2 abbinato e modificato, poiché la "prima" operazione di aggiornamento corrisponde alle voci dell'array esistenti e l'"ultimo" aggiornamento della modifica corrisponde al fatto che il documento non contiene la voce dell'array ed esegue $push da modificare.

Conclusione

Quindi ecco l'approccio combinato, dove:

  • La prima parte dell'"aggiornamento" nella tua domanda è molto semplice e può essere eseguita in una singola dichiarazione , come dimostrato nella prima sezione.

  • La seconda parte dove c'è un elemento dell'array che "non esiste attualmente" all'interno dell'array di documenti corrente, ciò richiede effettivamente l'utilizzo di bulkWrite() per emettere operazioni "multiple" in un'unica richiesta.

Pertanto aggiorna , è "SI" per una singola operazione. Ma aggiungere differenza significa più operazioni. Ma puoi combinare i due approcci proprio come mostrato qui.

Esistono molti modi "fantasiosi" in cui puoi costruire queste istruzioni in base al contenuto dell'array "change set" con il codice, quindi non è necessario "codificare" ogni membro.

Come caso di base per JavaScript e compatibile con l'attuale versione della shell mongo (che in qualche modo fastidiosamente non supporta gli operatori di diffusione degli oggetti):

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 });

Questo creerà dinamicamente un elenco di operazioni di aggiornamento "in blocco" che assomiglierebbe a:

[
  {
    "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
          }
        }
      }
    }
  }
]

Proprio come è stato descritto nella "forma lunga" della risposta generale, ma ovviamente usa semplicemente il contenuto "array" di input per costruire tutte queste affermazioni.

Puoi eseguire tale costruzione di oggetti dinamici in qualsiasi linguaggio e tutti i driver MongoDB accettano l'input di un tipo di struttura che puoi "manipolare" che viene quindi trasformato in BSON prima che venga effettivamente inviato al server per l'esecuzione.