MongoDB
 sql >> Database >  >> NoSQL >> MongoDB

Mongoose Query per filtrare un array e popolare contenuto correlato

Devi "proiettare" la corrispondenza qui poiché tutto ciò che fa la query MongoDB è cercare un "documento" che abbia "almeno un elemento" ovvero "maggiore di" la condizione che hai chiesto.

Quindi filtrare un "array" non è lo stesso della condizione di "query" che hai.

Una semplice "proiezione" riporterà semplicemente il "primo" elemento abbinato a quella condizione. Quindi probabilmente non è quello che vuoi, ma ad esempio:

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
    }
)

Quella "specie" fa quello che vuoi, ma il problema sarà davvero che tornerà sempre e solo al massimo uno elemento all'interno degli "articles" matrice.

Per farlo correttamente hai bisogno di .aggregate() per filtrare il contenuto dell'array. Idealmente questo viene fatto con MongoDB 3.2 e $filter . Ma c'è anche un modo speciale per .populate() qui:

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
            }
        )
    }
)

Quindi quello che succede qui è che l'effettivo "filtraggio" dell'array avviene all'interno di .aggregate() istruzione, ma ovviamente il risultato di questo non è più un "documento mangusta" perché un aspetto di .aggregate() è che può "alterare" la struttura del documento, e per questo motivo mangusta "presume" che sia così e restituisce semplicemente un "oggetto semplice".

Non è davvero un problema, dato che quando vedi il $project fase, stiamo effettivamente chiedendo tutti gli stessi campi presenti nel documento secondo lo schema definito. Quindi, anche se è solo un "oggetto semplice", non ci sono problemi a "trascinarlo" in un documento mangusta.

Qui è dove il .map() entra, poiché restituisce un array di "documenti" convertiti, che è quindi importante per la fase successiva.

Ora chiami Model.populate() che può quindi eseguire l'ulteriore "popolazione" sulla "matrice di documenti mangusta".

Il risultato quindi è finalmente quello che vuoi.

Versioni MongoDB precedenti alla 3.2.x

Le uniche cose che cambiano davvero qui sono la pipeline di aggregazione, quindi è tutto ciò che deve essere incluso per brevità.

MongoDB 2.6 - Può filtrare gli array con una combinazione di $map e $setDifference . Il risultato è un "set" ma non è un problema quando mongoose crea un _id campo su tutti gli array di documenti secondari per impostazione predefinita:

    [
        { "$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
        }}
    ],

Le revisioni precedenti devono utilizzare $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" }
        }}
    ],

L'alternativa $lookup

Un'altra alternativa è semplicemente fare tutto sul "server". Questa è un'opzione con $lookup di MongoDB 3.2 e versioni successive:

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 sebbene quelli siano solo semplici documenti, sono solo gli stessi risultati di quelli che avresti ottenuto da .populate() approccio. E ovviamente puoi sempre andare a "trasmettere" i documenti di mangusta in tutti i casi, se proprio devi.

Il sentiero "più breve"

Questo risale davvero all'affermazione originale in cui in pratica si "accetta" che la "query" non ha lo scopo di "filtrare" il contenuto dell'array. Il .populate() può felicemente farlo perché è solo un'altra "interrogazione" e sta riempiendo "documenti" per comodità.

Quindi, se davvero non stai salvando "bucketload" di larghezza di banda rimuovendo i membri dell'array aggiuntivi nell'array del documento originale, allora solo .filter() nel codice di post-elaborazione:

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            
    }
)