一、背景
本篇出自最近笔者所负责项目的一个需求。开发之余,就此次功能实现过程,结合最近所学所感,笔者做了些记录,留下此篇。
这是笔者此次需要实现的需求效果图:
首先就是这个当前页的时间轴数据展示,于前端讲就是个数组,于后端讲就是个list数据结构。
本次项目,前端采用Vue,后端则为spring boot+Mybatis。
二、数据格式
经过和前端反复扯皮,基本确定了返回数据格式(当然,这是笔者胡说的,事实上此后还经过两次变动)。格式如下:
data: [{
year: '2020 年',
area: [{ data: '06 - 20', num: 9, fileList: { fileId: 12, fullName: '农政改发[2020] 6 号', size: '200 kb' }, arealist: ['安徽省'] }]
}]
三、表设计和字段关联
笔者在原有基础上新增了一张表,字段如下:
以此表为主表,去关联另一张表,关联字段为area_id。
后面要用到的字段其实就是省份名称,试验区名称和批复日期。
四、业务逻辑和实现过程
开发是什么?大公司的是怎么样的,笔者不太清楚。但我本人的感受就是,开局一张图(PPT),其余全靠编。让笔者感慨的是,从小时候上学开始,一路走来,经历了多少看图写作文,命题作为,半命题作文。没想到,工作后也是如此。
之前提到需要list结构返回数据,但是list内部的数据结构如何组织并没有确定。仔细分析以后发现这就是一个套娃结构,里里外外套了5层。如果是单表的话还好说,可这是需要跨表关联查询。按照前端的要求,要按照年份分组;而分析需求,结合表结构和字段关联,内部还需要按照批复文件id即file_id分组;而后来前端又补充,同一省份的不同试验区也应该归为一组。总结一下,就是至少需要三次分组。年份字段需要从表字段approve_date截取,还有内层的日期字段包含月日。
谈到分组和字段的拼接,其实笔者最初是倾向于尽量用SQL实现,那样的话代码层就简简单单,几行了事。当然,这也是笔者的思维惯性:能SQL解决就尽量不写代码,还是怕麻烦,尤其是对脏数据和空值处理。
可惜没能如我所愿。SQL实现的话,有些复杂,而且项目负责人和同事也不建议那么做。那就上代码,如图:
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.fw.reform.mapper.ApproveFileMapper">
<!-- 查询获取批复文件信息列表 -->
<select id="getAllApproveFile" resultType="com.fw.reform.vo.response.developmentprocess.ApproveFileResVo">
SELECT to_char(af.approve_date, 'yyyy年') AS year, to_char(af.approve_date, 'mm-dd') AS approve_date,
af.file_id, f.full_name, f.size, rpa.province_region_name AS province_name, rpa.name AS area_name
FROM approve_file af LEFT JOIN reform_pilot_area rpa ON af.area_id = rpa.id
LEFT JOIN file f ON af.file_id = f.id WHERE af.id IS NOT NULL ORDER BY to_char(af.approve_date, 'yyyy') DESC
</select>
</mapper>
项目采用的是PG数据库,在Mybatis中采用to_char字段处理localDate类型数据,将其转换为字符串类型数据时完全可以做到的。有关to_char这个函数的应用相关技巧,网上资料多的是,笔者也不再赘述。这里要提一点最后order by函数排序,其实就最终要返回的数据而言,并没有什么作用。因为经过后续多次分组、遍历以后,最终拼接得到的数据早已无法按照原始数据那样排序。
关键代码,如图:
@Override
public List<DevProcessResVo> getDataList() {
List<ApproveFileResVo> approveFileResVoList = getAllApproveFile();
// 对数据源列表进行测试,如果判断为空,返回一个新建ArrayList列表
if (approveFileResVoList == null || approveFileResVoList.size() == 0)
return new ArrayList<>();
// 根据年份和批复文件id对批复文件信息列表分组
Map<String, Map<Long, List<ApproveFileResVo>>> approveFileMap = approveFileResVoList.stream().collect(Collectors
.groupingBy(ApproveFileResVo::getYear, Collectors.groupingBy(ApproveFileResVo::getFileId)));
List<DevProcessResVo> devProcessResVoList = new ArrayList<>();
// 遍历approveFileMap,拼接出所需要的数据结构
// 一次遍历,以批复文件年份为key,对应值为同一年份下的所有数据
approveFileMap.forEach((year, map) -> {
DevProcessResVo devProcessResVo = new DevProcessResVo();
devProcessResVo.setYear(year);
List<AreaResVo> areaResVoList = new ArrayList<>();
// 二次遍历,以批复文件id为key,对应值为同年份同批复文件id下的所有数据
map.forEach((key2, value) -> {
AreaResVo areaResVo = new AreaResVo();
areaResVo.setFileId(key2);
List<SimplePilotAreaResVo> simplePilotAreaResVos = new ArrayList<>();
value.forEach(v -> {
areaResVo.setApprovalDate(v.getApproveDate());
areaResVo.setFullName(v.getFullName());
areaResVo.setSize(v.getSize());
SimplePilotAreaResVo simplePilotAreaResVo = new SimplePilotAreaResVo();
simplePilotAreaResVo.setProvinceName(v.getProvinceName());
simplePilotAreaResVo.setAreaName(v.getAreaName());
simplePilotAreaResVos.add(simplePilotAreaResVo);
});
areaResVo.setNum(simplePilotAreaResVos.size());
List<AreaGroupByProvinceResVo> group = new ArrayList<>();
// 根据省份名称将试验区列表分组
Map<String, List<SimplePilotAreaResVo>> areaMap = simplePilotAreaResVos.stream().collect(Collectors.groupingBy(SimplePilotAreaResVo::getProvinceName));
areaMap.forEach((province, list) -> {
AreaGroupByProvinceResVo areaGroupByProvinceResVo = new AreaGroupByProvinceResVo();
areaGroupByProvinceResVo.setProvinceName(province);
List<String> areaNameList = new ArrayList<>();
list.forEach(t -> {
String name = t.getAreaName();
areaNameList.add(name);
});
areaGroupByProvinceResVo.setAreaNameList(areaNameList);
group.add(areaGroupByProvinceResVo);
});
areaResVo.setAreaList(group);
areaResVoList.add(areaResVo);
});
devProcessResVo.setArea(areaResVoList);
devProcessResVoList.add(devProcessResVo);
});
// 将拼接后获取的发展历程数据列表进行降序排序
Collections.sort(devProcessResVoList, Comparator.comparing(DevProcessResVo::getYear).reversed());
return devProcessResVoList;
}
在Java8的stream相关API中,对于list的处理方法很丰富,本次用到的单条件分组和多条件分组只是其中之一。需要指出的是,Stream流分组处理数据时遇到null值会抛异常并中断程序。所以,笔者在拿到原始数据后进行测试,判空后直接返回新建ArrayList。提到空值和空指针异常,我们都知道Java中这是最常见的情况。最近笔者查看了一部分Jdk1.8的源码,发现源码中很重视边界检查和测试。笔者是十分赞同这样的做法的。笔者认为,任何从数据库中拿到的原始数据,哪怕是自己本身十分确定数据的可靠性,但还是应该对原始数据做测试,加一道保险。
前面已经提到过,经过拼接后得到的devProcessResVoList不再是降序排序的,所以要经过一层排序处理然后才能得到所需要的数据。这里排序,笔者用了Collections.sort()。当然有其他的实现方式,但此方法笔者比较熟悉。这里引发了本人的一个思考,那就是能得到正确结果的排序方式有几种?各自的效率如何?哪个最优?哪个最适合大数据量的处理?由于时间问题,笔者只是简单的尝试了一下stream的sort方法,似乎不太理想。stream的sort方法直接提供了int、double、Long类型版本的Comparator.comparaing(),并没有string类型的直接方法,而并行流似乎也并没有提供。也许是限于笔者的stream相关技能的水平吧,并没有很快的做出对比。出于时间和代码简洁度的考虑,最后选择了Collections.sort()。
最后再提一点,就是关于ArrayList的采用。之前笔者测试过,不过测试数据是整型。在相同且相当大的数据量情况下,ArrayList的add方法效果要比LinkedList实际要差点,但是都在一个量级。其实理论上两者的add()方法都是默认加到最后一项,应该为O(N)。但如果是涉及到get方法和set方法,其差距就很明显。LinkedList所花时间是随着数据量增长而成平方增长的。即使是add(index, int),虽然LinkedList所需时间比ArrayList的少,大约差一到两个数量级,但是综合考虑,只要有get的调用,还是比较推荐ArrayList。当然,过大的数据集不在此考虑范围内。
五、总结
本篇并没有高大上的技能分享,也没有真知灼见。是笔者结合自己的项目经历,和最近的学习吸收,写了一些总结。关于最后的字符串list排序问题,如果大家有更高效,更优雅的代码实现,还请不吝赐教。如果能出一篇各方法对比和大数据量测试文章,那就更好了。