拒絕重復(fù)代碼,封裝一個(gè)多級(jí)菜單、多級(jí)評(píng)論、多級(jí)部門(mén)的統(tǒng)一工具類(lèi)!
一、介紹
你能看到很多人都在介紹如何實(shí)現(xiàn)多級(jí)菜單的效果,但是都有一個(gè)共同的缺點(diǎn),那就是沒(méi)有解決代碼會(huì)重復(fù)開(kāi)發(fā)的問(wèn)題。如果我需要實(shí)現(xiàn)多級(jí)評(píng)論呢,是否又需要自己再寫(xiě)一遍?
為了簡(jiǎn)化開(kāi)發(fā)過(guò)程并提高代碼的可維護(hù)性,我們可以創(chuàng)建一個(gè)統(tǒng)一的工具類(lèi)來(lái)處理這些需求。在本文中,我將介紹如何使用SpringBoot創(chuàng)建一個(gè)返回多級(jí)菜單、多級(jí)評(píng)論、多級(jí)部門(mén)、多級(jí)分類(lèi)的統(tǒng)一工具類(lèi)。
介紹數(shù)據(jù)庫(kù)字段設(shè)計(jì)
數(shù)據(jù)庫(kù)設(shè)計(jì)
「主要是介紹是否需要tree_path字段。」
多級(jí)節(jié)點(diǎn)的數(shù)據(jù)庫(kù)大家都知道,一般會(huì)有id,parentId字段,但是對(duì)于tree_path
字段,這個(gè)需要根據(jù)設(shè)計(jì)者來(lái)定。
優(yōu)點(diǎn):
- 如果你對(duì)數(shù)據(jù)的讀取操作比較頻繁,而且需要快速查詢某個(gè)節(jié)點(diǎn)的所有子節(jié)點(diǎn)或父節(jié)點(diǎn),那么使用
tree_path
字段可以提高查詢效率。 tree_path
字段可以使用路徑字符串表示節(jié)點(diǎn)的層級(jí)關(guān)系,例如使用逗號(hào)分隔的節(jié)點(diǎn)ID列表。這樣,可以通過(guò)模糊匹配tree_path
字段來(lái)查詢某個(gè)節(jié)點(diǎn)的所有子節(jié)點(diǎn)或父節(jié)點(diǎn),而無(wú)需進(jìn)行遞歸查詢。- 你可以使用模糊匹配的方式,找到所有以該節(jié)點(diǎn)的
tree_path
開(kāi)頭的子節(jié)點(diǎn),并將它們刪除。而無(wú)需進(jìn)行遞歸刪除。
缺點(diǎn):
- 每次插入時(shí),需要更新tree_path 字段,這可能會(huì)導(dǎo)致性能下降。
- tree_path 字段的長(zhǎng)度可能會(huì)隨著樹(shù)的深度增加而增加,可能會(huì)占用更多的存儲(chǔ)空間。
因此,在設(shè)計(jì)數(shù)據(jù)庫(kù)評(píng)論字段時(shí),需要權(quán)衡使用treepath字段和父評(píng)論ID字段的優(yōu)缺點(diǎn),并根據(jù)具體的應(yīng)用場(chǎng)景和需求做出選擇。如果你更關(guān)注讀取操作的效率和查詢、刪除的靈活性,可以考慮使用tree_path
字段。如果你更關(guān)注寫(xiě)入操作的效率和數(shù)據(jù)一致性,并且樹(shù)的深度不會(huì)很大,那么使用父評(píng)論ID字段來(lái)實(shí)現(xiàn)多級(jí)評(píng)論可能更簡(jiǎn)單和高效。
二、統(tǒng)一工具類(lèi)具體實(shí)現(xiàn)
1. 定義接口,統(tǒng)一規(guī)范
對(duì)于有 lombok 的小伙伴,實(shí)現(xiàn)這個(gè)方法很簡(jiǎn)單,只需要加上@Data即可
/**
* @Description: 固定屬性結(jié)構(gòu)屬性
* @Author: yiFei
*/
publicinterface ITreeNode<T> {
/**
* @return 獲取當(dāng)前元素Id
*/
Object getId();
/**
* @return 獲取父元素Id
*/
Object getParentId();
/**
* @return 獲取當(dāng)前元素的 children 屬性
*/
List<T> getChildren();
/**
* ( 如果數(shù)據(jù)庫(kù)設(shè)計(jì)有tree_path字段可覆蓋此方法來(lái)生成tree_path路徑 )
*
* @return 獲取樹(shù)路徑
*/
default Object getTreePath() { return""; }
}
2. 編寫(xiě)工具類(lèi)TreeNodeUtil
其中我們需要實(shí)現(xiàn)能將一個(gè)List元素構(gòu)建成熟悉結(jié)構(gòu)
我們需要實(shí)現(xiàn)生成tree_path
字段
我們需要優(yōu)雅的實(shí)現(xiàn)該方法
/**
* @Description: 樹(shù)形結(jié)構(gòu)工具類(lèi)
* @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);
}
/**
* 數(shù)據(jù)集合構(gòu)建成樹(shù)形結(jié)構(gòu) ( 注: 如果最開(kāi)始的 ids 不在 dataList 中,不會(huì)進(jìn)行任何處理 )
*
* @param dataList 數(shù)據(jù)集合
* @param ids 父元素的 Id 集合
* @param map 調(diào)用者提供 Function<T, T> 由調(diào)用著決定數(shù)據(jù)最終呈現(xiàn)形勢(shì)
* @param filter 調(diào)用者提供 Predicate<T> false 表示過(guò)濾 ( 注: 如果將父元素過(guò)濾掉等于剪枝 )
* @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. 將數(shù)據(jù)分為 父子結(jié)構(gòu)
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 如果未分出或過(guò)濾了父元素則將子元素返回
if (parent.size() == 0) {
return children;
}
// 2. 使用有序集合存儲(chǔ)下一次變量的 ids
List<Object> nextIds = new ArrayList<>(dataList.size());
// 3. 遍歷父元素 以及修改父元素內(nèi)容
List<T> collectParent = parent.stream().map(map).collect(Collectors.toList());
for (T parentItem : collectParent) {
// 3.1 如果子元素已經(jīng)加完,直接進(jìn)入下一輪循環(huán)
if (nextIds.size() == children.size()) {
break;
}
// 3.2 過(guò)濾出 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 發(fā)生錯(cuò)誤, 傳入?yún)?shù)中 children 不能為 null,解決方法: \n" +
"方法一、在map(推薦)或filter中初始化 \n" +
"方法二、List<T> children = new ArrayList<>() \n" +
"方法三、初始化塊對(duì)屬性賦初值\n" +
"方法四、構(gòu)造時(shí)對(duì)屬性賦初值");
}
});
}
buildTree(children, nextIds, map, filter);
return parent;
}
/**
* 生成路徑 treePath 路徑
*
* @param currentId 當(dāng)前元素的 id
* @param getById 用戶返回一個(gè) 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. 如果當(dāng)前節(jié)點(diǎn)是父節(jié)點(diǎn)直接返回
treePath.append(currentId);
} else {
// 2. 調(diào)用者將當(dāng)前元素的父元素查出來(lái),方便后續(xù)拼接
T byId = getById.apply(currentId);
// 3. 父元素的 treePath + "," + 父元素的id
if (!ObjectUtils.isEmpty(byId)) {
treePath.append(byId.getTreePath()).append(",").append(byId.getId());
}
}
return treePath.toString();
}
}
這樣我們就完成了 TreeNodeUtil
統(tǒng)一工具類(lèi),首先我們將元素分為父子兩類(lèi),讓其構(gòu)建出一個(gè)小型樹(shù),然后我們將構(gòu)建的子元素和下次遍歷的父節(jié)點(diǎn)傳入,遞歸的不斷進(jìn)行,這樣就構(gòu)建出了我們最終的想要實(shí)現(xiàn)的效果。
三、測(cè)試
定義一個(gè)類(lèi)實(shí)現(xiàn) ITreeNode
/**
* @Description: 測(cè)試子元素工具類(lèi)
* @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<>();
}
測(cè)試基本功能
測(cè)試基本功能代碼:
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)));
}
返回結(jié)果:
{
"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": []
}]
}]
}]
}
測(cè)試過(guò)濾以及重構(gòu)數(shù)據(jù)
測(cè)試代碼:
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)));
}
返回結(jié)果 :
{
"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": []
}]
}]
}]
}
測(cè)試過(guò)濾以及重構(gòu)數(shù)據(jù)
測(cè)試代碼:
// 對(duì) 3L 進(jìn)行剪枝,對(duì) 1L 進(jìn)行修改
testChildren = TreeNodeUtil.buildTree(testChildren, (item) -> {
if (item.getId().equals(1L)) {
item.setName("更改了 Id 為 1L 的數(shù)據(jù)名稱(chēng)");
}
return item;
}, (item) -> item.getId().equals(3L));
返回結(jié)果:
{
"code": "00000",
"msg": "操作成功",
"data": [{
"id": 1,
"name": "更改了 Id 為 1L 的數(shù)據(jù)名稱(chēng)",
"treePath": "",
"parentId": 0,
"children": [{
"id": 2,
"name": "子元素1",
"treePath": "1",
"parentId": 1,
"children": []
}]
}]
}
接下來(lái)的測(cè)試結(jié)果以口述的方式講解
測(cè)試傳入錯(cuò)誤的 ids
- 返回傳入的 testChildren
測(cè)試傳入具有父子結(jié)構(gòu),但是 ids 傳錯(cuò)的情況 (可以根據(jù)實(shí)際需求更改是否自動(dòng)識(shí)別父元素)
- 返回傳入的 testChildren
測(cè)試 testChildren 中 children元素為 null
- 給出提示,不構(gòu)建樹(shù)
測(cè)試 generateTreePath 生成路徑
- 返回路徑