不能靠 before delete 触发器单独防住误删,需结合权限控制、会话约束(如 sql_safe_updates=1)和软删除设计;触发器内必须用 signal 抛出错误才能中止删除,且仅能基于 old 行做简单判断。

不能靠 BEFORE DELETE 触发器单独防住误删——它拦不住 TRUNCATE、DROP,也拦不住权限足够高的账号,更拦不住应用直连绕过触发器。真要保核心配置表,得用触发器 + 权限 + 会话约束三层卡位。
MySQL 的 BEFORE DELETE 触发器必须用 SIGNAL 才生效
写 SELECT 1/0、INSERT INTO audit_log 或 SET @blocked = 1 都无效:这些语句能执行,但 MySQL 仍会继续删数据。唯一中止方式是抛出可中断错误:
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '禁止删除系统配置项';- 错误码
'45000'是 MySQL 5.5+ 原生支持的通用自定义错误,客户端会收到ERROR 1644 (45000) - 别用
RAISERROR(SQL Server 语法)或空SELECT,语法虽通,逻辑完全失效
校验必须基于 OLD 行,禁用子查询和函数调用
触发器逐行触发,OLD 是你唯一可用的数据快照。所有判断必须直接、无副作用:
- 允许:
IF OLD.is_protected = 1 THEN SIGNAL ... END IF; - 允许:
IF OLD.key IN ('app_timeout', 'max_retry') THEN SIGNAL ... END IF; - 禁止:
(SELECT COUNT(*) FROM dependencies WHERE config_key = OLD.key)—— 可能死锁,且违反触发器原子性原则 - 禁止:
IF is_core_config(OLD.key) THEN ...—— 自定义函数可能读表或耗时,触发器内不允许
TRUNCATE 和 DROP 完全不触发任何 DML 触发器
你在 config_settings 表上建十个 BEFORE DELETE,也挡不住一条 TRUNCATE TABLE config_settings 或 DROP TABLE config_settings:
-
TRUNCATE是 DDL 操作,不走 binlog 的 DELETE 事件,不可回滚,也不记录行级日志 -
DROP更彻底,直接删元数据,触发器机制根本不监听 - 真正兜底靠权限:对核心库执行
REVOKE DROP, TRUNCATE ON prod_db.* FROM 'app_user'@'%';
软删除字段比硬拦截更可控
把“禁止删”变成“只允许标记为已停用”,能规避绝大多数误操作场景:
- 加字段:
is_active TINYINT DEFAULT 1 COMMENT '0=停用,1=启用' - 改应用逻辑:所有读取加
WHERE is_active = 1;删除操作改为UPDATE config_settings SET is_active = 0 WHERE id = ? - 触发器只做兜底:比如防止对已停用项再执行硬删 ——
IF OLD.is_active = 0 THEN SIGNAL ... END IF; - 比纯拦截更安全:不会因触发器异常导致业务阻塞,也保留了审计与恢复路径
最易被忽略的是会话级防护——哪怕触发器和权限都设好了,开发连上数据库随手敲 DELETE FROM config_settings; 还是可能成功。必须配 sql_safe_updates=1 并通过 init_connect 强制生效,否则那条没带 WHERE 的删表语句,就真的只是缺个分号的事。











