拒绝重复代码,封装一个多级菜单、多级评论、多级部门的统一工具类
👉 这是一个或许对你有用的社群
🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入「芋道快速开发平台」知识星球。下面是星球提供的部分资料:
《项目实战(视频)》:从书中学,往事上“练” 《互联网高频面试题》:面朝简历学习,春暖花开 《架构 x 系统设计》:摧枯拉朽,掌控面试高频场景题 《精进 Java 学习指南》:系统学习,互联网主流技术栈 《必读 Java 源码专栏》:知其然,知其所以然
👉这是一个或许对你有用的开源项目
国产 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
*/
public interface 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
*/
public class TreeNodeUtil {
private static final Logger log = LoggerFactory.getLogger(TreeNodeUtil.class);
public static final String PARENT_NAME = "parent";
public static final String CHILDREN_NAME = "children";
public static final List<Object> IDS = Collections.singletonList(0L);
public static <T extends ITreeNode> List<T> buildTree(List<T> dataList) {
return buildTree(dataList, IDS, (data) -> data, (item) -> true);
}
public static <T extends ITreeNode> List<T> buildTree(List<T> dataList, Function<T, T> map) {
return buildTree(dataList, IDS, map, (item) -> true);
}
public static <T extends ITreeNode> List<T> buildTree(List<T> dataList, Function<T, T> map, Predicate<T> filter) {
return buildTree(dataList, IDS, map, filter);
}
public static <T extends ITreeNode> List<T> buildTree(List<T> dataList, List<Object> ids) {
return buildTree(dataList, ids, (data) -> data, (item) -> true);
}
public static <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
*/
public static <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
*/
public static <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
public class 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 生成路径
返回路径
欢迎加入我的知识星球,全面提升技术能力。
👉 加入方式,“长按”或“扫描”下方二维码噢:
星球的内容包括:项目实战、面试招聘、源码解析、学习路线。
文章有帮助的话,在看,转发吧。
谢谢支持哟 (*^__^*)
微信扫码关注该文公众号作者