Spring Boot+MyBatis实现无限层级组织架构设计|邻接表vs闭包表性能对比|树形结构数据存储方案

引子:新官上任的"一根棍子"

我叫小白,今天是我成为"宇宙第一科技有限公司"首席架构师的第一天。老板拍着我的肩膀,语重心长地说:"小白啊,咱们公司现在人少,组织架构就像一根棍子:我 -> 你 -> 程序员A -> 实习生B。你先把这套系统做出来。"

这还不简单?我大笔一挥,一张 employee 表跃然纸上:

sql 复制代码
CREATE TABLE employee (
    id INT PRIMARY KEY,
    name VARCHAR(100),
    manager_id INT -- 指向他的上级的ID
);

数据一填:

id name manager_id
1 老板 NULL
2 小白 1
3 程序员A 2
4 实习生B 3

完美!这就是著名的 邻接表模型。我得意地给老板演示:"看,我能轻松找到实习生B的上级是谁!"

推理时刻(严谨脸):

  • 核心思想:每个节点(员工)都存储其父节点(上级)的ID。

  • 优点:结构简单,插入(新员工入职)、移动(换部门)非常方便。

  • 缺点(伏笔):查询一个节点的所有子孙节点(比如查询老板手下所有人)会非常麻烦!你需要不断地循环查询,就像一根棍子,你只能一节一节往下摸。


第一幕:公司的扩张与"棍子"的崩塌

一个月后,公司融资成功,疯狂扩张。市场部、销售部、技术部纷纷成立,部门下又有小组,小组下还有团队... 我的"一根棍子"瞬间变成了一棵盘根错节的大树。

产品经理跑来问我:"小白,给我查一下'技术部'麾下所有的员工,包括所有子部门、子子部门... 无限层级的!"

我傻了。用邻接表怎么做?我只能先查技术部的直属员工,再查这些员工的直属下属,再查下属的下属... 我需要写一个循环,或者在我的程序里进行递归。这就像让你在一棵巨大的树上,从树根开始,把所有树枝、树叶的名字都记下来,你得不停地跑上跑下,累个半死,效率极低!

"棍子模型"在茂盛的"组织森林"面前,彻底崩塌了。


第二幕:折中方案 - 把"族谱"写在身份证上

就在我愁眉苦脸时,产品经理给了我一个灵感:"小白,你看咱们的文件夹路径,C:\工作\2024\项目A\文档,一眼就能看出它在哪,上级是谁。你能不能也这样?"

"路径枚举"! 对啊!我可以在每个节点上直接记录从根节点到自己的完整路径!

我立刻修改了表结构,加入了 path 字段:

sql 复制代码
CREATE TABLE employee (
    id INT PRIMARY KEY,
    name VARCHAR(100),
    manager_id INT,
    path VARCHAR(1000) -- 新增:存储完整路径,如 /1/2/3
);

然后重新填充数据:

id name manager_id path
1 老板 NULL /1
2 小白 1 /1/2
3 程序员A 2 /1/2/3
4 实习生B 3 /1/2/3/4
5 市场部经理 1 /1/5

现在,魔法开始了!

查询技术部(id=2)的所有下属,变得异常简单:

sql 复制代码
SELECT * FROM employee WHERE path LIKE '/1/2/%';

查询实习生B(id=4)的所有上级:

sql 复制代码
-- 先取出path
SELECT path FROM employee WHERE id = 4; -- 得到 /1/2/3/4
-- 然后解析出所有ID:1,2,3,4
SELECT * FROM employee WHERE id IN (1,2,3,4);

查询某个节点的深度:

sql 复制代码
-- 计算path中分隔符的个数
SELECT LENGTH(path) - LENGTH(REPLACE(path, '/', '')) - 1 AS depth 
FROM employee WHERE id = 4;

推理时刻(路径枚举的优劣):

  • 优点

    • 查询子孙节点非常高效(一个LIKE搞定)。

    • 比邻接表查询方便太多。

    • 很容易看出节点的层级关系。

  • 缺点

    • 路径长度限制VARCHAR 的长度是有限的,如果层级非常深,可能会不够用。

    • 依赖应用层维护:插入、移动节点时,需要更新该节点及其所有子孙的路径,维护起来比较麻烦,容易出错。

    • 数据库的 LIKE 查询在数据量大时可能性能不佳(虽然可以用索引优化前缀匹配)。

