概要
父子节点匹配的的所有节点通常会设计存储在同一张数据表中,通过自连接操作找对孩子节点的父节点,那么这种操作在java中如何处理呢?
一种方式是使用多重循环匹配父节点的孩子节点,另一种方式是使用递归来对当前节点的孩子节点进行匹配,但是个人认为都不算是最佳的父子节点匹配处理方式。这里推荐一种时间复杂度更低的方式来对此场景进行处理,这里会使用使用单层for循环+Map集合进行遍历和匹配
表设计
以菜单表为例:这里想组装成三级菜单,id:菜单id,pid:父菜单id,通过pid与id的匹配来关联菜单层级,0代表顶层菜单
数据返回效果如下
[
{
"value": 1,
"label": "主页",
"pid": 0,
"children": [
{
"value": 2,
"label": "菜单1",
"pid": 1,
"children": [
{
"value": 4,
"label": "子项1",
"pid": 2
},
{
"value": 5,
"label": "子项2",
"pid": 2
}
]
},
{
"value": 3,
"label": "菜单2",
"pid": 1,
"children": [
{
"value": 6,
"label": "子项3",
"pid": 3
},
{
"value": 7,
"label": "子项4",
"pid": 3
}
]
}
]
}
]
关键代码片段实现
后端代码
//持久层对象
public class Menu {
private Long id;
private String name;
private String icon;
private Long pid;
private List<Menu> menus;
//set和get方法省略...
}
//返回给前端的数据对象(elementUI的级联选择组件根据以下字段进行渲染)
public class MenuDto {
private Long value;
private String label;
private Long pid;
private List<MenuDto> children;
//set和get方法省略...
}
业务处理核心思路:遍历所有菜单,找出当前菜单是否存在父菜单,如果存在说明当前菜单是这个父菜单的子菜单,把子菜单添加到这个父菜单
,如果不存在则说明当前菜单就是一级菜单,添加到返回菜单列表中
//业务处理
@Override
public List<MenuDto> getMenuTree() {
List<Menu> menuTree = menuDao.getMenuTree();
Map<Long, MenuDto> menuDtoMap = menuTree.stream().map(menu -> {
MenuDto menuDto = new MenuDto();
menuDto.setValue(menu.getId());
menuDto.setLabel(menu.getName());
menuDto.setPid(menu.getPid());
return menuDto; //将持久层对象转换成前端渲染对象
}).collect(Collectors.toMap(MenuDto::getValue, menuDto -> menuDto)); //把list收集成map集合,key为id,value为menuDto对象
List<MenuDto> parents = new ArrayList<>(); //定义菜单返回列表
for (Map.Entry<Long, MenuDto> entry : menuDtoMap.entrySet()) {
MenuDto menuDto = entry.getValue(); //获取当前菜单
MenuDto parent = menuDtoMap.get(menuDto.getPid());//获取当前菜单的父菜单
if (ObjectUtil.isNotNull(parent)) { //如果当前菜单的父菜单不为空说明此菜单不是顶层菜单,而是子菜单
if (ObjectUtil.isNull(parent.getChildren())) { //当首次执行会给父菜单的子菜单列表进行初始化
parent.setChildren(new ArrayList<>());
}
parent.getChildren().add(menuDto); //把当前菜单作为子菜单添加到它的父菜单的子菜单列表中
} else { //如果找不到当前菜单的父菜单,那它自己就是顶层菜单添加到返回列表
parents.add(menuDto);
}
}
return parents;
}
前端代码
<template>
<div>
<el-cascader :options="options"></el-cascader>
</div>
</template>
<script>
import _axios from "../util/myaxios";
export default {
data() {
return {
options: [
// {value:100, label:'主页', children:[
// {value:101, label:'菜单1', children:[
// {value:105, label:'子项1'},
// {value:106, label:'子项2'}
// ]},
// {value:102, label:'菜单2', children:[
// {value:105, label:'子项3'},
// {value:106, label:'子项4'},
// {value:106, label:'子项5'}
// ]},
// {value:103, label:'菜单3', children:[
// {value:105, label:'子项6'},
// {value:106, label:'子项7'}
// ]},
// {value:106, label:'菜单4'}
// ]}
]
};
},
methods: {
async query() {
const resp = await _axios.get("/query/menu/tree");
this.options = resp.data.data; //将返回的列表数据渲染给级联选择组件
},
},
mounted() {
this.query();
}
};
</script>
界面展示
小结
数据组装往往涉及到循环遍历和匹配,有时候可以考虑使用更合适的数据结构或算法来提高业务处理效率,这些都是编码过程中的细节,博主以前最初使用过多重循环来处理过三级评论列表查询的场景,在三级商品菜单列表查询的场景下使用的是递归处理,而现在回顾起来并不是最优雅的处理方式,不过在实际开发中始终还是要牢牢结合业务场景进行代码分析和优化才有真正意义。