拒绝重复代码,封装一个多级菜单、多级评论、多级部门的统一工具类

小哈学Java

共 20911字,需浏览 42分钟

 ·

2024-06-19 10:43

来源:juejin.cn/post/7301909270907748378

👉 欢迎加入小哈的星球 ,你将获得: 专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡 /  每月赠书


新项目:仿小红书(微服务架构)正在更新中... 。全栈前后端分离博客项目 2.0 版本完结啦, 演示链接http://116.62.199.48/ 。全程手摸手,后端 + 前端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,直到项目上线。目前已更新了287小节,累计45w+字,讲解图:2008张,还在持续爆肝中.. 后续还会上新更多项目,目标是将Java领域典型的项目都整一波,如秒杀系统, 在线商城, IM即时通讯,Spring Cloud Alibaba 等等,戳我加入学习,已有1600+小伙伴加入(早鸟价超低)


一、介绍

你能看到很多人都在介绍如何实现多级菜单的效果,但是都有一个共同的缺点,那就是没有解决代码会重复开发的问题。如果我需要实现多级评论呢,是否又需要自己再写一遍?

为了简化开发过程并提高代码的可维护性,我们可以创建一个统一的工具类来处理这些需求。在本文中,我将介绍如何使用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
 */
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 生成路径

  • 返回路径

👉 欢迎加入小哈的星球 ,你将获得: 专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡 /  每月赠书


新项目:仿小红书(微服务架构)正在更新中... 。全栈前后端分离博客项目 2.0 版本完结啦, 演示链接http://116.62.199.48/ 。全程手摸手,后端 + 前端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,直到项目上线。目前已更新了287小节,累计45w+字,讲解图:2008张,还在持续爆肝中.. 后续还会上新更多项目,目标是将Java领域典型的项目都整一波,如秒杀系统, 在线商城, IM即时通讯,Spring Cloud Alibaba 等等,戳我加入学习,已有1600+小伙伴加入(早鸟价超低)



    
       

1. 我的私密学习小圈子~

2. Spring Cloud + Nacos + 负载均衡器实现全链路灰度发布实战

3. 熟悉 Redis 吗,那 Redis 的过期键删除策略是什么?

4. 面试官:如何实现一个合格的分布式锁?

最近面试BAT,整理一份面试资料Java面试BATJ通关手册,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。

获取方式:点“在看”,关注公众号并回复 Java 领取,更多内容陆续奉上。

PS:因公众号平台更改了推送规则,如果不想错过内容,记得读完点一下在看,加个星标,这样每次新文章推送才会第一时间出现在你的订阅列表里。

“在看”支持小哈呀,谢谢啦

浏览 93
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报