后端树形结构
案例
在后端开发中,树形结构数据的查询和处理是一个常见的需求,比如部门管理、分类目录展示等场景。接下来,我们以一个部门管理系统为例,详细介绍如何实现后端的树查询功能。
案例背景
假设我们正在开发一个公司的内部管理系统,其中部门管理模块需要展示部门之间的层级关系。部门数据以树形结构存储,每个部门都有自己的上级部门(根部门的上级部门 ID 为 0),我们需要实现接口查询出扁平结构的部门列表以及树形结构的部门数据。
表结构
sql
CREATE TABLE `department_info` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '部门ID',
`dep_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '部门名称',
`level` int NOT NULL COMMENT '层级',
`parent_id` int NOT NULL COMMENT '父ID,0表示根节点',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC;

在这个表结构中,id作为部门的唯一标识;dep_name存储部门名称;level表示部门在树形结构中的层级,方便后续对层级关系的处理;parent_id用于标识该部门的父部门,当parent_id为 0 时,表示该部门是根部门。
实体结构设计
在 Java 代码中,我们创建ParentDepartment实体类来映射数据库表中的数据,代码如下:
java
/**
* 父子关系方案部门实体类
* @TableName department_info
*/
@TableName(value = "department_info")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ParentDepartment implements Serializable {
// 序列化版本号
private static final long serialVersionUID = 1L;
/**
* 部门ID
*/
@TableId(type = IdType.AUTO)
private Integer id;
/**
* 部门名称
*/
private String depName;
/**
* 部门层级
*/
private Integer level;
/**
* 父部门ID(0表示根节点)
*/
private Integer parentId;
/**
* 子节点列表(非数据库字段)
*/
@TableField(exist = false)
private List<ParentDepartment> children;
/**
* 是否为叶子节点(非数据库字段)
*/
@TableField(exist = false)
private Boolean isLeaf;
}
实现思路
方法一:递归实现
递归是一种经典的处理树形结构数据的方法。基本思路是:首先从数据库中查询出所有的部门数据,然后找到所有根部门(即parent_id为 0 的部门),对于每个根部门,递归地查找它的子部门,将子部门添加到根部门的children列表中,直到所有部门都被正确添加到树形结构中。
java
/**
* 将扁平的部门列表转换为树形结构的部门列表。
* 该方法会先找出所有根部门(即父部门ID为0的部门),
* 然后递归构建每个根部门的子树。
*
* @param allDepartments 包含所有部门信息的扁平列表
* @return 包含所有根部门及其子部门的树形结构列表
*/
public List<ParentDepartment> formatToTree(List<ParentDepartment> allDepartments) {
// 用于存储所有根部门的列表
List<ParentDepartment> rootDepartments = new ArrayList<>();
// 遍历所有部门,找出父部门ID为0的根部门
for (ParentDepartment department : allDepartments) {
if (department.getParentId() == 0) {
// 将根部门添加到根部门列表中
rootDepartments.add(department);
}
}
// 遍历所有根部门,为每个根部门构建子树
for (ParentDepartment root : rootDepartments) {
buildTree(root, allDepartments);
}
// 返回包含所有根部门及其子部门的树形结构列表
return rootDepartments;
}
/**
* 递归构建指定父部门的子树。
* 该方法会遍历所有部门,找出当前父部门的所有子部门,
* 并为每个子部门递归调用自身构建子树。
*
* @param parent 父部门对象
* @param allDepartments 包含所有部门信息的扁平列表
*/
private void buildTree(ParentDepartment parent, List<ParentDepartment> allDepartments) {
// 用于存储当前父部门的所有子部门的列表
List<ParentDepartment> children = new ArrayList<>();
// 遍历所有部门,找出当前父部门的子部门
for (ParentDepartment department : allDepartments) {
if (department.getParentId().equals(parent.getId())) {
// 将子部门添加到子部门列表中
children.add(department);
// 递归构建子部门的子树
buildTree(department, allDepartments);
}
}
// 为父部门设置子部门列表
parent.setChildren(children);
}
方法二:hutool工具实现
Hutool 是一个功能丰富的 Java 工具类库,其中提供了方便的树形结构处理工具。使用 Hutool 实现树形结构查询更加简洁高效。
首先,需要在项目的pom.xml文件中引入 Hutool 依赖:
xml
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.0</version>
</dependency>
实现树形结构查询的代码如下:
java
/**
* 将扁平化的部门列表转换为树形结构
* @param departments 扁平部门列表(需包含至少id, parentId, depName字段)
* @return 树形结构列表(多个顶级节点构成森林结构)
*/
public List<Tree<String>> formatToTreeSimple(List<ParentDepartment> departments) {
// 1. 防御性编程:处理空输入
if (CollUtil.isEmpty(departments)) {
// 返回不可变空集合而非null,避免调用方NPE
return Collections.emptyList();
}
// 2. 初始化树节点配置
TreeNodeConfig config = new TreeNodeConfig();
// 指定实体类字段与树节点属性的映射关系
config.setIdKey("id"); // 节点ID对应实体类的id字段
config.setParentIdKey("parentId");// 父节点ID对应实体类的parentId字段
config.setChildrenKey("children");// 子节点集合的字段名称
config.setNameKey("name"); // 节点显示名称对应实体类的depName字段
// 3. 构建树形结构
return TreeUtil.build(
departments, // 数据源集合
"0", // 根节点的父ID值(通常为0或null)
config, // 树配置
(dept, tree) -> { // 自定义字段映射处理器
// ---- 核心字段映射 ----
// 设置节点ID(需转为String类型)
tree.setId(dept.getId().toString());
// 设置父节点ID(需转为String类型)
tree.setParentId(dept.getParentId().toString());
// 设置节点显示名称
tree.setName(dept.getDepName());
// ---- 扩展业务字段 ----
// 添加部门层级信息
tree.putExtra("level", dept.getLevel());
// 添加叶子节点标记(根据children是否为空自动计算)
tree.putExtra("isLeaf", dept.getIsLeaf());
// 可继续添加其他业务字段...
// tree.putExtra("manager", dept.getManagerName());
}
);
// 注:返回的List可能包含多个顶级节点(森林结构)
// 通常业务中只有一个parentId="0"的根节点,可用get(0)获取
}
在这段代码中,我们先创建TreeNodeConfig对象,配置好 ID、父 ID、子节点列表以及名称对应的属性名。然后调用TreeUtil.build方法,传入部门数据列表、根节点 ID、配置对象以及一个函数式接口,在函数式接口中,我们将部门实体类的属性赋值给Tree对象,并可以根据业务需求添加额外的扩展字段。
在后端树查询的实现中,tree.putExtra("level", dept.getLevel());
和 tree.putExtra("isLeaf", dept.getIsLeaf());
这两行代码的作用是向树形结构的节点对象 tree
中添加额外的业务数据字段,也就是扩展字段 ,具体来说,它们的作用体现在以下几个方面:
- 丰富节点信息 :默认情况下,树形结构的节点可能只包含基础的标识信息(如节点 ID、父节点 ID、节点名称等)。通过添加
level
和isLeaf
字段,可以让每个节点携带更多与业务相关的信息。比如level
字段表示部门在树形结构中的层级,前端拿到数据后,就可以根据层级来设置不同的缩进样式,直观展示部门的层级关系;isLeaf
字段表示该节点是否为叶子节点(即是否有子节点),这在前端进行交互操作时很有用,例如可以根据是否为叶子节点来决定是否显示展开 / 收缩按钮。 - 方便业务处理:在实际业务中,很多操作需要依赖这些额外的信息。比如在权限管理中,可能不同层级的部门有不同的权限;在数据统计时,可能需要区分叶子节点和非叶子节点进行不同的计算。将这些字段直接附加到树形结构的节点中,在后续业务逻辑处理时,就无需再通过复杂的查询或计算来获取,提高开发效率。
- 增强数据通用性:添加扩展字段使树形结构数据更具通用性和灵活性。即使当前业务不需要这些字段,未来如果有新的功能需求,比如添加部门层级相关的筛选功能,或者根据叶子节点状态进行特殊展示等,已经存在的扩展字段就能直接使用,而不需要对数据结构和代码进行大规模修改。
案例
在后端开发中,树形结构数据的查询和处理是一个常见的需求,比如部门管理、分类目录展示等场景。接下来,我们以一个部门管理系统为例,详细介绍如何实现后端的树查询功能。
案例背景
假设我们正在开发一个公司的内部管理系统,其中部门管理模块需要展示部门之间的层级关系。部门数据以树形结构存储,每个部门都有自己的上级部门(根部门的上级部门 ID 为 0),我们需要实现接口查询出扁平结构的部门列表以及树形结构的部门数据。
表结构
sql
CREATE TABLE `department_info` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '部门ID',
`dep_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '部门名称',
`level` int NOT NULL COMMENT '层级',
`parent_id` int NOT NULL COMMENT '父ID,0表示根节点',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC;

