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

Posted

技术标签:

【中文标题】Mongoose 查询过滤数组并填充相关内容【英文标题】:Mongoose Query to filter an array and Populate related content 【发布时间】:2016-07-22 03:17:57 【问题描述】:

我正在尝试查询属性,该属性是对另一个架构的引用和一些附加数据的数组。为了更好地说明,这里是架构:

    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

【问题讨论】:

您能解释一下为什么查询articles.quantity 不起作用吗?您可以在问题中添加任何输出吗? 我删除了错误的答案。您可以检查查询部分是否确实返回了您想要的结果,而无需填充?如果它返回整个数组,则查询中存在问题,因为填充对查询结果起作用。 你的问题和here一样吗? @MarioTrucco 它“有点”是,除了这里的概念是一些数据驻留在另一个集合中,并且希望.populate() 和“过滤”这些结果。我几乎搁置了(实际上我做了,但被删除了),因为这实际上具有“两个”长期存在的答案的元素,但实际上是以它自己的“独特”方式。所以恕我直言,它有点独立。 【参考方案1】:

您需要在此处“投影”匹配项,因为 MongoDB 查询所做的所有工作都是查找具有 “至少一个元素” 的“文档”,该元素 “大于”你要求的条件。

所以过滤一个“数组”和你的“查询”条件是不一样的。

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

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" 数组中的 一个 元素。

要正确执行此操作,您需要.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() 的一个方面是它可以“改变”文档结构,因此 mongoose “假定”就是这种情况,只返回一个“普通对象”。

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

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

现在您调用Model.populate(),然后它可以在“猫鼬文档数组”上运行进一步的“人口”。

结果就是你想要的。


MongoDB 3.2.x 以前的版本

这里唯一真正改变的是聚合管道,为了简洁起见,这就是所有需要包含的内容。

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() 可以很高兴地这样做,因为它只是另一个“查询”并且为了方便而填充“文档”。

因此,如果您确实没有通过删除原始文档数组中的其他数组成员来节省“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            
    
)

【讨论】:

解决方案使用"$filter"的代码有一些错误,即:Error: Arguments must be aggregate pipeline operators @uglycode 这意味着您没有 MongoDB 3.2 或更高版本。其他内容适合您。 谢谢。不过,我相信 符号存在一些问题,没有正确关闭或关闭太多......我收到错误:` 91,17: Expected '' to match '' from line 85 而看到的是 ' '.` 我认为这是所有可能的解决方案。 @uglycode 非常简单的语法错误。已更正。抱歉,在解释这样一个高级概念时,由于缺少一两个右括号,我在语法上并不完全正确。并在此处直接输入问题而不是 IDE。有些人可能会说“谢谢”花时间解释这些概念。 我不想表现得忘恩负义,我只是说代码中存在语法错误,如果其他人可能会找到此线程并复制/粘贴您的解决方案。我当然感谢您的努力和时间。我会接受你的回答。【参考方案2】:

exports.getStudentBy = async (req, res) => 
  try 
    // get search_criteria from query parameter
    // build a query object with it
    // send data to the frontend

    const  search_field, search_value  = req.query;

    const queryObj = ;

    if (search_field !== '' && search_value !== '') 
      queryObj[search_field] = search_value;
    

    const student = await Student.find(queryObj);

【讨论】:

以上是关于Mongoose 查询过滤数组并填充相关内容的主要内容,如果未能解决你的问题,请参考以下文章

Mongoose 先填充,然后根据填充的字段进行过滤

MongoDB Mongoose 聚合查询深度嵌套数组删除空结果并填充引用

Mongoose:如果值存在于对象数组中的数组中,则过滤数据

猫鼬自己填充自定义查询

Mongoose 查询其中 X 在两个数组中并且 Y 仅在一个数组中

Mongoose 查询其中 X 在两个数组中并且 Y 仅在一个数组中