Mongodb map-reduce实战

JerryXia 发表于 , 阅读 (0)
Aggregation pipeline map-reduce function single purpose aggregation methods

第1、3种都比较好理解,因为可以类比到我们熟悉的关系型数据库。第一种类似于group by查找,第三种类似于使用使用关系型数据库中自带的如count,distinct等函数做的简单的聚合查找。

第二种其实也不复杂,让我通过最近项目中一个实际应用的例子,来向你展示mongodb map-reduce的应用,你就会发现他的强大之处,往往用来解决第1、3种聚合方式都不能解决的问题,这也是mongodb相比于传统关系型数据库在数据查找方面的过人之处。

这个项目是面向建材行业的,用MEAN实现。其中有个需求:在项目表中分页查找出每个小区的名字和里面项目的数量,最近3天新增的和修改的项目数量,最近一次修改时间,并按最近修改时间排序(项目就是小区中要装修的某套房子的信息)。

简单分析下,第三种用简单的count、distinct函数肯定处理不了这个问题,第一种方式虽然可以按照小区对项目分组,但是分组后统计近三天的新增和修改数量则无法实现,因为分组后只能进行简单的表达式运算,不能实现复杂的业务逻辑。还好mongodb还提供了最终杀器——map-reduce。

首先,map函数

map function

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

之后,reduce函数

reduce function

在这里我们接收两个参数,分别是map函数传来的key和根据key归类的value数组values。我们遍历这个数组,把count累加就得到了小区内项目总数,然后简单的比较操作,找到最近一次更新的项目的更新时间。最后我们累计出最近3天的新增和修改数量(用update_date字段加3天和当前时间比较,另外如果update_date字段和create_date字段值相同,表示是新增的,不同则是修改的)。把结果封装到result对象返回。

OK,貌似搞定了,实则不然。因为有些小区恰好只有1个项目,这样的记录通过map函数之后就不会再经过reduce函数处理了,这样会导致最终返回的json数组中的对象属性不一致。因此我们还需要再过滤一下,以便得到结构一致的结果。代码如下:

finalize

不管有没有经过reduce函数,最终都会调用finalize函数,如果没有经过reduce,那么reduceVal传入的将是map函数的value对象我们通过!reducedVal.latestUpdateDate && reducedVal.create_date && reducedVal.update_date

这3个条件就可以判断出来,那么我们要做的就是把它转成reduce函数中的result对象,这样最终数组中的元素格式就一致了。

引入查找条件:

conditionoption对象是查找条件,这里就不展开了。mongo会先根据查找条件找出结果集,再进行map-reduce聚合。

最后我们将查找结果放到results数组里面。

接下来还有些善后工作:

排序:(悲剧,需求是根据最近更新时间排序,这个是reduce中算出来的。没办法利用数据库的排序和分页了, 用lodash吧)

sort

分页:

page

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

date 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);            }