Mongodb map-reduce实战
第1、3种都比较好理解,因为可以类比到我们熟悉的关系型数据库。第一种类似于group by查找,第三种类似于使用使用关系型数据库中自带的如count,distinct等函数做的简单的聚合查找。
第二种其实也不复杂,让我通过最近项目中一个实际应用的例子,来向你展示mongodb map-reduce的应用,你就会发现他的强大之处,往往用来解决第1、3种聚合方式都不能解决的问题,这也是mongodb相比于传统关系型数据库在数据查找方面的过人之处。
这个项目是面向建材行业的,用MEAN实现。其中有个需求:在项目表中分页查找出每个小区的名字和里面项目的数量,最近3天新增的和修改的项目数量,最近一次修改时间,并按最近修改时间排序(项目就是小区中要装修的某套房子的信息)。
简单分析下,第三种用简单的count、distinct函数肯定处理不了这个问题,第一种方式虽然可以按照小区对项目分组,但是分组后统计近三天的新增和修改数量则无法实现,因为分组后只能进行简单的表达式运算,不能实现复杂的业务逻辑。还好mongodb还提供了最终杀器——map-reduce。
首先,map函数

用communityName(小区名)和areaId(省市区id)联合作为key,可以唯一确定一个小区(不同的城市可能有名字一样的小区,所以需要这两个字段联合做key)。value对象中把数据库中的crate_date,update_date传入reduce函数,用来计算最近3天的新增和修改情况,areaName(省市区名)用来给前端显示,count默认为1,用来做小区内项目总数的计数。这样就完成了按照不同小区的归类。
之后,reduce函数

在这里我们接收两个参数,分别是map函数传来的key和根据key归类的value数组values。我们遍历这个数组,把count累加就得到了小区内项目总数,然后简单的比较操作,找到最近一次更新的项目的更新时间。最后我们累计出最近3天的新增和修改数量(用update_date字段加3天和当前时间比较,另外如果update_date字段和create_date字段值相同,表示是新增的,不同则是修改的)。把结果封装到result对象返回。
OK,貌似搞定了,实则不然。因为有些小区恰好只有1个项目,这样的记录通过map函数之后就不会再经过reduce函数处理了,这样会导致最终返回的json数组中的对象属性不一致。因此我们还需要再过滤一下,以便得到结构一致的结果。代码如下:

不管有没有经过reduce函数,最终都会调用finalize函数,如果没有经过reduce,那么reduceVal传入的将是map函数的value对象我们通过!reducedVal.latestUpdateDate && reducedVal.create_date && reducedVal.update_date
这3个条件就可以判断出来,那么我们要做的就是把它转成reduce函数中的result对象,这样最终数组中的元素格式就一致了。
引入查找条件:
option对象是查找条件,这里就不展开了。mongo会先根据查找条件找出结果集,再进行map-reduce聚合。
最后我们将查找结果放到results数组里面。
接下来还有些善后工作:
排序:(悲剧,需求是根据最近更新时间排序,这个是reduce中算出来的。没办法利用数据库的排序和分页了, 用lodash吧)

分页:

最后,由于mongodb保存的时间都是用的GMT时区,我们必须转一下时区,并且format日期格式。

OK,这才算真正完成了。完整代码如下:
this.getClueGroupByCommunity = function(option, page, size) { return new Promise(function (resolve, reject) { co(function*() { try { var results = yield self.model.mapReduce({ map: function () { var key = { communityName: this.address.communityName, areaId: this.address.areaId }; var value = { count: 1, create_date: this.create_date, update_date: this.update_date, areaName: this.address.areaName }; emit(key, value); }, reduce: function (key, values) { var result = { count: 0, latestUpdateDate: '', added: 0, updated: 0, areaName: values[0].areaName }; for (var i = 0; i < values.length; i++) { //project count result.count += values[i].count; //latest update date if (!result.latestUpdateDate || values[i].update_date > result.latestUpdateDate) { result.latestUpdateDate = values[i].update_date; } //added and updated var endDate = new Date(values[i].update_date); endDate.setDate(endDate.getDate() + 3); if (endDate.getTime() >= new Date().getTime()) { if (values[i].update_date.getTime() == values[i].create_date.getTime()) { result.added += 1; } else { result.updated += 1; } } } return result; }, finalize: function (key, reducedVal) { if (!reducedVal.latestUpdateDate && reducedVal.create_date && reducedVal.update_date) {//mapped but not reduced var fixedReduceVal = {}; fixedReduceVal.count = 1; fixedReduceVal.latestUpdateDate = reducedVal.update_date; fixedReduceVal.added = 0; fixedReduceVal.updated = 0; //added and updated var endDate = new Date(reducedVal.update_date); endDate.setDate(endDate.getDate() + 3); if (endDate.getTime() >= new Date().getTime()) { if (reducedVal.update_date.getTime() == reducedVal.create_date.getTime()) { fixedReduceVal.added = 1; } else { fixedReduceVal.updated = 1; } } fixedReduceVal.areaName = reducedVal.areaName; return fixedReduceVal; } else { return reducedVal; } }, query: option }); //sort var sortedResult = _.sortByOrder(results, ['value.latestUpdateDate'], ['desc']); //page var start = (page - 1) * size; var end = start + size; var length = sortedResult.length; if (end < length) { sortedResult = _.take(sortedResult, end); sortedResult = _.takeRight(sortedResult, size); } else if (start < length && length <= end) { sortedResult = _.takeRight(sortedResult, (length - start)); } else { sortedResult = []; } //date format for (var i = 0; i < sortedResult.length; i++) { sortedResult[i].value.latestUpdateDate = moment.tz(sortedResult[i].value.latestUpdateDate, 'Asia/Shanghai').format('YYYY-MM-DD'); } return resolve({data: sortedResult, total: length.toString()}); } catch (e) { return reject(e); }