mongodb - 如何动态返回同质对象数组中所有属性的总和?
问题描述
我有一个具有这种结构的 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 的属性相匹配,但可能或多或少取决于用户的偏好。因此,该方法不能直接硬编码每个属性的总和,如此处所示。
我正在尝试使用 Mongoose 获取每个销售类别的 salesTotals 中的属性总数。我还希望能够不考虑 sales 数组中已hidden
设置为true
或在计算日期范围之间的对象。使用 时,我已经弄清楚了最后两个要求aggregate()
,但我不知道如何动态地对整个数组中这些对象的所有内容求和。
这是我希望所需输出的样子:
{
"result": {
"cake": 60,
"pie": 60,
"baklava": 60
}
}
我正在运行 mongo 4.0.2 和 mongoose 5.12.16。
解决方案
使用“命名键”的主要关键是您实际上并不知道这些键的名称是什么,$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
in替换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()
用作在不知道其名称的情况下使用“命名键”提取作品的方式。
推荐阅读
- javascript - 如何将 JSON 数据显示为 HTML?
- php - 在 while 语句中使用 div 的正确方法?
- apache-spark - Spark 仓库 VS Hive 仓库
- facebook - Facebook private_replies 返回 (#200) 该页面没有 READ_PAGE_MAILBOXES 或 PAGES_MESSAGING 权限
- laravel - 调用未定义的方法 Illuminate\Database\Query\Builder::tags()
- c# - 重定向到部署在 MVC 应用程序中的 IIS 中的虚拟 Web 窗体应用程序中的页面时出现问题?
- javascript - 数组 indexOf 检查对象是否相等
- python - 写锁定的文件有时找不到内容(打开腌制的熊猫数据帧时)-EOFError: Ran out of input
- django - 将子文件夹请求重定向到乘客
- java - Selenium - 从 angularjs 组件中选择一个输入