这个方案就像给每个人都发了一张"族谱身份证",虽然能一眼看出出身,但搬家(调整部门)时,得通知所有家庭成员换身份证,有点麻烦。


第三幕:顿悟!把"树"拍扁成"地图"

路径枚举虽然解决了部分问题,但维护的复杂性和长度限制让我隐隐不安。就在我对着电脑抓狂时,隔壁做地图导航的算法大神凑了过来:"小白,愁啥呢?"

我吐完苦水,他笑了:"你这两个方案,一个是在树里钻来钻去,一个是把路径写在节点上。你为什么不为整棵树单独画一张'关系地图'呢?把所有'谁是谁的上级,无论隔多少代'的关系都明确记录下来。"

"单独画一张关系地图?" 我灵光一闪!

大神继续解释:"想象一下,你想知道北京海淀区所有街道的信息。你不需要从中国->北京->海淀区这样一层层找,也不需要去解析每个街道的路径名。你有一张专门的关系表,上面直接标记了每条街道和'中国'这个根节点的从属关系,无论隔了多少层。这就是 '闭包表' 思想。"

推理时刻(进阶):

我们引入一种更强大的解决方案------闭包表

它的核心思想是:不再是存储"父子"关系,而是存储"祖先-后代"的所有路径关系,无论中间隔了多少代。

我们新建一张 organization_closure 表:

sql 复制代码
CREATE TABLE organization_closure (
    ancestor_id INT,   -- 祖先ID
    descendant_id INT, -- 后代ID
    depth INT,         -- 深度:祖先到后代之间隔了几层
    PRIMARY KEY (ancestor_id, descendant_id)
);

怎么理解?还是以我们公司为例:

  • 老板 (id=1) 是自己的祖先,也是小白、程序员A、实习生B的祖先。

  • 小白 (id=2) 是自己的祖先,也是程序员A、实习生B的祖先。

  • 程序员A (id=3) 是自己的祖先,也是实习生B的祖先。

我们把所有这些关系都存进去:

| ancestor_id | descendant_id | depth |
|-------------|---------------|-------|----------------|
| 1 | 1 | 0 | -- 老板到自己,深度0 |
| 1 | 2 | 1 | -- 老板到小白,深度1 |
| 1 | 3 | 2 | -- 老板到程序员A,深度2 |
| 1 | 4 | 3 | -- 老板到实习生B,深度3 |
| 2 | 2 | 0 | -- 小白到自己 |
| 2 | 3 | 1 | -- 小白到程序员A |
| 2 | 4 | 2 | -- 小白到实习生B |
| 3 | 3 | 0 |
| 3 | 4 | 1 |
| 4 | 4 | 0 |

看!这就是我们组织的"全景地图"!


第四幕:用"地图"降维打击

现在,产品经理再让我查"技术部(假设id=2)麾下所有人",SQL变得无比简单:

sql 复制代码
SELECT e.*
FROM employee e
INNER JOIN organization_closure oc ON e.id = oc.descendant_id
WHERE oc.ancestor_id = 2; -- 小白的ID是2

一步到位! 数据库不需要递归,不需要模糊查询,直接通过一次精确的连接查询,就把所有后代(包括他自己)都找出来了。这就好比从"在森林里钻木取火"升级到了"用GPS导航",是降维打击!

再来几个炫酷的操作:

  1. 查询实习生B的所有上级(直到老板):

    sql 复制代码
    SELECT e.*
    FROM employee e
    INNER JOIN organization_closure oc ON e.id = oc.ancestor_id
    WHERE oc.descendant_id = 4; -- 实习生B的ID是4
  2. 计算技术部(id=2)的总人数:

    sql 复制代码
    SELECT COUNT(*)
    FROM organization_closure
    WHERE ancestor_id = 2;
  3. 查询小白和实习生B之间隔了多少层(汇报路径深度):

    sql 复制代码
    SELECT depth
    FROM organization_closure
    WHERE ancestor_id = 2 AND descendant_id = 4;

推理时刻(总结闭包表):

  • 优点:查询性能极高,各种复杂查询都非常方便,没有路径长度限制。

  • 缺点

    • 需要额外一张表,占用空间稍大(空间换时间)。

    • 增删节点时,需要维护这张关系表(比如新增一个员工,他要和自己建立关系,也要和他的所有祖先建立关系)。但这可以通过存储过程或触发器自动化,一劳永逸。


