触发器必须建在病历主表(如patient_records)上,历史表仅被动接收日志;字段变更判断需先比NULL性再比TRIM后值;日志须含record_id、operation_type、old_data、new_data、modified_by、modified_at五字段,且日志表结构要宽松防事务回滚。

必须建在患者病历主表(如 patient_records)上,否则修改根本不会被捕捉——这是最常踩的坑,也是留痕失效的根源。
触发器该建在病历表还是历史表上
只能建在病历主表,比如 patient_records。历史表(如 patient_records_audit)只是被动接收日志的地方,它不参与业务变更响应。试图在历史表上建 BEFORE INSERT 触发器来“拦截写入”,逻辑上就错了:没人会直接往历史表插业务数据,所有日志都应由主表变更驱动。
常见错误现象:
- 开发误以为“日志表才是主角”,把触发器建在
patient_records_audit上,结果患者改完病历,日志表空空如也 - 用
INSTEAD OF触发器覆盖主表操作,反而破坏了业务一致性,还让审计失去原始上下文
UPDATE 触发器里怎么判断字段是否真被改了
直接写 OLD.diagnosis != NEW.diagnosis 在遇到 NULL 时会失效——因为 NULL != 'xxx' 返回的是 NULL,不是 TRUE 或 FALSE,导致条件跳过。文本字段还要防空格、换行等隐形差异。
正确做法是分两步比:
- 先比 NULL 性:
(OLD.diagnosis IS NULL) != (NEW.diagnosis IS NULL) - 再比值(仅当两者都不为 NULL):
TRIM(OLD.diagnosis) != TRIM(NEW.diagnosis)
例如完整判断逻辑:
IF (OLD.diagnosis IS NULL) != (NEW.diagnosis IS NULL) OR (OLD.diagnosis IS NOT NULL AND NEW.diagnosis IS NOT NULL AND TRIM(OLD.diagnosis) != TRIM(NEW.diagnosis)) THEN -- 这里才确认 diagnosis 真变了 END IF;
INSERT/UPDATE/DELETE 日志字段设计的关键细节
单条日志必须含五个核心字段,缺一不可:
-
record_id:原病历 ID,用于反查 -
operation_type:固定为'I'/'U'/'D',不能靠应用层传,必须在触发器内用CASE WHEN EXISTS(SELECT * FROM inserted) AND EXISTS(SELECT * FROM deleted) THEN 'U' ...判定 -
old_data和new_data:用 JSON 格式存储,但只存真正变化的字段(INSERT时old_data为NULL,DELETE时new_data为NULL),避免整行序列化导致日志膨胀 -
modified_by:必须从会话变量读,如 MySQL 的@current_user_id或 SQL Server 的SUSER_SNAME(),绝不能依赖应用层传参——否则 DBA 直连改数据就无法溯源 -
modified_at:用带毫秒的NOW(3)(MySQL)或SYSDATETIME()(SQL Server),保证时序可排序
容易被忽略的一点:触发器失败会导致主事务回滚。比如日志表字段长度不够、JSON 字段超长、或加了非空约束,都会让一次病历更新直接失败。医疗系统里,宁可日志少几条,也不能让保存病历失败——所以日志表结构要宽松,文本全用 TEXT,时间用 DATETIME(3),除主键外不设任何约束。

















