案例
在后端開發中,樹形結構數據的查詢和處理是一個常見的需求,比如部門管理、分類目錄展示等場景。接下來,我們以一個部門管理系統為例,詳細介紹如何實現后端的樹查詢功能。
案例背景
假設我們正在開發一個公司的內部管理系統,其中部門管理模塊需要展示部門之間的層級關系。部門數據以樹形結構存儲,每個部門都有自己的上級部門(根部門的上級部門 ID 為 0),我們需要實現接口查詢出扁平結構的部門列表以及樹形結構的部門數據。
表結構
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實體類來映射數據庫表中的數據,代碼如下:
/*** 父子關系方案部門實體類* @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列表中,直到所有部門都被正確添加到樹形結構中。
/*** 將扁平的部門列表轉換為樹形結構的部門列表。* 該方法會先找出所有根部門(即父部門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 依賴:
<dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.0</version>
</dependency>
實現樹形結構查詢的代碼如下:
/*** 將扁平化的部門列表轉換為樹形結構* @param departments 扁平部門列表(需包含至少id, parentId, depName字段)* @return 樹形結構列表(多個頂級節點構成森林結構)*/
public List<Tree<String>> formatToTreeSimple(List<ParentDepartment> departments) {// 1. 防御性編程:處理空輸入if (CollUtil.isEmpty(departments)) {// 返回不可變空集合而非null,避免調用方NPEreturn 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
字段表示該節點是否為葉子節點(即是否有子節點),這在前端進行交互操作時很有用,例如可以根據是否為葉子節點來決定是否顯示展開 / 收縮按鈕。 - 方便業務處理:在實際業務中,很多操作需要依賴這些額外的信息。比如在權限管理中,可能不同層級的部門有不同的權限;在數據統計時,可能需要區分葉子節點和非葉子節點進行不同的計算。將這些字段直接附加到樹形結構的節點中,在后續業務邏輯處理時,就無需再通過復雜的查詢或計算來獲取,提高開發效率。
- 增強數據通用性:添加擴展字段使樹形結構數據更具通用性和靈活性。即使當前業務不需要這些字段,未來如果有新的功能需求,比如添加部門層級相關的篩選功能,或者根據葉子節點狀態進行特殊展示等,已經存在的擴展字段就能直接使用,而不需要對數據結構和代碼進行大規模修改。