在这个表结构中,id作为部门的唯一标识;dep_name存储部门名称;level表示部门在树形结构中的层级,方便后续对层级关系的处理;parent_id用于标识该部门的父部门,当parent_id为 0 时,表示该部门是根部门。
实体结构设计
在 Java 代码中,我们创建ParentDepartment实体类来映射数据库表中的数据,代码如下:
java
/**
* 父子关系方案部门实体类
* @TableName department_info
*/
@TableName(value = "department_info")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ParentDepartment implements Serializable {
// 序列化版本号
private static final long serialVersionUID = 1L;
/**
* 部门ID
*/
@TableId(type = IdType.AUTO)
private Integer id;
/**
* 部门名称
*/
private String depName;
/**
* 部门层级
*/
private Integer level;
/**
* 父部门ID(0表示根节点)
*/
private Integer parentId;
/**
* 子节点列表(非数据库字段)
*/
@TableField(exist = false)
private List<ParentDepartment> children;
/**
* 是否为叶子节点(非数据库字段)
*/
@TableField(exist = false)
private Boolean isLeaf;
}
实现思路
方法一:递归实现
递归是一种经典的处理树形结构数据的方法。基本思路是:首先从数据库中查询出所有的部门数据,然后找到所有根部门(即parent_id为 0 的部门),对于每个根部门,递归地查找它的子部门,将子部门添加到根部门的children列表中,直到所有部门都被正确添加到树形结构中。
java
/**
* 将扁平的部门列表转换为树形结构的部门列表。
* 该方法会先找出所有根部门(即父部门ID为0的部门),
* 然后递归构建每个根部门的子树。
*
* @param allDepartments 包含所有部门信息的扁平列表
* @return 包含所有根部门及其子部门的树形结构列表
*/
public List<ParentDepartment> formatToTree(List<ParentDepartment> allDepartments) {
// 用于存储所有根部门的列表
List<ParentDepartment> rootDepartments = new ArrayList<>();
// 遍历所有部门,找出父部门ID为0的根部门
for (ParentDepartment department : allDepartments) {
if (department.getParentId() == 0) {
// 将根部门添加到根部门列表中
rootDepartments.add(department);
}
}
// 遍历所有根部门,为每个根部门构建子树
for (ParentDepartment root : rootDepartments) {
buildTree(root, allDepartments);
}
// 返回包含所有根部门及其子部门的树形结构列表
return rootDepartments;
}
/**
* 递归构建指定父部门的子树。
* 该方法会遍历所有部门,找出当前父部门的所有子部门,
* 并为每个子部门递归调用自身构建子树。
*
* @param parent 父部门对象
* @param allDepartments 包含所有部门信息的扁平列表
*/
private void buildTree(ParentDepartment parent, List<ParentDepartment> allDepartments) {
// 用于存储当前父部门的所有子部门的列表
List<ParentDepartment> children = new ArrayList<>();
// 遍历所有部门,找出当前父部门的子部门
for (ParentDepartment department : allDepartments) {
if (department.getParentId().equals(parent.getId())) {
// 将子部门添加到子部门列表中
children.add(department);
// 递归构建子部门的子树
buildTree(department, allDepartments);
}
}
// 为父部门设置子部门列表
parent.setChildren(children);
}
方法二:hutool工具实现
Hutool 是一个功能丰富的 Java 工具类库,其中提供了方便的树形结构处理工具。使用 Hutool 实现树形结构查询更加简洁高效。
首先,需要在项目的pom.xml文件中引入 Hutool 依赖:
xml
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.0</version>
</dependency>
实现树形结构查询的代码如下:
java
/**
* 将扁平化的部门列表转换为树形结构
* @param departments 扁平部门列表(需包含至少id, parentId, depName字段)
* @return 树形结构列表(多个顶级节点构成森林结构)
*/
public List<Tree<String>> formatToTreeSimple(List<ParentDepartment> departments) {
// 1. 防御性编程:处理空输入
if (CollUtil.isEmpty(departments)) {
// 返回不可变空集合而非null,避免调用方NPE
return Collections.emptyList();
}
// 2. 初始化树节点配置
TreeNodeConfig config = new TreeNodeConfig();
// 指定实体类字段与树节点属性的映射关系
config.setIdKey("id"); // 节点ID对应实体类的id字段
config.setParentIdKey("parentId");// 父节点ID对应实体类的parentId字段
config.setChildrenKey("children");// 子节点集合的字段名称
config.setNameKey("name"); // 节点显示名称对应实体类的depName字段
// 3. 构建树形结构
return TreeUtil.build(
departments, // 数据源集合
"0", // 根节点的父ID值(通常为0或null)
config, // 树配置
(dept, tree) -> { // 自定义字段映射处理器
// ---- 核心字段映射 ----
// 设置节点ID(需转为String类型)
tree.setId(dept.getId().toString());
// 设置父节点ID(需转为String类型)
tree.setParentId(dept.getParentId().toString());
// 设置节点显示名称
tree.setName(dept.getDepName());
// ---- 扩展业务字段 ----
// 添加部门层级信息
tree.putExtra("level", dept.getLevel());
// 添加叶子节点标记(根据children是否为空自动计算)
tree.putExtra("isLeaf", dept.getIsLeaf());
// 可继续添加其他业务字段...
// tree.putExtra("manager", dept.getManagerName());
}
);
// 注:返回的List可能包含多个顶级节点(森林结构)
// 通常业务中只有一个parentId="0"的根节点,可用get(0)获取
}
在这段代码中,我们先创建TreeNodeConfig对象,配置好 ID、父 ID、子节点列表以及名称对应的属性名。然后调用TreeUtil.build方法,传入部门数据列表、根节点 ID、配置对象以及一个函数式接口,在函数式接口中,我们将部门实体类的属性赋值给Tree对象,并可以根据业务需求添加额外的扩展字段。
在后端树查询的实现中,tree.putExtra("level", dept.getLevel());
和 tree.putExtra("isLeaf", dept.getIsLeaf());
这两行代码的作用是向树形结构的节点对象 tree
中添加额外的业务数据字段,也就是扩展字段 ,具体来说,它们的作用体现在以下几个方面:
- 丰富节点信息 :默认情况下,树形结构的节点可能只包含基础的标识信息(如节点 ID、父节点 ID、节点名称等)。通过添加
level
和isLeaf
字段,可以让每个节点携带更多与业务相关的信息。比如level
字段表示部门在树形结构中的层级,前端拿到数据后,就可以根据层级来设置不同的缩进样式,直观展示部门的层级关系;isLeaf
字段表示该节点是否为叶子节点(即是否有子节点),这在前端进行交互操作时很有用,例如可以根据是否为叶子节点来决定是否显示展开 / 收缩按钮。 - 方便业务处理:在实际业务中,很多操作需要依赖这些额外的信息。比如在权限管理中,可能不同层级的部门有不同的权限;在数据统计时,可能需要区分叶子节点和非叶子节点进行不同的计算。将这些字段直接附加到树形结构的节点中,在后续业务逻辑处理时,就无需再通过复杂的查询或计算来获取,提高开发效率。
- 增强数据通用性:添加扩展字段使树形结构数据更具通用性和灵活性。即使当前业务不需要这些字段,未来如果有新的功能需求,比如添加部门层级相关的筛选功能,或者根据叶子节点状态进行特殊展示等,已经存在的扩展字段就能直接使用,而不需要对数据结构和代码进行大规模修改。