MongoDB
 sql >> Base de données >  >> NoSQL >> MongoDB

Tableau de mise à jour en masse du sous-document correspondant dans Mongodb

Dans la réponse la plus courte, c'est à la fois "oui" et "non".

Il existe en effet un moyen de faire correspondre des éléments de tableau individuels et de les mettre à jour avec des valeurs distinctes dans une seule instruction, car vous pouvez en fait fournir "plusieurs" arrayFilters conditions et utilisez ces identifiants dans votre déclaration de mise à jour.

Le problème avec votre exemple particulier ici est que l'une des entrées de votre "change set" (le dernier) ne correspond en fait à aucun membre du tableau actuellement présent. L'action "présumée" ici serait de $push ce nouveau membre sans correspondance dans le tableau où il n'a pas été trouvé. Cependant, cette action particulière ne peut pas se faire en une "opération unique" , mais vous pouvez utiliser bulkWrite() pour émettre des déclarations "multiples" pour couvrir ce cas.

Faire correspondre différentes conditions de baie

En expliquant cela en points, considérez les deux premiers éléments de votre "ensemble de modifications". Vous pouvez appliquer un "simple" instruction de mise à jour avec plusieurs arrayFilters comme ceci :

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 vous exécutiez cela, vous verriez que le document modifié devient :

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

Notez ici que vous spécifiez chaque "identifiant" dans la liste des arrayFilters avec plusieurs conditions pour faire correspondre l'élément comme suit :

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

Ainsi, chaque "condition" correspond effectivement à :

  <identifier>.<property>

Il sait donc regarder les "rates" tableau par l'instruction dans le bloc de mise à jour par le $[<indentifier>] :

 "rates.$[one]"

Et regarde chaque élément de "rates" pour correspondre aux conditions. Donc le "one" l'identifiant correspondrait aux conditions précédées de "one" et de même pour l'autre ensemble de conditions préfixé par "two" , par conséquent, l'instruction de mise à jour réelle s'applique uniquement à ceux qui correspondent aux conditions affectées à l'identifiant.

Si vous vouliez juste les "rates" propriété par opposition à l'objet entier, alors vous notez simplement comme :

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

Ajout d'objets sans correspondance

Donc, la première partie est relativement simple à comprendre, mais comme indiqué, faites un $push pour "l'élément qui n'est pas là", c'est une autre affaire, puisque nous avons essentiellement besoin d'une condition de requête au niveau du "document" afin de déterminer qu'un élément du tableau est "manquant".

Cela signifie essentiellement que vous devez publier une mise à jour avec le $push rechercher chaque élément du tableau pour voir s'il existe ou non. Lorsqu'il n'est pas présent, le document est une correspondance et le $push est effectuée.

C'est là que bulkWrite() entre en jeu, et vous l'utilisez en ajoutant une mise à jour supplémentaire à notre première opération ci-dessus pour chaque élément du "change set":

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

Notez le $elemMatch avec le filtre de requête, car il s'agit d'une exigence pour faire correspondre un élément de tableau par "plusieurs conditions". Nous n'avions pas besoin de cela sur les arrayFilters entrées parce qu'elles uniquement regardez chaque élément du tableau auquel ils sont déjà appliqués, mais en tant que "requête", les conditions nécessitent $elemMatch car une simple "notation par points" renverrait des correspondances incorrectes.

Voir aussi le $not est utilisé ici pour "annuler" le $elemMatch , car nos vraies conditions sont de ne faire correspondre qu'un document qui "n'a pas d'élément de tableau correspondant" aux conditions fournies, et c'est ce qui justifie la sélection pour ajouter un nouvel élément.

Et cette instruction unique envoyée au serveur tente essentiellement quatre opérations de mise à jour comme une pour tenter de mettre à jour les éléments de tableau correspondants, et une autre pour chacun des trois "change sets" essayant de $push où le document s'est avéré ne pas correspondre aux conditions de l'élément de tableau dans le "jeu de modifications".

Le résultat est donc celui attendu :

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

En fonction du nombre d'éléments qui ne correspondaient pas réellement à bulkWrite() La réponse indiquera combien de ces déclarations correspondaient et affectaient réellement un document. Dans ce cas c'est 2 mis en correspondance et modifié, puisque la "première" opération de mise à jour correspond aux entrées de tableau existantes, et la "dernière" mise à jour de modification correspond au fait que le document ne contient pas l'entrée de tableau et exécute la $push à modifier.

Conclusion

Voilà donc l'approche combinée, où :

  • La première partie de la "mise à jour" de votre question est très simple et peut être effectuée en une seule instruction , comme le montre la première section.

  • La deuxième partie où il y a un élément de tableau qui "n'existe pas actuellement" dans le tableau de documents actuel, cela nécessite en fait que vous utilisiez bulkWrite() afin d'émettre des opérations "multiples" en une seule requête.

Donc mettre à jour , est "OUI" à une seule opération. Mais ajouter une différence signifie plusieurs opérations. Mais vous pouvez combiner les deux approches comme cela est démontré ici.

Il existe de nombreuses façons "sophistiquées" de construire ces instructions en fonction du contenu du tableau "change set" avec du code, vous n'avez donc pas besoin de "coder en dur" chaque membre.

En tant que cas de base pour JavaScript et compatible avec la version actuelle du shell mongo (qui, de manière quelque peu ennuyeuse, ne prend pas en charge les opérateurs de propagation d'objets) :

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

Cela construira dynamiquement une liste d'opérations de mise à jour "en masse" qui ressemblera à :

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

Tout comme cela a été décrit dans la "forme longue" de la réponse générale, mais bien sûr utilise simplement le contenu du "tableau" d'entrée afin de construire toutes ces déclarations.

Vous pouvez faire une telle construction d'objet dynamique dans n'importe quel langage, et tous les pilotes MongoDB acceptent l'entrée d'un certain type de structure que vous êtes autorisé à "manipuler", qui est ensuite transformée en BSON avant d'être réellement envoyée au serveur pour exécution.