来自不同领域的有条件$ project

时间:2018-11-23 04:25:12

标签: mongodb mongoose mongodb-query aggregation-framework

不幸的是,大约六个月前,数据结构发生了变化。因此,我有一个以前看起来像...的文件。

{
  fruits: [
    {
      id: 123
    },
    {
      id: 456
    }
  ]
}

(需要注意的是id不是ObjectId BSON类型,它只是客户端生成的随机字符系列。)

...但是现在将id键更改为

{
  fruits: [
    {
      fruit_id: 'xxx'
    },
    {
      fruit_id: 'yyy'
    }
  ]
}

因此,我正在尝试做一个$project,将idfruit_id都更改为general_id之类的通用名称,以便我可以继续进行其他聚合像$group一样,只引用一个字段

我尝试了以下方法:

[
  $unwind: {
    path: '$fruits'
  },
  $project: {
    general_id: {
      $cond: {
        if: {
          'fruits.fruit_id': {
            $type: ['string']
          }
        },
        then: '$fruits.fruit_id',
        else: '$fruits.id'
      }
    }
  }
]

1 个答案:

答案 0 :(得分:1)

这实际上取决于您在此之后的工作,但是对于了解两个可能性的一般情况,最好使用$ifNull返回该字段的值(如果存在),否则返回另一个字段的值。

添加更多数据进行演示,因为您可能不想丢失数组元素中的任何其他内容:

{
  _id: 1,
  fruits: [
    {
      id: 123,
      data: 1
    },
    {
      id: 456,
      data: 2
    }
  ]
},
{
  _id: 2,
  fruits: [
    {
      fruit_id: 'xxx',
      data: 1
    },
    {
      fruit_id: 'yyy',
      data: 2
    }
  ]
},
{
  _id: 3,
  fruits: [
    {
      fruit_id: 'xxx',
      data: 1,
    },
    {
      fruit_id: 'yyy',
      data: 2
    },
    {
      id: 123,
      data: 3
    },
    {
      id: 456,
      data: 4
    }
  ]
}

然后,您可以使用$unwind作为第一步步骤来执行此过程,这确实使路径命名更加容易,尤其是使用$addFields而不是$project

Model.aggregate([
  { "$unwind": "$fruits" },
  { "$addFields": {
    "fruits": {
      "id": "$$REMOVE",
      "fruit_id": "$$REMOVE",
      "general_id": { "$ifNull": [ "$fruits.id", "$fruits.fruit_id" ] }
    }
  }}
])

它使用MongoDB 3.6及更高版本中的$$REMOVE(应该是您使用的最低版本)来“删除”不需要的字段。您不需要这样做,只要没有支持,就可以使用$project声明您真正想要的所有内容。

然后当然还有一个带有$ifNull表达式的替换项。

这将为诸如此类的数据提供结果

{ "_id" : 1, "fruits" : { "data" : 1, "general_id" : 123 } }
{ "_id" : 1, "fruits" : { "data" : 2, "general_id" : 456 } }
{ "_id" : 2, "fruits" : { "data" : 1, "general_id" : "xxx" } }
{ "_id" : 2, "fruits" : { "data" : 2, "general_id" : "yyy" } }
{ "_id" : 3, "fruits" : { "data" : 1, "general_id" : "xxx" } }
{ "_id" : 3, "fruits" : { "data" : 2, "general_id" : "yyy" } }
{ "_id" : 3, "fruits" : { "data" : 3, "general_id" : 123 } }
{ "_id" : 3, "fruits" : { "data" : 4, "general_id" : 456 } }

如果您想$group使用该值,则不需要任何形式的中间“项目”。只需在该阶段直接执行$ifNull

Model.aggregate([
  { "$unwind": "$fruits" },
  { "$group": {
    "_id": { "$ifNull": [ "$fruits.id", "$fruits.fruit_id" ] },
    "count": { "$sum": 1 }
  }}
])

并输出:

{ "_id" : "yyy", "count" : 2 }
{ "_id" : "xxx", "count" : 2 }
{ "_id" : 456, "count" : 2 }
{ "_id" : 123, "count" : 2 }

或者,如果您实际上不需要$unwind用于其他目的的数组,则可以对$objectToArray$arrayToObject使用$map和其他一些操作:

