引子:新官上任的"一根棍子"
我叫小白,今天是我成为"宇宙第一科技有限公司"首席架构师的第一天。老板拍着我的肩膀,语重心长地说:"小白啊,咱们公司现在人少,组织架构就像一根棍子:我 -> 你 -> 程序员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导航",是降维打击!
再来几个炫酷的操作:
-
查询实习生B的所有上级(直到老板):
sqlSELECT e.* FROM employee e INNER JOIN organization_closure oc ON e.id = oc.ancestor_id WHERE oc.descendant_id = 4; -- 实习生B的ID是4 -
计算技术部(id=2)的总人数:
sqlSELECT COUNT(*) FROM organization_closure WHERE ancestor_id = 2; -
查询小白和实习生B之间隔了多少层(汇报路径深度):
sqlSELECT 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)
);
技术总结与选择指南
给同为小白的你の心法:
-
邻接表:
-
适用场景:小项目、层级固定的系统
-
优点:简单直观,写入方便
-
缺点:复杂查询需要递归,性能差
-
-
路径枚举:
-
适用场景:需要快速实现且层级不会极深
-
优点:查询子孙节点方便
-
缺点:维护复杂,有路径长度限制
-
-
闭包表:
-
适用场景:中大型项目、需要无限层级和复杂查询
-
优点:查询性能极高,功能强大
-
缺点:需要额外表,维护复杂
-
-
推荐组合 :邻接表 + 闭包表
-
用邻接表处理简单的直接关系操作
-
用闭包表处理复杂的层级查询
-
两者结合,发挥各自优势
-
从此,小白我凭借着这套"组织森林地图"和精简高效的代码实现,在公司里混得风生水起。再复杂的组织架构需求,都能轻松搞定。
思考题:
在你的项目中,除了组织架构,还有哪些场景可能用到这种无限层级的设计呢?(比如:商品分类、论坛评论楼中楼、多级行政区划...)欢迎在评论区留言讨论!