当前位置: 首页 > news >正文

扁平表+递归拼树思想

引入

面试题:在数据库中存储省-市-区三级行政区域信息时,你会选用「一张表」还是「三张表」?为什么?

方案 A:三张表

  • province(id,name)

  • city(id,name,province_id)

  • district(id,name,city_id)

  • 优点:严格分层,结构清晰;外键约束直观。

  • 缺点:查询时要三表联查,代码冗长;如果将来要加第 4 级或支持灵活深度,就得再加表和改代码。

方案 B:一张表(邻接表模型)

CREATE TABLE region (id         BIGINT PRIMARY KEY,parent_id  BIGINT NOT NULL DEFAULT 0,  -- 顶级省 parent_id=0name       VARCHAR(100) NOT NULL,sort_order INT     NOT NULL DEFAULT 0
);

优点

  1. 一张表搞定所有层级,无需维护多表;

  2. 增删改直观,新增一层级不改表结构;

  3. 只要一次全表查询,加上一次递归,就能生成任意深度的树。

缺点

  • 递归逻辑要写,但模式简单;

  • 数据量极大时性能要做缓存或数据库层优化。

 接下来,我们就以「方案 B:一张表」为例,演示如何只用一条 SQL 拉取扁平列表,再用一段 Java 递归代码,快速拼出省→市→区的树形数据,让前端「开箱即用」。

若依AI动态菜单思想

 这种思想可以在若依AI动态菜单中体会(一级菜单和二级菜单)

首先查看数据库的字段

parent_id:父级菜单ID,顶级菜单 parent_id = 0。通过它可以串起多级树形结构。

order_num:同级菜单排序号,值越小显示越靠前。


Controller层和Service层

