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

Filtre d'agrégation après $lookup

La question ici concerne en fait quelque chose de différent et n'a pas besoin de $lookup du tout. Mais pour tous ceux qui arrivent ici uniquement à partir du titre de "filtrage après $lookup", voici les techniques pour vous :

MongoDB 3.6 - Sous-pipeline

db.test.aggregate([
    { "$match": { "id": 100 } },
    { "$lookup": {
      "from": "test",
      "let": { "id": "$id" },
      "pipeline": [
        { "$match": {
          "value": "1",
          "$expr": { "$in": [ "$$id", "$contain" ] }
        }}
      ],
      "as": "childs"
    }}
])

Plus tôt - $lookup + $unwind + $match coalescence

db.test.aggregate([
    { "$match": { "id": 100 } },
    { "$lookup": {
        "from": "test",
        "localField": "id",
        "foreignField": "contain",
        "as": "childs"
    }},
    { "$unwind": "$childs" },
    { "$match": { "childs.value": "1" } },
    { "$group": {
        "_id": "$_id",
        "id": { "$first": "$id" },
        "value": { "$first": "$value" },
        "contain": { "$first": "$contain" },
        "childs": { "$push": "$childs" }
     }}
])

Si vous vous demandez pourquoi voudriez-vous $unwind au lieu d'utiliser $filter sur le tableau, puis lisez Aggregate $lookup La taille totale des documents dans le pipeline correspondant dépasse la taille maximale du document pour tous les détails expliquant pourquoi cela est généralement nécessaire et bien plus optimal.

Pour les versions de MongoDB 3.6 et ultérieures, le "sous-pipeline" le plus expressif est généralement ce que vous voulez "filtrer" les résultats de la collection étrangère avant que quoi que ce soit ne soit renvoyé dans le tableau.

Revenons à la réponse qui décrit en fait pourquoi la question posée n'a besoin d'aucune "jointure" du tout....

Original

Utiliser $lookup comme ce n'est pas la façon la plus "efficace" de faire ce que vous voulez ici. Mais nous en reparlerons plus tard.

En tant que concept de base, utilisez simplement $filter sur le tableau résultant :

db.test.aggregate([ 
    { "$match": { "id": 100 } }, 
    { "$lookup": {
        "from": "test",
        "localField": "id",
        "foreignField": "contain",
        "as": "childs"
    }},
    { "$project": {
        "id": 1,
        "value": 1,
        "contain": 1,
        "childs": {
           "$filter": {
               "input": "$childs",
               "as": "child",
               "cond": { "$eq": [ "$$child.value", "1" ] }
           }
        }
    }}
]);

Ou utilisez $redact à la place :

db.test.aggregate([ 
    { "$match": { "id": 100 } }, 
    { "$lookup": {
        "from": "test",
        "localField": "id",
        "foreignField": "contain",
        "as": "childs"
    }},
    { "$redact": {
        "$cond": {
           "if": {
              "$or": [
                { "$eq": [ "$value", "0" ] },
                { "$eq": [ "$value", "1" ] }
              ]
           },
           "then": "$$DESCEND",
           "else": "$$PRUNE"
        }
    }}
]);

Les deux obtiennent le même résultat :

{  
  "_id":ObjectId("570557d4094a4514fc1291d6"),
  "id":100,
  "value":"0",
  "contain":[ ],
  "childs":[ {  
      "_id":ObjectId("570557d4094a4514fc1291d7"),
      "id":110,
      "value":"1",
      "contain":[ 100 ]
    },
    {  
      "_id":ObjectId("570557d4094a4514fc1291d8"),
      "id":120,
      "value":"1",
      "contain":[ 100 ]
    }
  ]
}

L'essentiel est que $lookup lui-même ne peut pas "encore" interroger pour sélectionner uniquement certaines données. Donc, tout "filtrage" doit avoir lieu après le $lookup

