仅在匹配时返回子文档,但保留空数组

时间:2016-04-19 13:19:31

标签: mongodb mongodb-query aggregation-framework

我有一个包含以下文档的集合:

{
    "_id": ObjectId("57065ee93f0762541749574e"),
    "name": "myName",
    "results" : [ 
        {
            "_id" : ObjectId("570e3e43628ba58c1735009b"),
            "color" : "GREEN",
            "week" : 17,
            "year" : 2016
        }, 
        {
            "_id" : ObjectId("570e3e43628ba58c1735009d"),
            "color" : "RED",
            "week" : 19,
            "year" : 2016
        }
    ]
}

我正在尝试建立一个查询,让我退回我收藏的所有文件,但只选择字段'结果'如果周>使用子文档X和年份>收率

我可以选择周的文件> X和年份> Y具有聚合函数和$匹配,但我错过了没有匹配的文档。 到目前为止,这是我的功能:

query = ModelUser.aggregate(
    {$unwind:{path:'$results', preserveNullAndEmptyArrays:true}},
    {$match:{
        $or: [
            {$and:[
                {'results.week':{$gte:parseInt(week)}},
                {'results.year':{$eq:parseInt(year)}}
            ]},
            {'results.year':{$gt:parseInt(year)}},
            {'results.week':{$exists: false}}
    {$group:{
        _id: {
            _id:'$_id',
            name: '$name'
        },
        results: {$push:{
            _id:'$results._id',
            color: '$results.color',
            numSemaine: '$results.numSemaine',
            year: '$results.year'
        }}
    }},
    {$project: {
        _id: '$_id._id',
        name: '$_id.name',
        results: '$results'
);

我唯一想念的是:我必须得到所有的名字'即使没有结果显示。

任何想法如何在没有2个查询的情况下执行此操作?

1 个答案:

答案 0 :(得分:1)

看起来你实际上有MongoDB 3.2,所以在数组上使用$filter。这将返回一个“空”数组[],其中提供的条件与任何内容都不匹配:

db.collection.aggregate([
  { "$project": {
    "name": 1,
    "user": 1,
    "results": {
      "$filter": {
        "input": "$results",
        "as": "result",
        "cond": {
          "$and": [
            { "$eq": [ "$$result.year", year ] },
            { "$or": [
              { "$gt": [ "$$result.week", week ] },
              { "$not": { "$ifNull": [ "$$result.week", false ] } }
            ]}
          ]
        }
      }
    }          
  }}
]) 

代替$exists作为逻辑形式的$ifNull测试实际上可以“压缩”条件,因为它返回了不存在属性的备用值,为:

db.collection.aggregate([
  { "$project": {
    "name": 1,
    "user": 1,
    "results": {
      "$filter": {
        "input": "$results",
        "as": "result",
        "cond": {
          "$and": [
            { "$eq": [ "$$result.year", year ] },
            { "$gt": [
                { "$ifNull": [ "$$result.week", week+1 ] },
                week
            ]}
          ]
        }
      }
    }          
  }}
]) 

在MongoDB 2.6版本中,你可以使用$redact$$DESCEND,但当然需要在顶级文档中伪造匹配。这与$ifNull运算符的用法类似:

db.collection.aggregate([
  { "$redact": {
    "$cond": {
      "if": {
        "$and": [
          { "$eq": [{ "$ifNull": [ "$year", year ] }, year ] },
          { "$gt": [
            { "$ifNull": [ "$week", week+1 ] }
            week
          ]}
        ]
      },
      "then": "$$DESCEND",
      "else": "$$PRUNE"
    }
  }}
])

如果您确实拥有MongoDB 2.4,那么您可能最好在客户端代码中过滤数组内容。每种语言都有过滤数组内容的方法,但作为一个可在shell中重现的JavaScript示例:

db.collection.find().forEach(function(doc) {
    doc.results = doc.results.filter(function(result) {
       return (
           result.year == year &&
           ( result.hasOwnProperty('week') ? result.week > week : true )
       )
    ]);
    printjson(doc);
})

原因是在MongoDB 2.6之前,您需要使用$unwind$group以及介于两者之间的各个阶段。这是服务器上“非常昂贵”的操作,考虑到您要做的只是从文档数组中删除项目,而不是实际上从数组中的项目“聚合”。

MongoDB版本已经不遗余力地提供不使用$unwind的数组处理,因为单独使用它不是一个高性能选项。在您从数组中删除“大量”数据的情况下,它应该

重点在于,否则聚合操作的“成本”可能大于通过网络传输数据以在客户端上进行过滤的“成本”。谨慎使用:

db.collection.aggregate([
  // Create an array if one does not exist or is already empty
  { "$project": {
    "name": 1,
    "user": 1,
    "results": {
      "$cond": [
        { "$ifNull": [ "$results.0", false ] },
        "$results",
        [false]
      ]
    }
  }},
  // Unwind the array
  { "$unwind": "$results" },
  // Conditionally $push based on match expression and conditionally count
  { "$group": {
    "_id": "_id",
    "name": { "$first": "$name" },
    "user": { "$first": "$user" },
    "results": {
      "$push": {
        "$cond": [
          { "$or": [
            { "$not": "$results" },
            { "$and": [
              { "$eq": [ "$results.year", year ] },
              { "$gt": [
                { "$ifNull": [ "$results.week", week+1 ] },
                week
              ]}
            ]}
          ] },
          "$results",
          false
        ]
      }
    },
    "count": {
      "$sum": {
        "$cond": [
          { "$and": [
            { "$eq": [ "$results.year", year ] },
            { "$gt": [
              { "$ifNull": [ "$results.week", week+1 ] },
              week
            ]}
          ] }
          1,
          0
        ]
      }
    }
  }},
  // $unwind again
  { "$unwind": "$results" }
  // Filter out false items unless count is 0
  { "$match": { 
    "$or": [
      "$results",
      { "count": 0 }
    ]
  }},
  // Group again
  { "$group": {
    "_id": "_id",
    "name": { "$first": "$name" },
    "user": { "$first": "$user" },
    "results": { "$push": "$results" }
  }},
  // Now swap [false] for []
  { "$project": {
    "name": 1,
    "user": 1,
    "results": {
      "$cond": [
        { "$ne": [ "$results", [false] ] },
        "$results",
        []
      ]
    }
  }}
])

现在,与其他所有非常简单的方法相比,这是一个很大的操作和混乱只是为了“过滤”来自数组的内容。除了复杂性之外,在服务器上执行它确实“花费”更多。

因此,如果您的服务器版本实际上支持可以最佳地执行此操作的较新运算符,则可以这样做。但是如果你坚持使用最后一个进程,那么你可能不应该这样做,而是在客户端进行数组过滤。