在java开发中,我们肯定会遇到树形结构构建,当然网上的树形结构treeUtil工具很多,但是为了扩展性,大都采用大量的泛型或者一些其他结构,初学者可能不太能够理解并应用,这里介绍两种简单且快速构建树的方法。(仅供初学者参考,大佬请忽略)
方法一:利用hutool工具包
一、引入依赖
Maven
在项目的pom.xml的dependencies中加入以下内容:
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.26</version>
</dependency>
#Gradle
implementation 'cn.hutool:hutool-all:5.8.26'
二、具体使用
介绍
考虑到菜单等需求的普遍性,有用户提交了一个扩展性极好的树状结构实现。这种树状结构可以根据配置文件灵活的定义节点之间的关系,也能很好的兼容关系数据库中数据。实现
关系型数据库数据 <-> Tree <-> JSON
树状结构中最大的问题就是关系问题,在数据库中,每条数据通过某个字段关联自己的父节点,每个业务中这个字段的名字都不同,如何解决这个问题呢?
PR的提供者提供了一种解决思路:自定义字段名,节点不再是一个bean,而是一个map,实现灵活的字段名定义。
#使用
#定义结构
我们假设要构建一个菜单,可以实现系统管理和店铺管理,菜单的样子如下:
系统管理
|- 用户管理
|- 添加用户
店铺管理
|- 商品管理
|- 添加商品
那这种结构如何保存在数据库中呢?一般是这样的:
id | parentId | name | weight |
---|---|---|---|
1 | 0 | 系统管理 | 5 |
11 | 1 | 用户管理 | 10 |
111 | 1 | 用户添加 | 11 |
2 | 0 | 店铺管理 | 5 |
21 | 2 | 商品管理 | 10 |
221 | 2 | 添加添加 | 11 |
构建Tree
// 构建node列表
List<TreeNode<String>> nodeList = CollUtil.newArrayList();
nodeList.add(new TreeNode<>("1", "0", "系统管理", 5));
nodeList.add(new TreeNode<>("11", "1", "用户管理", 222222));
nodeList.add(new TreeNode<>("111", "11", "用户添加", 0));
nodeList.add(new TreeNode<>("2", "0", "店铺管理", 1));
nodeList.add(new TreeNode<>("21", "2", "商品管理", 44));
nodeList.add(new TreeNode<>("221", "2", "商品管理2", 2));
TreeNode表示一个抽象的节点,也表示数据库中一行数据。 如果有其它数据,可以调用
setExtra
添加扩展字段。
// 0表示最顶层的id是0
List<Tree<String>> treeList = TreeUtil.build(nodeList, "0");
因为两个Tree是平级的,再没有上层节点,因此为List。
#自定义字段名
//配置
TreeNodeConfig treeNodeConfig = new TreeNodeConfig();
// 自定义属性名 都有默认值的
treeNodeConfig.setWeightKey("order");
treeNodeConfig.setIdKey("rid");
// 最大递归深度
treeNodeConfig.setDeep(3);
//转换器 (含义:找出父节点为字符串零的所有子节点, 并递归查找对应的子节点, 深度最多为 3)
List<Tree<String>> treeNodes = TreeUtil.<TreeNode, String>build(nodeList, "0", treeNodeConfig,
(treeNode, tree) -> {
tree.setId(treeNode.getId());
tree.setParentId(treeNode.getParentId());
tree.setWeight(treeNode.getWeight());
tree.setName(treeNode.getName());
// 扩展属性 ...
tree.putExtra("extraField", 666);
tree.putExtra("other", new Object());
});
通过TreeNodeConfig我们可以自定义节点的名称、关系节点id名称,这样就可以和不同的数据库做对应。
方法二:自己手写一个简单的树形工具包
以区域为例
package com.domain;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.eme.common.human.security.dao.entity.BaseEntity;
import lombok.Data;
/**
* <p>
* 区域表;
* </p>
*
* @since 2023-10-17
*/
@Data
@TableName("t_area")
public class AreaEntity extends BaseEntity {
private static final long serialVersionUID = 1L;
/**
* 区域编码
*/
@TableField("area_code")
private String areaCode;
/**
* 父级编码
*/
@TableField("pcode")
private String pcode;
/**
* 名称
*/
@TableField("area_name")
private String areaName;
/**
* 全称
*/
@TableField("full_name")
private String fullName;
/**
* 全编码
*/
@TableField("full_code")
private String fullCode;
/**
* 等级(1-省/直辖市,2-地级市,3-区县,4-乡镇,5-村/街道,6-片区)
*/
@TableField("level")
private Integer level;
/**
* 区域编码简称
*/
@TableField("area_code_simple")
private Long areaCodeSimple;
/**
* 父编码简称
*/
@TableField("parent_simple_code")
private Long parentSimpleCode;
/**
* 行政驻地经度
*/
@TableField("station_lon")
private String stationLon;
/**
* 行政驻地纬度
*/
@TableField("station_lat")
private String stationLat;
/**
* 最小经度
*/
@TableField("min_lon")
private String minLon;
/**
* 最小纬度
*/
@TableField("min_lat")
private String minLat;
/**
* 最大经度
*/
@TableField("max_lon")
private String maxLon;
/**
* 最大纬度
*/
@TableField("max_lat")
private String maxLat;
/**
* 中心经度
*/
@TableField("center_lon")
private String centerLon;
/**
* 中心纬度
*/
@TableField("center_lat")
private String centerLat;
/**
* 排序
*/
@TableField("sort")
private Integer sort;
/**
* 创建人所在机构
*/
@TableField("create_org_id")
private String createOrgId;
}
具体实现
这里如果前端给我们传区域编码了,我们就根据前端传的区域编码作为最顶级向下构建树,如果没传,我们就根据当前登录所在的区域作为最顶级构建树或者直接构建最大的一颗树。即以父编码为0来构建树。
public List<AreaTreeDTO> getList(String areaCode) {
// 查询最顶级的机构 (如果前段传行政区域编码了,传的编码作为顶级构建树,没传的话,就根据用户所在的区域构建树)
LambdaQueryWrapper<AreaEntity> query = Wrappers.lambdaQuery();
if (StringUtils.isNotBlank(areaCode)) {
query.eq(AreaEntity::getAreaCode, areaCode);
} else {
CurrentUserDTO user = currentUserUtil.getCurrentUser();
query.eq(AreaEntity::getAreaCode, user.getAreaCode());
}
query.orderByAsc(AreaEntity::getSort);
query.orderByDesc(AreaEntity::getCreateTime);
List<AreaEntity> rootList = this.list(query);
if (CollUtil.isEmpty(rootList)) {
return Collections.emptyList();
}
//entity与dto的转换
List<AreaTreeDTO> roots = AreaConverter.INSTANCE.toAreaEntityTreeDTO(rootList);
// 查询所有
List<AreaEntity> all = this.list();
if (CollUtil.isEmpty(all)) {
return roots;
}
// 构建树
Map<String, List<AreaEntity>> map = all.stream().collect(Collectors.groupingBy(AreaEntity::getPcode));
List<AreaTreeDTO> dtos = this.buildTree(roots, map);
return dtos;
}
/**
* 构建树
*/
private List<AreaTreeDTO> buildTree(List<AreaTreeDTO> roots, Map<String, List<AreaEntity>> map) {
if (CollUtil.isEmpty(roots)) {
return roots;
}
roots.forEach(r -> {
List<AreaEntity> children = map.get(r.getAreaCode());
if (CollUtil.isNotEmpty(children)) {
//entity与dto的转换
List<AreaTreeDTO> childList = AreaConverter.INSTANCE.toTree(children);
Collections.sort(childList, (a, b) -> a.getSort().compareTo(b.getSort()));//排序
r.setChildren(childList);
buildTree(childList, map);
}
});
return roots;
}