sql >> Databasteknik >  >> NoSQL >> MongoDB

Mongoose Query för att filtrera en array och fylla i relaterat innehåll

Du måste "projicera" matchningen här eftersom allt MongoDB-frågan gör är att leta efter ett "dokument" som har "minst ett element" det är "större än" villkoret du bad om.

Så att filtrera en "array" är inte detsamma som "query"-villkoret du har.

En enkel "projektion" kommer bara att returnera det "första" matchade objektet till det tillståndet. Så det är nog inte vad du vill ha, utan som ett exempel:

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

Den "typ" gör vad du vill, men problemet kommer verkligen att vara att bara någonsin kommer att återvända högst en element i "articles" array.

För att göra detta korrekt behöver du .aggregate() för att filtrera arrayinnehållet. Helst görs detta med MongoDB 3.2 och $filter . Men det finns också ett speciellt sätt att .populate() här:

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

Så vad som händer här är att själva "filtreringen" av arrayen sker inom .aggregate() uttalande, men resultatet av detta är naturligtvis inte längre ett "mongoose-dokument" eftersom en aspekt av .aggregate() är att den kan "ändra" dokumentstrukturen, och av denna anledning "förutsätter" mongoose att det är fallet och returnerar bara ett "vanligt objekt".

Det är egentligen inget problem, sedan när du ser $project stadiet ber vi faktiskt om alla samma fält som finns i dokumentet enligt det definierade schemat. Så även om det bara är ett "vanligt föremål" är det inga problem att "casta" det tillbaka till ett mangustdokument.

Det är här .map() kommer in, eftersom det returnerar en rad konverterade "dokument", som sedan är viktiga för nästa steg.

Nu anropar du Model.populate() som sedan kan köra den ytterligare "populationen" på "arrayen av mongoose-dokument".

Resultatet är då äntligen vad du vill ha.

MongoDB äldre versioner än 3.2.x

Det enda som verkligen förändras här är aggregeringspipelinen, så det är allt som behöver inkluderas för korthetens skull.

MongoDB 2.6 - Kan filtrera arrayer med en kombination av $map och $setDifference . Resultatet är en "uppsättning" men det är inget problem när mongoose skapar ett _id fältet på alla underdokumentmatriser som standard:

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

Äldre versioner av än så måste använda $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" }
        }}
    ],

Alternativet $lookup

Ett annat alternativ är att bara göra allt på "servern" istället. Detta är ett alternativ med $lookup av MongoDB 3.2 och senare:

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

    }
)

Och även om det bara är vanliga dokument, är det precis samma resultat som du skulle ha fått från .populate() närma sig. Och självklart kan du alltid gå och "casta" till mongoosedokument i alla fall igen om du verkligen måste.

Den "kortaste" vägen

Detta går egentligen tillbaka till det ursprungliga uttalandet där du i princip bara "accepterar" att "frågan" inte är avsedd att "filtrera" arrayinnehållet. .populate() kan med glädje göra det eftersom det bara är ytterligare en "fråga" och stoppar in "dokument" av bekvämlighet.

Så om du verkligen inte sparar "bucketloads" av bandbredd genom att ta bort ytterligare arraymedlemmar i den ursprungliga dokumentarrayen, är det bara .filter() dem ut i efterbehandlingskoden:

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


  1. Pivotera rader till kolumner i MongoDB

  2. Hur man testar selleri med django på en Windows-maskin

  3. Vad är HBase-znoder?

  4. Hur implementerar man Redis i CodeIgniter?