一個數據庫表的結構如下:
可以看到 province/city/district 這三個字段是邏輯上的主從結構,在展示的時候,有時候會以列表的形式展示,這種方式展示時
不需要做什么特殊處理,如果是以樹狀圖展示時,則需要進行特殊處理。如下圖
首先需要有一個類表示樹結構。
package cn.kanyun; import java.util.List; /** * 樹節點類 * @author KANYUN * */ public class TreeData { private String name; private Long value; private String path; private List<TreeData> children; public String getName() { return name; } public void setName(String name) { this.name = name; } public Long getValue() { return value; } public void setValue(Long value) { this.value = value; } public List<TreeData> getChildren() { return children; } public void setChildren(List<TreeData> children) { this.children = children; } public String getPath() { return path; } public void setPath(String path) { this.path = path; } }
有了樹的節點類之后,接下來我們就需要將數據庫中的表的記錄查出來,然后將結果集放到該節點類對象中。
接下來查看測試類
package cn.kanyun; import cn.hutool.db.Db; import cn.hutool.db.Entity; import java.sql.SQLException; import java.util.*; import com.google.gson.Gson; public class MainTest { public static void main(String[] args) { // TODO Auto-generated method stub try { List<Entity> result = Db.use() .query("SELECT\r\n" + " province,\r\n" + " city,\r\n" + " district ,\r\n" + " SUM(population) AS population\r\n" + "FROM\r\n" + " area \r\n" + "WHERE\r\n" + " province = '河南省' or province = '浙江省'\r\n" + "GROUP BY\r\n" + " province,\r\n" + " city,\r\n" + " district "); // 構建樹形的字段,必須出現在查詢的SQL中,也就是說你想用哪幾個字段來構造父子關系,同時也說明了,可以使用任意字段構造樹形結構(是否有意義則另說) // 其add()的順序也表示了每個字段的父子關系 List<String> dimensions = new ArrayList<String>(); dimensions.add("province"); dimensions.add("city"); dimensions.add("district"); TreeHandler treeHandler = new TreeHandler(); System.out.println("總數量:" + result.size()); List<TreeData> treeDataList = new ArrayList<>(); // root節點集合 Set<String> set = new HashSet<>(); // 所有節點集合 Set<String> paths = new HashSet<>(); // 遍歷結果集 for (Entity stringObjectMap : result) { // 找到root節點的值 String key = String.valueOf(stringObjectMap.get(dimensions.get(0))); TreeData treeData = null; // 判斷root節點是否被添加過 if (!set.contains(key)) { treeData = new TreeData(); treeData.setName(key); treeData.setPath(key); set.add(key); treeDataList.add(treeData); } else { // 如果當前root節點被添加過,則找過那個節點 for (TreeData node : treeDataList) { if (node.getName().equals(key)) { treeData = node; break; } } } // 待計數的字段名(也需要出現的查詢SQL中) String v_key = "population"; // 處理樹形結構 treeHandler.tree(paths, treeData, (Map<String, Object>) stringObjectMap, dimensions, v_key); } CountHandler countHandler = new CountHandler(); // 進行計數 countHandler.count(treeDataList); Gson gson = new Gson(); String vaString = gson.toJson(treeDataList); System.out.println(vaString); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
測試類中,除了數據庫查詢操作外(數據庫查詢使用了HuTool工具庫),還有兩個比較重要的類 TreeHandler/CountHandler
其中TreeHandler主要用來處理樹形結構
package cn.kanyun; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import cn.hutool.db.sql.Condition; /** * 樹形處理類 * @author KANYUN * */ public class TreeHandler { /** * 構造樹的方法 * @param paths * @param treeData * @param rowData * @param dimensions * @param v_key */ public void tree(Set<String> paths, TreeData treeData, Map<String, Object> rowData, List<String> dimensions,String v_key) { // 最低等級 String lowLevel = dimensions.get(dimensions.size()-1); // 記錄當前的path,path的作用是用來判斷層級 String currentPath = ""; for (String dimension : dimensions) { String cellData = String.valueOf(rowData.get(dimension)); currentPath += cellData; if (paths.contains(currentPath)) { // 如果當前path包含在paths集合中,說明該path已經加入了樹對象中,則繼續循環 currentPath += "/"; continue; } System.out.println(currentPath); paths.add(currentPath); if (!dimension.equals(lowLevel)) { // 如果當前key不是最低等級,則執行addChildNode方法 addChildNode(treeData, cellData, currentPath); } else { // 如果當前key是最低等級,則執行addNode()方法 int measureValue = 0; try { measureValue = Integer.parseInt(String.valueOf(rowData.get(v_key))); }catch(Exception e) { System.out.println("============"); System.out.println(rowData); System.out.println(rowData.get(v_key)); throw e; } addNode(treeData, cellData, currentPath, measureValue); } currentPath += "/"; } } /** * @return void * @Description 添加葉子節點,即最低級的節點 * @Date 18:16 2020/8/31 * @Param [treeData, cellData, beforeName, measureValue] **/ private void addNode(TreeData treeData, String cellData, String currentPath, long measureValue) { if ((treeData.getPath() + "/" + cellData).equals(currentPath)) { // 還是先判斷路徑是否一致,一致說明待插入節點是當前節點的子節點 TreeData data = new TreeData(); data.setName(cellData); data.setValue(measureValue); data.setPath(currentPath); if (treeData.getChildren() == null) { List<TreeData> treeDataList = new ArrayList<>(); treeData.setChildren(treeDataList); } treeData.getChildren().add(data); } else { if (treeData.getChildren() == null) { // 判斷當前節點是否為空,為空設置其value,然后直接返回 treeData.setValue(measureValue); return; } for (TreeData child : treeData.getChildren()) { // 繼續遞歸 addNode(child, cellData, currentPath, measureValue); } } } /** * @return boolean * @Description 添加節點,遞歸調用 * @Date 10:42 2020/8/31 * @Param [treeData, name] **/ public void addChildNode(TreeData treeData, String cellData, String currentPath) { if (treeData.getPath().equals(currentPath)) { // 如果當前的節點路徑,和傳遞過來的節點路徑一致,則直接返回 return; } if ((treeData.getPath() + "/" + cellData).equals(currentPath)) { // 判斷(當前的節點路徑 + "/" + cellData) 與傳遞過來的帶插入的路徑是否一致,如果一致,說明待插入的節點是當前節點的子節點 // 構建待插入的節點對象 TreeData data = new TreeData(); data.setName(cellData); data.setPath(currentPath); if (treeData.getChildren() == null) { // 判斷當前節點是否存在子節點list,存在則直接插入,不存在則先構造子節點list List<TreeData> treeDataList = new ArrayList<>(); treeData.setChildren(treeDataList); } treeData.getChildren().add(data); } // 走到這里說明沒有發現能插入的節點 if (treeData.getChildren() != null) { for (TreeData tree : treeData.getChildren()) { // 繼續遍歷遞歸 addChildNode(tree, cellData, currentPath); } } } }
需要注意的是測試類中的每一行結果集,都會調用TreeHandler中的tree()方法,在tree()方法中,會遍歷該結果集中的每一個字段。
先判斷該字段的值是否被添加到了樹狀結構中,如果沒有則繼續添加,如果已經添加過,則進行下一個字段。需要注意的是如果判斷一個節點是否是
另外一個節點的子節點。
這里使用的是TreeData類中的 path字段。通過組裝path字段,來判斷父子關系
如圖,該行數據包含三個字段,也就是三個節點。
其中父節點的path 為 “山東省”
中間節點解的path 為 “山東省/德州市”
葉子節點的 path 為“山東省/德州市/樂陵市”
由於每次遍歷時,就已經知道了當前節點 的path 和 value ,因此判斷待插入節點是否與當前節點是父子關系是,就可以判斷當前節點的
path + "/" + 待插入節點的 value 是否與 待插入節點的path一致,如果一致說明當前節點是待插入節點的父節點,如果不是則繼續遞歸判斷。
該類的主要方法為addNode()/addChildNode()方法。
其中CountHandler用來計數
package cn.kanyun; import java.util.List; /** * 計數處理類 * @author KANYUN * */ public class CountHandler { /** * 外部調用該方法 * @param treeDataList root節點下的數組 */ public void count(List<TreeData> treeDataList) { for (TreeData treeData : treeDataList) { if (treeData.getValue() == null) { groupCount(treeData); // 當上面方法執行完成,就說明當前節點下的所有子節點都已經有值了,因此直接將該節點進行計數 calc(treeData); } } } /** * 分組計數(遞歸方法) * @param treeData */ public void groupCount(TreeData treeData) { // 如果當前節點的value為空 if (treeData.getValue() == null) { // 則判斷是否可以為該節點進行計數(其主要依據是看該節點的子節點的value是否都有值) if (!isCount(treeData)) { // 如果該節點不能被計數,則遍歷該節點的所有子節點,進行遞歸 List<TreeData> treeDataList = treeData.getChildren(); if (treeDataList == null) return ; for (TreeData data : treeDataList) { // 遞歸方法 groupCount(data); } } else { // 如果該節點可以計數,則進行計數 calc(treeData); } } } /** * 統計計數,當isCount()方法返回true時執行 * @param treeNode */ public void calc(TreeData treeNode) { long i = 0; List<TreeData> treeDataList = treeNode.getChildren(); if (treeDataList == null) return ; for (TreeData treeData : treeDataList) { i += treeData.getValue(); } treeNode.setValue(i); } /** * 判斷是否可以進行計數操作 * @param treeNode * @return */ public boolean isCount(TreeData treeNode) { List<TreeData> treeDataList = treeNode.getChildren(); if (treeDataList == null) return false; // 如果該方法的入參對象,其所有子節點的value都不為空,說明可以為當前入參對象進行計數了 for (TreeData treeData : treeDataList) { if (treeData.getValue() == null) { return false; } } return true; } }
從第一張圖上我們看出,該表最后一個字段表示的是人口數,那么我們組裝完tree之后,非葉子節點的 value將為空,因為我們取到的value都是葉子節點的value。
即我們現在知道“山東省/德州市/樂陵市”的人口數是420406 但是“山東省/德州市”的人口數是不知道的 “山東省”的也是不知道的,因此我們需要統計非葉子節點的值
在這里需要認清楚葉子節點是有值的,因此需要不斷遞歸的給父節點算值。
具體步驟是。先判斷一個節點是否有值,有值自然不需要管了,如果沒有值,則遍歷其所有子節點是否有值,如果所有子節點都有值,則可以為當前節點計算值,如果
當前節點的所有節點不全都有值,則繼續遞歸當前節點的子節點,直至所有子節點都有值