一般来说遇到组装树形结构,大多数人会想到递归,但递归的方式有一些弊端!
那我们直接看代码!
1.1实体
租户表:租户就是一个组织或者公司,所以每个租户都有自己的部门。如图
@Data
public class PmTenant {
/**
* 主键Id
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 租户名称
*/
private String tenantName;
/**
* 租户唯一编码,对外暴露
*/
private String tenantCode;
/**
* 租户Id
*/
private String tenantId;
/**
* 租户状态,0可用,1禁用
*/
private Integer status;
}
公司里都会有许多的部门,一个部门里还有部门。
@Data
public class PmDept {
/**
* 主键id
*/
@TableId(type = IdType.ASSIGN_ID)
private Integer id;
/**
* 父部门Id
*/
private Integer parentDeptId;
/**
* 部门id,全局唯一,所有系统用
*/
private Integer deptId;
/**
* 部门名称
*/
private String deptName;
/**
* 部门所处的排序
*/
private Integer orderNum;
/**
* 部门所处的层级
*/
private Integer depth;
/**
* 部门状态,0可用,1删除
*/
private Integer status;
/**
* 租户id
*/
private String tenantId;
/**
* 租户编码
*/
private String tenantCode;
}
1.2返回VO
这个返回的VO里面的子节点集合属性 childrenNodeList
,是一个关键字段,所有该方式返回树结构的 VO 都需要有该字段来”封装自己“。
@Data
public class DeptTreeNodeVO implements Serializable {
/**
* 子节点 list 集合,封装自己
*/
private List<DeptTreeNodeVO> childrenNodeList;
/**
* 部门Id
*/
protected Integer deptId;
/**
* 父部门Id
*/
protected Integer parentDeptId;
/**
* 部门名称
*/
protected String deptName;
}
1.3具体实现
下面直接上代码,注释已经说的比较清楚了:
@Resource
private PmTenantService pmTenantService;
@Resource
private PmDeptMapper pmDeptMapper;
@Override
@Cache(expiryTime = 300)
public List<DeptTreeNodeVO> assembleTree(){
//租户信息列表,这里是两个租户
List<PmTenant> tenantList = this.pmTenantService.list();
//step1:最外层根据租户去组装,有两个租户那么 Stream 就会遍历组装两次;换句话说,如果只有一个租户,就不需要最外层的 Stream
List<DeptTreeNodeVO> resultList = tenantList.stream().map(tenant -> {
//注:这里 map 只是简单转换了返回的对象属性(返回需要的类型),本质还是该租户下的所有部门数据
List<DeptTreeNodeVO> deptTreeNodeVOList = this.selectAllDeptByTenantCode(tenant.getTenantCode())
.stream().map(val -> val.convertExt(DeptTreeNodeVO.class)).collect(Collectors.toList());
//step2:利用父节点分组,即按照该租户下的所有部门的父Id进行分组,把所有的子节点List集合都找出来并一层层分好组
Map<Integer, List<DeptTreeNodeVO>> listMap = deptTreeNodeVOList.parallelStream()
.collect(Collectors.groupingBy(DeptTreeNodeVO::getParentDeptId));
//step3:关键一步,关联上子部门,将子部门的List集合经过遍历一层层地放置好,最终会得到完整的部门父子关系List集合
deptTreeNodeVOList.forEach(val -> val.setChildrenNodeList(listMap.get(val.getDeptId())));
//step4:过滤出顶级部门,即所有的子部门数据都归属于一个顶级父Id
List<DeptTreeNodeVO> allChildrenList = deptTreeNodeVOList.stream()
.filter(val -> val.getParentDeptId().equals(NumberUtils.INTEGER_ZERO)).collect(Collectors.toList());
//组装最外层关于租户需要的数据,实质已经不是处理部门数据了
DeptTreeNodeVO node = new DeptTreeNodeVO();
node.setChildrenNodeList(allChildrenList);
node.setDeptName(tenant.getTenantName());
return node;
}).collect(Collectors.toList());
return Optional.of(resultList).orElse(null);
}
/**
* 获取某个租户下的所有部门信息
*
* @return
*/
public List<PmDept> selectAllDeptByTenantCode(String tenantCode) {
return pmDeptMapper.selectList(new LambdaQueryWrapper<PmDept>()
.eq(PmDept::getTenantCode, tenantCode)
.eq(PmDept::getStatus, PmDeptStatus.DISABLE.getStatus()));
}
1.4效果展示
我这里测试的例子是只有三层,数据也没有完全展开,当然五六层也是没问题的。
只要总的部门数据量在一两万条以内,速度都是比较快的,服务器性能好的话,基本整个请求/响应(抛开网络I/O消耗)可以在一秒内完成。
图片
再看一个例子,这里的实体是放在 MongoDB 里的,不熟悉 MongoDB 也不要紧,这里只需要使用一次查全量的语句。
2.1实体
全国行政区表:全国的行政区包括省/直辖市/自治区、地级市、区/县级市/县这三级,再往下的街道/镇、以及下面的村/小组就不包含了。同样也是只留关键属性:
@Data
public class Region {
/**
* 区域id
*/
@Id
public Long id;
/**
* 父Id
*/
public Long parentId;
/**
* 地区名称
*/
public String name;
/**
* 地区全称
*/
public String district;
/**
* 所属省
*/
public String province;
/**
* 所属地级市
*/
public String city;
/**
* 所属省Id
*/
public Long provinceId;
/**
* 所属地级市Id
*/
public Long cityId;
/**
* 所处层级
*/
public Integer depth;
}
2.2返回VO
同样,这个里面的子节点集合属性 childrenRegionList
,是一个关键字段,所有该方式返回树结构的 VO 都需要有该字段来”封装自己“。
@Data
public class RegionCascadeVO extends RegionVO {
/**
* 子节点 list 集合
*/
private List<RegionCascadeVO> childrenRegionList;
/**
* 区域id
*/
public Long id;
/**
* 地区名称
*/
public String name;
/**
* 所处层级
*/
public Integer depth;
/**
* 省
*/
public String province;
/**
* 城市
*/
public String city;
/**
* 地区全称
*/
public String district;
/**
* 父Id
*/
public Long parentId;
/**
* 所属省Id
*/
public Long provinceId;
/**
* 所属地级市Id
*/
public Long cityId;
}
2.3具体实现
下面同样直接上代码,注释比较详细:
@Resource
private RegionRepository regionRepository;
@Override
@Cache(expiryTime = 300)
public List<RegionCascadeVO> quickAllTree() {
//第一步,从数据库中查出所有数据,按照排序条件进行排序,本质上还是这个所有数据的 List 集合
List<RegionCascadeVO> regionCascadeVOList = this.regionRepository.findAll().stream()
//注:这里使用 map 映射了需要返回的VO,即相同的属性字段就会转换
.map(val -> val.convertExt(RegionCascadeVO.class))
//业务需要的排序规则,使用工具来处理
.sorted((s1, s2) -> RegionSortUtil.citySort(s1.getName(), s2.getName()))
.sorted((s1, s2) -> RegionSortUtil.countySort(s1.getName(), s2.getName()))
.collect(Collectors.toList());
//第二步,根据父Id 字段进行分组,即所有数据都会按照第一层至最后一层都按照父子关系进行分组;注意,是对所有数据分组
Map<Long, List<RegionCascadeVO>> listMap = regionCascadeVOList.parallelStream().collect(Collectors.groupingBy(RegionCascadeVO::getParentId));
//第三步,也是最关键的一步,将父Id下面的所有子数据List集合,经过遍历后都一层层地放置好,最终会得到一个包含父子关系的完整List
regionCascadeVOList.forEach(val -> val.setChildrenRegionList(listMap.get(val.getId())));
//第四步,过滤出符合顶层父Id的所有数据,即所有数据都归属于一个顶层父Id
return regionCascadeVOList.stream().filter(val -> RegionConstant.CHINA_ID.equals(val.getParentId())).collect(Collectors.toList());
}
2.4效果展示
我这里测试环境的例子是只有省/直辖市/自治区、地级市、区/县级市/县这三级,数据也没有完全展开,当然到下面的镇/街道,乃至村/小组也是没问题的。
中国行政区域信息层次结构效果
时间消耗,这里响应只有两百多毫秒,如下图的接口的性能展示:
原因只有一个:数据库只查一次,把查到的全部数据放内存里,剩下的就是 Stream 的内存操作,都是地址的引用,性能是比较高的。
最后说一句(求关注!别白嫖!)
如果这篇文章对您有所帮助,或者有所启发的话,求一键三连:点赞、转发、在看。
关注公众号:woniuxgg,在公众号中回复:笔记 就可以获得蜗牛为你精心准备的java实战语雀笔记,回复面试、开发手册、有超赞的粉丝福利!