详细介绍
Hutools官网
我们平时在进行业务开发时,经常会碰到需要构建树形结构的业务,比如常见的区域树结构
└── 省└── 市└── 区
这类数据的特点就是每条数据有明确的父节点和子节点,如果没有父节点或者子节点的话,没有父节点的数据为树的顶级节点,如果没有子节点的话为树的底级节点。
示例数据表及数据
CREATE TABLE area (area_id BIGINT NOT NULL AUTO_INCREMENT,parent_area_id BIGINT,area_name VARCHAR(255) NOT NULL,PRIMARY KEY (area_id)
);-- 插入省份
INSERT INTO area (area_id, parent_area_id, area_name) VALUES (1, NULL, '北京市');
INSERT INTO area (area_id, parent_area_id, area_name) VALUES (2, NULL, '上海市');
INSERT INTO area (area_id, parent_area_id, area_name) VALUES (3, NULL, '广东省');
INSERT INTO area (area_id, parent_area_id, area_name) VALUES (4, NULL, '浙江省');-- 插入北京市的市区
INSERT INTO area (area_id, parent_area_id, area_name) VALUES (101, 1, '东城区');
INSERT INTO area (area_id, parent_area_id, area_name) VALUES (102, 1, '西城区');
INSERT INTO area (area_id, parent_area_id, area_name) VALUES (103, 1, '朝阳区');-- 插入上海市的市区
INSERT INTO area (area_id, parent_area_id, area_name) VALUES (201, 2, '黄浦区');
INSERT INTO area (area_id, parent_area_id, area_name) VALUES (202, 2, '徐汇区');
INSERT INTO area (area_id, parent_area_id, area_name) VALUES (203, 2, '长宁区');-- 插入广东省的市区
INSERT INTO area (area_id, parent_area_id, area_name) VALUES (301, 3, '广州市');
INSERT INTO area (area_id, parent_area_id, area_name) VALUES (302, 3, '深圳市');
INSERT INTO area (area_id, parent_area_id, area_name) VALUES (303, 3, '珠海市');-- 插入浙江省的市区
INSERT INTO area (area_id, parent_area_id, area_name) VALUES (401, 4, '杭州市');
INSERT INTO area (area_id, parent_area_id, area_name) VALUES (402, 4, '宁波市');
INSERT INTO area (area_id, parent_area_id, area_name) VALUES (403, 4, '温州市');
使用hutools 的TreeUtil工具类可以非常方便的构建一个树形结构。
首先,springboot应用引入需要的hutools的依赖
<dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.26</version></dependency>
然后处理查询出来的需构建数据
@RestController
@RequestMapping("/area")
public class AreaController {@Autowiredprivate IAreaService areaService;@GetMapping("/getAreaTree")public List<Tree<Long>> getAreaTree(){List<Area> areaList = areaService.list();List<Tree<Long>> trees = buildAreaTree(areaList);return trees;}private List<Tree<Long>> buildAreaTree(List<Area> list){TreeNodeConfig config = new TreeNodeConfig();config.setIdKey("id");config.setParentIdKey("pid");List<Tree<Long>> treeList = TreeUtil.build(list, 0L, config, (node, tree) -> {tree.setId(node.getAreaId());tree.setParentId(node.getParentAreaId() == null ? 0L : node.getParentAreaId());tree.setName(node.getAreaName());
// tree.putExtra("code", node.getAreaCode()); putExtra方法可以在树结构的节点里面加入其他字段});return treeList;}
}
关键代码解释
buildAreaTree()方法
这里面主要是使用了TreeUtil.build()方法将区域areaList转化为树形结构。(将一个list转化为tree)
方法功能
- 方法名:
buildAreaTree
- 参数:
List<Area> list
,传入的区域列表,类型为Area
。 - 返回值:
List<Tree<Long>>
,返回构建好的树形结构列表,每个节点类型为Tree<Long>
。
主要逻辑
- 配置树节点 (
TreeNodeConfig
):- 创建
TreeNodeConfig
对象config
,用于配置树节点属性的映射关系。 config.setIdKey("id")
:设置节点的唯一标识字段名为"id"
。config.setParentIdKey("pid")
:设置父节点标识字段名为"pid"
。
- 创建
- 构建树 (
TreeUtil.build
):- 使用工具类
TreeUtil.build
方法构建树形结构。 list
:传入的区域列表,包含所有节点。0L
:根节点的父节点 ID,通常为0
或null
。config
:使用前面设置好的TreeNodeConfig
配置。(node, tree)
:构建树的回调函数,用于设置每个节点的属性。node.getAreaId()
:获取当前节点的areaId
。node.getParentAreaId()
:获取当前节点的parentAreaId
,如果为null
,则设置为0L
。node.getAreaName()
:获取当前节点的areaName
。
- 使用工具类
- 设置节点属性:
tree.setId(node.getAreaId())
:设置树节点的 ID。tree.setParentId(...)
:设置树节点的父 ID。tree.setName(node.getAreaName())
:设置树节点的名称。
- 扩展字段 (
putExtra
):- 注释掉的
putExtra
方法说明了可以为树节点添加额外的字段,比如areaCode
。
- 注释掉的
返回示例节选
[{"id": 4,"pid": 0,"name": "浙江省","children": [{"id": 401,"pid": 4,"name": "杭州市","children": [{"id": 40101,"pid": 401,"name": "西湖区"},{"id": 40102,"pid": 401,"name": "上城区"},{"id": 40103,"pid": 401,"name": "下城区"}]},{"id": 403,"pid": 4,"name": "温州市"}]}
]
这样就成功构成了一个树形结构。
测试代码gitee链接
数据库用到的数据项目中model.sql中都有,自己测试的时候导入到自己的数据库中。
多说一点
有时候在构造一个树结构的时候,数据的来源并非是一张表中的数据,可能涉及到多张表。比如构造一个有关区域和地铁站的树结构,树结构中有区域,然后区域下面是地铁站。(地铁站跟区域肯定是有关联的,每一条地铁站的数据中都存有areaId(area表的主键))
这个时候就要将区域和地铁站融合到一个List中,然后使用TreeUtil,怎么融合呢?
条件:
- 比如现在我们有区域的List areaList
- 地铁站的List stationList
- 新建一个实体类ItemDTO
public class ItemDTO {private Long id;private Long pid;private String name; // 区域名或地铁站名private String otherField;
}
操作:
- 将stationList处理成一个map,key为areaId,value为stationList
- 新建 List ItemDTOList = new ArrayList;
- 遍历areaList,将Area对象的对应字段处理为ItemDTO的id、pid、name;(areaId->id, parentId->pid, areaName->name)
- 再次遍历areaList,获取areaIdAndStationMap中的station,将station对应的字段处理为ItemDTO的id、pid、name
- 然后使用TreeUtil构建树结构
示例代码:
public void buildTree() {List<Area> areaList = new ArrayList<>();List<Station> stationList = new ArrayList<>();List<ItemDTO> itemDTOList = new ArrayList<>();Map<Long, List<Station>> areaIdAndStationMap = stationList.stream().collect(Collectors.groupingBy(Station::getAreaId));for (Area area : areaList) {ItemDTO itemDTO = new ItemDTO();itemDTO.setId(area.getAreaId());itemDTO.setPid(area.getParentId());itemDTO.setName(area.getAreaName());itemDTOList.add(itemDTO);}for (Area area : areaList) {List<Station> stations = areaIdAndStationMap.get(area.getAreaId());for (Station station : stations) {ItemDTO itemDTO = new ItemDTO();itemDTO.setId(station.getStationId());itemDTO.setPid(station.getAreaId());itemDTO.setName(station.getStationName());itemDTOList.add(itemDTO);}}// 之后,itemDTO作为参数使用TreeUtil构造树结构
}
以此类推,不管是几个有关系的实体类,都将他们处理到一个List中,就能正确的构造树结构