拒絕重複程式碼,封裝一個多級選單、多級評論、多級部門的統一工具類

👉 這是一個或許對你有用的社群
🐱 一對一交流/面試小冊/簡歷最佳化/求職解惑,歡迎加入芋道快速開發平臺知識星球。下面是星球提供的部分資料:
👉這是一個或許對你有用的開源專案
國產 Star 破 10w+ 的開源專案,前端包括管理後臺 + 微信小程式,後端支援單體和微服務架構。
功能涵蓋 RBAC 許可權、SaaS 多租戶、資料許可權、商城、支付、工作流、大屏報表、微信公眾號、CRM 等等功能:
  • Boot 倉庫:https://gitee.com/zhijiantianya/ruoyi-vue-pro
  • Cloud 倉庫:https://gitee.com/zhijiantianya/yudao-cloud
  • 影片教程:https://doc.iocoder.cn
【國內首批】支援 JDK 21 + SpringBoot 3.2.2、JDK 8 + Spring Boot 2.7.18 雙版本 

一、介紹

你能看到很多人都在介紹如何實現多級選單的效果,但是都有一個共同的缺點,那就是沒有解決程式碼會重複開發的問題。如果我需要實現多級評論呢,是否又需要自己再寫一遍?
為了簡化開發過程並提高程式碼的可維護性,我們可以建立一個統一的工具類來處理這些需求。在本文中,我將介紹如何使用SpringBoot建立一個返回多級選單、多級評論、多級部門、多級分類的統一工具類。
基於 Spring Boot + MyBatis Plus + Vue & Element 實現的後臺管理系統 + 使用者小程式,支援 RBAC 動態許可權、多租戶、資料許可權、工作流、三方登入、支付、簡訊、商城等功能
  • 專案地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 影片教程:https://doc.iocoder.cn/video/

介紹資料庫欄位設計

資料庫設計
「主要是介紹是否需要tree_path欄位。」
多級節點的資料庫大家都知道,一般會有id,parentId欄位,但是對於tree_path欄位,這個需要根據設計者來定。
優點:
  • 如果你對資料的讀取操作比較頻繁,而且需要快速查詢某個節點的所有子節點或父節點,那麼使用tree_path 欄位可以提高查詢效率。
  • tree_path 欄位可以使用路徑字串表示節點的層級關係,例如使用逗號分隔的節點ID列表。這樣,可以透過模糊匹配tree_path 欄位來查詢某個節點的所有子節點或父節點,而無需進行遞迴查詢。
  • 你可以使用模糊匹配的方式,找到所有以該節點的 tree_path 開頭的子節點,並將它們刪除。而無需進行遞迴刪除。
缺點:
  • 每次插入時,需要更新tree_path 欄位,這可能會導致效能下降。
  • tree_path 欄位的長度可能會隨著樹的深度增加而增加,可能會佔用更多的儲存空間。
因此,在設計資料庫評論欄位時,需要權衡使用treepath欄位和父評論ID欄位的優缺點,並根據具體的應用場景和需求做出選擇。如果你更關注讀取操作的效率和查詢、刪除的靈活性,可以考慮使用tree_path 欄位。如果你更關注寫入操作的效率和資料一致性,並且樹的深度不會很大,那麼使用父評論ID欄位來實現多級評論可能更簡單和高效。
基於 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實現的後臺管理系統 + 使用者小程式,支援 RBAC 動態許可權、多租戶、資料許可權、工作流、三方登入、支付、簡訊、商城等功能
  • 專案地址:https://github.com/YunaiV/yudao-cloud
  • 影片教程:https://doc.iocoder.cn/video/

二、統一工具類具體實現

1. 定義介面,統一規範

對於有 lombok 的小夥伴,實現這個方法很簡單,只需要加上@Data即可

/**

 * 

@Description

: 固定屬性結構屬性

 * 

@Author

: yiFei

 */


publicinterfaceITreeNode<T

{

/**

     * 

@return

 獲取當前元素Id

     */


Object getId()

;

/**

     * 

@return

 獲取父元素Id

     */


Object getParentId()

;

/**

     * 

@return

 獲取當前元素的 children 屬性

     */


List<T> getChildren()

;

/**

     * ( 如果資料庫設計有tree_path欄位可覆蓋此方法來生成tree_path路徑 )

     *

     * 

@return

 獲取樹路徑

     */


default Object getTreePath()

return""

; }

}

2. 編寫工具類TreeNodeUtil

其中我們需要實現能將一個List元素構建成熟悉結構
我們需要實現生成tree_path欄位
我們需要優雅的實現該方法

/**

 * 

@Description

: 樹形結構工具類

 * 

@Author

: yiFei

 */


publicclassTreeNodeUtil

