拒絕重復代碼,封裝一個多級菜單、多級評論、多級部門的統一工具類!
一、介紹
你能看到很多人都在介紹如何實現多級菜單的效果,但是都有一個共同的缺點,那就是沒有解決代碼會重復開發的問題。如果我需要實現多級評論呢,是否又需要自己再寫一遍?
為了簡化開發過程并提高代碼的可維護性,我們可以創建一個統一的工具類來處理這些需求。在本文中,我將介紹如何使用SpringBoot創建一個返回多級菜單、多級評論、多級部門、多級分類的統一工具類。
介紹數據庫字段設計
數據庫設計
「主要是介紹是否需要tree_path字段。」
多級節點的數據庫大家都知道,一般會有id,parentId字段,但是對于tree_path
字段,這個需要根據設計者來定。
優點:
- 如果你對數據的讀取操作比較頻繁,而且需要快速查詢某個節點的所有子節點或父節點,那么使用
tree_path
字段可以提高查詢效率。 tree_path
字段可以使用路徑字符串表示節點的層級關系,例如使用逗號分隔的節點ID列表。這樣,可以通過模糊匹配tree_path
字段來查詢某個節點的所有子節點或父節點,而無需進行遞歸查詢。- 你可以使用模糊匹配的方式,找到所有以該節點的
tree_path
開頭的子節點,并將它們刪除。而無需進行遞歸刪除。
缺點:
- 每次插入時,需要更新tree_path 字段,這可能會導致性能下降。
- tree_path 字段的長度可能會隨著樹的深度增加而增加,可能會占用更多的存儲空間。
因此,在設計數據庫評論字段時,需要權衡使用treepath字段和父評論ID字段的優缺點,并根據具體的應用場景和需求做出選擇。如果你更關注讀取操作的效率和查詢、刪除的靈活性,可以考慮使用tree_path
字段。如果你更關注寫入操作的效率和數據一致性,并且樹的深度不會很大,那么使用父評論ID字段來實現多級評論可能更簡單和高效。
二、統一工具類具體實現
1. 定義接口,統一規范
對于有 lombok 的小伙伴,實現這個方法很簡單,只需要加上@Data即可
/**
* @Description: 固定屬性結構屬性
* @Author: yiFei
*/
publicinterface ITreeNode<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
*/
publicclass TreeNodeUtil {
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
publicclass TestChildren implements ITreeNode<TestChildren> {
private Long id;
private String name;
private String treePath;
private Long parentId;
public TestChildren(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<>();
}
測試基本功能
測試基本功能代碼:
public static void main(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": []
}]
}]
}]
}
測試過濾以及重構數據
測試代碼:
public static void main(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 生成路徑
- 返回路徑