MongoDB
 sql >> Base de Dados >  >> NoSQL >> MongoDB

Filtro de agregação após $lookup


A questão aqui é na verdade sobre algo diferente e não precisa de $lookup de forma alguma. Mas para quem chega aqui puramente pelo título de "filtragem após $ lookup", estas são as técnicas para você:

MongoDB 3.6 - Sub-pipeline

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

Anteriormente - $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" }
     }}
])

Se você questionar por que você $unwind ao invés de usar $filter no array, então leia Agregado $lookup O tamanho total dos documentos no pipeline de correspondência excede o tamanho máximo do documento para todos os detalhes sobre por que isso é geralmente necessário e muito mais ideal.

Para versões do MongoDB 3.6 e posteriores, o "sub-pipeline" mais expressivo geralmente é o que você deseja "filtrar" os resultados da coleção estrangeira antes que qualquer coisa seja retornada ao array.

De volta à resposta, que realmente descreve por que a pergunta feita não precisa de "nenhuma junção" ....

Original


Usando $lookup como esta não é a maneira mais "eficiente" de fazer o que você quer aqui. Mas mais sobre isso mais tarde.

Como conceito básico, basta usar $filter na matriz resultante:
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 use $redact em vez de:
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"
        }
    }}
]);

Ambos obtêm o mesmo resultado:
{  
  "_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 ]
    }
  ]
}

A conclusão é que $lookup em si não pode consultar "ainda" para selecionar apenas determinados dados. Portanto, toda a "filtragem" precisa acontecer após o $lookup

Mas realmente para este tipo de "auto-junção" é melhor não usar $lookup e evitando a sobrecarga de uma leitura adicional e "hash-merge" inteiramente. Basta buscar os itens relacionados e $group em vez de:
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 ] }
      }
    }
  }}
])

O que só sai um pouco diferente porque eu removi deliberadamente os campos estranhos. Adicione-os em você mesmo se você realmente quiser:
{
  "_id" : 100,
  "value" : "0",
  "childs" : [
    {
      "_id" : ObjectId("570557d4094a4514fc1291d7"),
      "id" : 110,
      "value" : "1",
      "contain" : [ 100 ]
    },
    {
      "_id" : ObjectId("570557d4094a4514fc1291d8"),
      "id" : 120,
      "value" : "1",
      "contain" : [ 100 ]
    }
  ]
}

Portanto, o único problema real aqui é "filtrar" qualquer null resultado do array, criado quando o documento atual era o parent no processamento de itens para $push .

O que você também parece estar faltando aqui é que o resultado que você está procurando não precisa de agregação ou "sub-consultas". A estrutura que você concluiu ou possivelmente encontrou em outro lugar é "projetada" para que você possa obter um "nó" e todos os seus "filhos" em uma única solicitação de consulta.

Isso significa que apenas a "consulta" é tudo o que é realmente necessário, e a coleta de dados (que é tudo o que está acontecendo, pois nenhum conteúdo está realmente sendo "reduzido") é apenas uma função de iterar o resultado do cursor:
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);

Isso faz exatamente a mesma coisa:
{
  "_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
      ]
    }
  ]
}

E serve como prova de que tudo o que você realmente precisa fazer aqui é emitir a consulta "única" para selecionar o pai e os filhos. Os dados retornados são exatamente os mesmos, e tudo o que você está fazendo no servidor ou no cliente é "massagem" em outro formato coletado.

Este é um daqueles casos em que você pode ficar "pego" pensando em como fez as coisas em um banco de dados "relacional" e não perceber que, como a maneira como os dados são armazenados "mudou", você não precisa mais usar a mesma abordagem.

Esse é exatamente o objetivo do exemplo de documentação "Modelo de estruturas de árvore com referências filho" em sua estrutura, onde facilita a seleção de pais e filhos em uma consulta.