最终回:小白の选择与核心代码实现

故事讲完了,作为架构师的小白我,回顾了这三个方案,最终选择了 "邻接表" + "闭包表" 的组合拳

下面是精简的核心代码实现,包含详细的注释说明:

1. 核心实体类

java 复制代码
/**
 * 员工实体类 - 对应邻接表
 * 核心思想:每个节点存储其直接父节点ID,简单直观
 */
@Data
public class Employee {
    private Long id;        // 节点ID
    private String name;    // 节点名称
    private Long managerId; // 直接上级ID - 邻接表核心字段
}

/**
 * 组织关系闭包表实体
 * 核心思想:存储所有"祖先-后代"关系,无论中间隔了多少代
 */
@Data
public class OrganizationClosure {
    private Long ancestorId;    // 祖先ID
    private Long descendantId;  // 后代ID  
    private Integer depth;      // 深度:祖先到后代间隔的层数
}

2. 核心Mapper接口

java 复制代码
@Mapper
public interface EmployeeMapper {
    /**
     * 根据ID查找员工 - 基础查询操作
     */
    @Select("SELECT * FROM employee WHERE id = #{id}")
    Employee findById(Long id);
    
    /**
     * 新增员工 - 邻接表的插入很简单
     */
    @Insert("INSERT INTO employee(name, manager_id) VALUES(#{name}, #{managerId})")
    @Options(useGeneratedKeys = true, keyProperty = "id")
    int insert(Employee employee);
    
    /**
     * 更新员工信息 - 用于移动操作
     */
    @Update("UPDATE employee SET manager_id = #{managerId} WHERE id = #{id}")
    int updateManager(@Param("id") Long id, @Param("managerId") Long managerId);
    
    /**
     * 查询直接下属 - 邻接表的优势:查询直接关系很快
     */
    @Select("SELECT * FROM employee WHERE manager_id = #{managerId}")
    List<Employee> findDirectSubordinates(Long managerId);
}

@Mapper
public interface OrganizationClosureMapper {
    /**
     * 批量插入闭包关系 - 核心:维护所有祖先-后代关系
     */
    @Insert("<script>" +
            "INSERT INTO organization_closure(ancestor_id, descendant_id, depth) VALUES " +
            "<foreach collection='list' item='item' separator=','>" +
            "(#{item.ancestorId}, #{item.descendantId}, #{item.depth})" +
            "</foreach>" +
            "</script>")
    int batchInsert(List<OrganizationClosure> closures);
    
    /**
     * 删除指定后代的所有关系 - 移动或删除节点时清理关系
     */
    @Delete("DELETE FROM organization_closure WHERE descendant_id = #{descendantId}")
    int deleteByDescendant(Long descendantId);
    
    /**
     * 查询节点的所有祖先 - 用于构建新节点的闭包关系
     */
    @Select("SELECT * FROM organization_closure WHERE descendant_id = #{descendantId}")
    List<OrganizationClosure> findAncestors(Long descendantId);
    
    /**
     * 查询子树的所有节点 - 闭包表的强大查询能力
     */
    @Select("SELECT e.* FROM employee e " +
            "INNER JOIN organization_closure oc ON e.id = oc.descendant_id " +
            "WHERE oc.ancestor_id = #{ancestorId} AND oc.depth > 0")
    List<Employee> findSubTree(Long ancestorId);
}

3. 核心业务服务

java 复制代码
@Service
@Transactional
public class OrganizationService {
    
    @Autowired
    private EmployeeMapper employeeMapper;
    
    @Autowired
    private OrganizationClosureMapper closureMapper;
    
    /**
     * 新增员工 - 核心操作:同时维护邻接表和闭包表
     */
    public Employee addEmployee(Employee employee, Long managerId) {
        // 1. 设置直接上级(邻接表操作)
        employee.setManagerId(managerId);
        employeeMapper.insert(employee);
        
        // 2. 维护闭包表关系
        updateClosureForNewEmployee(employee.getId(), managerId);
        
        return employee;
    }
    
