Skip to content

部门数据权限控制方案 - 闭包表(Closure Table)

一、方案概述

闭包表是处理树形结构最高效的方案之一,通过空间换时间的策略,将所有祖先-后代关系存储在独立的关系表中,实现O(1)复杂度的树形查询。

核心优势

  • 查询性能极高:无需递归或复杂计算,直接通过索引查询
  • 灵活性强:支持获取任意节点的祖先、后代、兄弟节点
  • 维护相对简单:只在节点增删时更新关系表
  • 支持多种查询:层级查询、路径查询、子树移动等

性能对比

操作类型闭包表物化路径邻接表
查询所有子孙O(1)O(n)O(n)
查询所有祖先O(1)O(1)O(n)
插入节点O(k)O(1)O(1)
移动子树O(k²)O(n)O(1)

注:k为祖先数量,n为子孙数量

二、数据库设计

2.1 表结构设计

sql
-- 部门主表
CREATE TABLE sys_dept (
    dept_id BIGINT PRIMARY KEY AUTO_INCREMENT,
    parent_id BIGINT DEFAULT 0 COMMENT '直接父部门ID',
    dept_name VARCHAR(50) COMMENT '部门名称',
    order_num INT DEFAULT 0 COMMENT '显示顺序',
    status CHAR(1) DEFAULT '0' COMMENT '状态(0正常 1停用)',
    create_time DATETIME,
    INDEX idx_parent_id (parent_id)
) COMMENT='部门表';

-- 部门闭包表(核心)
CREATE TABLE sys_dept_closure (
    ancestor_id BIGINT NOT NULL COMMENT '祖先部门ID',
    descendant_id BIGINT NOT NULL COMMENT '后代部门ID',
    distance INT NOT NULL DEFAULT 0 COMMENT '层级距离(0表示自身)',
    PRIMARY KEY (ancestor_id, descendant_id),
    INDEX idx_descendant (descendant_id),
    INDEX idx_distance (distance),
    FOREIGN KEY (ancestor_id) REFERENCES sys_dept(dept_id) ON DELETE CASCADE,
    FOREIGN KEY (descendant_id) REFERENCES sys_dept(dept_id) ON DELETE CASCADE
) COMMENT='部门关系闭包表';

2.2 数据示例

部门结构:

总公司(1)
├─ 深圳分公司(100)
│   ├─ 研发部(200)
│   │   ├─ 前端组(300)
│   │   └─ 后端组(301)
│   └─ 市场部(201)
└─ 北京分公司(101)

闭包表数据:

ancestor_iddescendant_iddistance
110
11001
12002
13003
13013
1001000
1002001
1003002
2002000
2003001

三、核心实现

3.1 Service层实现

java
@Service
@Transactional(rollbackFor = Exception.class)
public class SysDeptServiceImpl implements ISysDeptService {
    
    @Autowired
    private SysDeptMapper deptMapper;
    
    @Autowired
    private SysDeptClosureMapper closureMapper;
    
    /**
     * 新增部门
     */
    @Override
    public int insertDept(SysDept dept) {
        // 1. 插入部门主表
        deptMapper.insert(dept);
        
        // 2. 插入闭包表 - 自身关系
        SysDeptClosure selfClosure = new SysDeptClosure();
        selfClosure.setAncestorId(dept.getDeptId());
        selfClosure.setDescendantId(dept.getDeptId());
        selfClosure.setDistance(0);
        closureMapper.insert(selfClosure);
        
        // 3. 插入闭包表 - 与所有祖先的关系
        if (dept.getParentId() != null && dept.getParentId() > 0) {
            closureMapper.insertAncestorRelations(dept.getDeptId(), dept.getParentId());
        }
        
        return 1;
    }
    
    /**
     * 删除部门(级联删除所有子部门)
     */
    @Override
    public int deleteDept(Long deptId) {
        // 检查是否有子部门
        if (hasChildDept(deptId)) {
            throw new ServiceException("存在下级部门,不允许删除");
        }
        
        // 删除部门(闭包表通过外键级联删除)
        return deptMapper.deleteById(deptId);
    }
    
    /**
     * 移动部门
     */
    @Override
    public int moveDept(Long deptId, Long newParentId) {
        // 1. 删除原有的祖先关系(保留自身和子孙关系)
        closureMapper.deleteAncestorRelations(deptId);
        
        // 2. 建立新的祖先关系
        closureMapper.insertNewAncestorRelations(deptId, newParentId);
        
        // 3. 更新部门主表
        SysDept dept = new SysDept();
        dept.setDeptId(deptId);
        dept.setParentId(newParentId);
        return deptMapper.updateById(dept);
    }
}

