菜单功能列表嵌套菜单解决方案
1、通过mapper定义sql的自循环,在查询时就完成菜单的嵌套
数据库字段分析
首先我们来看看数据库中的表字段
显然parentId就是用来做嵌套的切入点
完善表字段
在IDE中我们需要为Menu这个实体类定义个Lis列表数组,用来装载菜单嵌套的数据
注意:我们使用的持久层框架是MybatisPlus,所以添加注解告诉mybatisplus,这个List字段在数据库不存在
接下来我们在mapper.xml文件中定义sql语句,这里建议在Navicat或者sqlyog中编写sql语句,并模拟数据执行测试,这是为了假如后期报错能更容易的定位。如果sql之前能运行成功,那么出错一定在我们后端逻辑
分析sql逻辑
定义实体类映射
<!-- 通用查询映射结果 -->
<resultMap id="BaseResultMap" type="com.shuang.server.pojo.Menu">
<id column="id" property="id" />
<result column="url" property="url" />
<result column="path" property="path" />
<result column="component" property="component" />
<result column="name" property="name" />
<result column="iconCls" property="iconCls" />
<result column="keepAlive" property="keepAlive" />
<result column="requireAuth" property="requireAuth" />
<result column="parentId" property="parentId" />
<result column="enabled" property="enabled" />
</resultMap>
<!-- 这里继承BaseResultMap,避免重复定义映射字段 -->
<resultMap id="Menus" type="com.shuang.server.pojo.Menu" extends="BaseResultMap">
<collection property="children" ofType="com.shuang.server.pojo.Menu">
<id column="id2" property="id" />
<result column="url2" property="url" />
<result column="path2" property="path" />
<result column="component2" property="component" />
<result column="name2" property="name" />
<result column="iconCls2" property="iconCls" />
<result column="keepAlive2" property="keepAlive" />
<result column="requireAuth2" property="requireAuth" />
<result column="parentId2" property="parentId" />
<result column="enabled2" property="enabled" />
</collection>
</resultMap>
<!--根据用户id查询用户菜单-->
<select id="getMenusByAdminId" resultMap="Menus">
SELECT DISTINCT m1.*,
m2.id AS id2,
m2.url AS url2,
m2.path AS path2,
m2.component AS component2,
m2.`name` AS name2,
m2.iconCls AS iconCls2,
m2.keepAlive AS keepAlive2,
m2.requireAuth AS requireAuth2,
m2.parentId AS parentId2,
m2.enabled AS enabled2
FROM yeb.t_menu m1,
yeb.t_menu m2,
yeb.t_admin_role ar,
yeb.t_menu_role mr
WHERE m1.id = m2.parentId
AND m2.id = mr.mid
AND mr.rid = ar.rid
AND ar.adminId = #{id}
and m2.enabled = true
</select>
执行测试
成功完成父子菜单的数据嵌套
2、在后端逻辑中完成菜单关系的嵌套
在service层,根据用户id查询用户所拥有的的菜单
@Override
public List<SysMenuDto> getCurrentUserNav() {
String username = (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
SysUser sysUser = sysUserService.getByUsername(username);
// 根据用户id查询用户所拥有的的菜单
List<Long> navMenuIds = sysUserMapper.getNavMenuIds(sysUser.getId());
List<SysMenu> sysMenus = this.listByIds(navMenuIds);
// 转树状结构
List<SysMenu> menuTree = buildTreeMenu(sysMenus);
// 实体转DTO
return convert(menuTree);
}
上面这段代码涉及了好几个方法,接下来我们逐个来解释
(String) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
这里获取用户名是由于我们项目里集成了springsecurity后完成的自动登录,便可以直接获取用户登录后的信息
sysUserService.getByUsername(username)
是我们在serviceImpl实现类中定义的方法,用的mybatisplus的内置方法
@Override
public SysUser getByUsername(String username) {
return getOne(new QueryWrapper<SysUser>().eq("username", username));
}
sysUserMapper.getNavMenuIds(sysUser.getId());
这是在mapper中定义的根据用户id查询菜单的信息的方法,这段代码使用到了多表连接,与上面类似这里不再详述
<select id="getNavMenuIds" resultType="java.lang.Long">
SELECT Distinct rm.menu_id
FROM vueadmin.sys_user_role ur
LEFT JOIN vueadmin.sys_role_menu rm ON ur.role_id = rm.role_id
where ur.user_id = #{userId}
</select>
this.listByIds(navMenuIds);
this指的是本类,从下面这个图片可以看到通过mybatisplus的自动代码生成器,帮我们在service层完成对mapper的继承使用,但是如果涉及到复杂的sql语句的编写, 还是建议使用mapper
List<SysMenu> menuTree = buildTreeMenu(sysMenus);
这里对从数据库查询出来的菜单列表,转换成树状的形式,也就是完成菜单关系的嵌套
private List<SysMenu> buildTreeMenu(List<SysMenu> menus) {
List<SysMenu> finalMenus = new ArrayList<>();
// 给当前的menu的所有子类都找到
// 先各自寻找的各自的孩子
for (SysMenu menu : menus){
for (SysMenu child : menus){
if (menu.getId().equals(child.getParentId())){
menu.getChildren().add(child);
}
}
if (menu.getParentId()==0L){
finalMenus.add(menu);
}
}
// 提取出父节点
return finalMenus;
}
实体转树状方法的逻辑思路:我们先使用foreach(SysMenu menu : menus)
循环遍历每一个菜单项,在第一次循环体中在进行一个foreach(SysMenu child : menus)
,循环对象跟前面的foreach是一样的,因为我们在SysMenu中有个参数parentId,所以在第二次循环的时候通过判断当第一次遍历的对象id跟第二次循环体的parentId是否相同,menu.getId().equals(child.getParentId())
,如果相同就说明child是menu的子菜单项,就把child添加进menu,menu.getChildren().add(child);
,如果不相同则不做任何处理,经过双重循环遍历就能够获取出菜单树\
onvert(menuTree);
对返回结果的数据格式进行转换
我们在查询获取到菜单树状信息后,需要转换为前端想要接受的参数格式,所以我们在前面定义了SysMenuDto,正是要在这里进行转换,在返回转换后的Dto实体类,在转换过程中我们在判断菜单项是否有子菜单,如果有则进行递归转换的方法
3、通过mybatis提供的循环递归去实现多级菜单
mapper.xml文件
注意第17行的代码,column中的id是第一次查询出来children的id,通过children的id作为父id继续去查改children是否还存在children,一开始是用-1去查询parentId,将查询出来的记录的id作为第二次查询的parentId,进行递归查询知道parentId的child为空,递归结束
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.shuang.server.mapper.DepartmentMapper">
<!-- 通用查询映射结果 -->
<resultMap id="BaseResultMap" type="com.shuang.server.pojo.Department">
<id column="id" property="id" />
<result column="name" property="name" />
<result column="parentId" property="parentId" />
<result column="depPath" property="depPath" />
<result column="enabled" property="enabled" />
<result column="isParent" property="isParent" />
</resultMap>
<resultMap id="DepartmentWithChildren" type="com.shuang.server.pojo.Department" extends="BaseResultMap">
<collection property="children" ofType="com.shuang.server.pojo.Department"
select="com.shuang.server.mapper.DepartmentMapper.getAllDepartments" column="id">
</collection>
</resultMap>
<!-- 通用查询结果列 -->
<sql id="Base_Column_List">
id, name, parentId, depPath, enabled, isParent
</sql>
<!--获取所有部门-->
<select id="getAllDepartments" resultMap="DepartmentWithChildren">
select
<include refid="Base_Column_List"/>
from yeb.t_department
where parentId = #{parentId}
</select>
</mapper>
4、总结
前一种方法是在定义sql时就已经完成对菜单关系的嵌套,并通过定义实体类映射关系来完成菜单查询
而第二种方法是直接从数据库查询出数据后,在对菜单id与父id的值进行比较,之后通过嵌套循环的方式来完成菜单关系的嵌套
最后一种方法则是通过mybatis的一种递归查询实现的