web-dev-qa-db-fra.com

Mise à jour d'un tableau imbriqué avec MongoDB

J'essaie de mettre à jour une valeur dans le tableau imbriqué mais je ne peux pas le faire fonctionner.

Mon objet est comme ça

 {
    "_id": {
        "$oid": "1"
    },
    "array1": [
        {
            "_id": "12",
            "array2": [
                  {
                      "_id": "123",
                      "answeredBy": [],
                  },
                  {
                      "_id": "124",
                      "answeredBy": [],
                  }
             ],
         }
     ]
 }

J'ai besoin de pousser une valeur dans le tableau "répondues par".

Dans l'exemple ci-dessous, j'ai essayé de pousser la chaîne "success" dans le tableau "replyBy" de l'objet "123 _id" mais cela ne fonctionne pas.

callback = function(err,value){
     if(err){
         res.send(err);
     }else{
         res.send(value);
     }
};
conditions = {
    "_id": 1,
    "array1._id": 12,
    "array2._id": 123
  };
updates = {
   $Push: {
     "array2.$.answeredBy": "success"
   }
};
options = {
  upsert: true
};
Model.update(conditions, updates, options, callback);

J'ai trouvé cela lien , mais sa réponse dit seulement que je devrais utiliser une structure similaire à un objet au lieu d'un tableau. Cela ne peut pas être appliqué dans ma situation. J'ai vraiment besoin que mon objet soit imbriqué dans des tableaux

Ce serait génial si vous pouviez m'aider ici. J'ai passé des heures à comprendre cela.

Merci d'avance!

28
masanorinyo

Portée générale et explication

Il y a quelques problèmes avec ce que vous faites ici. Tout d'abord vos conditions de requête. Vous faites référence à plusieurs valeurs _id Où vous ne devriez pas en avoir besoin, et dont au moins une n'est pas au niveau supérieur.

Afin d'entrer dans une valeur "imbriquée" et en supposant également que la valeur _id Est unique et n'apparaîtra dans aucun autre document, votre formulaire de requête devrait être comme ceci:

Model.update(
    { "array1.array2._id": "123" },
    { "$Push": { "array1.0.array2.$.answeredBy": "success" } },
    function(err,numAffected) {
       // something with the result in here
    }
);

Maintenant, cela fonctionnerait réellement, mais ce n'est vraiment qu'un coup de chance, car il existe de très bonnes raisons pour lesquelles cela ne devrait pas fonctionner pour vous.

La lecture importante se trouve dans la documentation officielle de l'opérateur positionnel $ sous le thème "Nested Arrays". Ce que cela dit est:

L'opérateur positionnel $ ne peut pas être utilisé pour les requêtes qui traversent plusieurs tableaux, telles que les requêtes qui traversent des tableaux imbriqués dans d'autres tableaux, car le remplacement de l'espace réservé $ est une valeur unique

Plus précisément, cela signifie que l'élément qui sera mis en correspondance et renvoyé dans l'espace réservé positionnel est la valeur de l'index du premier tableau correspondant. Cela signifie dans votre cas l'index correspondant sur le tableau de niveau "supérieur".

Donc, si vous regardez la notation de la requête comme indiqué, nous avons "codé en dur" la première position (ou 0 index) dans le tableau de niveau supérieur, et il se trouve que l'élément correspondant dans "array2" est également l'entrée d'index zéro.

Pour démontrer cela, vous pouvez changer la valeur _id Correspondante en "124" et le résultat sera $Push Une nouvelle entrée sur l'élément avec _id "123" car ils sont tous les deux dans l'entrée d'index zéro de "array1" et qui est la valeur renvoyée à l'espace réservé.

Voilà donc le problème général avec les tableaux d'imbrication. Vous pouvez supprimer l'un des niveaux et vous pourrez toujours $Push à l'élément correct dans votre tableau "supérieur", mais il y aura toujours plusieurs niveaux.

Essayez d'éviter l'imbrication des tableaux car vous rencontrerez des problèmes de mise à jour comme indiqué.

Le cas général consiste à "aplatir" les choses que vous "pensez" être des "niveaux" et à faire ces "attributs" sur les éléments de détail finaux. Par exemple, la forme "aplatie" de la structure dans la question devrait être quelque chose comme:

 {
   "answers": [
     { "by": "success", "type2": "123", "type1": "12" }
   ]
 }

Ou même lorsque l'acceptation du tableau interne est $Push uniquement, et jamais mise à jour:

 {
   "array": [
     { "type1": "12", "type2": "123", "answeredBy": ["success"] },
     { "type1": "12", "type2": "124", "answeredBy": [] }
   ]
 }

Les deux se prêtant à des mises à jour atomiques dans le cadre de opérateur positionnel $


MongoDB 3.6 et supérieur

