Java小技能:多级菜单排序并返回树结构菜单列表

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 需求: 菜单管理(服务商角色配置权限管理)、文章分类、MCC类目、区域信息。

引言

需求: 菜单管理(服务商角色配置权限管理)、文章分类、MCC类目、区域信息。

地区和菜单数据添加到Redis缓存

框架: SpringBoot+MybatisPlus 对数据表中的菜单进行排序并返回树形Json格式的菜单列表

实现思路:先获取全部菜单,然后再对菜单进行装配,生成树形结构。

先获取一级菜单,再递归获取子节点
{
  "data": [
    {
      "id": "1595742481192857601",
      "createTime": "2022-11-24 19:33:50",
      "createId": "1",
      "updateId": "1",
      "updateTime": "2022-11-24 19:33:50",
      "code": "1",
      "name": "string",
      "parentId": null,
      "url": "string",
      "isExpand": "0",
      "isShow": "0",
      "type": "1",
      "typeText": "菜单",
      "sortNum": "0",
      "tagsType": "PT",
      "tagsTypeText": "平台",
      "remark": "string",
      "children": []
    },
    {
      "id": "1595742537635606529",
      "createTime": "2022-11-24 19:34:03",
      "createId": "1",
      "updateId": "1",
      "updateTime": "2022-11-24 19:34:03",
      "code": "2",
      "name": "string",
      "parentId": null,
      "url": "string",
      "isExpand": "0",
      "isShow": "0",
      "type": "1",
      "typeText": "菜单",
      "sortNum": "0",
      "tagsType": "PT",
      "tagsTypeText": "平台",
      "remark": "string",
      "children": []
    },
    {
      "id": "1595742587770122242",
      "createTime": "2022-11-24 19:34:15",
      "createId": "1",
      "updateId": "1",
      "updateTime": "2022-11-24 19:34:15",
      "code": "3",
      "name": "string",
      "parentId": null,
      "url": "string",
      "isExpand": "0",
      "isShow": "0",
      "type": "1",
      "typeText": "菜单",
      "sortNum": "0",
      "tagsType": "PT",
      "tagsTypeText": "平台",
      "remark": "string",
      "children": [
        {
          "id": "1595742715377627138",
          "createTime": "2022-11-24 19:34:45",
          "createId": "1",
          "updateId": "1",
          "updateTime": "2022-11-24 19:34:45",
          "code": "4",
          "name": "string",
          "parentId": "1595742587770122242",
          "url": "string",
          "isExpand": "0",
          "isShow": "0",
          "type": "1",
          "typeText": "菜单",
          "sortNum": "0",
          "tagsType": "PT",
          "tagsTypeText": "平台",
          "remark": "string",
          "children": []
        },
        {
          "id": "1595742770520141826",
          "createTime": "2022-11-24 19:34:59",
          "createId": "1",
          "updateId": "1",
          "updateTime": "2022-11-24 19:34:59",
          "code": "5",
          "name": "string",
          "parentId": "1595742587770122242",
          "url": "string",
          "isExpand": "0",
          "isShow": "0",
          "type": "1",
          "typeText": "菜单",
          "sortNum": "0",
          "tagsType": "PT",
          "tagsTypeText": "平台",
          "remark": "string",
          "children": [
            {
              "id": "1595742832834916354",
              "createTime": "2022-11-24 19:35:13",
              "createId": "1",
              "updateId": "1",
              "updateTime": "2022-11-24 19:35:13",
              "code": "6",
              "name": "string",
              "parentId": "1595742770520141826",
              "url": "string",
              "isExpand": "0",
              "isShow": "0",
              "type": "1",
              "typeText": "菜单",
              "sortNum": "0",
              "tagsType": "PT",
              "tagsTypeText": "平台",
              "remark": "string",
              "children": []
            },
            {
              "id": "1595742854490107906",
              "createTime": "2022-11-24 19:35:19",
              "createId": "1",
              "updateId": "1",
              "updateTime": "2022-11-24 19:35:19",
              "code": "7",
              "name": "string",
              "parentId": "1595742770520141826",
              "url": "string",
              "isExpand": "0",
              "isShow": "0",
              "type": "1",
              "typeText": "菜单",
              "sortNum": "0",
              "tagsType": "PT",
              "tagsTypeText": "平台",
              "remark": "string",
              "children": []
            }
          ]
        }
      ]
    }
  ]

I 序列化:生成树形结构菜单列表

1.1 读取表数据,获取全部菜单

    /**
     * 先获取全部菜单,然后再对菜单进行装配,生成树形结构
     */

        List<TSysMenu> list = tSysMenuMapper.selectList(queryWrapper);

        List<SysMenuDto> listDto = getSortMenus(list);

1.2 序列化:生成树形结构菜单列表

获取一级菜单,递归获取子节点。

    @Override
    public List<SysMenuDto> getSortMenus(List<TSysMenu> sourceList) throws Exception {
        if (sourceList.size() < 1) {
            return null;
        }
        List<SysMenuDto> dtos = sourceList.stream().map(ele -> {
            SysMenuDto dto = new SysMenuDto();
            BeanUtils.copyProperties(ele, dto);
            return dto;
        }).collect(Collectors.toList());
        // 获取第一层SysMenuDto: 筛选parentId为空的   或者0的情况: item.getParentId().equals("0")
        return getChild(null,dtos);
    }

    /**
     * 递归设置节点
     *
     * @param id
     * @param allMenu
     * @return
     */
    private List<SysMenuDto> getChild(Long parentId, List<SysMenuDto> allMenu) {
        //item.getParentId().equals(id) 会出现空指针的情况
        return allMenu.stream()
                .filter(item -> {
                    if (parentId == null) {
                        return item.getParentId() == null;
                    } else {
                        return item.getParentId() != null && item.getParentId().equals(parentId);//集合filter过滤Integer数值为空问题解决方案:使用equal取代==判断。
                    }
                })
                .map(item -> item.setChildren(getChild(item.getId(), allMenu)))
                .sorted(Comparator.comparingInt(menu -> (menu.getSortNum() == null ? 0 : menu.getSortNum())))
                .collect(Collectors.toList());
    }

1.3 实体

  • 单表查询不用添加事务注解` @Transactional

`

  • 新生成实体时,加一下fill,时间和用户ID自动填充。

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("t_sys_menu")
@ApiModel(value="TSysMenu对象", description="系统菜单表")
public class SysMenu extends Model<SysMenu> {
    @ApiModelProperty(value = "主键id")
    @TableId("id")
    private Long id;
        @ApiModelProperty(value = "菜单父级id")
    @TableField("parent_id")
    private Long parentId;
        @ApiModelProperty(value = "排序")
    @TableField("sort_num")
    private Integer sortNum;
        @Override
    protected Serializable pkVal() {
        return this.id;
    }

}

1.4 Dto

@Data
@Accessors(chain = true)//链式访问
public class SysMenuDto extends SysMenu {

    @ApiModelProperty(value = "子菜单")
    private List<SysMenuDto> children;
}

II 查询优化

2.1 分页查询树结构菜单列表

  1. PageHelper直接对List进行分页
    /*
     * PageHelper直接对List进行分页
     */
    private PageInfo getPageInfoByList(List all,Integer pageNum,Integer pageSize) {
        //创建Page类
        Page page = new Page(pageNum, pageSize);
//为Page类中的total属性赋值
        int total = all.size();
        page.setTotal(total);
//计算当前需要显示的数据下标起始值
        int startIndex = (pageNum - 1) * pageSize;
        int endIndex = Math.min(startIndex + pageSize,total);
//从链表中截取需要显示的子链表,并加入到Page
        page.addAll(all.subList(startIndex,endIndex));
//以Page创建PageInfo
        PageInfo pageInfo = new PageInfo<>(page);
        return pageInfo;
    }
  1. PageHelper.startPage 开启分页,通过拦截MySQL的方式,把你的查询语句拦截下来加limit.将查询语句放到PageHelper.startPage后面进行执行
        PageHelper.startPage(input.getPageNum(), input.getPageSize());
         List<TSysTaoCollege> collegeList = tSysTaoCollegeService.list(lambda);
         PageInfo pageInfo = new PageInfo(collegeList);
         PageHelper.clearPage();

2.2 地区和菜单数据添加到Redis缓存

使用StringRedisTemplate

  1. 写入Redis时,手动把对象序列化为json格式字符串。
  2. 读取Redis时,手动把读取到的JSON反序列化成对象。
    /*
      先获取全部菜单,然后再对菜单进行装配,生成树形结构
     */
    public List<SysMenuDto> getMenus(ETagsType tagsType, Boolean isReadDb) throws Exception {
        List<TSysMenu> list = getTmenus(tagsType,isReadDb);//读取表数据
        List<SysMenuDto> listDto = getSortMenus(list);//序列化数据
        return listDto;
    }

    public List<TSysMenu> getTmenus(ETagsType tagsType, Boolean isReadDb) throws Exception{
        if (tagsType == null) {
            throw CommonException.create(ServerResponse.createByError("菜单对象类型不能为空"));
        }
        //rediskey名:rediskey常量类+菜单对象类型
        String keyName = RedisKeyConstant.MENU + "." + tagsType.getCode();//
        QueryWrapper<TSysMenu> queryWrapper = new QueryWrapper<>();
        //是否读取数据库的菜单,true读取,false不读取
        if (isReadDb) {
            queryWrapper.eq("tags_type", tagsType);
            List<TSysMenu> list = tSysMenuMapper.selectList(queryWrapper);
            stringRedisTemplate.opsForValue().set(keyName, JSONUtil.toJsonStr(list));//写入数据到缓存
            return list;
        } else {
            //读取缓存
            if (!stringRedisTemplate.hasKey(keyName)) {
                queryWrapper.eq("tags_type", tagsType);
                List<TSysMenu> list = tSysMenuMapper.selectList(queryWrapper);
                stringRedisTemplate.opsForValue().set(keyName, JSONUtil.toJsonStr(list));//写入数据到缓存
                return list;
            } else {
                //直接读取缓存数据进行序列化
                String jsonStr = stringRedisTemplate.opsForValue().get(keyName);
                List<TSysMenu> list = JSONUtil.toList(jsonStr, TSysMenu.class);
                return list;
            }
        }
    }

III 排序

3.1 新增节点的默认排序

/*
    用于新增时的排序字段
    */
    private void setSortNum4create(TSysMenu menu){
        LambdaQueryWrapper<TSysMenu> queryWrapper = new LambdaQueryWrapper<>();
        if(menu.getParentId() == null){
            queryWrapper.isNull(TSysMenu::getParentId);
        }else{
            queryWrapper.eq(null != menu.getParentId(),TSysMenu::getParentId,menu.getParentId());
        }
        queryWrapper.orderByDesc(TSysMenu::getSortNum);
        var sortCate = tSysMenuMapper.selectOne(queryWrapper.last("limit 1"));
        if (sortCate!=null)
        {
            menu.setSortNum(sortCate.getSortNum()+1);
        }else{
            menu.setSortNum(0);
        }
    }

3.2 对分类进行上移和下移排序

查询最近的兄弟节点进行交换

/*
    对分类进行排序
     */
    public TSysCollegeCategory sortCategory(Long id,Integer type) throws Exception {
        if(id ==null){
            throw CommonException.create(ServerResponse.createByError("id不存在"));
        }
        var current = tSysCollegeCategoryMapper.selectById(id);
        if(current ==null){
            throw CommonException.create(ServerResponse.createByError("分类不存在"));
        }
        LambdaQueryWrapper<TSysCollegeCategory> queryWrapper = new LambdaQueryWrapper<>();
        // 检查ParentId为空的情况。
        if(current.getParentId() == null){
            queryWrapper.isNull(TSysCollegeCategory::getParentId);
        }else{
            queryWrapper.eq(null != current.getParentId(),TSysCollegeCategory::getParentId,current.getParentId());
        }
        if(type ==1){//上移
            queryWrapper.lt(TSysCollegeCategory::getSortNum,current.getSortNum())
                    .orderByAsc(TSysCollegeCategory::getSortNum).last("limit 1");
        }else if(type ==2){//下移
            queryWrapper.gt(TSysCollegeCategory::getSortNum,current.getSortNum())
                    .orderByAsc(TSysCollegeCategory::getSortNum).last("limit 1");
        }else{
            throw CommonException.create(ServerResponse.createByError("type类型错误"));
        }
        var next = tSyCollegeCategoryMapper.selectOne(queryWrapper);
        if(next !=null)
        {
            var sort = current.getSortNum();
            current.setSortNum(next.getSortNum());
            next.setSortNum(sort);
            tSysCollegeCategoryMapper.updateById(current);
            tSysCollegeCategoryMapper.updateById(next);
        }
        return current;
    }

IV 常见问题

预备知识:序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。

4.1 no instance(s) of type variable(s) R exist so that void conforms to R,

实体类上标注有 @Accessors(chain = true)//链式访问

                .map(item -> item.setChildren(getChild(item.getId(), dtos)))

4.2 MybatisPlus QueryWrapper的null查询

        if(category.getParentId() == null){
        queryWrapper.isNull("parent_id");
        }else{
            queryWrapper
                    .eq("parent_id", category.getParentId());
        }

4.3 集合filter过滤Integer数值为空问题解决方案

    /**
     * @param parentId 传递的父id  用来过滤用 ,可以为空
     * @return
     * @throws Exception
     */
    private List<SysMenuDto> getChilds(Long parentId, List<SysMenuDto> sourceList) {

        List<SysMenuDto> menus = sourceList.stream()
                .filter(menu -> {
                    if (parentId == null) {
                        return menu.getParentId() == null;
                    } else {
                        return menu.getParentId() != null && menu.getParentId().equals(parentId);
                    }
                }).collect(Collectors.toList());
        //排序 0表示最前面
        return menus;
    }
相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
2月前
|
安全 Java 调度
解锁Java并发编程高阶技能:深入剖析无锁CAS机制、揭秘魔法类Unsafe、精通原子包Atomic,打造高效并发应用
【8月更文挑战第4天】在Java并发编程中,无锁编程以高性能和低延迟应对高并发挑战。核心在于无锁CAS(Compare-And-Swap)机制,它基于硬件支持,确保原子性更新;Unsafe类提供底层内存操作,实现CAS;原子包java.util.concurrent.atomic封装了CAS操作,简化并发编程。通过`AtomicInteger`示例,展现了线程安全的自增操作,突显了这些技术在构建高效并发程序中的关键作用。
59 1
|
2月前
|
Java API
|
2月前
|
存储 Java
Java中ArrayList 元素的排序
本文提供了Java中根据`ArrayList`元素的某个属性进行排序的示例代码,包括实现`Comparable`接口和重载`compareTo`方法,然后使用`Collections.sort`方法进行排序。
|
3月前
|
Java API 存储
Java如何对List进行排序?
【7月更文挑战第26天】
124 9
Java如何对List进行排序?
|
2月前
|
存储 Java API
【Java高手必备】揭秘!如何优雅地对List进行排序?掌握这几种技巧,让你的代码瞬间高大上!
【8月更文挑战第23天】本文深入探讨了Java中对List集合进行排序的各种方法,包括使用Collections.sort()、自定义Comparator以及Java 8的Stream API。通过示例代码展示了不同情况下如何选择合适的方法:从简单的整数排序到自定义类对象的排序,再到利用Comparator指定特殊排序规则,最后介绍了Stream API在排序操作中的简洁应用。理解这些技术的区别与应用场景有助于提高编程效率。
22 4
|
2月前
|
存储 Java
如何在 Java 中打印字符串数组列表
【8月更文挑战第23天】
30 2
|
2月前
|
存储 Java
|
2月前
|
搜索推荐 算法 Java
堆排序实战:轻松实现高效排序,附详细Java代码
嗨,大家好!我是小米,一名热爱技术分享的程序员。今天要带大家了解堆排序——一种基于二叉堆的数据结构,具有O(n log n)时间复杂度的选择排序算法。堆排序分为构建大顶堆和排序两个阶段:先建堆使根节点为最大值,再通过交换根节点与末尾节点并调整堆来逐步排序。它稳定高效,空间复杂度仅O(1),适合对稳定性要求高的场合。虽然不如快速排序快,但在避免递归和节省空间方面有优势。一起动手实现吧!如果有任何疑问,欢迎留言交流!
56 2
|
2月前
|
Java 开发者
在Java编程中,if-else与switch作为核心的条件控制语句,各有千秋。if-else基于条件分支,适用于复杂逻辑;而switch则擅长处理枚举或固定选项列表,提供简洁高效的解决方案
在Java编程中,if-else与switch作为核心的条件控制语句,各有千秋。if-else基于条件分支,适用于复杂逻辑;而switch则擅长处理枚举或固定选项列表,提供简洁高效的解决方案。本文通过技术综述及示例代码,剖析两者在性能上的差异。if-else具有短路特性,但条件增多时JVM会优化提升性能;switch则利用跳转表机制,在处理大量固定选项时表现出色。通过实验对比可见,switch在重复case值处理上通常更快。尽管如此,选择时还需兼顾代码的可读性和维护性。理解这些细节有助于开发者编写出既高效又优雅的Java代码。
30 2
|
2月前
|
Java 容器
07 Java数组与数组操作(定义+遍历+排序+增删改查)(上)
07 Java数组与数组操作(定义+遍历+排序+增删改查)
34 8
下一篇
无影云桌面