如何动态返回同构对象数组中所有属性的总和?

时间:2018-11-14 07:59:54

标签: mongodb aggregation-framework

我有一个具有以下结构的MongoDB集合:

{
  "_id": "5bea815d2791a76283a2747a",
  "salesCategories": [
    "cake",
    "pie",
    "baklava"
  ],
  "sales": [
    {
      "hidden": true,
      "updatedAt": "2018-11-14T04:33:05.703Z",
      "_id": "5beba580b60f1a52755a85ec",
      "date": "2018-11-13T23:57:42.826Z",
      "salesTotals": {
        "cake": 10,
        "pie": 10,
        "baklava": 10
      }
    },
    {
      "hidden": true,
      "updatedAt": "2018-11-14T04:33:06.352Z",
      "_id": "5beba581b60f1a52755a85ed",
      "date": "2018-11-13T23:57:42.826Z",
      "salesTotals": {
        "cake": 10,
        "pie": 10,
        "baklava": 10
      }
    },
    {
      "hidden": false,
      "updatedAt": "2018-11-14T04:33:06.995Z",
      "_id": "5beba582b60f1a52755a85ee",
      "date": "2018-11-15T23:57:42.826Z",
      "salesTotals": {
        "cake": 10,
        "pie": 10,
        "baklava": 10
      }
    },
    {
      "hidden": true,
      "updatedAt": "2018-11-14T04:35:49.212Z",
      "_id": "5beba582b60f1a52755a85ef",
      "date": "2018-11-13T23:57:42.826Z",
      "salesTotals": {
        "cake": 10,
        "pie": 10,
        "baklava": 10
      }
    },
    {
      "hidden": true,
      "updatedAt": "2018-11-14T04:36:19.590Z",
      "_id": "5beba625601d1e53cabbb6d8",
      "date": "2018-11-13T23:57:42.826Z",
      "salesTotals": {
        "cake": 10,
        "pie": 10,
        "baklava": 10
      }
    },
    {
      "hidden": false,
      "updatedAt": "2018-11-14T04:35:42.027Z",
      "_id": "5beba643601d1e53cabbb6d9",
      "date": "2018-11-13T23:57:42.826Z",
      "salesTotals": {
        "cake": 10,
        "pie": 10,
        "baklava": 10
      }
    }
  ],
  "deposits": [],
  "name": "katie 3",
  "cogsPercentage": 0.12,
  "taxPercentage": 0.0975,
  "createdAt": "2018-11-13T07:46:37.955Z",
  "updatedAt": "2018-11-14T04:36:19.647Z",
  "__v": 0
}

salesTotals的属性将与salesCategories的属性匹配,但是根据用户的喜好可能有所不同。因此,这种方法不能直接对每个属性的总和进行硬编码,如此处所示。

我正在尝试使用猫鼬在每种销售类别的salesTotals中获取属性的总计。我还希望不能考虑销售数组中将hidden设置为true或在计算的日期范围之间的对象。使用aggregate()时,我已经弄清了最后两个要求,但是我不知道如何在整个数组中动态求和这些对象的所有内容。

这是我希望所需的输出看起来像的样子:

{
  "result": {
    "cake": 60,
    "pie": 60,
    "baklava": 60
  }
}

我正在运行mongo 4.0.2和mongoose 5.12.16。

1 个答案:

答案 0 :(得分:1)

与“命名键”一起使用的主键实际上并不事先知道这些键的名称,这是使用$objectToArray,它将对象转换为“键/值”对作为元素可以实际使用它们的方式排列数组。这是MongoDB 3.4的更高版本中增加的MongoDB的现代功能,当然还有所有当前的将来版本。

有几种改变复杂度和性能的方法。

现代归约数组

db.collection.aggregate([
  { "$project": {
    "sales": {
      "$reduce": {
        "input": {
          "$map": {
            "input": {
              "$filter": {
                "input": "$sales",
                "cond": { "$not": "$$this.hidden" }
              }
            },
            "in": { "$objectToArray": "$$this.salesTotals" }
          }
        },
        "initialValue": [],
        "in": { "$concatArrays": [ "$$value", "$$this" ] }
      }
    }

  }},
  { "$unwind": "$sales" },
  { "$group": {
    "_id": "$sales.k",
    "v": { "$sum": "$sales.v" }
  }},
  { "$group": {
    "_id": null,
    "data": { "$push": { "k": "$_id", "v": "$v" } }
  }},
  { "$replaceRoot": {
    "newRoot": { "$arrayToObject": "$data" }
  }}
])

使用$objectToArray并使用$arrayToObject进行转换,因此实际上所有代码都不需要“硬编码”要累积的命名密钥。

$filter本质上删除了hidden值,而$map仅变换了您需要的内容。 $reduce可以走得更远,但是要累积在文档中,您以后仍然需要$unwind

当然,如果您只是说“每个文档”,那么您可以进一步调整$reduce

