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

Consulta do Mongoose para filtrar uma matriz e preencher o conteúdo relacionado


Você precisa "projetar" a correspondência aqui, pois tudo o que a consulta do MongoDB faz é procurar um "documento" que tenha "pelo menos um elemento" que é "maior que" a condição que você pediu.

Portanto, filtrar um "array" não é o mesmo que a condição de "consulta" que você possui.

Uma simples "projeção" apenas retornará o "primeiro" item correspondente a essa condição. Então provavelmente não é o que você quer, mas como exemplo:
Order.find({ "articles.quantity": { "$gte": 5 } })
    .select({ "articles.$": 1 })
    .populate({
        "path": "articles.article",
        "match": { "price": { "$lte": 500 } }
    }).exec(function(err,orders) {
       // populated and filtered twice
    }
)

Esse "mais ou menos" faz o que você quer, mas o problema realmente será que só retornará no máximo um elemento dentro do "articles" variedade.

Para fazer isso corretamente você precisa de .aggregate() para filtrar o conteúdo da matriz. Idealmente, isso é feito com MongoDB 3.2 e $filter . Mas há também uma maneira especial de .populate() aqui:
Order.aggregate(
    [
        { "$match": { "artciles.quantity": { "$gte": 5 } } },
        { "$project": {
            "orderdate": 1,
            "articles": {
                "$filter": {
                    "input": "$articles",
                    "as": "article",
                    "cond": {
                       "$gte": [ "$$article.quantity", 5 ]
                    }
                }
            },
            "__v": 1
        }}
    ],
    function(err,orders) {
        Order.populate(
            orders.map(function(order) { return new Order(order) }),
            {
                "path": "articles.article",
                "match": { "price": { "$lte": 500 } }
            },
            function(err,orders) {
                // now it's all populated and mongoose documents
            }
        )
    }
)

Então, o que acontece aqui é que a "filtragem" real do array acontece dentro do .aggregate() declaração, mas é claro que o resultado disso não é mais um "documento mangusto" porque um aspecto de .aggregate() é que ele pode "alterar" a estrutura do documento e, por esse motivo, o mangusto "presume" que é o caso e apenas retorna um "objeto simples".

Isso não é realmente um problema, pois quando você vê o $project estágio, na verdade estamos solicitando todos os mesmos campos presentes no documento de acordo com o esquema definido. Portanto, mesmo que seja apenas um "objeto simples", não há problema em "lançá-lo" de volta em um documento mangusto.

É aqui que o .map() entra, pois retorna uma matriz de "documentos" convertidos, o que é importante para o próximo estágio.

Agora você chama Model.populate() que pode então executar a "população" adicional na "matriz de documentos do mangusto".

O resultado então é finalmente o que você quer.

Versões mais antigas do MongoDB que 3.2.x


As únicas coisas que realmente mudam aqui são o pipeline de agregação, então isso é tudo o que precisa ser incluído para brevidade.

MongoDB 2.6 - Pode filtrar arrays com uma combinação de $map e $setDifference . O resultado é um "conjunto", mas isso não é um problema quando o mangusto cria um _id campo em todas as matrizes de subdocumento por padrão:
    [
        { "$match": { "artciles.quantity": { "$gte": 5 } } },
        { "$project": {
            "orderdate": 1,
            "articles": {
                "$setDiffernce": [
                   { "$map": {
                      "input": "$articles",
                      "as": "article",
                      "in": {
                         "$cond": [
                             { "$gte": [ "$$article.price", 5 ] },
                             "$$article",
                             false
                         ]
                      }
                   }},
                   [false]
                ]
            },
            "__v": 1
        }}
    ],

Revisões mais antigas devem usar $unwind :
    [
        { "$match": { "artciles.quantity": { "$gte": 5 } }},
        { "$unwind": "$articles" },
        { "$match": { "artciles.quantity": { "$gte": 5 } }},
        { "$group": {
          "_id": "$_id",
          "orderdate": { "$first": "$orderdate" },
          "articles": { "$push": "$articles" },
          "__v": { "$first": "$__v" }
        }}
    ],

A alternativa $lookup


Outra alternativa é fazer tudo no "servidor". Esta é uma opção com $lookup do MongoDB 3.2 e superior:
Order.aggregate(
    [
        { "$match": { "artciles.quantity": { "$gte": 5 } }},
        { "$project": {
            "orderdate": 1,
            "articles": {
                "$filter": {
                    "input": "$articles",
                    "as": "article",
                    "cond": {
                       "$gte": [ "$$article.quantity", 5 ]
                    }
                }
            },
            "__v": 1
        }},
        { "$unwind": "$articles" },
        { "$lookup": {
            "from": "articles",
            "localField": "articles.article",
            "foreignField": "_id",
            "as": "articles.article"
        }},
        { "$unwind": "$articles.article" },
        { "$group": {
          "_id": "$_id",
          "orderdate": { "$first": "$orderdate" },
          "articles": { "$push": "$articles" },
          "__v": { "$first": "$__v" }
        }},
        { "$project": {
            "orderdate": 1,
            "articles": {
                "$filter": {
                    "input": "$articles",
                    "as": "article",
                    "cond": {
                       "$lte": [ "$$article.article.price", 500 ]
                    }
                }
            },
            "__v": 1
        }}
    ],
    function(err,orders) {

    }
)

E embora esses sejam apenas documentos simples, são os mesmos resultados que você obteria do .populate() abordagem. E é claro que você sempre pode ir e "lançar" para documentos do mangusto em todos os casos novamente, se realmente precisar.

O caminho "mais curto"


Isso realmente volta para a declaração original onde você basicamente apenas "aceita" que a "consulta" não se destina a "filtrar" o conteúdo da matriz. O .populate() felizmente pode fazê-lo porque é apenas mais uma "consulta" e está enchendo "documentos" por conveniência.

Então, se você realmente não está salvando "bucketloads" de largura de banda pela remoção de membros adicionais do array no array do documento original, então apenas .filter() -los no código de pós-processamento:
Order.find({ "articles.quantity": { "$gte": 5 } })
    .populate({
        "path": "articles.article",
        "match": { "price": { "$lte": 500 } }
    }).exec(function(err,orders) {
        orders = orders.filter(function(order) {
            order.articles = order.articles.filter(function(article) {
                return (
                    ( article.quantity >= 5 ) &&
                    ( article.article != null )
                )
            });
            return order.aricles.length > 0;
        })

        // orders has non matching entries removed            
    }
)