Mais vraiment pour ce type d'"auto-jointure", il vaut mieux ne pas utiliser $lookup du tout et en évitant entièrement la surcharge d'une lecture supplémentaire et d'une "fusion par hachage". Récupérez simplement les éléments associés et $group à la place :

db.test.aggregate([
  { "$match": { 
    "$or": [
      { "id": 100 },
      { "contain.0": 100, "value": "1" }
    ]
  }},
  { "$group": {
    "_id": {
      "$cond": {
        "if": { "$eq": [ "$value", "0" ] },
        "then": "$id",
        "else": { "$arrayElemAt": [ "$contain", 0 ] }
      }
    },
    "value": { "$first": { "$literal": "0"} },
    "childs": {
      "$push": {
        "$cond": {
          "if": { "$ne": [ "$value", "0" ] },
          "then": "$$ROOT",
          "else": null
        }
      }
    }
  }},
  { "$project": {
    "value": 1,
    "childs": {
      "$filter": {
        "input": "$childs",
        "as": "child",
        "cond": { "$ne": [ "$$child", null ] }
      }
    }
  }}
])

Ce qui est seulement un peu différent parce que j'ai délibérément supprimé les champs superflus. Ajoutez-les vous-même si vous le souhaitez vraiment :

{
  "_id" : 100,
  "value" : "0",
  "childs" : [
    {
      "_id" : ObjectId("570557d4094a4514fc1291d7"),
      "id" : 110,
      "value" : "1",
      "contain" : [ 100 ]
    },
    {
      "_id" : ObjectId("570557d4094a4514fc1291d8"),
      "id" : 120,
      "value" : "1",
      "contain" : [ 100 ]
    }
  ]
}

Donc, le seul vrai problème ici est de "filtrer" tout null résultat du tableau, créé lorsque le document courant était le parent dans le traitement des éléments vers $push .

Ce qui vous semble également manquer ici, c'est que le résultat que vous recherchez n'a pas du tout besoin d'agrégation ou de "sous-requêtes". La structure que vous avez conclue ou éventuellement trouvée ailleurs est "conçue" pour que vous puissiez obtenir un "nœud" et tous ses "enfants" en une seule requête.

Cela signifie que seule la "requête" est tout ce qui est vraiment nécessaire, et la collecte de données (qui est tout ce qui se passe puisqu'aucun contenu n'est vraiment "réduit") est juste une fonction d'itération du résultat du curseur :

var result = {};

db.test.find({
  "$or": [
    { "id": 100 },
    { "contain.0": 100, "value": "1" }
  ]
}).sort({ "contain.0": 1 }).forEach(function(doc) {
  if ( doc.id == 100 ) {
    result = doc;
    result.childs = []
  } else {
    result.childs.push(doc)
  }
})

printjson(result);

Cela fait exactement la même chose :

{
  "_id" : ObjectId("570557d4094a4514fc1291d6"),
  "id" : 100,
  "value" : "0",
  "contain" : [ ],
  "childs" : [
    {
      "_id" : ObjectId("570557d4094a4514fc1291d7"),
      "id" : 110,
      "value" : "1",
      "contain" : [
              100
      ]
    },
    {
      "_id" : ObjectId("570557d4094a4514fc1291d8"),
      "id" : 120,
      "value" : "1",
      "contain" : [
              100
      ]
    }
  ]
}

Et sert de preuve que tout ce que vous avez vraiment besoin de faire ici est d'émettre la requête "unique" pour sélectionner à la fois le parent et les enfants. Les données renvoyées sont les mêmes, et tout ce que vous faites sur le serveur ou le client est de "masser" dans un autre format collecté.

C'est l'un de ces cas où vous pouvez être "pris" en pensant à la façon dont vous avez fait les choses dans une base de données "relationnelle", et ne pas réaliser que puisque la façon dont les données sont stockées a "changé", vous n'avez plus besoin d'utiliser la même approche.

C'est exactement le but de l'exemple de documentation "Model Tree Structures with Child References" dans sa structure, où il est facile de sélectionner les parents et les enfants dans une seule requête.