闭包表的实现
为什么要用闭包表?你来看这个:
Spring Boot+MyBatis构建无限层级组织架构设计|邻接表vs闭包表性能对比|树形结构资料存储方案 - jzssuanfa - 博客园
闭包表
闭包表(Closure Table)的核心思想其实非常简单:把“谁是谁的子孙”这个需要递归计算的关系,提前算好并存成一张扁平的二维表。
查询时,你不再需要“找爸爸、找爷爷、找太爷爷”,而是直接在这张表里问:“谁是 X 的所有后代?”或者“X 属于哪些祖先的管辖范围?”。
CREATE TABLE system_dept_closure (
ancestor_id BIGINT NOT NULL COMMENT '祖先部门ID',
descendant_id BIGINT NOT NULL COMMENT '后代部门ID',
depth INT NOT NULL COMMENT '层级深度(0表示自己)',
PRIMARY KEY (ancestor_id, descendant_id),
INDEX idx_descendant (descendant_id)
) ENGINE=InnoDB COMMENT='部门闭包表';
数据规则:
例如:部门 A(id=1) 下有 B(id=2),B 下有 C(id=3)。则表中包含:(1,1,0), (1,2,1), (1,3,2), (2,2,0), (2,3,1), (3,3,0)。
不太理解,举个例子?
- 假设有一个这样的部门树
部门架构:
总公司 (id=1)
├── 技术中心 (id=2)
│ ├── 后端组 (id=4)
│ └── 前端组 (id=5)
└── 市场部 (id=3)
└── 华东大区 (id=6)
system_dept_closure表长这样
| ancestor_id (祖先) | descendant_id (后代) | depth (距离) | 含义解读 |
|---|---|---|---|
| 1 | 1 | 0 | 总公司是自己的祖先(距离0) |
| 1 | 2 | 1 | 总公司是技术中心的父级(距离1) |
| 1 | 3 | 1 | 总公司是市场部的父级(距离1) |
| 1 | 4 | 2 | 总公司是后端组的祖父级(距离2) |
| 1 | 5 | 2 | 总公司是前端组的祖父级(距离2) |
| 1 | 6 | 2 | 总公司是华东大区的祖父级(距离2) |
| 2 | 2 | 0 | 技术中心是自己 |
| 2 | 4 | 1 | 技术中心是后端组的父级 |
| 2 | 5 | 1 | 技术中心是前端组的父级 |
| 3 | 3 | 0 | 市场部是自己 |
| 3 | 6 | 1 | 市场部是华东大区的父级 |
| 4 | 4 | 0 | 后端组是自己 |
| 5 | 5 | 0 | 前端组是自己 |
| 6 | 6 | 0 | 华东大区是自己 |
这张表一共有 14 行。每个节点都会有一条 depth=0 的记录指向自己。任意两个有血缘关系的节点之间,都有且仅有一行记录。
- 场景下怎么用
场景 A:查某个部门及其所有子部门的公告(向下查)
“技术中心(id=2) 发了一份公告,后端组和前端组都能看到”
-- 查出技术中心管辖的所有部门ID
SELECT descendant_id FROM system_dept_closure WHERE ancestor_id = 2;
-- 结果: 2, 4, 5 ✅ 正好是技术中心 + 后端组 + 前端组
然后拿这三个 ID 去关联公告 可见范围表做分页,无需任何递归。
场景 B:查当前用户能看到哪些公告(向上查)
“后端组(id=4) 的员工登录系统,他能看到哪些公告?”
-- 查出后端组的所有祖先(即所有对他可见的公告发布方)
SELECT ancestor_id FROM system_dept_closure WHERE descendant_id = 4;
-- 结果: 1, 2, 4 ✅ 总公司发的、技术中心发的、后端组自己发的,都能看到
场景 C:只查直接子部门(不包含孙子辈)
SELECT descendant_id FROM system_dept_closure
WHERE ancestor_id = 1 AND depth = 1;
-- 结果: 2, 3 ✅ 只有技术中心和市场部,不包含后端组等
场景 D:判断两个部门是否有上下级关系
-- 判断总公司(1)是否是华东大区(6)的上级
SELECT COUNT(*) FROM system_dept_closure
WHERE ancestor_id = 1 AND descendant_id = 6;
-- 结果 > 0 → 是上级;结果 = 0 → 没有隶属关系
- 部门变动时怎么维护?
这是闭包表唯一的“代价”,但逻辑很固定:
| 操作 | 维护方式 |
|---|---|
| 新增部门 | 新节点插入一条 (self, self, 0);再把它父节点的所有祖先记录复制一份,descendant_id 改为新节点,depth+1 |
| 删除部门 | 删除所有 descendant_id = 被删节点 以及 ancestor_id = 被删节点 的记录(级联删除子树) |
| 移动部门 | 先断开旧父节点链路,再接上新父节点链路(本质是删旧+插新的组合) |
- 一句话总结
闭包表就是把“树”拍成了“表格”。写入时多花一点力气把关系算好存下来,读取时就把复杂的树遍历变成了简单的 WHERE ancestor_id = ? 索引查询。对于公告这种 “读远多于写” 的场景,这笔交易非常划算。
生成闭包表数据
直接执行即可生成闭包表数据。
cte_max_recursion_depth:防止无限递归:当递归逻辑存在环状结构(如 A→B→C→A)或终止条件缺失时,该设置能强制在达到 50 层后报错停止,避免数据库陷入死循环或资源耗尽 。
system_dept_closure 闭包表名。
system_dept 闭包表维护的表(d.id,d.parent_id不多说)
SET SESSION cte_max_recursion_depth = 50;
INSERT INTO system_dept_closure (ancestor_id, descendant_id, depth)
WITH RECURSIVE dept_tree AS (
SELECT id AS ancestor_id, id AS descendant_id, 0 AS depth
FROM system_dept WHERE deleted = b'0'
UNION ALL
SELECT dt.ancestor_id, d.id, dt.depth + 1
FROM dept_tree dt
INNER JOIN system_dept d ON d.parent_id = dt.descendant_id
WHERE d.deleted = b'0' AND dt.depth < 30
)
SELECT ancestor_id, descendant_id, depth FROM dept_tree;
如何维护闭包表数据?
抄作业!🐕🦺
为了保证数据绝对一致,所有操作必须在同一个数据库事务中完成。
抄作业
- Service
public interface DeptClosureService {
/**
* 【新增部门】创建部门后调用
* @param newDeptId 新部门ID
* @param parentId 父部门ID(根节点传0或null)
*/
void onDeptCreate(Long newDeptId, Long parentId);
/**
* 【删除部门】逻辑删除部门前/后调用
* ⚠️ 注意:此方法仅删除单个节点的闭包关系
* 如果业务是级联删除子树,需要对每个子节点都调用此方法
* @param deptId 被删除的部门ID
*/
void onDeptDelete(Long deptId);
/**
* 【移动部门】修改部门parent_id时调用
* @param movedDeptId 被移动的部门ID
* @param newParentId 新的父部门ID(移到根节点传0或null)
*/
void onDeptMove(Long movedDeptId, Long newParentId);
}
- ServiceImpl
@Service
@Validated
@Slf4j
public class DeptClosureServiceImpl implements DeptClosureService {
@Resource
private DeptClosureMapper closureMapper;
/**
* 【新增部门】创建部门后调用
* @param newDeptId 新部门ID
* @param parentId 父部门ID(根节点传0或null)
*/
@Transactional(rollbackFor = Exception.class, propagation = Propagation.MANDATORY)
public void onDeptCreate(Long newDeptId, Long parentId) {
// 1. 插入自引用 (depth=0)
closureMapper.insertSelf(newDeptId);
// 2. 如果有父节点,复制父节点的整条祖先链路
if (parentId != null && parentId != 0L) {
closureMapper.copyAncestorChain(newDeptId, parentId);
}
log.info("闭包表新增完成: deptId={}, parentId={}", newDeptId, parentId);
}
/**
* 【删除部门】逻辑删除部门前/后调用
* ⚠️ 注意:此方法仅删除单个节点的闭包关系
* 如果业务是级联删除子树,需要对每个子节点都调用此方法
* @param deptId 被删除的部门ID
*/
@Transactional(rollbackFor = Exception.class, propagation = Propagation.MANDATORY)
public void onDeptDelete(Long deptId) {
closureMapper.deleteByDescendant(deptId);
closureMapper.deleteByAncestor(deptId);
log.info("闭包表删除完成: deptId={}", deptId);
}
/**
* 【移动部门】修改部门parent_id时调用
* @param movedDeptId 被移动的部门ID
* @param newParentId 新的父部门ID(移到根节点传0或null)
*/
@Transactional(rollbackFor = Exception.class, propagation = Propagation.MANDATORY)
public void onDeptMove(Long movedDeptId, Long newParentId) {
// 0. 防御性校验:不能把自己移到自己子树下(会造成死循环)
if (newParentId != null && newParentId != 0L) {
int count = closureMapper.existsRelation(movedDeptId, newParentId);
if (count > 0) {
throw new IllegalArgumentException("不能将部门移动到其自身的子部门下");
}
}
// 1. 分离旧树(剪断旧祖先与被移动子树的连线)
closureMapper.detachSubtree(movedDeptId);
// 2. 组合新树(仅当新父节点有效时)
if (newParentId != null && newParentId != 0L) {
closureMapper.attachSubtree(movedDeptId, newParentId);
}
log.info("闭包表移动完成: movedDeptId={}, newParentId={}", movedDeptId, newParentId);
}
}
- Mapper/Repository
@Mapper
public interface DeptClosureMapper {
void insertSelf(@Param("deptId") Long deptId);
void copyAncestorChain(@Param("newDeptId") Long newDeptId, @Param("parentId") Long parentId);
void deleteByDescendant(@Param("deptId") Long deptId);
void deleteByAncestor(@Param("deptId") Long deptId);
void detachSubtree(@Param("movedDeptId") Long movedDeptId);
void attachSubtree(@Param("movedDeptId") Long movedDeptId, @Param("newParentId") Long newParentId);
int existsRelation(@Param("potentialAncestor") Long potentialAncestor, @Param("potentialDescendant") Long potentialDescendant);
}
- Mapper.xml
<mapper namespace="cn.iocoder.yudao.module.system.dal.mysql.dept.DeptClosureMapper">
<!-- ==================== 新增部门 ==================== -->
<!-- 1. 插入自引用记录 -->
<insert id="insertSelf">
INSERT INTO system_dept_closure (ancestor_id, descendant_id, depth)
VALUES (#{deptId}, #{deptId}, 0)
</insert>
<!-- 2. 复制父节点的祖先链路 -->
<insert id="copyAncestorChain">
INSERT INTO system_dept_closure (ancestor_id, descendant_id, depth)
SELECT ancestor_id, #{newDeptId}, depth + 1
FROM system_dept_closure
WHERE descendant_id = #{parentId}
</insert>
<!-- ==================== 删除部门 ==================== -->
<!-- 删除该节点作为后代的所有记录(断开与祖先的联系) -->
<delete id="deleteByDescendant">
DELETE FROM system_dept_closure WHERE descendant_id = #{deptId}
</delete>
<!-- 删除该节点作为祖先的所有记录(断开与子孙的联系) -->
<delete id="deleteByAncestor">
DELETE FROM system_dept_closure WHERE ancestor_id = #{deptId}
</delete>
<!-- ==================== 移动部门 ==================== -->
<!-- 分离旧树:仅剪断跨越边界的连线,保留子树内部结构 -->
<delete id="detachSubtree">
DELETE c
FROM system_dept_closure c
INNER JOIN system_dept_closure sub ON c.descendant_id = sub.descendant_id
WHERE sub.ancestor_id = #{movedDeptId}
AND c.ancestor_id != sub.ancestor_id
</delete>
<!-- 组合新树:新祖先链路 × 被移动子树 = 新连线 -->
<insert id="attachSubtree">
INSERT INTO system_dept_closure (ancestor_id, descendant_id, depth)
SELECT
new_anc.ancestor_id,
sub_tree.descendant_id,
new_anc.depth + sub_tree.depth + 1
FROM system_dept_closure new_anc
CROSS JOIN system_dept_closure sub_tree
WHERE new_anc.descendant_id = #{newParentId}
AND sub_tree.ancestor_id = #{movedDeptId}
</insert>
<!-- 防环检测:检查 potentialDescendant 是否是 potentialAncestor 的后代 -->
<select id="existsRelation" resultType="int">
SELECT COUNT(1)
FROM system_dept_closure
WHERE ancestor_id = #{potentialAncestor}
AND descendant_id = #{potentialDescendant}
</select>
</mapper>
怎么用
调用它的方法,必须在事务里。
@Transactional(rollbackFor = Exception.class)上面的
onDeptDelete只删单个节点。如果你的业务是"删除父部门时连带删除所有子部门",需要在遍历子树时对每个子节点 ID 都调用一次onDeptDelete,或者写一个批量删除的 SQL。(建议有子节点的不允许删除,防止出现悬浮节点)如果系统支持多人同时调整组织架构,建议在
onDeptMove上加分布式锁lock:dept:move:{movedDeptId},防止两次移动交叉执行导致闭包表错乱。
@Service
@RequiredArgsConstructor
public class SystemDeptServiceImpl implements ISystemDeptService {
private final SystemDeptMapper deptMapper;
private final DeptClosureService closureService; // 👈 注入闭包服务
@Override
@Transactional(rollbackFor = Exception.class)
public void createDept(SystemDept dept) {
deptMapper.insert(dept);
// 👇 紧接着维护闭包表(同一事务)
closureService.onDeptCreate(dept.getId(), dept.getParentId());
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteDept(Long deptId) {
// 先逻辑删除部门本身
deptMapper.updateDeletedById(deptId);
// 👇 再清理闭包表
closureService.onDeptDelete(deptId);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void moveDept(Long deptId, Long newParentId) {
// 先更新部门的 parent_id
deptMapper.updateParentId(deptId, newParentId);
// 👇 再维护闭包表
closureService.onDeptMove(deptId, newParentId);
}
}