3.2 Mapper实现

xml
<!-- SysDeptClosureMapper.xml -->
<mapper namespace="com.ruoyi.system.mapper.SysDeptClosureMapper">
    
    <!-- 插入部门与所有祖先的关系 -->
    <insert id="insertAncestorRelations">
        INSERT INTO sys_dept_closure (ancestor_id, descendant_id, distance)
        SELECT ancestor_id, #{deptId}, distance + 1
        FROM sys_dept_closure
        WHERE descendant_id = #{parentId}
    </insert>
    
    <!-- 删除部门的所有祖先关系(移动部门时使用) -->
    <delete id="deleteAncestorRelations">
        DELETE FROM sys_dept_closure
        WHERE descendant_id IN (
            SELECT descendant_id FROM (
                SELECT descendant_id FROM sys_dept_closure 
                WHERE ancestor_id = #{deptId}
            ) AS tmp
        )
        AND ancestor_id IN (
            SELECT ancestor_id FROM (
                SELECT ancestor_id FROM sys_dept_closure 
                WHERE descendant_id = #{deptId} 
                AND ancestor_id != #{deptId}
            ) AS tmp2
        )
    </delete>
    
    <!-- 建立新的祖先关系(移动部门时使用) -->
    <insert id="insertNewAncestorRelations">
        INSERT INTO sys_dept_closure (ancestor_id, descendant_id, distance)
        SELECT supertree.ancestor_id, subtree.descendant_id, 
               supertree.distance + subtree.distance + 1
        FROM sys_dept_closure AS supertree
        CROSS JOIN sys_dept_closure AS subtree
        WHERE supertree.descendant_id = #{newParentId}
        AND subtree.ancestor_id = #{deptId}
    </insert>
    
    <!-- 查询部门的所有子孙部门ID -->
    <select id="selectDescendantIds" resultType="Long">
        SELECT descendant_id 
        FROM sys_dept_closure 
        WHERE ancestor_id = #{deptId}
        AND descendant_id != #{deptId}
    </select>
    
    <!-- 查询部门的所有祖先部门ID -->
    <select id="selectAncestorIds" resultType="Long">
        SELECT ancestor_id 
        FROM sys_dept_closure 
        WHERE descendant_id = #{deptId}
        AND ancestor_id != #{deptId}
        ORDER BY distance DESC
    </select>
</mapper>

3.3 数据权限实现

java
@Aspect
@Component
public class DataScopeAspect {
    
    public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, 
                                     String deptAlias, String userAlias) {
        StringBuilder sqlString = new StringBuilder();
        
        for (SysRole role : user.getRoles()) {
            String dataScope = role.getDataScope();
            
            if ("4".equals(dataScope)) { // 本部门及以下
                // 使用闭包表的高性能查询
                sqlString.append(String.format(
                    " OR %s.dept_id IN (" +
                    "   SELECT descendant_id FROM sys_dept_closure " +
                    "   WHERE ancestor_id = %d" +
                    " )",
                    deptAlias, user.getDeptId()
                ));
            }
            // ... 其他数据权限类型
        }
        
        BaseEntity baseEntity = (BaseEntity) joinPoint.getArgs()[0];
        baseEntity.getParams().put("dataScope", " AND (" + sqlString.substring(4) + ")");
    }
}

四、常用查询操作

4.1 基础查询

sql
-- 1. 查询所有子孙部门(包含自己)
SELECT d.* FROM sys_dept d
JOIN sys_dept_closure c ON d.dept_id = c.descendant_id
WHERE c.ancestor_id = 100;

-- 2. 查询所有子孙部门(不包含自己)
SELECT d.* FROM sys_dept d
JOIN sys_dept_closure c ON d.dept_id = c.descendant_id
WHERE c.ancestor_id = 100 AND c.distance > 0;

-- 3. 查询直接子部门
SELECT d.* FROM sys_dept d
JOIN sys_dept_closure c ON d.dept_id = c.descendant_id
WHERE c.ancestor_id = 100 AND c.distance = 1;

-- 4. 查询所有祖先部门
SELECT d.* FROM sys_dept d
JOIN sys_dept_closure c ON d.dept_id = c.ancestor_id
WHERE c.descendant_id = 300
ORDER BY c.distance DESC;

-- 5. 查询部门层级
SELECT distance FROM sys_dept_closure
WHERE ancestor_id = 1 AND descendant_id = 300;

4.2 业务查询示例