{

privatestaticfinal

 Logger log = LoggerFactory.getLogger(TreeNodeUtil

.class)

;

publicstaticfinal

 String PARENT_NAME = 

"parent"

;

publicstaticfinal

 String CHILDREN_NAME = 

"children"

;

publicstaticfinal

 List<Object> IDS = Collections.singletonList(

0L

);

publicstatic

 <T extends ITreeNode> 

List<T> buildTree(List<T> dataList)

{

return

 buildTree(dataList, IDS, (data) -> data, (item) -> 

true

);

    }

publicstatic

 <T extends ITreeNode> 

List<T> buildTree(List<T> dataList, Function<T, T> map)

{

return

 buildTree(dataList, IDS, map, (item) -> 

true

);

    }

publicstatic

 <T extends ITreeNode> 

List<T> buildTree(List<T> dataList, Function<T, T> map, Predicate<T> filter)

{

return

 buildTree(dataList, IDS, map, filter);

    }

publicstatic

 <T extends ITreeNode> 

List<T> buildTree(List<T> dataList, List<Object> ids)

{

return

 buildTree(dataList, ids, (data) -> data, (item) -> 

true

);

    }

publicstatic

 <T extends ITreeNode> 

List<T> buildTree(List<T> dataList, List<Object> ids, Function<T, T> map)

{

return

 buildTree(dataList, ids, map, (item) -> 

true

);

    }

/**

     * 資料集合構建成樹形結構 ( 注: 如果最開始的 ids 不在 dataList 中,不會進行任何處理 )

     *

     * 

@param

 dataList 資料集合

     * 

@param

 ids      父元素的 Id 集合

     * 

@param

 map      呼叫者提供 Function<T, T> 由呼叫著決定資料最終呈現形勢

     * 

@param

 filter   呼叫者提供 Predicate<T> false 表示過濾 ( 注: 如果將父元素過濾掉等於剪枝 )

     * 

@param

 <T>      extends ITreeNode

     * 

@return

     */


publicstatic

 <T extends ITreeNode> 

List<T> buildTree(List<T> dataList, List<Object> ids, Function<T, T> map, Predicate<T> filter)

{

if

 (CollectionUtils.isEmpty(ids)) {

return

 Collections.emptyList();

        }

// 1. 將資料分為 父子結構

        Map<String, List<T>> nodeMap = dataList.stream()

                .filter(filter)

                .collect(Collectors.groupingBy(item -> ids.contains(item.getParentId()) ? PARENT_NAME : CHILDREN_NAME));
        List<T> parent = nodeMap.getOrDefault(PARENT_NAME, Collections.emptyList());

        List<T> children = nodeMap.getOrDefault(CHILDREN_NAME, Collections.emptyList());

// 1.1 如果未分出或過濾了父元素則將子元素返回
if

 (parent.size() == 

0

) {

return

 children;

        }

// 2. 使用有序集合儲存下一次變數的 ids

        List<Object> nextIds = 

new

 ArrayList<>(dataList.size());

// 3. 遍歷父元素 以及修改父元素內容

        List<T> collectParent = parent.stream().map(map).collect(Collectors.toList());

for

 (T parentItem : collectParent) {

// 3.1 如果子元素已經加完,直接進入下一輪迴圈
if

 (nextIds.size() == children.size()) {

break

;

            }

// 3.2 過濾出 parent.id == children.parentId 的元素

            children.stream()

                    .filter(childrenItem -> parentItem.getId().equals(childrenItem.getParentId()))

                    .forEach(childrenItem -> {

// 3.3 這次的子元素為下一次的父元素

                        nextIds.add(childrenItem.getParentId());

// 3.4 新增子元素到 parentItem.children 中
try

 {

                            parentItem.getChildren().add(childrenItem);

                        } 

catch

 (Exception e) {

                            log.warn(

"TreeNodeUtil 發生錯誤, 傳入引數中 children 不能為 null,解決方法: \n"

 +

"方法一、在map(推薦)或filter中初始化 \n"

 +

"方法二、List<T> children = new ArrayList<>() \n"

 +

"方法三、初始化塊對屬性賦初值\n"

 +

"方法四、構造時對屬性賦初值"

);

                        }

                    });

        }

        buildTree(children, nextIds, map, filter);

return

 parent;

    }

/**

     * 生成路徑 treePath 路徑

     *

     * 

@param

 currentId 當前元素的 id

     * 

@param

 getById   使用者返回一個 T

     * 

@param

 <T>

     * 

@return

     */


publicstatic

 <T extends ITreeNode> 

String generateTreePath(Serializable currentId, Function<Serializable, T> getById)

{

        StringBuffer treePath = 

new

 StringBuffer();

if

 (SystemConstants.ROOT_NODE_ID.equals(currentId)) {

// 1. 如果當前節點是父節點直接返回

            treePath.append(currentId);

        } 

else

 {

// 2. 呼叫者將當前元素的父元素查出來,方便後續拼接

            T byId = getById.apply(currentId);

// 3. 父元素的 treePath + "," + 父元素的id
if

 (!ObjectUtils.isEmpty(byId)) {

                treePath.append(byId.getTreePath()).append(

","

).append(byId.getId());

            }

        }

return

 treePath.toString();

    }
}

這樣我們就完成了 TreeNodeUtil 統一工具類,首先我們將元素分為父子兩類,讓其構建出一個小型樹,然後我們將構建的子元素和下次遍歷的父節點傳入,遞迴的不斷進行,這樣就構建出了我們最終的想要實現的效果。

