• 【进阶篇】使用 Stream 流和 Lambda 组装复杂父子树形结构


    前言

    在最近的开发中,遇到了两个类似的需求:都是基于 Stream 的父子树形结构操作,返回 List 集合对象给前端。于是在经过需求分析和探索实践后有了新的认识,现在拿出来和大家作分享交流。

    一般来说完成这样的需求大多数人会想到递归,但递归的方式弊端过于明显:方法多次自调用效率很低、数据量大容易导致堆栈溢出、随着树深度的增加其时间复杂度会呈指数级增加等。

    核心思路如下:

    • 数据库全量查询(几万条),内存中使用 stream 流操作、Lambda 表达式、Java 地址引用

    • 使用缓存注解(底层Redis分布式缓存实现),过期后自动更新缓存,再次调用接口则先命中缓存,没有的话再查数据库

    • 使用 MQ 来做异步通知更新,即当数据有更改时,可以异步将数据先更新,再写入缓存,使业务更合理,考虑更全面

    一、以企业部门结构为例

    这里的实体是放在 MySQL 里的,使用简单的封装好的查询语句,这个很简单。

    1.1实体

    部门表:一个公司里都会有许多的部门,一个部门里还会有部门。从最顶层到你所在的的部门,可能会有多达六、七层。以下只展示核心字段:

    @Data
    public class Department {
        /**
         * 主键Id
         */
        private Integer id;
        /**
         * 该部门的父部门Id
         */
        private Integer parentDeptId;
        /**
         * 真正部门Id
         */
        private Integer deptId;
        /**
         * 部门的名称
         */
        private String name;
        /**
         * 部门在结构中所处的层级
         */
        private Integer level;
        /**
         * 状态是否启用
         */
        private Integer status;
    }
    

    1.2返回VO

    这个返回的VO是给前端的,里面的子节点集合属性 childrenList,是一个关键字段,所有该方式返回树结构的 VO 都需要有该字段来”封装自己“。

    @Data
    public class DepartmentVO implements Serializable {
        /**
         * 子节点集合,封装自己
         */
        private List childrenList;
        /**
         * 部门Id
         */
        protected Integer deptId;
        /**
         * 父部门Id
         */
        protected Integer parentDeptId;
        /**
         * 部门名称
         */
        protected String name;
    }
    

    1.3具体实现

    下面直接上 demo 代码,注释已经说的比较清楚了:

        @Override
        public List departmentStructure(String id){
                //step1:这里 map 只是简单转换了返回的对象属性(返回需要的类型),本质还是所有部门数据
                List departmentVOList = this.getDepartmentListById(id).stream()
                        .map(e -> e.copyProperties(DepartmentVO.class))
                        .collect(Collectors.toList());
                //step2:利用父节点分组,所有部门的父 Id 进行分组,把所有的子节点 List 集合都找出来并一层层分好组
                Map> departmentListMap = departmentVOList.stream()
                        .collect(Collectors.groupingBy(DepartmentVO::getParentDeptId));
                //step3:关键一步,关联上子部门,将子部门的 List 集合经过遍历一层层地放置好,最终会得到完整的部门父子关系 List 集合
                departmentVOList.forEach(e -> e.setChildrenList(departmentListMap.get(e.getDeptId())));
                //step4:过滤出顶级部门,即所有的子部门数据都归属于一个顶级父部门 Id
                List resultList = departmentVOList.stream()
                        .filter(e -> Constants.TOP_DEPARTMENT_NUM.equals(e.getParentDeptId()))
                        .collect(Collectors.toList());
            return Optional.of(resultList).orElse(null);
        }
    

    1.4效果展示

    我这里测试的例子是只有三层,数据也没有完全展开,当然五六层也是没问题的。

    只要总的部门数据量在一两万条以内(啥情况部门数量会有几万个?部门表一般是独立于其它表的)速度都是比较快的,服务器性能(主要内存给力)好的话,基本整个请求/响应(抛开网络I/O消耗)可以在一秒内完成。

    部门结构效果图

    二、以中国行政区域结构为例

    实体只需要使用一次查全量的语句,没有其它别的操作,很大程度上是因为省市县的结构是比较固定的。

    2.1实体

    全国行政区表:全国的行政区包括省/直辖市/自治区、地级市、区/县级市/县这三级,再往下的街道/镇、以及下面的村/小组就不包含了。同样也是只留关键属性:

    @Data
    public class Area {
        /**
         * 地区id
         */
        public Long id;
        /**
         * 父Id
         */
        public Long parentId;
        /**
         * 地区名称
         */
        public String name;
        /**
         * 所属省Id
         */
        public Long provinceId;
        /**
         * 所属地级市Id
         */
        public Long cityId;
        /**
         * 所处层级
         */
        public Integer level;
    }
    

    2.2返回VO

    同样,这个里面的子节点集合属性 childrenAreaVOList,是一个关键字段,所有该方式返回树结构的 VO 都需要有该字段来”封装自己“。

    @Data
    public class AreaVO {
        /**
         * 子节点 list 集合
         */
        private List childrenAreaVOList;
        /**
         * 区域id
         */
        public Long id;
        /**
         * 地区名称
         */
        public String name;
        /**
         * 所处层级
         */
        public Integer level;
        /**
         * 父Id
         */
        public Long parentId;
        /**
         * 所属省Id
         */
        public Long provinceId;
        /**
         * 所属地级市Id
         */
        public Long cityId;
    }
    

    2.3具体实现

    下面同样直接上 demo 代码,注释比较详细:

        @Override
        public List getAreaStructure() {
            //第一步,从数据库中查出所有数据,按照排序条件进行排序,本质上还是这个所有数据的 List 集合
            List areaVOList = this.findAll(Sort.by("id").descending()).stream()
                    //注:这里使用 map 映射了需要返回的 VO,即相同的属性字段就会转换
                    .map(e -> e.copyProperties(AreaVO.class)).collect(Collectors.toList());
            if (CollectionUtils.isNotEmpty(areaVOList)){
                //第二步,根据父Id 字段进行分组,即所有数据都会按照第一层至最后一层都按照父子关系进行分组;注意,是对所有数据分组
                Map> areaVoListMap = areaVOList.parallelStream().collect(Collectors.groupingBy(AreaVO::getParentId));
                //第三步,也是最关键的一步,将所有子数据 List 集合经过遍历后都一层层地放置好,最终会得到一个包含父子关系的完整List
                areaVOList.forEach(e -> e.setChildrenAreaVOList(areaVoListMap.get(e.getId())));
                //第四步,过滤出符合顶层父Id的所有数据,即所有数据都归属于一个顶层父Id
                List resultList = areaVOList.stream()
                        .filter(e -> Constants.COUNTRY_CHINA_TOP_NUM.equals(e.getParentId()))
                        .collect(Collectors.toList());
                return Optional.of(resultList).orElse(null);
            }
            return new ArrayList<>();
        }
    

    2.4效果展示

    我这里测试环境的例子是只有省/直辖市/自治区、地级市、区/县级市/县这三级,数据也没有完全展开,当然到下面的镇/街道,乃至村/小组也是没问题的。

    这里总的测试数据量是几千条,如果加上镇/街道应该得有几万条,速度也还是是比较快的,服务器性能(主要内存给力)好的话,基本整个请求/响应(抛开网络I/O消耗)可以在一秒内完成。

    中国行政区域信息层次结构效果

    时间消耗,这里响应只有两百多毫秒,如下图的接口的性能展示:

    接口性能展示

    三、文章小结

    使用 Stream 流组装复杂父子树形结构(List 集合形式)的分享到这里就结束了,编码没有捷径,都是项目实践里出真知,一点点摸索攒经验。

    如有不足和错误,或者你有更好的解决思路,欢迎大家的指正和交流!

  • 相关阅读:
    C语言指针操作(一)指针变量
    查看电脑jdk/jre版本以及安装路径并测试是否可以正常使用(检查运行环境)
    虫儿从零学c++ 2:类的基础相关知识点复习1
    Spring boot mybatis 简单示例
    【PyTorch教程】PyTorch分布式并行模块DistributedDataParallel(DDP)详解
    洛谷 模板汇总 算法基础 python解析
    学习vue3踩坑分享( 1 )- 使用Element Plus <script lang=“ts“ setup> 加上lang=“ts“后编译错误
    【Python】《Python编程:从入门到实践 (第2版) 》笔记-Chapter2-变量和简单数据类型
    使用正则表达式模块“re”遇到的错误
    SPSS到底怎么入门?这些干货你收藏了么?
  • 原文地址:https://www.cnblogs.com/CodeBlogMan/p/17965824