Retourner Seulement les éléments de sous-document appariés dans un tableau imbriqué

la collection principale est retailer, qui contient un tableau pour les magasins. Chaque magasin contient un éventail d'offres (vous pouvez acheter dans ce magasin). Ce tableau offre a un tableau de tailles. (Voir l'exemple ci-dessous)

Maintenant, j'essaie de trouver toutes les offres, qui sont 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 une Sortie 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 aussi l'offre non assortie avec size XS, X and M.

comment forcer MongoDB à retourner Seulement les offres, qui correspondent à ma requête?

Salutations et remerciements.

42
demandé sur Blakes Seven 2016-03-26 01:34:01

2 réponses

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

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

MongoDB a un position $ l'opérateur qui renvoie un élément de tableau à l'index apparié à partir d'une condition de requête. Cependant, ceci renvoie seulement le "premier" index apparié de l'élément "externe" de la plupart des tableaux.

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

Dans ce cas, cela signifie que le "stores" position du tableau seulement. Donc, s'il y avait plusieurs entrées "magasins", alors seulement "un" des éléments qui contiennent votre état apparié serait retourné. Mais, qui ne fait rien pour l'intérieur de tableau de "offers", et en tant que tel, chaque "offre" dans le matchd "stores" le tableau serait quand même retourné.

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

les seuls outils que MongoDB a réellement pour faire ce niveau de manipulation est avec le cadre d'agrégation. Mais l'analyse devrait vous montrer pourquoi vous "probablement" ne devriez pas faire cela, et à la place filtrer simplement le tableau dans le code.


dans l'ordre où vous pouvez réaliser ceci par version.

Premier MongoDB 3.2.x en utilisant le $filter fonctionnement:

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 MongoDB 2.6.x et $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 la version ci-dessus MongoDB 2.2.x où le cadre 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 briser la explication.

MongoDB 3.2.x et une plus grande

Donc généralement parlant, $filter est le chemin à parcourir car il est conçu avec le but à l'esprit. Puisqu'il y a plusieurs niveaux du tableau, vous devez appliquer ceci à chaque niveau. Donc d'abord vous plongez dans chacun "offers" à l'intérieur de "stores" pour examime et $filter que du contenu.

la simple comparaison ici est "l' "size" array contenant l'élément, je suis à la recherche pour". Dans ce contexte logique, le court-chose à faire est d'utiliser le $setIsSubset opération de comparer un tableau ("set")["L"] au tableau cible. Où cette condition est true (il contient "L" ) Puis l'élément du tableau pour "offers" est retenu et retourné dans le résultat.

Dans le niveau supérieur $filter, vous cherchez alors à voir si le résultat de ce précédent $filter retourné un tableau vide []"offers". Si elle n'est pas vide, alors l'élément est renvoyé ou sinon il est supprimé.

MongoDB 2.6.x

C'est très similaire au processus moderne sauf que depuis il n'y a pas de $filter dans cette version, vous pouvez utiliser $map pour inspecter chaque élément et ensuite utiliser $setDifference pour filtrer tous les éléments retournés comme false.

$map va retourner l'ensemble du tableau, mais l' $cond opération vient décide de retour l'élément ou à la place un false valeur. Dans la comparaison de $setDifference pour un seul élément "set" de [false]false les éléments du tableau retourné seraient supprimés.

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

MongoDB 2.2.x et

donc en dessous de MongoDB 2.6 le seul outil pour travailler avec des tableaux est $unwind, et pour cette seule fin, vous devriez utilisez le cadre d'agrégation "juste" pour ce but.

le processus semble en effet simple, en "démontant" simplement chaque tableau, en filtrant les choses dont vous n'avez pas besoin et en le remettant en place. Le principal soin est dans les "deux" $group étapes, avec le "premier" à reconstruire le tableau intérieur, et le suivant à reconstruire le tableau extérieur. Il y a des _id valeurs à tous les niveaux, de sorte que ces juste besoin d'être inclus à chaque niveau de regroupement.

Mais le problème est que $unwind très coûteux. Bien qu'il ait toujours un but, son but principal n'est pas de faire ce genre de filtrage par document. En fait, dans les versions modernes, son seul usage devrait être lorsqu'un élément du ou des tableaux doit faire partie de la "clé de regroupement" elle-même.


Conclusion

donc ce n'est pas un processus simple pour obtenir des correspondances à plusieurs niveaux d'un tableau comme celui-ci, et en fait cela peut être extrêmement cher si mis en œuvre incorrectement.

seules les deux listes modernes devraient jamais être utilisées à cette fin, car elles emploient une étape de pipeline" simple "en plus de la" requête"$match afin de faire le"filtrage". L'effet résultant est un peu plus de frais généraux que les formes standard de .find().

en général, cependant, ces listes ont encore un certain degré de complexité pour eux, et en effet à moins que vous ne réduisiez drastiquement le contenu retourné par un tel filtrage dans un de cette façon, il est possible d'améliorer de manière significative la bande passante utilisée entre le serveur et le client, alors il est préférable de filtrer le résultat de la requête initiale et de 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);
})

donc travailler avec le traitement de requête "post" de l'objet retourné est beaucoup moins obtus que d'utiliser le pipeline d'agrégation pour le faire. Et comme indiqué la seule différence "réelle" serait que vous jetez les autres éléments sur le "serveur" par opposition à les supprimer "par document" quand reçu, qui peut sauver un peu de bande passante.

Mais si vous le faites dans une version moderne avec $match et $project, alors le " coût "du traitement sur le serveur l'emportera largement sur le" gain " de réduire la charge du réseau en enlevant d'abord les éléments non appariés.

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"
                                        ]
                                }
                        ]
                }
        ]
}
86
répondu Blakes Seven 2018-03-08 18:18:42

comme votre tableau est embeded, nous ne pouvons pas utiliser $elemMatch, à la place vous pouvez utiliser le cadre 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()

Ce Que fait cette requête est de décompresser les tableaux (deux fois), puis de faire correspondre la taille et puis de remodeler le document à la forme précédente. Vous pouvez supprimer $ Group steps et voir comment il imprime. Amusez-vous!

9
répondu profesor79 2016-03-26 00:35:21