Mongoose Query过滤数组并填充相关内容

时间:2022-05-10 18:18:21

I'm trying to query the property that is an array of both reference to another schema and some additional data. For better clarification, here's the schema:

我正在尝试查询属性,该属性是对另一个模式和一些其他数据的引用数组。为了更好地澄清,这是架构:

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

While I managed to successfully query the reference, i.e.:

虽然我设法成功查询参考,即:

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

I have some issues querying the quantity attribute, i.e. this doesn't work:

我有一些问题查询数量属性,即这不起作用:

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

Is it even possible to base the query on quantity? And if so, what would be the best approach?

甚至可以将查询基于数量吗?如果是这样,最好的方法是什么?

Thank you!

谢谢!

UPDATE:

更新:

The problem is, the result is either a complete array, or nothing (see updated question). I want to get only those records that have quantity more or the same as 5. With your (and mine) approach I get either no records at all (if I set $gte: 5001) or both records (if I set $gte:5000)

问题是,结果是一个完整的数组,或者什么都没有(请参阅更新的问题)。我想只获得那些数量大于或等于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
}

1 个解决方案

#1


3  

You need to "project" the match here since all the MongoDB query does is look for a "document" that has "at least one element" that is "greater than" the condition you asked for.

您需要在此处“预测”匹配项,因为所有MongoDB查询都会查找“文档”,其中“至少有一个元素”“大于”您要求的条件。

So filtering an "array" is not the same as the "query" condition you have.

因此,过滤“数组”与您拥有的“查询”条件不同。

A simple "projection" will just return the "first" matched item to that condtion. So it's probably not what you want, but as an example:

一个简单的“投影”只会将“第一个”匹配的项目返回到该条件。所以它可能不是你想要的,但作为一个例子:

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

That "sort of" does what you want, but the problem is really going to be that will only ever return at most one element within the "articles" array.

那种“有点”可以做你想要的,但问题实际上只会返回“articles”数组中的最多一个元素。

To do this properly you need .aggregate() to filter the array content. Ideally this is done with MongoDB 3.2 and $filter. But there is also a special way to .populate() here:

要正确执行此操作,您需要.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
            }
        )
    }
)

So what happens here is the actual "filtering" of the array happens within the .aggregate() statement, but of course the result from this is no longer a "mongoose document" because one aspect of .aggregate() is that it can "alter" the document structure, and for this reason mongoose "presumes" that is the case and just returns a "plain object".

所以这里发生的是数组的实际“过滤”发生在.aggregate()语句中,但当然结果不再是“mongoose文档”,因为.aggregate()的一个方面是它可以“改变“文档结构,因此mongoose”假设“就是这种情况,只返回一个”普通对象“。

That's not really a problem, since when you see the $project stage, we are actually asking for all of the same fields present in the document according to the defined schema. So even though it's just a "plain object" there is no problem "casting" it back into an mongoose document.

这不是一个真正的问题,因为当你看到$ project阶段时,我们实际上是根据定义的模式要求文档中存在的所有相同字段。因此即使它只是一个“普通对象”,也没有问题将其“转换”回到一个mongoose文档中。

This is where the .map() comes in, as it returns an array of converted "documents", which is then important for the next stage.

这是.map()的用武之地,因为它返回一个转换后的“文档”数组,这对下一阶段很重要。

Now you call Model.populate() which can then run the further "population" on the "array of mongoose documents".

现在,您调用Model.populate(),然后可以在“mongoose文档数组”上运行更多“填充”。

The result then is finally what you want.

结果最终是你想要的。


MongoDB older versions than 3.2.x

The only things that really change here are the aggregation pipeline, So that is all that needs to be included for brevity.

这里真正改变的唯一事情是聚合管道,所以为了简洁,所有这些都需要包含在内。

MongoDB 2.6 - Can filter arrays with a combination of $map and $setDifference. The result is a "set" but that is not a problem when mongoose creates an _id field on all sub-document arrays by default:

MongoDB 2.6 - 可以使用$ map和$ setDifference的组合过滤数组。结果是“set”但是当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
        }}
    ],

Older revisions of than that must use $unwind:

旧的修订版必须使用$ 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" }
        }}
    ],

The $lookup Alternative

Another alternate is to just do everything on the "server" instead. This is an option with $lookup of MongoDB 3.2 and greater:

另一种替代方法是在“服务器”上执行所有操作。这是一个使用$ lookup查找MongoDB 3.2及更高版本的选项:

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

    }
)

And though those are just plain documents, it's just the same results as what you would have got from the .populate() approach. And of course you can always go and "cast" to mongoose documents in all cases again if you really must.

虽然这些只是简单的文档,但它与.populate()方法的结果相同。当然,如果你真的必须的话,你可以在所有情况下再次“演员”到mongoose文件。

The "shortest" Path

This really goes back to the orginal statement where you basically just "accept" that the "query" is not meant to "filter" the array content. The .populate() can happilly do so becuse it's just another "query" and is stuffing in "documents" by convenience.