Model.aggregate([
  { "$addFields": {
    "fruits": {
      "$map": {
        "input": "$fruits",
        "in": {
          "$mergeObjects": [
            { "$arrayToObject": {
              "$filter": {
                "input": { "$objectToArray": "$$this" },
                "cond": { "$not": { "$in": [ "$$this.k", ["fruit_id","id"] ] } }
              }
            }},
            {
              "general_id": { "$ifNull": ["$$this.id","$$this.fruit_id"] }
            }
          ]
        }
      }
    }
  }}
])

返回的结果如下:

{
        "_id" : 1,
        "fruits" : [
                {
                        "data" : 1,
                        "general_id" : 123
                },
                {
                        "data" : 2,
                        "general_id" : 456
                }
        ]
}
{
        "_id" : 2,
        "fruits" : [
                {
                        "data" : 1,
                        "general_id" : "xxx"
                },
                {
                        "data" : 2,
                        "general_id" : "yyy"
                }
        ]
}
{
        "_id" : 3,
        "fruits" : [
                {
                        "data" : 1,
                        "general_id" : "xxx"
                },
                {
                        "data" : 2,
                        "general_id" : "yyy"
                },
                {
                        "data" : 3,
                        "general_id" : 123
                },
                {
                        "data" : 4,
                        "general_id" : 456
                }
        ]
}

添加一个$unwind 之后,其返回结果与之前相同。但是,更复杂的操作可能更适合您要将其保留为数组的位置。

这次,我们通过$objectToArray将每个数组元素转换为“键/值”对数组,从而删除了idfruit_id。然后,我们根据这些字段的名称"k" $filter数组。 $arrayToObject再次使该对象成为对象,除了这些字段以外的所有其他内容。

$mergeObjects$map对根“文档”而言$addFields相同,因为它接受多个对象并将它们“合并”在一起。因此,如前所述,“过滤的”对象以及只有general_id键的新对象及其值从存在的任何字段转换而来。

两个以上字段的列表

最后一点,$ifNull$cond的效果更好,在这里您只有两个值,但是如果可能的列表更大,那么两者都不是那么好。您可以嵌套$cond表达式,甚至可以使用$switch,但实际上最好是通过$objectToArray过滤内容,如前所示:

var valid_names = [ "id", "fruit_id", "apple_id", "orange_id" ];

Model.aggregate([
  { "$unwind": "$fruits" },
  { "$group": {
    "_id": {
      "$arrayElemAt": [
        { "$map": {
          "input": {
            "$filter": {
              "input": { "$objectToArray": "$fruits" },
              "cond": { "$in": [ "$$this.k", valid_names ] }
            }
          },
          "in": "$$this.v"
        }},
        0
      ]
    },
    "count": { "$sum": 1 }
  }}  
])

通常最有意义,否则,以动态方式使用这样的列表,最终会在代码中构建聚合管道阶段,例如使用$switch将会是

var valid_names = [ "id", "fruit_id", "apple_id", "orange_id" ];

var branches = valid_names.map(name => 
  ({
    "case": { "$gt": [`$fruits.${name}`, null ] },
    "then": `$fruits.${name}`
  })
)

Model.aggregate([
  { "$unwind": "$fruits" },
  { "$group": {
    "_id": { "$switch": { branches, "default": null } },
    "count": { "$sum": 1 }
  }}
])

在您的代码中看起来更清晰的代码,但实际上在BSON中发送了更大的管道:

[
    { "$unwind" : "$fruits" },
    { "$group" : {
      "_id" : {
        "$switch" : {
          "branches" : [
           { 
             "case" : { "$gt" : [ "$fruits.id", null ] },
             "then" : "$fruits.id"
           },
           {
             "case" : { "$gt" : [ "$fruits.fruit_id", null ] },
             "then" : "$fruits.fruit_id"
           },
           {
             "case" : { "$gt" : [ "$fruits.apple_id", null ] },
             "then" : "$fruits.apple_id"
           },
           {
             "case" : { "$gt" : [ "$fruits.orange_id", null ] },
             "then" : "$fruits.orange_id"
           }
         ],
         "default" : null
        }
      },
      "count" : { "$sum" : 1 }
    }}
]