MySQL触发器不支持递归调用自身,这是硬性限制;必须在单次触发内通过自连接或CTE一次性拼接完整路径,不能依赖多层触发链,否则会报错“Can't update table 'xxx' in stored function/trigger”。

触发器里不能直接递归调用自身
MySQL 触发器不支持递归触发(即 AFTER UPDATE 触发器修改同一张表,再触发自己),这是硬性限制。所以“递归更新路径”必须靠单次触发内完成全量路径拼接,不能依赖多层触发链。常见错误是写个 UPDATE 去改父节点,结果报错 Can't update table 'xxx' in stored function/trigger because it is already used by statement which invoked this stored function/trigger。
真正可行的思路是:在当前节点变更时,**一次性查出整条祖先链,用 CONCAT + REPEAT 或自连接拼出完整路径字符串**,然后更新当前节点的 path 字段。
用 JOIN 查询祖先链并拼接路径(推荐)
假设表结构为:id, parent_id, name, path,其中 path 存类似 '/1/5/23' 的字符串。触发器需在 INSERT 或 UPDATE 时重算 path。
关键点在于:不能用存储过程循环,而要用自连接或递归 CTE —— 但 MySQL 5.7 不支持 CTE,所以得用固定深度的 LEFT JOIN 模拟(适用于层级 ≤ 10 的业务场景):
SELECT CONCAT( '/', COALESCE(t3.id, ''), '/', COALESCE(t2.id, ''), '/', COALESCE(t1.id, ''), '/' ) AS full_path FROM your_table t1 LEFT JOIN your_table t2 ON t2.id = t1.parent_id LEFT JOIN your_table t3 ON t3.id = t2.parent_id WHERE t1.id = NEW.id;
实际触发器中,把这逻辑套进 SET NEW.path = (SELECT ...) 即可。注意顺序要从根到叶子,所以最深的祖先放在最左。
- 层级越多,
JOIN数量越多,性能越差;建议加索引:KEY idx_parent_id (parent_id) -
COALESCE防止NULL导致整个CONCAT结果为NULL - 如果业务层级不确定且很深(如 > 10),该方案不可行,应改用应用层维护路径
UPDATE 时 parent_id 变更的特殊处理
仅当 NEW.parent_id != OLD.parent_id 时才需要重算路径 —— 否则浪费计算。但要注意:父节点变了,所有子树节点的 path 都应级联更新,而触发器只能改当前行。这意味着子树路径不会自动同步。
解决方案只有两个:
- 接受“路径非实时”,在应用层或定时任务里跑一次全量修正(适合低频变动树)
- 在触发器里用
UPDATE批量更新所有后代(需用UPDATE ... JOIN+ 自连接查后代),但必须确保不触碰当前表的触发器(比如用临时表中转,或改用存储过程 + 应用层调用) - MySQL 8.0+ 可用递归 CTE 查后代,但触发器内仍不能执行
UPDATE同表,所以仍需绕过
实践中,大多数系统选择第一种:路径字段仅用于快速查询(如 WHERE path LIKE '/1/5/%'),不要求绝对实时,靠业务侧保证父变更后主动刷新子树。
字符长度与分隔符的实际约束
path 字段类型建议用 VARCHAR(2000) 或更大,避免截断。例如 10 层深、每 ID 平均 5 字符,加上斜杠就是 10×6 = 60 字符;但若 ID 是 UUID,则单段就 36 字符,10 层轻松超 400 字符。
分隔符选 / 是因它在 URL 和文件路径中语义清晰,且 MySQL 的 LIKE 和正则都容易处理。但注意:LIKE '/1/%' 会匹配 /10/,正确写法是 LIKE '/1/%' AND path NOT LIKE '/1[^/]% —— 实际中更推荐用 FIND_IN_SET 或 JSON 函数(MySQL 5.7+)替代字符串匹配。
最后提醒:路径字符串是冗余字段,任何绕过触发器的直接 INSERT/UPDATE 都会让它失效。上线前务必检查所有 DAO 层是否禁用直连 SQL,或统一走存储过程入口。


















