web-dev-qa-db-fra.com

Renvoyer uniquement les éléments de sous-document correspondants dans un tableau imbriqué

La collection principale est le détaillant, qui contient un tableau pour les magasins. Chaque magasin contient une gamme d'offres (vous pouvez acheter dans ce magasin). Cette offre tableau a un tableau de tailles. (Voir exemple ci-dessous)

J'essaie maintenant de trouver toutes les offres disponibles dans la taille L.

{
    "_id" : ObjectId("56f277b1279871c20b8b4567"),
    "stores" : [
        {
        "_id" : ObjectId("56f277b5279871c20b8b4783"),
        "offers" : [
            {
                "_id" : ObjectId("56f277b1279871c20b8b4567"),
                "size": [
                    "XS",
                    "S",
                    "M"
                ]
            },
            {
                "_id" : ObjectId("56f277b1279871c20b8b4567"),
                "size": [
                    "S",
                    "L",
                    "XL"
                ]
            }
        ]
    }
}

J'ai essayé cette requête: db.getCollection('retailers').find({'stores.offers.size': 'L'})

J'attends des résultats comme ça:

 {
"_id" : ObjectId("56f277b1279871c20b8b4567"),
"stores" : [
    {
        "_id" : ObjectId("56f277b5279871c20b8b4783"),
        "offers" : [
            {
                "_id" : ObjectId("56f277b1279871c20b8b4567"),
                "size": [
                    "S",
                    "L",
                    "XL"
                ]
            }
        ]
    }
}

Mais la sortie de ma requête contient également l'offre non correspondante avec size XS, X et M.

Comment puis-je forcer MongoDB à ne renvoyer que les offres correspondant à ma requête?

Salutations et merci.

55
Vico

La requête que vous avez sélectionnée sélectionne donc le "document" comme il se doit. Mais ce que vous recherchez, c'est de "filtrer les tableaux" contenus afin que les éléments retournés ne correspondent qu'à la condition de la requête.

La vraie réponse est bien sûr que, sauf si vous économisez beaucoup de bande passante en filtrant de tels détails, vous ne devriez même pas essayer, ou du moins au-delà de la première correspondance de position.

MongoDB a un opérateur positional _$_ qui retournera un élément de tableau à l'index correspondant à partir d'une condition de requête. Toutefois, cela ne renvoie que le "premier" index correspondant de l'élément de tableau le plus "externe".

_db.getCollection('retailers').find(
    { 'stores.offers.size': 'L'},
    { 'stores.$': 1 }
)
_

Dans ce cas, cela signifie uniquement la position du tableau _"stores"_. Ainsi, s'il y avait plusieurs entrées "magasins", seul "un" des éléments contenant votre condition correspondante serait renvoyé. Mais , cela ne fait rien pour le tableau interne de _"offers"_ et, en tant que tel, chaque "offre" du tableau matchd _"stores"_ serait toujours renvoyée.

MongoDB n'a aucun moyen de "filtrer" cela dans une requête standard, donc ce qui suit ne fonctionne pas:

_db.getCollection('retailers').find(
    { 'stores.offers.size': 'L'},
    { 'stores.$.offers.$': 1 }
)
_

La seule manière dont MongoDB dispose de faire ce niveau de manipulation est avec le framework d'agrégation. Mais l'analyse devrait vous montrer pourquoi vous "probablement" ne devriez pas le faire, mais simplement filtrer le tableau dans le code.


En ordre de comment vous pouvez réaliser ceci par version.

D'abord avec MongoDB 3.2.x avec l'utilisation de l'opération $filter:

_db.getCollection('retailers').aggregate([
  { "$match": { "stores.offers.size": "L" } },
  { "$project": {
    "stores": {
      "$filter": {
        "input": {
          "$map": {
            "input": "$stores",
            "as": "store",
            "in": {
              "_id": "$$store._id",
              "offers": {
                "$filter": {
                  "input": "$$store.offers",
                  "as": "offer",
                  "cond": {
                    "$setIsSubset":  [ ["L"], "$$offer.size" ]
                  }
                }
              }
            }
          }
        },
        "as": "store",
        "cond": { "$ne": [ "$$store.offers", [] ]}
      }
    }
  }}
])
_

Puis avec MongoDB 2.6.x et ci-dessus avec $map et $setDifference:

_db.getCollection('retailers').aggregate([
  { "$match": { "stores.offers.size": "L" } },
  { "$project": {
    "stores": {
      "$setDifference": [
        { "$map": {
          "input": {
            "$map": {
              "input": "$stores",
              "as": "store",
              "in": {
                "_id": "$$store._id",
                "offers": {
                  "$setDifference": [
                    { "$map": {
                      "input": "$$store.offers",
                      "as": "offer",
                      "in": {
                        "$cond": {
                          "if": { "$setIsSubset": [ ["L"], "$$offer.size" ] },
                          "then": "$$offer",
                          "else": false
                        }
                      }
                    }},
                    [false]
                  ]
                }
              }
            }
          },
          "as": "store",
          "in": {
            "$cond": {
              "if": { "$ne": [ "$$store.offers", [] ] },
              "then": "$$store",
              "else": false
            }
          }
        }},
        [false]
      ]
    }
  }}
])
_

Et enfin, dans toute version ci-dessus MongoDB 2.2.x où le framework d'agrégation a été introduit.

_db.getCollection('retailers').aggregate([
  { "$match": { "stores.offers.size": "L" } },
  { "$unwind": "$stores" },
  { "$unwind": "$stores.offers" },
  { "$match": { "stores.offers.size": "L" } },
  { "$group": {
    "_id": {
      "_id": "$_id",
      "storeId": "$stores._id",
    },
    "offers": { "$Push": "$stores.offers" }
  }},
  { "$group": {
    "_id": "$_id._id",
    "stores": {
      "$Push": {
        "_id": "$_id.storeId",
        "offers": "$offers"
      }
    }
  }}
])
_

Permet de décomposer les explications.

MongoDB 3.2.x et supérieur

Donc, d’une manière générale, $filter est la voie à suivre ici car elle est conçue dans un but précis. Étant donné qu'il existe plusieurs niveaux du tableau, vous devez l'appliquer à chaque niveau. Vous commencez donc par plonger dans chaque _"offers"_ dans un _"stores"_ à examiner et un _$filter_ à ce contenu.

La comparaison simple est la suivante "Le tableau _"size"_ contient-il l’élément recherché" . Dans ce contexte logique, il suffit d'utiliser l'opération $setIsSubset pour comparer un tableau ("set") de _["L"]_ au tableau cible. Lorsque cette condition est true (elle contient "L"), l'élément de tableau pour _"offers"_ est conservé et renvoyé dans le résultat.

Au niveau supérieur _$filter_, vous cherchez alors à savoir si le résultat de cette précédente _$filter_ a renvoyé un tableau vide _[]_ pour _"offers"_. S'il n'est pas vide, l'élément est renvoyé ou sinon il est supprimé.

MongoDB 2.6.x

Ceci est très similaire au processus moderne, sauf que puisqu'il n'y a pas de _$filter_ dans cette version, vous pouvez utiliser $map pour inspecter chaque élément, puis utiliser $setDifference pour filtrer tous les éléments retournés sous la forme false.

Donc _$map_ va renvoyer le tableau entier, mais l'opération _$cond_ décide simplement de renvoyer l'élément ou plutôt une valeur false. Lors de la comparaison de _$setDifference_ avec un seul élément, "un ensemble" de _[false]_, tous les éléments false du tableau renvoyé seraient supprimés.

À tous autres égards, la logique est la même que ci-dessus.

MongoDB 2.2.x et plus

Ainsi, en dessous de MongoDB 2.6, le seul outil permettant de travailler avec des tableaux est $unwind , et vous ne devez utiliser que pour pas utiliser le cadre d'agrégation " juste "à cette fin.

Le processus semble en effet simple: il suffit de "séparer" chaque tableau, de filtrer les éléments inutiles, puis de les reconstituer. L'attention principale est dans les "deux" $group , avec le "premier" pour reconstruire le tableau interne et le suivant pour reconstruire le tableau externe. Il existe des valeurs distinctes __id_ à tous les niveaux. Il suffit donc de les inclure à tous les niveaux de regroupement.

Mais le problème est que _$unwind_ est très coûteux . Bien que cela ait toujours un but, son objectif principal n’est pas de faire ce type de filtrage par document. En fait, dans les versions modernes, son utilisation doit uniquement être utilisée lorsqu'un élément du ou des tableaux doit faire partie de la "clé de regroupement" elle-même.


Conclusion

Ce n'est donc pas un processus simple d'obtenir des correspondances à plusieurs niveaux d'un tableau comme celui-ci. En fait, cela peut être extrêmement coûteux si implémenté de manière incorrecte.

Seules les deux listes modernes doivent être utilisées à cette fin, car elles utilisent un "unique" étage de pipeline en plus de la "requête" _$match_ afin de procéder au "filtrage". L'effet résultant est un peu plus lourd que les formes standard de .find().

En général, cependant, ces listes ont encore une certaine complexité et, à moins que vous ne réduisiez vraiment de manière radicale le contenu renvoyé par ce filtrage de manière à améliorer de manière significative la bande passante utilisée entre le serveur et le client, vous êtes alors mieux. de filtrer le résultat de la requête initiale et la projection de base.

_db.getCollection('retailers').find(
    { 'stores.offers.size': 'L'},
    { 'stores.$': 1 }
).forEach(function(doc) {
    // Technically this is only "one" store. So omit the projection
    // if you wanted more than "one" match
    doc.stores = doc.stores.filter(function(store) {
        store.offers = store.offers.filter(function(offer) {
            return offer.size.indexOf("L") != -1;
        });
        return store.offers.length != 0;
    });
    printjson(doc);
})
_

Par conséquent, le traitement de la requête "post" renvoyé par l'objet est beaucoup moins obtus que l'utilisation du pipeline d'agrégation. Et comme indiqué, la seule différence "réelle" serait que vous supprimiez les autres éléments du "serveur" au lieu de les supprimer "par document" lors de la réception, ce qui peut économiser un peu de bande passante.

Mais à moins que vous ne le fassiez dans une version moderne avec seulement _$match_ et _$project_, le "coût" du traitement sur le serveur sera largement supérieur à la "gain" de réduction de la surcharge du réseau en supprimant d’abord les éléments incomparables.

Dans tous les cas, vous obtenez le même résultat:

_{
        "_id" : ObjectId("56f277b1279871c20b8b4567"),
        "stores" : [
                {
                        "_id" : ObjectId("56f277b5279871c20b8b4783"),
                        "offers" : [
                                {
                                        "_id" : ObjectId("56f277b1279871c20b8b4567"),
                                        "size" : [
                                                "S",
                                                "L",
                                                "XL"
                                        ]
                                }
                        ]
                }
        ]
}
_
103
Blakes Seven

comme votre tableau est incorporé, nous ne pouvons pas utiliser $ elemMatch, vous pouvez utiliser le framework d'agrégation pour obtenir vos résultats:

db.retailers.aggregate([
{$match:{"stores.offers.size": 'L'}}, //just precondition can be skipped
{$unwind:"$stores"},
{$unwind:"$stores.offers"},
{$match:{"stores.offers.size": 'L'}},
{$group:{
    _id:{id:"$_id", "storesId":"$stores._id"},
    "offers":{$Push:"$stores.offers"}
}},
{$group:{
    _id:"$_id.id",
    stores:{$Push:{_id:"$_id.storesId","offers":"$offers"}}
}}
]).pretty()

le but de cette requête est de dérouler les tableaux (deux fois), de faire correspondre la taille, puis de remodeler le document au format précédent. Vous pouvez supprimer les étapes de $ groupe et voir comment elles s’impriment. Amusez vous!

9
profesor79