Depuis MongoDB 3.6, de nouvelles fonctionnalités sont disponibles pour travailler avec des tableaux imbriqués. Cela utilise la syntaxe positionnelle filtrée $[<identifier>] afin de faire correspondre les éléments spécifiques et d'appliquer différentes conditions via arrayFilters dans l'instruction de mise à jour:

Model.update(
  {
    "_id": 1,
    "array1": {
      "$elemMatch": {
        "_id": "12","array2._id": "123"
      }
    }
  },
  {
    "$Push": { "array1.$[outer].array2.$[inner].answeredBy": "success" }
  },
  {
    "arrayFilters": [{ "outer._id": "12" },{ "inner._id": "123" }] 
  }
)

Le "arrayFilters" Transmis aux options pour .update() ou même .updateOne() , .updateMany() , .findOneAndUpdate() ou .bulkWrite() la méthode spécifie les conditions à mettre en correspondance avec l'identifiant donné dans le déclaration de mise à jour. Tous les éléments qui correspondent à la condition donnée seront mis à jour.

Parce que la structure est "imbriquée", nous utilisons en fait "plusieurs filtres" comme spécifié avec un "tableau" de définitions de filtres comme indiqué. L'identifiant marqué est utilisé pour faire la correspondance avec la syntaxe positionnelle filtrée $[<identifier>] réellement utilisée dans le bloc de mise à jour de l'instruction. Dans ce cas, inner et outer sont les identifiants utilisés pour chaque condition comme spécifié avec la chaîne imbriquée.

Cette nouvelle extension rend possible la mise à jour du contenu des tableaux imbriqués, mais elle n'aide pas vraiment avec la praticité de "l'interrogation" de ces données, donc les mêmes mises en garde s'appliquent comme expliqué précédemment.

En général, vous "voulez" vraiment dire "des attributs", même si votre cerveau pense au départ à "s'emboîter", c'est généralement une réaction à la façon dont vous croyez que les "parties relationnelles précédentes" se rejoignent. En réalité, vous avez vraiment besoin de plus de dénormalisation.

Voir aussi Comment mettre à jour plusieurs éléments de tableau dans mongodb , car ces nouveaux opérateurs de mise à jour correspondent et mettent à jour "plusieurs éléments de tableau" plutôt que juste le en premier , qui a été l'action précédente des mises à jour de position.

[~ # ~] note [~ # ~] Ironiquement, car cela est spécifié dans l'argument "options" pour .update() et comme les méthodes, la syntaxe est généralement compatible avec toutes les versions récentes des pilotes de publication.

Cependant, ce n'est pas le cas du shell mongo, car la façon dont la méthode y est implémentée ("ironiquement pour la compatibilité descendante") l'argument arrayFilters n'est pas reconnu et supprimé par une méthode interne qui analyse les options afin de fournir une "rétrocompatibilité" avec les versions précédentes du serveur MongoDB et une syntaxe d'appel "héritée" .update() API.

Donc, si vous souhaitez utiliser la commande dans le shell mongo ou d'autres produits "basés sur le shell" (notamment Robo 3T), vous avez besoin d'une dernière version de la branche de développement ou de la version de production à partir de la version 3.6 ou supérieure.

Voir aussi positional all $[] qui met également à jour "plusieurs éléments du tableau" mais sans appliquer aux conditions spécifiées et s'applique à tous des éléments du tableau où c'est l'action souhaitée.

44
Neil Lunn

Je sais que c'est une très vieille question, mais je me suis juste débattu avec ce problème et j'ai trouvé, ce que je pense être, une meilleure réponse.

Un moyen de résoudre ce problème consiste à utiliser Sub-Documents . Cela se fait en imbriquant des schémas dans vos schémas

MainSchema = new mongoose.Schema({
   array1: [Array1Schema]
})

Array1Schema = new mongoose.Schema({
   array2: [Array2Schema]
})

Array2Schema = new mongoose.Schema({
   answeredBy": [...]
})

De cette façon, l'objet ressemblera à celui que vous montrez, mais maintenant chaque tableau est rempli de sous-documents. Cela permet de pointer votre chemin dans le sous-document que vous souhaitez. Au lieu d'utiliser un .update, Vous utilisez ensuite un .find Ou .findOne Pour obtenir le document que vous souhaitez mettre à jour.

Main.findOne((
    {
        _id: 1
    }
)
.exec(
    function(err, result){
        result.array1.id(12).array2.id(123).answeredBy.Push('success')
        result.save(function(err){
            console.log(result)
        });
    }
)

Je n'ai pas utilisé la fonction .Push() De cette façon moi-même, donc la syntaxe n'est peut-être pas correcte, mais j'ai utilisé à la fois .set() et .remove(), et les deux fonctionnent parfaitement bien.

6
Jesper Nielsen