三、測試

定義一個類實現 ITreeNode

/**

 * 

@Description

: 測試子元素工具類

 * 

@Author

: yiFei

 */


@Data
@EqualsAndHashCode

(callSuper = 

false

)

@Accessors

(chain = 

true

)

@AllArgsConstructor
publicclassTestChildrenimplementsITreeNode<TestChildren

{

private

 Long id;

private

 String name;

private

 String treePath;

private

 Long parentId;

publicTestChildren(Long id, String name, String treePath, Long parentId)

{

this

.id = id;

this

.name = name;

this

.treePath = treePath;

this

.parentId = parentId;

    }

@TableField

(exist = 

false

)

private

 List<TestChildren> children = 

new

 ArrayList<>();

}

測試基本功能
測試基本功能程式碼:
publicstaticvoidmain(String[] args)

{

    List<TestChildren> testChildren = 

new

 ArrayList<>();

    testChildren.add(

new

 TestChildren(

1L

"父元素"

""

0L

));

    testChildren.add(

new

 TestChildren(

2L

"子元素1"

"1"

1L

));

    testChildren.add(

new

 TestChildren(

3L

"子元素2"

"1"

1L

));

    testChildren.add(

new

 TestChildren(

4L

"子元素2的孫子元素"

"1,3"

3L

));
    testChildren = TreeNodeUtil.buildTree(testChildren);
    System.out.println(JSONUtil.toJsonStr(Result.success(testChildren)));

}

返回結果:

{

"code"

"00000"

,

"msg"

"操作成功"

,

"data"

: [{

"id"

1

,

"name"

"父元素"

,

"treePath"

""

,

"parentId"

0

,

"children"

: [{

"id"

2

,

"name"

"子元素1"

,

"treePath"

"1"

,

"parentId"

1

,

"children"

: []

  }, {

"id"

3

,

"name"

"子元素2"

,

"treePath"

"1"

,

"parentId"

1

,

"children"

: [{

"id"

4

,

"name"

"子元素2的孫子元素"

,

"treePath"

"1,3"

,

"parentId"

3

,

"children"

: []

   }]

  }]

 }]

}  

測試過濾以及重構資料
測試程式碼:
publicstaticvoidmain(String[] args)

{

    List<TestChildren> testChildren = 

new

 ArrayList<>();

    testChildren.add(

new

 TestChildren(

1L

"父元素"

""

0L

));

    testChildren.add(

new

 TestChildren(

2L

"子元素1"

"1"

1L

));

    testChildren.add(

new

 TestChildren(

3L

"子元素2"

"1"

1L

));

    testChildren.add(

new

 TestChildren(

4L

"子元素2的孫子元素"

"1,3"

3L

));
    testChildren = TreeNodeUtil.buildTree(testChildren);
    System.out.println(JSONUtil.toJsonStr(Result.success(testChildren)));

}

返回結果 :

{

"code"

"00000"

,

"msg"

"操作成功"

,

"data"

: [{

"id"

1

,

"name"

"父元素"

,

"treePath"

""

,

"parentId"

0

,

"children"

: [{

"id"

2

,

"name"

"子元素1"

,

"treePath"

"1"

,

"parentId"

1

,

"children"

: []

  }, {

"id"

3

,

"name"

"子元素2"

,

"treePath"

"1"

,

"parentId"

1

,

"children"

: [{

"id"

4

,

"name"

"子元素2的孫子元素"

,

"treePath"

"1,3"

,

"parentId"

3

,

"children"

: []

   }]

  }]

 }]

}  

測試過濾以及重構資料
測試程式碼:
// 對 3L 進行剪枝,對 1L 進行修改

testChildren = TreeNodeUtil.buildTree(testChildren, (item) -> {

if

 (item.getId().equals(

1L

)) {

        item.setName(

"更改了 Id 為 1L 的資料名稱"

);

    }

return

 item;

}, (item) -> item.getId().equals(

3L

));

返回結果:

{

"code"

"00000"

,

"msg"

"操作成功"

,

"data"

: [{

"id"

1

,

"name"

"更改了 Id 為 1L 的資料名稱"

,

"treePath"

""

,

"parentId"

0

,

"children"

: [{

"id"

2

,

"name"

"子元素1"

,

"treePath"

"1"

,

"parentId"

1

,

"children"

: []

  }]

 }]

}

接下來的測試結果以口述的方式講解
測試傳入錯誤的 ids
  • 返回傳入的 testChildren
測試傳入具有父子結構,但是 ids 傳錯的情況 (可以根據實際需求更改是否自動識別父元素)
  • 返回傳入的 testChildren
測試  testChildren 中 children元素為 null
  • 給出提示,不構建樹
測試 generateTreePath 生成路徑
  • 返回路徑

歡迎加入我的知識星球,全面提升技術能力。
👉 加入方式,長按”或“掃描”下方二維碼噢
星球的內容包括:專案實戰、面試招聘、原始碼解析、學習路線。
文章有幫助的話,在看,轉發吧。
謝謝支援喲 (*^__^*)

相關文章