    /**
     * 为新员工维护闭包表关系
     * 核心思想:新员工的所有上级,也都是新员工的祖先
     */
    private void updateClosureForNewEmployee(Long newEmployeeId, Long managerId) {
        List<OrganizationClosure> newRelations = new ArrayList<>();
        
        // 规则1:每个节点都是自己的祖先(深度0)
        newRelations.add(createClosure(newEmployeeId, newEmployeeId, 0));
        
        if (managerId != null) {
            // 规则2:查找上级的所有祖先(包括上级自己)
            List<OrganizationClosure> managerAncestors = closureMapper.findAncestors(managerId);
            
            // 规则3:新员工继承上级的所有祖先关系,深度+1
            for (OrganizationClosure ancestor : managerAncestors) {
                int newDepth = ancestor.getDepth() + 1;
                newRelations.add(createClosure(ancestor.getAncestorId(), newEmployeeId, newDepth));
            }
        }
        
        // 批量插入所有新关系
        closureMapper.batchInsert(newRelations);
    }
    
    /**
     * 创建闭包关系对象 - 辅助方法
     */
    private OrganizationClosure createClosure(Long ancestorId, Long descendantId, int depth) {
        OrganizationClosure closure = new OrganizationClosure();
        closure.setAncestorId(ancestorId);
        closure.setDescendantId(descendantId);
        closure.setDepth(depth);
        return closure;
    }
    
    /**
     * 移动员工 - 核心操作:更新邻接表并重建闭包关系
     */
    public void moveEmployee(Long employeeId, Long newManagerId) {
        // 1. 更新直接上级(邻接表操作)
        employeeMapper.updateManager(employeeId, newManagerId);
        
        // 2. 重新构建闭包关系
        rebuildClosureRelations(employeeId);
    }
    
    /**
     * 重新构建闭包关系 - 移动操作的核心
     */
    private void rebuildClosureRelations(Long employeeId) {
        // 步骤1:删除该员工现有的所有闭包关系(作为后代)
        closureMapper.deleteByDescendant(employeeId);
        
        // 步骤2:获取员工当前信息
        Employee employee = employeeMapper.findById(employeeId);
        
        // 步骤3:重新建立闭包关系(类似新增员工)
        updateClosureForNewEmployee(employeeId, employee.getManagerId());
        
        // 步骤4:递归更新所有下属的闭包关系
        updateDescendantsClosure(employeeId);
    }
    
    /**
     * 递归更新下属的闭包关系 - 移动操作的延伸影响
     */
    private void updateDescendantsClosure(Long managerId) {
        List<Employee> subordinates = employeeMapper.findDirectSubordinates(managerId);
        for (Employee subordinate : subordinates) {
            rebuildClosureRelations(subordinate.getId());
        }
    }
    
    /**
     * 查询子树所有成员 - 闭包表的强大查询能力
     */
    public List<Employee> findSubTree(Long rootId) {
        return closureMapper.findSubTree(rootId);
    }
    
    /**
     * 复制组织架构 - 深度复制整棵树
     */
    public Long copyOrganization(Long sourceRootId, Long newManagerId) {
        // 1. 复制根节点
        Employee sourceRoot = employeeMapper.findById(sourceRootId);
        Employee newRoot = new Employee();
        newRoot.setName(sourceRoot.getName() + "(副本)");
        newRoot.setManagerId(newManagerId);
        employeeMapper.insert(newRoot);
        
        // 2. 维护闭包表
        updateClosureForNewEmployee(newRoot.getId(), newManagerId);
        
        // 3. 递归复制子树
        copySubTree(sourceRootId, newRoot.getId());
        
        return newRoot.getId();
    }
    
    /**
     * 递归复制子树 - 深度复制的核心逻辑
     */
    private void copySubTree(Long sourceParentId, Long targetParentId) {
        // 获取源节点的所有直接下属
        List<Employee> children = employeeMapper.findDirectSubordinates(sourceParentId);
        
        for (Employee child : children) {
            // 复制子节点
            Employee newChild = new Employee();
            newChild.setName(child.getName());
            newChild.setManagerId(targetParentId);
            employeeMapper.insert(newChild);
            
            // 维护闭包表
            updateClosureForNewEmployee(newChild.getId(), targetParentId);
            
            // 递归复制孙子节点
            copySubTree(child.getId(), newChild.getId());
        }
    }
}

4. 核心Controller

java 复制代码
@RestController
@RequestMapping("/api/organization")
public class OrganizationController {
    
    @Autowired
    private OrganizationService organizationService;
    
