1、需求:
有一个菜单表,字段catId用于唯一标识每一条记录,每一条记录代表一个菜单项;另有parentCid用于记录该记录的父级菜单,现需为每个菜单绑定他们的子菜单,表结构如下
CREATE TABLE `pms_category` (
`cat_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '分类id',
`name` char(50) DEFAULT NULL COMMENT '分类名称',
`parent_cid` bigint(20) DEFAULT NULL COMMENT '父分类id',
`cat_level` int(11) DEFAULT NULL COMMENT '层级',
`show_status` tinyint(4) DEFAULT NULL COMMENT '是否显示[0-不显示,1显示]',
`sort` int(11) DEFAULT NULL COMMENT '排序',
`icon` char(255) DEFAULT NULL COMMENT '图标地址',
`product_unit` char(50) DEFAULT NULL COMMENT '计量单位',
`product_count` int(11) DEFAULT NULL COMMENT '商品数量',
PRIMARY KEY (`cat_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1433 DEFAULT CHARSET=utf8mb4 COMMENT='商品三级分类';
2、实现(初版代码有bug,先理解思路再看bug解决)
1、根据表字段创建实体类:CategoryEntity
2、为实体类添加子菜单成员变量,每个菜单的子菜单有多个,是一个列表
//mybatis-puls注解,表示这不是一个表字段 @TableField(exist = false) private List<CategoryEntity> children;
3、在菜单业务实现类CategoryServiceImpl创建方法用于实现需求,其中baseMapper为父类ServiceImpl中的成员变量,在继承ServiceImpl指定泛型可指定baseMapper的dao层类型,从而执行sql
具体代码:
@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {
@Override
public List<CategoryEntity> listWithTree() {
//查出所有分类
List<CategoryEntity> entities = baseMapper.selectList(null);
//找出一级菜单及进行树形构建
List<CategoryEntity> level1Menus = entities.stream().filter(categoryEntity ->
//筛选出一级菜单,
categoryEntity.getParentCid() == 0
).map(menu -> {
//为一级菜单递归设置子菜单
menu.setChildren(getChildren(menu, entities));
return menu;
}).sorted((menu1, menu2) ->
//对一级菜单排序
(menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort())
).collect(Collectors.toList());
return level1Menus;
}
/**
* 递归设置子菜单
* @param root 父菜单
* @param all 所有菜单
* @return
*/
private List<CategoryEntity> getChildren(CategoryEntity root, List<CategoryEntity> all) {
//递归结束条件为filter结果为空
List<CategoryEntity> childrenList = all.stream().filter(children ->
//筛选出root的子菜单
root.getCatId() == children.getParentCid()
).map(menu -> {
//递归设置子菜单的子菜单
menu.setChildren(getChildren(menu, all));
return menu;
}).sorted((menu1, menu2) ->
//对root的子菜单排序
(menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort())
).collect(Collectors.toList());
return childrenList;
}
}
3、列表不全bug修复
1、原因及解决
在完成树形菜单的代码后,经测试发现某些记录没有被收集,即有些上级菜单丢失了下级菜单,排查结果是在过滤时的条件出现问题了,实体类的catId和parentId都是采用的Long包装类型,使用
'=='比较的是对象的内存地址而非值,故需要进行以下修改,将==改为equals比较:
/**
* 递归设置子菜单
* @param root 父菜单
* @param all 所有菜单
* @return
*/
private List<CategoryEntity> getChildren(CategoryEntity root, List<CategoryEntity> all) {
//递归结束条件为filter结果为空
List<CategoryEntity> childrenList = all.stream().filter(children ->
//此处修改
root.getCatId().equals(children.getParentCid())
).map(menu -> {
//递归设置子菜单的子菜单
menu.setChildren(getChildren(menu, all));
return menu;
}).sorted((menu1, menu2) ->
//对root的子菜单排序
(menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort())
).collect(Collectors.toList());
return childrenList;
}
排查过程中还发现编号为22的菜单没有丢失下级菜单,是mybatis进行调用了实体类的set方法,采用的是以下赋值方法:
编号为22的菜单:
Long catId = 22L;
编号22的子菜单:
Long parentCid = 22L;
而java对-128到127的值进行了缓存,因此不会出现问题
2、排查过程如下:
1、在测试工程中模拟该上级菜单的子菜单组装过程,发现在过滤时缺失了相关子菜单
@SpringBootTest
class MxshopProductApplicationTests {
//注入mapper
@Autowired
BaseMapper<CategoryEntity> baseMapper;
@Test
void contextLoads() {
//查询出所有数据
List<CategoryEntity> all = baseMapper.selectList(null);
//筛选出缺失下级菜单的上级菜单,id为163
List<CategoryEntity> list = all.stream().filter(categoryEntity -> {
return categoryEntity.getCatId() == 163;
}).map(menu -> {
menu.setChildren(getChildrens(menu, all));
return menu;
}).collect(Collectors.toList());
//查看该菜单的数据
System.out.println(list.get(0));
}
private List<CategoryEntity> getChildrens(CategoryEntity root,List<CategoryEntity> all){
List<CategoryEntity> children = all.stream().filter(categoryEntity -> {
return categoryEntity.getParentCid() == root.getCatId();
}).map(categoryEntity -> {
//1、找到子菜单
categoryEntity.setChildren(getChildrens(categoryEntity,all));
return categoryEntity;
}).sorted((menu1,menu2)->{
//2、菜单的排序
return (menu1.getSort()==null?0:menu1.getSort()) - (menu2.getSort()==null?0:menu2.getSort());
}).collect(Collectors.toList());
//查看子菜单数量,上级菜单是2级,子菜单为末级菜单
System.out.println("================");
System.out.println(children.size());
System.out.println("================");
return children;
}
}
打印结果如下:
1、上级菜单的数据,children列表为空:
CategoryEntity(catId=163, name=全新整车, parentCid=21, catLevel=2, showStatus=1, sort=0, icon=null, productUnit=null, productCount=0, children=[])
2、递归方法中也显示子菜单为空:
2、查询数据库发现有下级菜单,说明菜单丢失
3、设置对比样例
//获取编号163的菜单实体
CategoryEntity categoryEntity = baseMapper.selectById(163);
System.out.println("========");
List<CategoryEntity> l1 = all.stream().filter(c -> {
return c.getParentCid() == 163;
}).collect(Collectors.toList());
List<CategoryEntity> l2 = all.stream().filter(c -> {
return c.getParentCid() == categoryEntity.getCatId();
}).collect(Collectors.toList());
System.out.println("l1===" + l1.size());
System.out.println("l2===" + l2.size());
System.out.println("=========");
对比结果:
基本数据类型与对象中的成员变量进行==比对正常,从对象获取成员变量进行==比对异常,考虑是引用类型自动拆箱导致前者正常
4、查看实体类
package com.mx.mxshop.product.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
import lombok.Data;
/**
*
*
* @author mx
* @email mx@gmail.com
* @date 2021-12-29 19:12:58
*/
@Data
@TableName("pms_category")
public class CategoryEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
*
*/
@TableId
private Long catId;
/**
*
*/
private String name;
/**
*
*/
private Long parentCid;
/**
*
*/
private Integer catLevel;
/**
*
*/
@TableLogic
private Integer showStatus;
/**
*
*/
private Integer sort;
/**
* ͼ
*/
private String icon;
/**
*
*/
private String productUnit;
/**
*
*/
private Integer productCount;
/**
* 子菜单,非表字段
*/
@TableField(exist = false)
private List<CategoryEntity> children;
}
果然是引用类型导致的,问题解决