xml
<!-- 带数据权限的用户查询 -->
<select id="selectUserList" parameterType="SysUser" resultMap="SysUserResult">
    SELECT u.*, d.dept_name 
    FROM sys_user u
    LEFT JOIN sys_dept d ON u.dept_id = d.dept_id
    <where>
        <if test="userName != null and userName != ''">
            AND u.user_name LIKE CONCAT('%', #{userName}, '%')
        </if>
        <!-- 数据权限过滤 -->
        ${params.dataScope}
    </where>
</select>

<!-- 统计部门及子部门的用户数 -->
<select id="countUsersByDept" resultType="Map">
    SELECT d.dept_id, d.dept_name, COUNT(u.user_id) as user_count
    FROM sys_dept d
    JOIN sys_dept_closure c ON d.dept_id = c.descendant_id
    LEFT JOIN sys_user u ON d.dept_id = u.dept_id
    WHERE c.ancestor_id = #{deptId}
    GROUP BY d.dept_id, d.dept_name
</select>

五、性能优化

5.1 索引优化

sql
-- 1. 闭包表索引(已在建表时创建)
-- PRIMARY KEY (ancestor_id, descendant_id) - 覆盖查询子孙
-- INDEX idx_descendant (descendant_id) - 覆盖查询祖先
-- INDEX idx_distance (distance) - 层级查询

-- 2. 查询计划分析
EXPLAIN SELECT descendant_id FROM sys_dept_closure WHERE ancestor_id = 100;
-- 结果:Using index,说明使用了覆盖索引

-- 3. 统计信息更新
ANALYZE TABLE sys_dept_closure;

5.2 缓存策略

java
@Service
public class DeptCacheService {
    
    @Autowired
    private RedisTemplate<String, Set<Long>> redisTemplate;
    
    private static final String DEPT_DESCENDANTS_KEY = "dept:descendants:";
    private static final String DEPT_ANCESTORS_KEY = "dept:ancestors:";
    
    /**
     * 获取部门的所有子孙部门ID(使用缓存)
     */
    @Cacheable(value = "deptDescendants", key = "#deptId")
    public Set<Long> getDescendantIds(Long deptId) {
        List<Long> ids = closureMapper.selectDescendantIds(deptId);
        return new HashSet<>(ids);
    }
    
    /**
     * 清除部门缓存(部门变更时调用)
     */
    @CacheEvict(value = {"deptDescendants", "deptAncestors"}, allEntries = true)
    public void clearDeptCache() {
        // 清除所有部门相关缓存
    }
    
    /**
     * 预热缓存
     */
    public void warmupCache() {
        List<SysDept> allDepts = deptMapper.selectList(null);
        for (SysDept dept : allDepts) {
            getDescendantIds(dept.getDeptId());
        }
    }
}

5.3 批量操作优化

java
@Service
public class DeptBatchService {
    
    /**
     * 批量插入部门
     */
    @Transactional
    public void batchInsertDepts(List<DeptDTO> deptDTOs) {
        // 1. 批量插入部门主表
        List<SysDept> depts = convertToDepts(deptDTOs);
        deptMapper.insertBatch(depts);
        
        // 2. 构建闭包关系
        List<SysDeptClosure> closures = new ArrayList<>();
        for (SysDept dept : depts) {
            // 添加自身关系
            closures.add(new SysDeptClosure(dept.getDeptId(), dept.getDeptId(), 0));
            
            // 添加祖先关系
            if (dept.getParentId() > 0) {
                List<SysDeptClosure> ancestorClosures = buildAncestorClosures(dept);
                closures.addAll(ancestorClosures);
            }
        }
        
        // 3. 批量插入闭包表
        if (!closures.isEmpty()) {
            closureMapper.insertBatch(closures);
        }
    }
}

六、数据迁移方案

6.1 从邻接表迁移到闭包表

sql
-- 创建存储过程,递归构建闭包表
DELIMITER $$

CREATE PROCEDURE migrate_to_closure_table()
BEGIN
    -- 清空闭包表
    TRUNCATE TABLE sys_dept_closure;
    
    -- 插入所有自身关系
    INSERT INTO sys_dept_closure (ancestor_id, descendant_id, distance)
    SELECT dept_id, dept_id, 0 FROM sys_dept;
    
    -- 递归插入祖先关系
    INSERT INTO sys_dept_closure (ancestor_id, descendant_id, distance)
    WITH RECURSIVE dept_tree AS (
        -- 初始化:直接父子关系
        SELECT parent_id as ancestor_id, dept_id as descendant_id, 1 as distance
        FROM sys_dept WHERE parent_id > 0
        
        UNION ALL
        
        -- 递归:祖先关系
        SELECT t.parent_id, d.descendant_id, d.distance + 1
        FROM dept_tree d
        JOIN sys_dept t ON d.ancestor_id = t.dept_id
        WHERE t.parent_id > 0
    )
    SELECT * FROM dept_tree;
END$$

DELIMITER ;

-- 执行迁移
CALL migrate_to_closure_table();

6.2 数据一致性校验

sql
-- 1. 检查闭包表完整性
SELECT d.dept_id, d.dept_name
FROM sys_dept d
WHERE NOT EXISTS (
    SELECT 1 FROM sys_dept_closure c 
    WHERE c.descendant_id = d.dept_id AND c.ancestor_id = d.dept_id
);

-- 2. 检查父子关系一致性
SELECT d.dept_id, d.parent_id
FROM sys_dept d
WHERE d.parent_id > 0
AND NOT EXISTS (
    SELECT 1 FROM sys_dept_closure c
    WHERE c.ancestor_id = d.parent_id 
    AND c.descendant_id = d.dept_id 
    AND c.distance = 1
);

-- 3. 统计每个部门的层级深度
SELECT d.dept_id, d.dept_name, MAX(c.distance) as depth
FROM sys_dept d
JOIN sys_dept_closure c ON d.dept_id = c.descendant_id
GROUP BY d.dept_id, d.dept_name
ORDER BY depth DESC;

七、触发器自动维护(可选)

sql
-- 插入部门时自动维护闭包表
DELIMITER $$

CREATE TRIGGER dept_closure_insert_trigger
AFTER INSERT ON sys_dept
FOR EACH ROW
BEGIN
    -- 插入自身关系
    INSERT INTO sys_dept_closure (ancestor_id, descendant_id, distance)
    VALUES (NEW.dept_id, NEW.dept_id, 0);
    
    -- 插入祖先关系
    IF NEW.parent_id > 0 THEN
        INSERT INTO sys_dept_closure (ancestor_id, descendant_id, distance)
        SELECT ancestor_id, NEW.dept_id, distance + 1
        FROM sys_dept_closure
        WHERE descendant_id = NEW.parent_id;
    END IF;
END$$

DELIMITER ;

八、使用示例

8.1 Controller层

java
@RestController
@RequestMapping("/system/user")
public class SysUserController {
    
    /**
     * 查询用户列表(支持数据权限)
     */
    @GetMapping("/list")
    @DataScope(deptAlias = "u")
    public TableDataInfo list(SysUser user) {
        startPage();
        List<SysUser> list = userService.selectUserList(user);
        return getDataTable(list);
    }
    
    /**
     * 获取部门树形结构
     */
    @GetMapping("/dept/tree")
    public AjaxResult deptTree() {
        List<TreeNode> tree = deptService.buildDeptTree();
        return AjaxResult.success(tree);
    }
}

8.2 复杂查询示例

java
@Service
public class DeptQueryService {
    
    /**
     * 获取部门的完整路径
     */
    public List<SysDept> getDeptPath(Long deptId) {
        return deptMapper.selectDeptPath(deptId);
    }
    
    /**
     * 获取同级部门
     */
    public List<SysDept> getSiblingDepts(Long deptId) {
        SysDept dept = deptMapper.selectById(deptId);
        if (dept == null || dept.getParentId() == 0) {
            return Collections.emptyList();
        }
        
        return deptMapper.selectList(new LambdaQueryWrapper<SysDept>()
            .eq(SysDept::getParentId, dept.getParentId())
            .ne(SysDept::getDeptId, deptId));
    }
    
    /**
     * 获取部门的第N级子部门
     */
    public List<SysDept> getNthLevelDescendants(Long deptId, int level) {
        return deptMapper.selectNthLevelDescendants(deptId, level);
    }
}

九、优缺点总结

优点

  1. 查询性能极高:O(1)复杂度,直接通过索引查询
  2. 支持复杂查询:轻松实现各种树形查询需求
  3. 无递归查询:避免了递归带来的性能问题
  4. 事务安全:所有操作都是简单的INSERT/DELETE

缺点

  1. 空间占用大:需要存储所有祖先-后代关系
  2. 写入成本高:插入/移动节点需要维护多条记录
  3. 数据冗余:存在大量冗余数据

适用场景

  • 读多写少的系统
  • 部门层级较深(>5层)
  • 需要频繁查询子树
  • 对查询性能要求极高

不适用场景

  • 频繁重组部门结构
  • 部门数量巨大(>10万)
  • 存储空间有限

十、总结

闭包表是处理树形结构数据权限的最优方案之一,特别适合读多写少、对查询性能要求高的场景。通过牺牲一定的存储空间和写入性能,换取了极致的查询性能,是大型企业系统的理想选择。