web-dev-qa-db-ja.com

配列をフィルタリングし、関連するコンテンツを入力するためのMongooseクエリ

別のスキーマへの参照といくつかの追加データの両方の配列であるプロパティをクエリしようとしています。より明確にするために、ここにスキーマがあります:

    var orderSchema = new Schema({
        orderDate: Date,
        articles: [{
            article: {
                type: Schema.Types.ObjectId,
                ref: 'Article'
            },
            quantity: 'Number'
        }]
    }),
    Order = mongoose.model('Order', orderSchema);

参照を正常にクエリすることができましたが、つまり:

Order.find({}).populate('articles.article', null, {
    price: {
        $lte: 500
    }
}).exec(function(err, data) {
    for (var order of data) {
        for (var article of order.articles) {
            console.log(article);
        }
    }
});

quantity属性のクエリに問題があります。つまり、これは機能しません。

Order.find({}).where({
    'articles.quantity': {
        $gte: 5
    }
}).populate('articles.article', null, {
    /*price: {
        $lte: 500
    }*/
}).exec(function(err, data) {
    for (var order of data) {
        for (var article of order.articles) {
            console.log(article);
        }
    }
});

quantityに基づいてクエリを実行することもできますか?もしそうなら、何が最善のアプローチでしょうか?

ありがとうございました!

更新:

問題は、結果が完全な配列であるか、何もないことです(更新された質問を参照)。 5と同じかそれ以上の数のレコードのみを取得したいと思います。あなた(と私の)のアプローチでは、レコードがまったくない($ gte:5001を設定した場合)か、両方のレコード($ gteを設定した場合)を取得します。 5000)

{
    "_id": ObjectId('56fe76c12f7174ac5018054f'),
    "orderDate": ISODate('2016-04-01T13:25:21.055Z'),
    "articles": [
        {
            "article": ObjectId('56fe76c12f7174ac5018054b'),
            "quantity": 5000,
            "_id": ObjectId('56fe76c12f7174ac50180551')
        },
        {
            "article": ObjectId('56fe76c12f7174ac5018054c'),
            "quantity": 1,
            "_id": ObjectId('56fe76c12f7174ac50180552')
        }
    ],
    "__v": 1
}
8
uglycode

MongoDBクエリが行う「ドキュメント」は「少なくとも1つの要素」、つまりである「ドキュメント」を探すため、ここで一致を「投影」する必要があります。 )「より大きい」要求した状態。

したがって、「配列」のフィルタリングは、「クエリ」条件と同じではありません。

単純な「投影」では、「最初に」一致したアイテムをその条件に戻すだけです。したがって、おそらくあなたが望むものではありませんが、例として:

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

その「並べ替え」はあなたが望むことを行いますが、問題は実際には"articles"配列内の多くてもone要素しか返さないことです。

これを適切に行うには、.aggregate()で配列の内容をフィルタリングする必要があります。理想的には、これはMongoDB 3.2と $filter で行われます。しかし、ここにも.populate()への特別な方法があります:

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

したがって、ここで行われるのは、配列の実際の「フィルタリング」が.aggregate()ステートメント内で行われることですが、もちろん、.aggregate()の1つの側面は、ドキュメント構造を「変更」できるため、マングースはそれを「想定」し、単に「プレーンオブジェクト」を返します。

$projectステージが表示されたとき、実際には、定義されたスキーマに従ってドキュメントに存在する同じフィールドすべてを要求しているので、それは実際には問題ではありません。したがって、単なる「プレーンオブジェクト」であっても、マングースドキュメントに「キャスト」しても問題はありません。

これが.map()の出番です。変換された「ドキュメント」の配列を返すため、次のステージで重要になります。

ここで Model.populate() を呼び出すと、「マングースドキュメントの配列」でさらに「母集団」を実行できます。

結果は最終的にあなたが望むものです。


3.2.xよりも古いバージョンのMongoDB

ここで実際に変化するのは集約パイプラインだけなので、簡潔にするために含める必要があるのはそれだけです。

MongoDB 2.6- $map$setDifference の組み合わせで配列をフィルタリングできます。結果は「セット」ですが、mongooseがデフォルトですべてのサブドキュメント配列に_idフィールドを作成する場合は問題ありません。

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

それより古いリビジョンでは $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" }
        }}
    ],

$ lookupの代替

別の方法は、代わりに「サーバー」ですべてを実行することです。これは、MongoDB 3.2以降の $lookup を使用するオプションです。

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

    }
)

これらは単なるプレーンドキュメントですが、.populate()アプローチから得られる結果と同じです。もちろん、本当に必要な場合は、いつでも、いつでもマングースドキュメントに「キャスト」して再びマングースドキュメントにキャストできます。

「最短」パス

これは本当に元のステートメントに戻り、基本的には、「クエリ」は配列のコンテンツを「フィルタリング」することを目的としていないことを「受け入れる」だけです。 .populate()は幸いにもそうすることができます。これは単なる「クエリ」であり、便宜上「ドキュメント」に詰め込まれているためです。

したがって、元のドキュメント配列から追加の配列メンバーを削除して、帯域幅の「バケットロード」を本当に保存していない場合は、後処理コードでそれらを.filter()にしてください。

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            
    }
)
3
Blakes Seven