组合模式不可被SQL递归查询替代,因其核心价值在于统一行为响应而非仅数据读取;Component接口需含getChildren()和统一行为方法(如execute()),叶子节点返回空集合,组合节点递归调用,且须避免环引用与重复计算。

为什么不能直接用 SQL 递归查询替代组合模式
因为数据库的 WITH RECURSIVE 查询只解决「读取」,不解决「行为统一」。比如你要对某个部门执行 calculateBudget(),它下面有的子部门要走审批流,有的要触发通知,有的还要聚合下属员工考勤——这些逻辑无法塞进一条 SQL 里。组合模式的核心价值不是“怎么查”,而是“查出来之后,怎么让不同层级的对象自动响应同一指令”。
Component 接口必须包含哪些方法才撑得起无限层级
关键不在多,而在「最小闭环」:必须有 getChildren() 和统一的行为入口(如 execute(Operation op) 或具体业务方法如 sumSalary())。叶子节点的 getChildren() 返回空集合而非 null,否则递归遍历时要处处判空;execute() 在叶子节点直接执行,在组合节点先处理自己再遍历子节点调用 execute()。
-
getChildren()返回List,不可返回void或Optional - 避免在接口里放
add()/remove()—— 这些只属于Composite实现类,强行塞进接口会让叶子节点暴露无意义方法 - 如果需要区分类型,用
isLeaf()比instanceof更安全,尤其在序列化/远程调用场景下
递归聚合时最容易爆栈或重复计算的两个点
一是没做访问标记导致环形引用(比如 A 部门误加了 B,B 又反向引用 A);二是聚合逻辑写在 getChildren() 里,每次调用都重新加载子节点甚至查库。真实业务中,子节点数据往往来自缓存或分页加载,不能假设“一查就全有”。
- 初始化树结构时,用拓扑排序或深度限制(如
maxDepth = 10)检测环,抛出CycleDetectedException而非等栈溢出 - 聚合方法(如
totalStaffCount())应带内部缓存字段 + 标记位,首次调用才计算,后续直接返回;清缓存时机明确绑定到add()/remove()操作后 - 不要在
Composite.sumSalary()里写children.stream().mapToLong(c -> c.sumSalary()).sum()—— 这会强制展开全部子树;改用惰性求值或分段聚合(如按层级缓存中间结果)
MyBatis / JPA 场景下如何避免 N+1 和树重建开销
ORM 本身不理解组合结构。直接让 DepartmentMapper.selectById(id) 返回一个带嵌套 children 的对象,大概率触发 N+1 查询或一次性拉取整库。必须切断 ORM 默认的级联行为,改用「一次查平表 + 内存构树」。
- SQL 查出所有相关节点(含
id、parent_id、name等),按parent_id分组后,用Map建立父子索引> - 从根节点出发,通过索引快速组装子树,不用递归查库;
Department类里children字段只存引用,不触发懒加载 - JPA 用户禁用
@OneToMany(fetch = FetchType.EAGER),改用@Query手写 SQL +ResultTransformer或自定义ResultSetExtractor
实际应用中最容易被忽略的是:**组合模式不是用来替代数据库设计的,而是用来隔离业务操作与数据形态的差异**。你可以在 DAO 层用扁平表存储,在 Service 层用组合对象封装行为,两者之间用一次干净的转换桥接——而不是试图让 MyBatis 直接映射出一棵活的树。