    /**
     * 新增员工接口
     */
    @PostMapping("/employees")
    public ResponseEntity<Employee> addEmployee(@RequestBody Employee employee, 
                                               @RequestParam Long managerId) {
        Employee savedEmployee = organizationService.addEmployee(employee, managerId);
        return ResponseEntity.ok(savedEmployee);
    }
    
    /**
     * 移动员工接口
     */
    @PutMapping("/employees/{employeeId}/move")
    public ResponseEntity<Void> moveEmployee(@PathVariable Long employeeId,
                                           @RequestParam Long newManagerId) {
        organizationService.moveEmployee(employeeId, newManagerId);
        return ResponseEntity.ok().build();
    }
    
    /**
     * 查询子树接口 - 展示闭包表的查询威力
     */
    @GetMapping("/managers/{managerId}/subtree")
    public ResponseEntity<List<Employee>> getSubTree(@PathVariable Long managerId) {
        List<Employee> subtree = organizationService.findSubTree(managerId);
        return ResponseEntity.ok(subtree);
    }
    
    /**
     * 复制组织接口
     */
    @PostMapping("/organizations/{sourceId}/copy")
    public ResponseEntity<Long> copyOrganization(@PathVariable Long sourceId,
                                               @RequestParam Long newManagerId) {
        Long newRootId = organizationService.copyOrganization(sourceId, newManagerId);
        return ResponseEntity.ok(newRootId);
    }
}

5. 配置文件

application.yml

spring:

datasource:

url: jdbc:mysql://localhost:3306/organization_db?useSSL=false

username: root

password: 123456

mybatis:

mapper-locations: classpath:mapper/*.xml

type-aliases-package: com.example.entity

configuration:

map-underscore-to-camel-case: true

6. SQL初始化脚本

sql 复制代码
-- 员工表(邻接表)
CREATE TABLE employee (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    manager_id BIGINT
);

-- 组织关系闭包表
CREATE TABLE organization_closure (
    ancestor_id BIGINT NOT NULL,
    descendant_id BIGINT NOT NULL,
    depth INT NOT NULL,
    PRIMARY KEY (ancestor_id, descendant_id)
);

技术总结与选择指南

给同为小白的你の心法:

  1. 邻接表

    • 适用场景:小项目、层级固定的系统

    • 优点:简单直观,写入方便

    • 缺点:复杂查询需要递归,性能差

  2. 路径枚举

    • 适用场景:需要快速实现且层级不会极深

    • 优点:查询子孙节点方便

    • 缺点:维护复杂,有路径长度限制

  3. 闭包表

    • 适用场景:中大型项目、需要无限层级和复杂查询

    • 优点:查询性能极高,功能强大

    • 缺点:需要额外表,维护复杂

  4. 推荐组合邻接表 + 闭包表

    • 用邻接表处理简单的直接关系操作

    • 用闭包表处理复杂的层级查询

    • 两者结合,发挥各自优势

从此,小白我凭借着这套"组织森林地图"和精简高效的代码实现,在公司里混得风生水起。再复杂的组织架构需求,都能轻松搞定。


思考题:

在你的项目中,除了组织架构,还有哪些场景可能用到这种无限层级的设计呢?(比如:商品分类、论坛评论楼中楼、多级行政区划...)欢迎在评论区留言讨论!

相关推荐
安当加密4 小时前
基于ASP身份认证服务器实现远程办公VPN双因素认证的架构与实践
java·服务器·架构
ysdysyn4 小时前
Java奇幻漂流:从Spring秘境到微服务星辰的冒险指南
java·spring·微服务
FJW0208144 小时前
关系型数据库大王Mysql——DML语句操作示例
数据库·mysql
DARLING Zero two♡4 小时前
【优选算法】D&C-Mergesort-Harmonies:分治-归并的算法之谐
java·数据结构·c++·算法·leetcode
禁默4 小时前
基于金仓KFS工具,破解多数据并存,浙人医改造实战医疗信创
数据库·人工智能·金仓数据库
天天摸鱼的java工程师4 小时前
领导:“线程池又把服务器搞崩了!” 八年 Java 开发:按业务 + 服务器配,从此稳抗大促
java·后端
pen-ai4 小时前
【数据工程】15. Stream Query Processing
数据库
初级程序员Kyle5 小时前
开始改变第四天 Java并发(2)
java·后端
it码喽5 小时前
Redis存储经纬度信息
数据库