跳转到主内容
极星编程网:以代码为星,赴技术山海!

怎么利用组合模式处理复杂的组织架构树或多级菜单的递归查询优化

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

相关文章