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

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

Ainsi, la requête que vous avez sélectionne réellement le "document" comme il se doit. Mais ce que vous cherchez, c'est de "filtrer les tableaux" contenus afin que les éléments renvoyés correspondent uniquement à la condition de la requête.

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

MongoDB a un $ positionnel opérateur qui renverra un élément de tableau à l'index correspondant à partir d'une condition de requête. Cependant, 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 les "stores" position du tableau uniquement. Donc, s'il y avait plusieurs entrées "stores", alors seulement "un" des éléments contenant votre condition correspondante serait renvoyé. Mais , cela ne fait rien pour le tableau interne de "offers" , et ainsi chaque "offre" dans les "stores" correspondants tableau serait toujours renvoyé.

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 dont dispose MongoDB pour effectuer ce niveau de manipulation sont le framework d'agrégation. Mais l'analyse devrait vous montrer pourquoi vous ne devriez "probablement" pas faire cela, et plutôt simplement filtrer le tableau dans le code.

Dans l'ordre de comment vous pouvez y parvenir par version.

D'abord avec MongoDB 3.2.x en utilisant le $filter opération :

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 au-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 supérieure à 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"
      }
    }
  }}
])

Décomposons les explications.

MongoDB 3.2.x et supérieur

Donc, d'une manière générale, $filter est la voie à suivre ici car il est conçu avec le but à l'esprit. Puisqu'il existe plusieurs niveaux de tableau, vous devez l'appliquer à chaque niveau. Donc, vous plongez d'abord dans chaque "offers" dans les "stores" examiner et $filter ce contenu.

La comparaison simple ici est "Est-ce que la "size" tableau contient l'élément que je recherche" . Dans ce contexte logique, la chose courte à faire est d'utiliser le $setIsSubset opération pour comparer un tableau ("set") de ["L"] au tableau cible. Où cette condition est true ( il contient "L" ) puis l'élément de tableau pour "offers" est conservé et renvoyé dans le résultat.

Au niveau supérieur $filter , vous cherchez alors à voir si le résultat de ce précédent $filter a renvoyé un tableau vide [] pour "offers" . S'il n'est pas vide, alors 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 renvoyés comme false .

Alors $map va retourner le tableau entier, mais le $cond l'opération décide simplement de renvoyer l'élément ou à la place un false valeur. Dans la comparaison de $setDifference à un seul élément "set" de [false] tous false les éléments du tableau renvoyé seraient supprimés.

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

MongoDB 2.2.x et versions ultérieures

Donc, sous MongoDB 2.6, le seul outil pour travailler avec des tableaux est $unwind , et dans ce seul but, vous ne devez pas utilisez le cadre d'agrégation "juste" à cette fin.

Le processus semble en effet simple, en "démontant" simplement chaque tableau, en filtrant les choses dont vous n'avez pas besoin puis en le reconstituant. Le soin principal est dans les "deux" $group étapes, avec la "première" pour reconstruire le tableau interne, et la suivante pour reconstruire le tableau externe. Il existe des _id distincts valeurs à tous les niveaux, il suffit donc de les inclure à chaque niveau de regroupement.

Mais le problème est que $unwind est très coûteux . Bien qu'il ait toujours un but, son intention principale d'utilisation n'est pas de faire ce type de filtrage par document. En fait, dans les versions modernes, son utilisation ne devrait être que 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 pour obtenir des correspondances à plusieurs niveaux d'un tableau comme celui-ci, et en fait cela peut être extrêmement coûteux si mis en œuvre de manière incorrecte.

Seules les deux listes modernes doivent être utilisées à cette fin, car elles utilisent une étape de pipeline "unique" en plus de la "requête" $match pour faire le "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 en effet, à moins que vous ne réduisiez vraiment considérablement le contenu renvoyé par un tel filtrage d'une manière qui améliore considérablement la bande passante utilisée entre le serveur et le client, alors vous êtes mieux 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);
})

Ainsi, travailler avec l'objet renvoyé "post" le traitement de la requête est beaucoup moins obtus que d'utiliser le pipeline d'agrégation pour ce faire. Et comme indiqué, la seule différence "réelle" serait que vous supprimez les autres éléments sur le "serveur" au lieu de les supprimer "par document" lorsqu'ils sont reçus, ce qui peut économiser un peu de bande passante.

Mais à moins que vous ne le fassiez dans une version moderne avec uniquement $match et $project , le « coût » du traitement sur le serveur l'emportera largement sur le « gain » de la réduction de la surcharge du réseau en supprimant d'abord les éléments sans correspondance.

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