就我个人而言,我不太喜欢将“数据”转换为结果中的键名称。聚合框架原则往往一致,因为也不支持此类操作。
因此,个人偏好是将“数据”维护为“数据”,并接受处理后的输出实际上对于一致的对象设计来说更好、更符合逻辑:
db.people.aggregate([
{ "$group": {
"_id": "$sex",
"hobbies": { "$push": "$hobbies" },
"total": { "$sum": 1 }
}},
{ "$unwind": "$hobbies" },
{ "$unwind": "$hobbies" },
{ "$group": {
"_id": {
"sex": "$_id",
"hobby": "$hobbies"
},
"total": { "$first": "$total" },
"hobbyCount": { "$sum": 1 }
}},
{ "$group": {
"_id": "$_id.sex",
"total": { "$first": "$total" },
"hobbies": {
"$push": { "name": "$_id.hobby", "count": "$hobbyCount" }
}
}}
])
产生这样的结果:
[
{
"_id" : "female",
"total" : 1,
"hobbies" : [
{
"name" : "tennis",
"count" : 1
},
{
"name" : "football",
"count" : 1
}
]
},
{
"_id" : "male",
"total" : 2,
"hobbies" : [
{
"name" : "swimming",
"count" : 1
},
{
"name" : "tennis",
"count" : 2
},
{
"name" : "football",
"count" : 2
}
]
}
]
所以最初的$group
计算每个“性别”并将爱好堆叠到一个数组中。然后让你去规范化$unwind
两次获得单一物品,$group
获取每种性别下每种爱好的总数,最后单独为每种性别重新组合一个数组。
它们是相同的数据,具有一致且有机的结构,易于处理,MongoDB 和聚合框架非常乐意生成此输出。
如果您确实必须将数据转换为键名称(我仍然建议您不要这样做,因为这不是设计中遵循的良好模式),那么从最终状态进行这样的转换对于客户端代码处理来说是相当简单的。作为适合 shell 的基本 JavaScript 示例:
var out = db.people.aggregate([
{ "$group": {
"_id": "$sex",
"hobbies": { "$push": "$hobbies" },
"total": { "$sum": 1 }
}},
{ "$unwind": "$hobbies" },
{ "$unwind": "$hobbies" },
{ "$group": {
"_id": {
"sex": "$_id",
"hobby": "$hobbies"
},
"total": { "$first": "$total" },
"hobbyCount": { "$sum": 1 }
}},
{ "$group": {
"_id": "$_id.sex",
"total": { "$first": "$total" },
"hobbies": {
"$push": { "name": "$_id.hobby", "count": "$hobbyCount" }
}
}}
]).toArray();
out.forEach(function(doc) {
var obj = {};
doc.hobbies.sort(function(a,b) { return a.count < b.count });
doc.hobbies.forEach(function(hobby) {
obj[hobby.name] = hobby.count;
});
doc.hobbies = obj;
printjson(doc);
});
然后,您基本上将每个游标结果处理为所需的输出形式,这实际上并不是服务器上真正需要的聚合函数:
{
"_id" : "female",
"total" : 1,
"hobbies" : {
"tennis" : 1,
"football" : 1
}
}
{
"_id" : "male",
"total" : 2,
"hobbies" : {
"tennis" : 2,
"football" : 2,
"swimming" : 1
}
}
将这种操作实现到游标结果的流处理中以根据需要进行转换也应该是相当简单的,因为它基本上是相同的逻辑。
另一方面,您始终可以使用 mapReduce 在服务器上实现所有操作:
db.people.mapReduce(
function() {
emit(
this.sex,
{
"total": 1,
"hobbies": this.hobbies.map(function(key) {
return { "name": key, "count": 1 };
})
}
);
},
function(key,values) {
var obj = {},
reduced = {
"total": 0,
"hobbies": []
};
values.forEach(function(value) {
reduced.total += value.total;
value.hobbies.forEach(function(hobby) {
if ( !obj.hasOwnProperty(hobby.name) )
obj[hobby.name] = 0;
obj[hobby.name] += hobby.count;
});
});
reduced.hobbies = Object.keys(obj).map(function(key) {
return { "name": key, "count": obj[key] };
}).sort(function(a,b) {
return a.count < b.count;
});
return reduced;
},
{
"out": { "inline": 1 },
"finalize": function(key,value) {
var obj = {};
value.hobbies.forEach(function(hobby) {
obj[hobby.name] = hobby.count;
});
value.hobbies = obj;
return value;
}
}
)
其中,mapReduce 具有自己独特的输出风格,但在累积和操作中使用相同的原理,即使效率不如聚合框架:
"results" : [
{
"_id" : "female",
"value" : {
"total" : 1,
"hobbies" : {
"football" : 1,
"tennis" : 1
}
}
},
{
"_id" : "male",
"value" : {
"total" : 2,
"hobbies" : {
"football" : 2,
"tennis" : 2,
"swimming" : 1
}
}
}
]
归根结底,我仍然说第一种处理形式是最有效的,并且在我看来提供了最自然和一致的数据输出工作,甚至不需要尝试将数据点转换为键的名称。最好考虑遵循该模式,但如果您确实必须这样做,那么可以通过各种处理方法将结果操纵为所需的形式。