// controller    /*** 获取菜单下拉树列表*/
@GetMapping("/treeselect")
public AjaxResult treeselect(SysMenu menu) {// ① 查询扁平列表List<SysMenu> menus = menuService.selectMenuList(menu, getUserId());// ② 递归拼树,并转换为 TreeSelect VOreturn success(menuService.buildMenuTreeSelect(menus));
}// serviceImpl/*** 查询系统菜单列表* * @param menu 菜单信息* @return 菜单列表*/
@Override
public List<SysMenu> selectMenuList(SysMenu menu, Long userId) {if (SysUser.isAdmin(userId)) {// 管理员:不走权限,查询所有return menuMapper.selectMenuList(menu);} else {// 普通用户:带上 userId,走按角色过滤的查询menu.getParams().put("userId", userId);return menuMapper.selectMenuListByUserId(menu);}
}

selectMenuList的SQL分析:

    <select id="selectMenuList" parameterType="SysMenu" resultMap="SysMenuResult"><include refid="selectMenuVo"/><where><if test="menuName != null and menuName != ''">AND menu_name like concat('%', #{menuName}, '%')</if><if test="visible != null and visible != ''">AND visible = #{visible}</if><if test="status != null and status != ''">AND status = #{status}</if></where>order by parent_id, order_num</select>

 这段 MyBatis 的 <select> 配置,整体功能就是从 sys_menu 表(或视图)里查询一批菜单记录,并支持按名称、可见性、状态三个字段做可选过滤,最后按父菜单和排序号输出一个“扁平”的、已经排好序的列表。

构建前端所需的下拉树结构通过递归获得

/*** 构建前端所需要下拉树结构* * @param menus 菜单列表* @return 下拉树结构列表*/@Overridepublic List<TreeSelect> buildMenuTreeSelect(List<SysMenu> menus){List<SysMenu> menuTrees = buildMenuTree(menus);return menuTrees.stream().map(TreeSelect::new).collect(Collectors.toList());}/*** 构建前端所需要树结构* * @param menus 菜单列表* @return 树结构列表*/
public List<SysMenu> buildMenuTree(List<SysMenu> menus) {List<SysMenu> result = new ArrayList<>();// 取所有 menuId,方便判断哪些是“顶级”节点List<Long> allIds = menus.stream().map(SysMenu::getMenuId).collect(Collectors.toList());for (SysMenu m : menus) {// 如果 m.parentId 不在所有菜单的 id 列表里,说明 m 是顶级if (!allIds.contains(m.getParentId())) {// 从这个顶级节点开始,递归挂子节点recursionFn(menus, m);result.add(m);}}// 如果没查到顶级,直接把原列表当“平铺树”返回return result.isEmpty() ? menus : result;
}

 

buildMenuTree 方法

做了两件事

  1. 找出所有顶层菜单;

  2. 对每个顶层菜单,调用 recursionFn,给它“挂”完整的子树。

查找顶级举个简单例子

假设 menus 里有三条记录:

menuIdparentIdmenuName
10根菜单A
21子菜单B
31子菜单C

allIds = [1,2,3]

遍历时:

  • 对于 m.menuId=1,它的 parentId=00 不在 [1,2,3] 里 ⇒ 认为它是顶级,对菜单A 再调用 recursionFn,递归挂上它的 B、C 子节点,加入 result
  • 对于 m.menuId=2,它的 parentId=11[1,2,3] 里 ⇒ 不是顶级,跳过。
  • 对于 m.menuId=3,同理 parentId=1,也跳过。

 

 recursionFn 递归方法

思路:给当前节点 node 找到所有 parent_id=node.id 的记录,挂到 node.children,再对每个子节点重复同样操作。

/*** 递归列表* * @param list 分类表* @param t 子节点*/
private void recursionFn(List<SysMenu> list, SysMenu t) {// 3.1 先给 t 找到它的所有一级子节点List<SysMenu> childList = getChildList(list, t);// 3.2 把这个子列表挂到 t.children 上t.setChildren(childList);// 3.3 对每一个直接子节点,再看它有没有自己的子节点for (SysMenu tChild : childList) {if (hasChild(list, tChild)) {// 如果有,就递归调用,把所有更深层次的节点都挂上去recursionFn(list, tChild);}}
}
recursionFn方法参数解释 
  • list:包含了当前用户所有可见的菜单项,一份排好序的“平铺”列表,每个元素都有 menuIdparentId

  • t:从外层传进来的某个菜单节点(如顶级菜单),我们要为它“挂”上所有后代子节点。

找直接子节点:getChildList(list, t)
 /*** 得到子节点列表*/
private List<SysMenu> getChildList(List<SysMenu> list, SysMenu t) {List<SysMenu> tlist = new ArrayList<>();Iterator<SysMenu> it = list.iterator();while (it.hasNext()) {SysMenu n = it.next();// 如果 n.parentId == t.menuId,就说明 n 是 t 的一级子节点if (n.getParentId().longValue() == t.getMenuId().longValue()) {tlist.add(n);}}return tlist;
}

目的:扫描整张扁平列表,挑出所有 parentId == t.getMenuId() 的项,返回给上层。

示例

  • 假设 t.menuId=1(广东省),list 包含 [1(GD), 2(GZ), 3(SZ), 4(TH)],其中 2、3 的 parentId=1,4 的 parentId=2

  • getChildList(list, t) 会返回 [2(广州市), 3(深圳市)] 这两个直接子节点,不会返回天河区(它是孙级)。

 

步骤拆解

1. getChildList

      得到 t 的第一层子节点列表 childList

2. t.setChildren(childList)

       把这批子节点保存到 t 对象里,建立父子关联。

3. for (tChild : childList)

        遍历每一个刚找到的子节点。

4. hasChild(list, tChild)

判断这个子节点 tChild 是否还有“孙节点”。

(内部实现通常是看 getChildList(list, tChild).isEmpty()

当某个节点 t 没有子节点时,getChildList 返回空列表,hasChild 为 false,递归自然停止,不会继续往下。

5. recursionFn(list, tChild)

如果 tChild 还有孩子,就把它当成新的 “当前节点” 再次执行整个流程:

查它的直接子节点 → 挂到它的 children → 继续往下……

如何存储省-市-区三级行政区域信息(照葫芦画瓢)

表结构设计

直接贴个简单表:

CREATE TABLE region (id         BIGINT PRIMARY KEY,         -- 每条记录唯一IDparent_id  BIGINT NOT NULL DEFAULT 0,  -- 父节点ID,顶层用0name       VARCHAR(100) NOT NULL,      -- 区域名称sort_order INT NOT NULL DEFAULT 0      -- 同级排序用的
);
  • parent_id:指自己属于哪个上级。

  • sort_order:数字越小,排得越靠前。

一条 SQL 拿到扁平列表

<select id="selectAllRegions" resultType="Region">SELECT id, parent_id, name, sort_orderFROM regionORDER BY parent_id, sort_order
</select>

 这一步把所有数据一次性拉出来,按 parent_idsort_order 排好序。

 

Java 端拼树

1. 定义两个类

R1egion:跟表字段一一对应。

RegionVo:多一个 List<RegionVo> children 用来装子节点。

2. 递归方法

List<RegionVo> buildTree(List<Region> all, Long pid) {return all.stream()// ① 找到所有 parentId == pid 的节点.filter(r -> Objects.equals(r.getParentId(), pid))// ② 对每个这样的节点,做两件事://    a) 把 Region 转成 RegionVo//    b) 递归调用 buildTree 去找它的 children.map(r -> {RegionVo vo = toVo(r);vo.setChildren(buildTree(all, r.getId()));return vo;})// ③ 收集成 List<RegionVo> 返回.collect(Collectors.toList());
}

这样,buildTree 就能从顶层一直“钻”到最底层,层层挂起 children,最后构造出完整的树形结构。

  • 递归入口buildTree(all, 0L)

  • 递归条件:对任意 r,只要有 parentId == r.id 的元素,都会进入下一层递归;否则该层返回空列表,递归停止。

  • 终止:当某层 filter 结果为空(没有子节点)时,map 不会执行,方法直接返回 [],向上回溯。直接对每个节点都调用一次 buildTree(all, r.getId()),如果它根本没有子节点,那么那一层的 .filter(...) 就会立刻返回一个空流,.map(...) 一次都不跑,最终 .collect(...) 出来的是一个空列表 []。然后你把这个空列表赋给它的 children,也就相当于“它没有孩子”。

最终效果

拼好的树,形如:

[{"id":1, "name":"广东省", "children":[{"id":2,"name":"广州市","children":[{"id":4,"name":"天河区"},{"id":5,"name":"越秀区"}]},{"id":3,"name":"深圳市","children":[]}]}
]

相关文章:

  • PyTorch终极实战:从自定义层到模型部署全流程拆解​
  • JS深入之从原型到原型链
  • 替代爬虫!亚马逊API采集商品详情实时数据开发教程
  • 苹果签名应用掉签频繁原因排查,以及如何避免
  • 第十六章 I2C
  • python 中线程、进程、协程
  • 【动作】AVA:时空定位原子视觉动作视频数据集
  • java 数据结构-HashMap
  • 零基础玩转物联网-串口转以太网模块如何快速实现与MQTT服务器通信
  • 如何提升企微CRM系统数据的准确性?5大核心策略详解
  • opencv RGB图像转灰度图
  • 华为云Flexus+DeepSeek征文 | 华为云ModelArts Studio快速上手:DeepSeek-R1-0528商用服务的开通与使用
  • 软件定义汽车的转型之路已然开启
  • SkyWalking 10.2.0 SWCK 配置过程
  • ARM内存理解(一)
  • AutoCAD 2024 保姆级安装教程【2025最新】(附安装包)
  • 智能卷料系统仿真|从动建模到零停机优化—MapleSim卷料处理库工业级解决方案
  • 使用 VSCode 开发 FastAPI 项目(1)
  • 华为云Flexus+DeepSeek征文|体验华为云ModelArts快速搭建Dify-LLM应用开发平台并创建联网大模型
  • VScode - 我的常用插件01 - 主题插件Noctis
  • 怎么用PS做网站广告图/互联网营销方式
  • 广东建设网三库一平台/seo排名点击器原理
  • 网站建设需要了解的信息/如何在百度发布文章
  • 沧州疫情最新消息今天封城/seo网站首页推广
  • 网站后台数据处理编辑主要是做什么的啊/中国刚刚发生8件大事
  • redis做网站/推广软文发稿