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

Retorna apenas elementos de subdocumento correspondentes em uma matriz aninhada


Portanto, a consulta que você realmente seleciona o "documento" como deveria. Mas o que você procura é "filtrar os arrays" contidos para que os elementos retornados correspondam apenas à condição da consulta.

A resposta real é, claro, que, a menos que você realmente esteja economizando muita largura de banda filtrando esses detalhes, você nem deve tentar, ou pelo menos além da primeira correspondência posicional.

O MongoDB tem um $ posicional operador que retornará um elemento de matriz no índice correspondente de uma condição de consulta. No entanto, isso retorna apenas o "primeiro" índice correspondente do elemento mais "externo" da matriz.
db.getCollection('retailers').find(
    { 'stores.offers.size': 'L'},
    { 'stores.$': 1 }
)

Neste caso, significa que as "stores" apenas a posição da matriz. Portanto, se houvesse várias entradas de "lojas", apenas "um" dos elementos que continham sua condição correspondente seria retornado. Mas , que não faz nada para o array interno de "offers" , e como tal, cada "oferta" dentro das "stores" correspondentes array ainda seria retornado.

O MongoDB não tem como "filtrar" isso em uma consulta padrão, então o seguinte não funciona:
db.getCollection('retailers').find(
    { 'stores.offers.size': 'L'},
    { 'stores.$.offers.$': 1 }
)

As únicas ferramentas que o MongoDB realmente tem para fazer esse nível de manipulação é com o framework de agregação. Mas a análise deve mostrar por que você "provavelmente" não deve fazer isso e, em vez disso, apenas filtrar a matriz no código.

Em ordem de como você pode conseguir isso por versão.

Primeiro com o MongoDB 3.2.x usando o $filter Operação:
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", [] ]}
      }
    }
  }}
])

Em seguida, com o MongoDB 2.6.x e acima com $map e $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]
      ]
    }
  }}
])

E finalmente em qualquer versão acima do MongoDB 2.2.x onde a estrutura de agregação foi introduzida.
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"
      }
    }
  }}
])

Vamos quebrar as explicações.

MongoDB 3.2.xe superior


De modo geral, $filter é o caminho a percorrer aqui, pois foi projetado com o objetivo em mente. Como existem vários níveis da matriz, você precisa aplicar isso em cada nível. Então, primeiro você está mergulhando em cada "offers" dentro de "stores" para examinar e $filter esse conteúdo.

A comparação simples aqui é "O "size" array contém o elemento que estou procurando" . Neste contexto lógico, a coisa curta a fazer é usar o $setIsSubset operação para comparar um array ("conjunto") de ["L"] para a matriz de destino. Onde essa condição é true ( ele contém "L" ) então o elemento de array para "offers" é retido e retornado no resultado.

No nível superior $filter , você está olhando para ver se o resultado do $filter anterior retornou uma matriz vazia [] para "offers" . Se não estiver vazio, o elemento será retornado ou, caso contrário, será removido.

MongoDB 2.6.x


Isso é muito semelhante ao processo moderno, exceto que, como não há $filter nesta versão você pode usar $map para inspecionar cada elemento e então use $setDifference para filtrar quaisquer elementos que foram retornados como false .

Então $map vai retornar o array inteiro, mas o $cond a operação apenas decide se deve retornar o elemento ou um false valor. Na comparação de $setDifference para um único elemento "conjunto" de [false] tudo false elementos na matriz retornada seriam removidos.

Em todas as outras formas, a lógica é a mesma acima.

MongoDB 2.2.xe superior


Então, abaixo do MongoDB 2.6, a única ferramenta para trabalhar com arrays é $unwind , e apenas para este propósito você não use a estrutura de agregação "apenas" para essa finalidade.

O processo realmente parece simples, simplesmente "desmontando" cada matriz, filtrando as coisas que você não precisa e depois juntando-as novamente. O principal cuidado está nos "dois" $group estágios, com o "primeiro" para reconstruir a matriz interna e o próximo para reconstruir a matriz externa. Existem _id distintos valores em todos os níveis, então eles só precisam ser incluídos em todos os níveis de agrupamento.

Mas o problema é que $unwind é muito caro . Embora ainda tenha um propósito, sua principal intenção de uso não é fazer esse tipo de filtragem por documento. Na verdade, nas versões modernas, seu único uso deve ser quando um elemento do(s) array(s) precisa se tornar parte da própria "chave de agrupamento".

Conclusão


Portanto, não é um processo simples obter correspondências em vários níveis de uma matriz como essa e, na verdade, pode ser extremamente caro se implementado incorretamente.

Apenas as duas listagens modernas devem ser usadas para essa finalidade, pois empregam um estágio de pipeline "único" além da "consulta" $match para fazer a "filtragem". O efeito resultante é um pouco mais pesado do que as formas padrão de .find() .

Em geral, porém, essas listagens ainda têm uma certa complexidade e, de fato, a menos que você esteja reduzindo drasticamente o conteúdo retornado por essa filtragem de uma maneira que faça uma melhoria significativa na largura de banda usada entre o servidor e o cliente, então é melhor de filtrar o resultado da consulta inicial e projeção básica.
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);
})

Portanto, trabalhar com o processamento de consulta "pós" do objeto retornado é muito menos obtuso do que usar o pipeline de agregação para fazer isso. E, como afirmado, a única diferença "real" seria que você está descartando os outros elementos no "servidor" em vez de removê-los "por documento" quando recebidos, o que pode economizar um pouco de largura de banda.

Mas, a menos que você esteja fazendo isso em uma versão moderna com somente $match e $project , o "custo" de processamento no servidor superará em muito o "ganho" de reduzir a sobrecarga da rede eliminando primeiro os elementos incompatíveis.

Em todos os casos, você obtém o mesmo resultado:
{
        "_id" : ObjectId("56f277b1279871c20b8b4567"),
        "stores" : [
                {
                        "_id" : ObjectId("56f277b5279871c20b8b4783"),
                        "offers" : [
                                {
                                        "_id" : ObjectId("56f277b1279871c20b8b4567"),
                                        "size" : [
                                                "S",
                                                "L",
                                                "XL"
                                        ]
                                }
                        ]
                }
        ]
}