这实际上可以追溯到原始声明,您基本上只是“接受”“查询”并不意味着“过滤”数组内容。 .populate()可以高兴地这样做,因为它只是另一个“查询”,并且方便地填充在“文档”中。

So if you really are not saving "bucketloads" of bandwith by the removal of additional array members in the orginal document array, then just .filter() them out in post processing code:

因此,如果您真的没有通过删除原始文档数组中的其他数组成员来保存bandwith的“bucketloads”,那么只需.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            
    }
)

#1


3  

You need to "project" the match here since all the MongoDB query does is look for a "document" that has "at least one element" that is "greater than" the condition you asked for.

您需要在此处“预测”匹配项,因为所有MongoDB查询都会查找“文档”,其中“至少有一个元素”“大于”您要求的条件。

So filtering an "array" is not the same as the "query" condition you have.

因此,过滤“数组”与您拥有的“查询”条件不同。

A simple "projection" will just return the "first" matched item to that condtion. So it's probably not what you want, but as an example:

一个简单的“投影”只会将“第一个”匹配的项目返回到该条件。所以它可能不是你想要的,但作为一个例子:

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

That "sort of" does what you want, but the problem is really going to be that will only ever return at most one element within the "articles" array.

那种“有点”可以做你想要的,但问题实际上只会返回“articles”数组中的最多一个元素。

To do this properly you need .aggregate() to filter the array content. Ideally this is done with MongoDB 3.2 and $filter. But there is also a special way to .populate() here:

要正确执行此操作,您需要.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
            }
        )
    }
)

So what happens here is the actual "filtering" of the array happens within the .aggregate() statement, but of course the result from this is no longer a "mongoose document" because one aspect of .aggregate() is that it can "alter" the document structure, and for this reason mongoose "presumes" that is the case and just returns a "plain object".

所以这里发生的是数组的实际“过滤”发生在.aggregate()语句中,但当然结果不再是“mongoose文档”,因为.aggregate()的一个方面是它可以“改变“文档结构,因此mongoose”假设“就是这种情况,只返回一个”普通对象“。

That's not really a problem, since when you see the $project stage, we are actually asking for all of the same fields present in the document according to the defined schema. So even though it's just a "plain object" there is no problem "casting" it back into an mongoose document.

这不是一个真正的问题,因为当你看到$ project阶段时,我们实际上是根据定义的模式要求文档中存在的所有相同字段。因此即使它只是一个“普通对象”,也没有问题将其“转换”回到一个mongoose文档中。

This is where the .map() comes in, as it returns an array of converted "documents", which is then important for the next stage.

这是.map()的用武之地,因为它返回一个转换后的“文档”数组,这对下一阶段很重要。

Now you call Model.populate() which can then run the further "population" on the "array of mongoose documents".

现在,您调用Model.populate(),然后可以在“mongoose文档数组”上运行更多“填充”。

The result then is finally what you want.

结果最终是你想要的。


MongoDB older versions than 3.2.x

The only things that really change here are the aggregation pipeline, So that is all that needs to be included for brevity.

这里真正改变的唯一事情是聚合管道,所以为了简洁,所有这些都需要包含在内。

MongoDB 2.6 - Can filter arrays with a combination of $map and $setDifference. The result is a "set" but that is not a problem when mongoose creates an _id field on all sub-document arrays by default:

MongoDB 2.6 - 可以使用$ map和$ setDifference的组合过滤数组。结果是“set”但是当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
        }}
    ],

Older revisions of than that must use $unwind:

旧的修订版必须使用$ 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" }
        }}
    ],

The $lookup Alternative

Another alternate is to just do everything on the "server" instead. This is an option with $lookup of MongoDB 3.2 and greater:

另一种替代方法是在“服务器”上执行所有操作。这是一个使用$ lookup查找MongoDB 3.2及更高版本的选项:

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

    }
)

And though those are just plain documents, it's just the same results as what you would have got from the .populate() approach. And of course you can always go and "cast" to mongoose documents in all cases again if you really must.

虽然这些只是简单的文档,但它与.populate()方法的结果相同。当然,如果你真的必须的话,你可以在所有情况下再次“演员”到mongoose文件。

The "shortest" Path

This really goes back to the orginal statement where you basically just "accept" that the "query" is not meant to "filter" the array content. The .populate() can happilly do so becuse it's just another "query" and is stuffing in "documents" by convenience.

这实际上可以追溯到原始声明,您基本上只是“接受”“查询”并不意味着“过滤”数组内容。 .populate()可以高兴地这样做,因为它只是另一个“查询”,并且方便地填充在“文档”中。

So if you really are not saving "bucketloads" of bandwith by the removal of additional array members in the orginal document array, then just .filter() them out in post processing code:

因此,如果您真的没有通过删除原始文档数组中的其他数组成员来保存bandwith的“bucketloads”,那么只需.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            
    }
)