db.collection.aggregate([
  { "$replaceRoot": {
    "newRoot": {
      "$mergeObjects": [
        { "_id": "$_id" },
        {
          "$arrayToObject": {
            "$reduce": {
              "input": {
                "$reduce": {
                  "input": {
                    "$map": {
                      "input": {
                        "$filter": {
                          "input": "$sales",
                          "cond": { "$not": "$$this.hidden" }
                        }
                      },
                      "in": { "$objectToArray": "$$this.salesTotals" }
                    }
                  },
                  "initialValue": [],
                  "in": {
                    "$concatArrays": [ "$$value", "$$this" ]
                  }
                }
              },
              "initialValue": [],
              "in": {
                "$concatArrays": [
                  { "$filter": {
                    "input": "$$value",
                    "as": "val",
                    "cond": { "$ne": [ "$$this.k", "$$val.k" ] }
                  }},
                  [{ 
                    "k": "$$this.k",
                    "v": {
                      "$cond": {
                        "if": { "$in": [ "$$this.k", "$$value.k" ] },
                        "then": {
                          "$sum": [
                            { "$arrayElemAt": [
                              "$$value.v",
                              { "$indexOfArray": [ "$$value.k", "$$this.k" ] }
                            ]},
                            "$$this.v"
                          ]
                        },
                        "else": "$$this.v"
                      }
                    }
                  }]
                ]
              }
            }
          }
        }
      ]
    }
  }}
])

相同的动态键名,但只是每个文档完成一次,在这种情况下,您根本不需要$unwind

没有$ reduce

当然,您总是可以在传统上做这种事情:

db.collection.aggregate([
   { "$project": { "sales": "$sales" } },
   { "$unwind": "$sales" },
   { "$match": {
     "sales.hidden": { "$ne": true }
   }},
   { "$project": {
     "sales": { "$objectToArray": "$sales.salesTotals" }
   }},
   { "$unwind": "$sales" },
   { "$group": {
     "_id": "$sales.k",
     "v": { "$sum": "$sales.v" }
   }},
   { "$group": {
     "_id": null,
     "data": { "$push": { "k": "$_id", "v": "$v" } }
   }},
   { "$replaceRoot": {
     "newRoot": { "$arrayToObject": "$data" }
   }}
])

它看起来并不复杂,但是它要经过很多阶段才能得出结果。因此,您不是$filter而是$unwind,而是$match,而不是$map只是对所需属性进行了$project

因为每个$unwind都将这些数组分开,所以不需要在文档中串联数组。

总的来说,它可能很简单易读,但是随着集合的增加,执行开销会大大增加。

“单个文档”格式也是如此:

db.collection.aggregate([
   { "$project": { "sales": "$sales" } },
   { "$unwind": "$sales" },
   { "$match": {
     "sales.hidden": { "$ne": true }
   }},
   { "$project": {
     "sales": { "$objectToArray": "$sales.salesTotals" }
   }},
   { "$unwind": "$sales" },
   { "$group": {
     "_id": {
       "_id": "$_id",
       "k": "$sales.k"
     },
     "v": { "$sum": "$sales.v" }
   }},
   { "$group": {
     "_id": "$_id._id",
     "data": { "$push": { "k": "$_id.k", "v": "$v" } }
   }},
   { "$replaceRoot": {
     "newRoot": {
       "$mergeObjects": [
         { "_id": "$_id" },
         { "$arrayToObject": "$data" }
       ]
     }
   }}
])

$group阶段的末尾只有很小的变化,并且在重建键时,最终保留了文档的_id值。


当然结果与预期的一样:

{ 
  "baklava" : 20,
  "pie" : 20,
  "cake" : 20
}

或每个文档(您仅提供了一个):

{
    "_id" : "5bea815d2791a76283a2747a",
    "cake" : 20,
    "pie" : 20,
    "baklava" : 20
}

后一种形式至少向您显示的一件事是从学习的角度来看,一次简单地添加一个管道阶段并查看每个阶段如何通过实际进行的更改来影响结果要容易得多。

把初始形式分开看可能要复杂一些,但是如果您花时间看每个部分,您最终应该看到它们如何组合在一起。


备用mapReduce

尽管您无法获得与聚合框架相同的性能,但是如果您在3.4发行版之前拥有MongoDB,则可以始终使用mapReduce

db.collection.mapReduce(
  function() {
    this.sales.forEach(s => {
      if (!s.hidden)
        emit(null, s.salesTotals);
    })
  },
  function(key,values) {
    var obj = {};

    values.forEach(value =>
      Object.keys(value).forEach(k => {
        if (!obj.hasOwnProperty(k))
          obj[k] = 0;
        obj[k] += value[k];
      })
    )

    return obj;
  },
  { out: { inline: 1 } }
)

输出有所不同,因为mapReduce具有严格的“键/值”输出形式:

    {
        "_id" : null,
        "value" : {
                "cake" : 20,
                "pie" : 20,
                "baklava" : 20
        }
    }

“每个文档”,只需用当前文档null的值替换emit()中的_id

db.collection.mapReduce(
  function() {
    var id = this._id;
    this.sales.forEach(s => {
      if (!s.hidden)
        emit(id, s.salesTotals);
    })
  },
  function(key,values) {
    var obj = {};

    values.forEach(value =>
      Object.keys(value).forEach(k => {
        if (!obj.hasOwnProperty(k))
          obj[k] = 0;
        obj[k] += value[k];
      })
    )

    return obj;
  },
  { out: { inline: 1 } }
)

具有相当明显的结果:

    {
        "_id" : "5bea815d2791a76283a2747a",
        "value" : {
                "cake" : 20,
                "pie" : 20,
                "baklava" : 20
        }
    }

不是那么快,而是一个相当简单的过程,它再次使用Object.keys()作为提取具有“命名键”的作品而又不知道其名称的方法。