因WITH RECURSIVE仅返回扁平数据且无行为逻辑,无法支持审批、通知等分支操作;ORM级联易致N+1或全量加载浪费资源;应采用“一次查平表+内存构树”方式,配合环检测与缓存机制保障安全高效。
为什么不能直接用 SQL 的 WITH RECURSIVE 查完就完事
因为
只返回扁平数据,不承载行为逻辑。你要对某个部门执行
,而它的子部门中:前端组要走三级审批,后端组要触发钉钉通知,测试组要汇总每日缺陷率——这些动作无法塞进一条 SQL 的 SELECT 里。
更现实的问题是:ORM(比如 MyBatis 或 Hibernate)默认的
级联加载,一查根节点就触发 N+1 查询,或一次性把全树拉下来却只用到前两层,内存和网络都浪费。
数据库递归查询适合「只读快照」,不适合「带状态、带分支逻辑的遍历」
组合模式不是替代 SQL,而是补足 SQL 做不了的事:统一调度、按需响应、避免重复计算
真正要优化的不是“怎么查得更快”,而是“查一次之后,怎么让后续操作不反复查”
必须用「一次查平表 + 内存构树」而非 ORM 级联
假设你有一张
表,含
、
、
、
(如 "team" / "dept" / "center")。别写
,那会让每个节点都触发一次 SQL。
正确做法是:用一条 SQL 查出全部有效节点(加 WHERE 过滤掉已停用/测试部门),然后在 Java/C# 中构建树:
查完后用
缓存所有节点,再遍历一次,用
挂载子节点到对应父节点的
列表中
类里,
返回
,绝不返回
叶子节点(比如无下属的小组)的
返回空
,不是
—— 否则每层递归都要
Component 接口里不要放 add()/remove() 方法
这是最容易被误抄的反模式。很多教程示例把
和
放在
接口里,导致
类被迫实现并抛异常,破坏了接口语义。
真实业务中,叶子节点(如“UI 设计师”岗位)根本不可能添加子节点;强行暴露方法,只会让调用方在运行时才发现错误,且 IDE 无法静态检查。
把
、
、
全部移到
实现类里
接口只保留行为方法(如
、
)和只读结构方法(如
、
)
如果真需要动态增删,用单独的
服务封装,而不是污染通用接口
防环引用和重复计算是 runtime 最容易崩的点
组织架构里出现 A → B → C → A 这种环,不是理论风险,是真实发生过的线上事故。一旦
递归进去,JVM 直接栈溢出;或者某节点被多个父节点引用,
被执行两次,预算翻倍。
构树阶段就要校验环:维护一个
,从每个根节点向下 DFS,遇到已访问 ID 就报错并记录路径
聚合类方法(如
)必须支持缓存:在
节点里加
,首次调用才计算,后续直接返回
不要在
里查库或做重算——这个方法会被遍历逻辑高频调用,必须是 O(1) 的取值
实际落地时,最常被跳过的一步是「构树后的环检测」。它不难写,但没人愿意在开发阶段加;等到生产环境出现无限递归,排查成本远高于预防成本。
WITH RECURSIVEcalculateBudget()@OneToManydepartmentidnameparent_idtype@OneToMany(mappedBy = "parent")SELECT id, name, parent_id, type FROM department WHERE status = 'ACTIVE' ORDER BY parent_id, idMapparent_idchildrenDepartmentNodegetChildren()Collections.unmodifiableList(children)nullgetChildren()ArrayListnullif (node.getChildren() != null)add(Component)remove(Component)ComponentLeafadd()remove()getChildCount()CompositeComponentexecute()calculate()getChildren()getName()TreeEditorexecute()calculateBudget()Set visited sumSalary()Compositeprivate volatile BigDecimal cachedSum;getChildren()