前阵子有个朋友把一套老系统从 Oracle 往达梦数据库迁,其他都挺顺,偏偏在触发器上卡了壳:同样的建表脚本、同样的触发器逻辑,在 Oracle 里跑得好好的,到达梦一执行就报错,要不就是触发了但结果不对。我远程看了一眼,发现是兼容性细节没处理好——达梦的触发器语法和 Oracle 很像,但有些行为习惯完全不一样。
这篇文章就把达梦数据库的触发器从头到尾捋一遍:是什么、能干什么、怎么写、有哪些坑。不管你是 DBA、后端开发还是运维,只要你的业务库要跑在达梦上,触发器这件事迟早会碰到。另外先提醒一句:搜索引擎里搜“触发器”会跳出一堆 D 触发器、RS 触发器、二分频电路,那是数字电路里的概念,跟达梦数据库里的触发器是两码事,别被带偏了。
1. 触发器到底是什么,为什么绕不开它
1.1 从业务需求看触发器的价值
数据库触发器是一种被动执行的对象,它不会主动被应用代码调用,而是在某个 DML 操作发生前或发生后,由数据库自动触发执行一段预定义的程序逻辑。你可以把它理解成“数据库里的看门狗”:一旦表上发生 INSERT、UPDATE、DELETE,就把你写好的那套规则自动跑一遍。
触发器的典型应用场景非常明确。最常见的是审计日志,比如财务表被谁改过、改之前的值是什么、改完之后变成什么,全部记录下来,这个过程不能依赖开发人员记得在代码里写日志,因为总有漏网之鱼。其次是数据完整性校验,比如新插入的员工薪资不允许为负数,这种规则写在触发器里,不管谁来写这条数据都躲不过检查。还有派生数据同步,比如新增客户时根据客户名称自动生成首拼码存入另一个字段,这种“一个入口、多处维护”的操作,用触发器可以做到从源头统一处理。
适合系统学习触发器的主要是三类人:一类是负责数据库运维的 DBA,因为触发器一旦出问题,直接影响线上业务;一类是后端开发,尤其是写通用业务模块的,需要理解触发器对事务和性能的影响;还有一类是数据迁移和架构设计人员,因为从 Oracle、MySQL 迁到达梦时,触发器往往是最后一批被梳理的对象,也是迁移完成后最容易爆雷的地方。
1.2 达梦触发器和 Oracle、MySQL 的差异在哪儿
达梦数据库(DM8)给开发者最直观的感受是:语法高度兼容 Oracle。如果你写过 Oracle 的 PL/SQL 触发器,到达梦之后几乎可以无缝迁移大部分代码。这里说的“几乎”很关键,因为细节差异不少。
首先是兼容模式。达梦安装时可以选择不同的兼容模式,比如兼容 Oracle、兼容 MySQL 等。在 Oracle 兼容模式下,:NEW、:OLD伪记录、INSERTING、UPDATING、DELETING条件谓词、RAISE_APPLICATION_ERROR这类 Oracle 风格语法都能正常使用;但如果你用的是标准模式或 MySQL 兼容模式,同样是这两行代码,可能报错也找不到原因。所以拿到一个新的达梦环境,第一步不是写触发器,而是确认当前实例的兼容模式。
其次是事务和提交规则。达梦和 Oracle 一样,普通触发器内部不允许随意提交事务,COMMIT和ROLLBACK直接写在触发器里会报错。如果你需要在触发器里写审计日志、又希望日志提交不受主事务回滚影响,得用自治事务。MySQL 里相对随意一些,这也是很多从 MySQL 转过来的开发者最容易踩的坑。
再一个是错误处理机制。达梦支不支持异常抛出?支持。在兼容 Oracle 模式下,RAISE_APPLICATION_ERROR(-20001, '自定义错误')是可以直接用的,而且抛出的错误会作为 SQL 执行错误返回给应用端。这个机制比 MySQL 的SIGNAL SQLSTATE用起来顺手得多,但也更容易出问题——恶意抛错会导致所有写入瞬间中断。
1.3 与硬件触发器彻底划清界限
这里必须单独花一段说明,因为我见过太多人搜“触发器”搜到崩溃。数据库里的触发器是“trigger”,但这个词在数字电路里同样是“触发器”,比如 D 触发器、RS 触发器、SR 触发器,还有 D 触发器二分频、复位优先 RS 触发器这些电路图。你如果在搜索引擎里搜“触发器语法”,很容易看到一堆触发器电路图、D 触发器二分频原理之类的硬件内容,和数据库完全没关系。
还有“异步触发器”这个词,数据库领域很少用,异步通常意味着触发动作不阻塞主流程,但数据库触发器默认是同步的——触发器没跑完,DML 就不会返回结果。看到“异步触发器”先别激动,确认一下对方说的到底是不是数据库。
2. 语法拆解:触发时机、事件与级别
2.1 基础语法模板
达梦创建触发器的通用语法结构如下:
CREATE OR REPLACE TRIGGER 触发器名 {BEFORE | AFTER | INSTEAD OF} {INSERT | UPDATE | DELETE} ON 表名 [FOR EACH ROW [WHEN (条件)]] DECLARE -- 局部变量声明(可选) BEGIN -- 触发器主体逻辑 EXCEPTION -- 异常处理(可选) END;这个结构和 Oracle 几乎一样。注意CREATE OR REPLACE在达梦中是可以使用的,这意味着你可以反复修改触发器而不必先 DROP 再 CREATE,这在调试阶段非常方便。
BEFORE和AFTER是触发时机,一个在操作执行前触发,一个在操作执行后触发。INSTEAD OF比较特殊,主要用于视图上的触发,把本来要执行 DML 操作替换成你写的逻辑,这个在复杂视图写操作场景中很有用。
举个例子,一个最简单的触发器:
CREATE OR REPLACE TRIGGER TRG_EMP_BI BEFORE INSERT ON EMP FOR EACH ROW BEGIN -- 新插入的员工工资不能为负 IF :NEW.SAL < 0 THEN RAISE_APPLICATION_ERROR(-20001, '工资不能为负数'); END IF; END;这段代码的逻辑很直白:在往 EMP 表插入数据之前,逐行检查新值,如果 SAL 字段小于 0,直接报错,插入操作被打断。
2.2 触发事件与 WHEN 条件
一张表上可以挂多个触发器,每个触发器可以响应一种或多种 DML 事件。常见的写法有:
CREATE OR REPLACE TRIGGER TRG_EMP_AUDIT AFTER INSERT OR UPDATE OR DELETE ON EMP FOR EACH ROW BEGIN NULL; END;INSERT OR UPDATE OR DELETE表示三种操作都会触发。如果你只想在特定列被 UPDATE 时触发,可以写成AFTER UPDATE OF SAL ON EMP,表示只有在 SAL 列被更新时才会触发。这个细节在性能优化时很有用,可以减少无效触发次数。
WHEN 条件是行级触发器专属的过滤手段,它写在FOR EACH ROW后面,只有满足条件的行才会执行触发器主体。需要注意的是,WHEN 条件里引用伪记录时不需要冒号,这一点和触发器主体内不一样:
CREATE OR REPLACE TRIGGER TRG_EMP_SAL_CHANGE AFTER UPDATE OF SAL ON EMP FOR EACH ROW WHEN (NEW.SAL > OLD.SAL) BEGIN -- 只有涨薪操作才会进入这里 NULL; END;我第一次用达梦的时候,就是在这里踩了坑。在主体里写得顺手了,WHEN条件里也习惯性写:NEW.SAL > :OLD.SAL,结果直接编译报错。后来才反应过来,WHEN 条件是一个独立的 SQL 表达式上下文,不是 PL/SQL 主体。
2.3 行级触发与语句级触发的选择
FOR EACH ROW决定了触发器是行级还是语句级。行级触发器对每一行数据都执行一次,可以访问当前行的:NEW和:OLD值;语句级触发器不带FOR EACH ROW,整个 SQL 语句执行完只触发一次,访问不到具体某一行的数据。
选择逻辑其实并不复杂。如果你关心的是“每一行的变化记录”,比如审计日志要记录每条数据的修改前后值,必须用行级触发器。如果你关心的是“这条 SQL 是否影响到了数据”,比如在非工作时间内禁止任何修改 EMP 表的操作,语句级就够了。
性能上,行级触发器的开销远大于语句级。举个例子,一条 UPDATE 语句更新了 10000 行,行级触发器就要跑 10000 次;如果里面再带几个 SELECT 查询,数据库压力瞬间拉满。所以能用语句级解决的问题,不要一上来就FOR EACH ROW。
这里补充一个达梦的原生短板:它不支持类似 Oracle 的复合触发器(Compound Trigger),也就是那种同时具备语句级和行级能力、并且能在多个触发点共享状态的触发器。网上搜得到的一些 Oracle 高级玩法,在达梦上不一定能直接用,迁移前需要重点确认这些能力是否受支持。
3. 实操:从零建表到触发器跑通
3.1 准备测试环境与基础表
我本地测试用的是一台华为欧拉系统服务器,直接安装的达梦 DM8,日常开发用 Navicat 连接达梦数据库跑 SQL,也会用自带的 DM 管理工具查看对象结构。Navicat 连接达梦需要选择达梦数据源类型,不是当成 MySQL 或 PostgreSQL 去连,连接参数里填好 IP、端口(默认 5236)、用户名密码就可以。
先建两张最基础的表。一张是员工表,一张是审计日志表:
CREATE TABLE EMP ( EMPNO INT PRIMARY KEY, ENAME VARCHAR(50), SAL DECIMAL(10,2), DEPTNO INT, PY_CODE VARCHAR(20) ); CREATE TABLE AUDIT_LOG ( ID INT IDENTITY(1,1), TARGET_TABLE VARCHAR(50) NOT NULL, OPERATION VARCHAR(10), OLD_DATA VARCHAR(500), NEW_DATA VARCHAR(500), OP_USER VARCHAR(50), OP_TIME DATETIME DEFAULT SYSDATE );AUDIT_LOG 表用了一个自增列ID INT IDENTITY(1,1),IDENTITY是达梦的自增写法,等价于 MySQL 的AUTO_INCREMENT。OP_USER保存操作人,OP_TIME保存操作时间,默认取SYSDATE。
3.2 前置校验触发器:把非法数据挡在门外
业务上经常有这样的需求:某些关键字段不允许插入空值或非法值。如果只靠应用端校验,总会有漏网之鱼——比如有人绕过应用直接连数据库执行 INSERT,或者批量导入工具没走业务接口。
这时候一个 BEFORE 行级触发器是最靠谱的防线:
CREATE OR REPLACE TRIGGER TRG_EMP_BI BEFORE INSERT ON EMP FOR EACH ROW BEGIN IF :NEW.EMPNO IS NULL THEN RAISE_APPLICATION_ERROR(-20001, 'EMPNO 不允许为空'); END IF; IF :NEW.SAL < 0 THEN RAISE_APPLICATION_ERROR(-20002, 'SAL 不能为负数'); END IF; END;执行插入测试:
-- 这条会报错:SAL 不能为负数 INSERT INTO EMP(EMPNO, ENAME, SAL, DEPTNO) VALUES (1001, '张三', -500, 10);实测下来,第二条 INSERT 会直接报错,错误信息就是SAL 不能为负数,插入失败。这个机制比应用层判断强在很多:只要数据写入 EMP 表,就必须经过这道校验,没有例外。
BEFORE 行级触发器还有一个用途:修改:NEW的值,在数据落库之前把派生字段补上。需要注意,修改:NEW字段只能在 BEFORE 触发器中生效,AFTER 触发器中改则没有意义,因为数据已经落库了。
3.3 审计日志触发器:记录每一次变动
审计日志是我认为触发器最实用的场景之一。不依赖应用代码,只要有人改数据,它就会记录。来看看一个完整的行级审计触发器:
CREATE OR REPLACE TRIGGER TRG_EMP_AUDIT AFTER INSERT OR UPDATE OR DELETE ON EMP FOR EACH ROW DECLARE v_operation VARCHAR(10); BEGIN IF INSERTING THEN v_operation := 'INSERT'; ELSIF UPDATING THEN v_operation := 'UPDATE'; ELSIF DELETING THEN v_operation := 'DELETE'; END IF; INSERT INTO AUDIT_LOG(TARGET_TABLE, OPERATION, OLD_DATA, NEW_DATA, OP_USER) VALUES ( 'EMP', v_operation, :OLD.EMPNO || ',' || :OLD.ENAME || ',' || TO_CHAR(:OLD.SAL), :NEW.EMPNO || ',' || :NEW.ENAME || ',' || TO_CHAR(:NEW.SAL), USER ); END;这里有几个细节值得讲。INSERTING、UPDATING、DELETING是三个条件谓词,用来判断本次触发是哪种操作。:OLD在 INSERT 时所有字段都是 NULL,:NEW在 DELETE 时所有字段都是 NULL,所以拼接字符串时需要根据场景选择,否则日志里会出现一长串“空值”。
按上面的写法,INSERT 时 OLD_DATA 会是,,这种带空位的串,不好看。更严谨的写法是分操作拼接:
IF INSERTING THEN INSERT INTO AUDIT_LOG(TARGET_TABLE, OPERATION, NEW_DATA, OP_USER) VALUES ('EMP', 'INSERT', :NEW.EMPNO || ',' || :NEW.ENAME, USER); ELSIF DELETING THEN INSERT INTO AUDIT_LOG(TARGET_TABLE, OPERATION, OLD_DATA, OP_USER) VALUES ('EMP', 'DELETE', :OLD.EMPNO || ',' || :OLD.ENAME, USER); ELSE INSERT INTO AUDIT_LOG(TARGET_TABLE, OPERATION, OLD_DATA, NEW_DATA, OP_USER) VALUES ('EMP', 'UPDATE', :OLD.EMPNO || ',' || :OLD.ENAME, :NEW.EMPNO || ',' || :NEW.ENAME, USER); END IF;这么做日志更干净,看起来也更专业。
这个触发器还有一个隐患:它和主事务共享事务边界。如果用户执行了一个大事务,最后 ROLLBACK 了,那审计日志也会一并回滚。在某些监管场景下,你可能希望“操作一旦发生,无论事务是否提交,日志都留下”——这就需要用自治事务。
达梦的自治事务写法:
CREATE OR REPLACE TRIGGER TRG_EMP_AUDIT_AT AFTER INSERT OR UPDATE OR DELETE ON EMP FOR EACH ROW DECLARE PRAGMA AUTONOMOUS_TRANSACTION; BEGIN -- 这里执行 INSERT 日志并 COMMIT INSERT INTO AUDIT_LOG(...) VALUES (...); COMMIT; END;PRAGMA AUTONOMOUS_TRANSACTION声明了自治事务后,触发器内部的 COMMIT 只提交当前这个独立事务,不影响外部事务。这个功能在 Oracle 里很经典,达梦也支持,但是在普通触发器里直接写COMMIT是会被拒绝的,很多人第一次写总在这上面翻车。
3.4 用触发器自动生成首拼码
热词里有个“达梦数据库 生成首拼码函数”,这在业务系统里非常常见:客户表、供应商表通常会有一个“首拼码”字段,用于实现输入拼音首字母模糊查询。比如“张三”的首拼码是“ZS”。
达梦没有内置的中文转拼音函数,项目里一般是自己维护一个拼音码对照表,或者创建自定义函数 F_GET_PY,输入一个字符串,返回拼音首字母串。有了这个函数,触发器的写法就非常简洁:
CREATE OR REPLACE TRIGGER TRG_EMP_PY BEFORE INSERT OR UPDATE OF ENAME ON EMP FOR EACH ROW BEGIN :NEW.PY_CODE := F_GET_PY(:NEW.ENAME); END;每次插入新员工,或者修改员工姓名时,触发器自动根据 ENAME 重新计算首拼码并写入 PY_CODE 字段。应用端完全不用关心这个字段怎么维护,查询时直接用 PY_CODE 做 LIKE 查询即可。
这个场景是触发器的“甜点区”:规则集中、逻辑单一、数据量不大,写进触发器比写进业务代码更可靠。
3.5 语句级触发器与 DDL 触发器的简单示例
语句级触发器很适合做“开关型控制”。例如:不允许在非工作时间通过任意手段修改部门表:
CREATE OR REPLACE TRIGGER TRG_DEPT_BI_STMT BEFORE INSERT OR UPDATE OR DELETE ON DEPT BEGIN IF TO_CHAR(SYSDATE, 'HH24') NOT BETWEEN '08' AND '18' THEN RAISE_APPLICATION_ERROR(-20010, '非工作时间禁止修改部门数据'); END IF; END;没有FOR EACH ROW,不管这条 SQL 影响 1 行还是 1000 行,触发器只执行一次,性能开销极小。
达梦还支持 DDL 触发器,即针对 CREATE、ALTER、DROP 等 DDL 事件触发。例如禁止普通应用连接受理 DDL 操作:
CREATE OR REPLACE TRIGGER TRG_CTRL_DDL BEFORE DDL ON DATABASE BEGIN IF USER != 'SYSDBA' THEN RAISE_APPLICATION_ERROR(-20011, '当前用户无权执行 DDL 操作'); END IF; END;这个触发器在某些安全管理严格的系统里很实用。不过要注意,DDL 触发器作用范围是整个数据库,影响面很大,调试过程中容易误伤正常运维操作,谨慎使用。
4. 常见坑与排查技巧实录
4.1 触发器为什么没触发,先查这四件事
触发器没有触发,是最常遇到的疑惑。排查时我一般按四步走:
第一步查看触发器状态。达梦中查看用户下的所有触发器:
SELECT TRIGGER_NAME, STATUS FROM USER_TRIGGERS;如果 STATUS 不是 ENABLED,而是 DISABLED,说明触发器被禁用了。启用它:
ALTER TRIGGER TRG_EMP_BI ENABLE;第二步确认触发事件和时机。确认你定义的是 BEFORE 还是 AFTER,是 INSERT、UPDATE 还是 DELETE。比如AFTER UPDATE触发器,你执行 INSERT 当然不会触发,这个看起来简单,但多人协作时真的容易看走眼。
第三步看 WHEN 条件。WHEN 条件定义了行级过滤,如果条件写错了,比如NEW.SAL > OLD.SAL但实际执行的是降薪操作,触发器主体就不会执行。这不算 bug,是条件不满足。
第四步检查触发器和表的属主。如果你的应用通过 A 用户连接数据库,但触发器建在 B 用户下,那么在 A 用户下操作表时,触发器不会触发。触发器是建立在表上的对象,必须和表在同一个 schema 下才能随表生效。
4.2 递归触发与死循环:一场真实的翻车现场
有一次我在达梦上给 A 表写了个 AFTER INSERT 触发器,触发器内部又往 A 表插一条数据。结果第一条 INSERT 下去,数据库直接报错,提示类似“递归触发深度超过限制”。原因很简单:插入一行触发触发器,触发器再插入一行,又触发自己,无限套娃,直到数据库强行终止。
递归触发有几种常见场景。最典型的是没有加触发条件的事务内级联。解决方案一般是加一个“守卫”字段或“守卫”表。比如在表上增加一个标志列,或者用 DBMS_APPLICATION_INFO 之类的上下文标记当前会话正在执行触发器内部操作,在触发器开头判断标志位,如果已经处于触发器内部,直接跳过:
CREATE OR REPLACE TRIGGER TRG_EMP_AUDIT AFTER INSERT ON EMP FOR EACH ROW DECLARE v_ct INT; BEGIN SELECT COUNT(*) INTO v_ct FROM TRIGGER_GUARD WHERE TRIGGER_NAME = 'TRG_EMP_AUDIT'; IF v_ct > 0 THEN RETURN; END IF; -- 正常逻辑 END;或者用自治事务写日志,就不会因为再次对原表产生 DML 而触发递归。
另一个和递归相关的问题是:两个表互相挂触发器。A 表的 UPDATE 触发器去更新 B 表,B 表的 UPDATE 触发器又回头更新 A 表,形成一个环。这种环在数据量小的时候看不出来,数据量一大,性能直线下降甚至超时报错。设计触发器时一定要画出触发链路图,搞清楚一个操作最终会引发多少个触发器的连锁动作。
4.3 触发器编译错误怎么定位
达梦编译触发器时如果出错,不像 MySQL 那样直接弹一个错误框就完事,很多情况下触发器会被自动置为 INVALID 状态。这时候你执行 DML 不会报错,但触发器也不执行,非常隐蔽。
定位编译错误的常用语句:
SELECT * FROM USER_ERRORS WHERE NAME = 'TRG_EMP_AUDIT' ORDER BY LINE;它会列出触发器名、错误行号、错误文本。比如你写了一个不存在的函数,或者字段名写错了,都能从这里看到。
还有一种情况:触发器能编译,但运行时出错。这种更麻烦,因为错误信息会返回给执行 DML 的会话,可能表现为“数据库突然连不上”——应用端疯狂报错,实际是某个写入操作触发了一个异常未被捕获的触发器,导致所有写入全部失败。遇到这种问题,先看达梦的日志文件,尤其是dm_实例名_日期.log,里面会记录详细的错误堆栈。再用如下语句确认所有触发器状态:
SELECT TRIGGER_NAME, STATUS, TRIGGER_TYPE, TRIGGERING_EVENT FROM USER_TRIGGERS ORDER BY TRIGGER_NAME;如果发现某个触发器变成了 INVALID,十有八九就是它惹的祸。
4.4 触发器内部能不能用 DDL 和动态 SQL
达梦的触发器内部支持动态 SQL,也就是EXECUTE IMMEDIATE,这在某些场景下很灵活。比如用表名作为参数拼接语句做通用审计。但也带来风险:动态 SQL 的语法和错误排查难度比静态 SQL 高得多,而且容易留下 SQL 注入的隐患——虽然触发器内部一般是固定逻辑,但如果你拼接的内容里有外部输入,一样会出问题。
我的建议是:触发器里尽量写静态 SQL,动态 SQL 能不用就不用。非要动态的话,把所有表名、列名写成白名单校验,不要直接拼接用户传入的字符串。
触发器内部对 DDL 的限制不如存储过程严格,但操作 DDL 尤其是CREATE TABLE这种,一旦涉及临时表,生命周期和事务边界很难控制,线上环境不建议这么玩。
4.5 一个容易忽略的坑:大批量初始化数据时触发器拖后腿
这是特别容易踩的一个坑。业务系统从老库迁到达梦,导入历史数据时通常用批量 INSERT,如果表上的审计触发器还开着,那每插一行数据就记一条日志,几百万行数据可能把日志表撑爆,导入速度慢得怀疑人生。
更麻烦的是,如果前置校验触发器里有RAISE_APPLICATION_ERROR,历史数据里有一条非法数据,整个导入都会中断,而且报错位置和原因很难追。
所以在大批量初始化、导入 dmp 文件、ETL 刷数之前,先把目标表上的业务触发器禁用掉,导完数据以后再做数据质量校验,确认没问题再重新启用。
ALTER TRIGGER TRG_EMP_AUDIT DISABLE; -- 执行批量导入 ALTER TRIGGER TRG_EMP_AUDIT ENABLE;达梦还支持在表级别禁用全部触发器:
ALTER TABLE EMP DISABLE ALL TRIGGERS; ALTER TABLE EMP ENABLE ALL TRIGGERS;注意这种做法会禁用该表上所有触发器,包括系统维护用的,用之前必须和团队打好招呼。
5. 从触发器延伸到运维:备份、高可用与应用集成
5.1 导出导入 DMP 时触发器会不会跟着走
达梦的逻辑导入导出工具是 dexp 和 dimp,对应业界常见的 exp/imp 概念。很多人关心:用 dexp 导出 dmp 文件时,触发器会不会一起被导出?
答案是:在 schema 级别导出时,触发器默认会一起导出。恢复时用 dimp 导入,触发器会随表结构一起还原。但这里有个细节——系统级的 DDL 触发器(就是作用在 DATABASE 上的那个)不随业务 schema 走,需要单独处理。我在实际迁移中遇到过一次:导出的 dmp 里包含表、数据、存储过程,但系统级触发器没跟着走,恢复环境后没有这个防控机制,业务可以直接执行 DDL,后来手工重建才补上。
导入 dmp 后也一定要检查触发器状态。因为导入过程中如果对象冲突、索引失效,触发器可能被置为 INVALID。导入完成后执行这个语句是标配:
SELECT TRIGGER_NAME, STATUS, TABLE_NAME FROM USER_TRIGGERS WHERE STATUS != 'ENABLED';有任何一行结果,就说明有触发器需要重新编译。手动编译的方式是 ALTER TRIGGER ... COMPILE:
ALTER TRIGGER TRG_EMP_AUDIT COMPILE;5.2 触发器在 DW 主备和 DSC 集群里的特殊行为
达梦的高可用方案主要有两种,热词里的 DW 和 DSC 经常被放在一起对比。简单区分:DW 是实时主备模式,备库通过主库的 REDO 日志实时同步数据,类似于传统的主从复制;DSC 是共享存储集群,多个节点访问同一份数据文件,类似 Oracle RAC。
触发器这两种架构下的行为差异很大,值得单独说。
DW 模式下,应用只写主库,触发器只在主库执行。备库通过日志同步数据,不会因为同步而产生二次触发。这其实是个好消息:不用担心备库执行日志同步时把触发器又跑一遍,因为备库根本不执行触发器。但反过来,如果你在触发器里用了序列、会话级临时表或者自定义包变量,这些状态在主库上生成之后,同步到备库的可能只是最终数据,而不是状态本身。主备切换后,原本在备库升主库的过程中,这些状态可能全部丢失,应用行为会发生变化。所以高可用环境下的触发器,尽量不要依赖会话级状态。
DSC 模式下,多个节点共享一份数据,触发器本身是共享的,但触发时机和执行节点取决于应用的连接落在哪个节点。如果两个节点上同时执行互相依赖的 DML,触发器的并发控制就成了问题,严重时会出现锁等待甚至死锁。DSC 环境里写触发器,代码要尽量短小、避免持有锁过长,能写进约束的不要写触发器。
5.3 应用集成场景:当达梦遇见 Nacos 和微服务
现在不少业务系统在做达梦适配,热词里的“nacos 适配达梦数据库”就是典型场景。Nacos 是常用的配置中心和注册中心,早期官方默认支持 MySQL,现在已经有不少实践把后端存储切换到达梦。
在这种微服务架构下,数据库触发器依然有价值,但要特别谨慎。因为微服务实例数量多,请求并发大,触发器一旦出现性能瓶颈,放大效应会非常明显。比如一个 AFTER 触发器里做了一次全表查询,单次执行只要 5 毫秒,看起来毫不起眼,但每分钟几万次 DML,这 5 毫秒就会被放大成巨大的数据库开销。
我的建议是,在微服务架构里,触发器只承担两类职责:一类是不可绕过的高危校验,另一类是低成本的状态记录。其余逻辑尽量放到服务层,不要什么都让数据库背。毕竟触发器的执行对应用是透明的,出了问题,排查链路比普通代码长得多。
5.4 触发器使用的几条实战原则
写了这么多年触发器,踩了无数坑,总结几条对我自己最管用的原则:
第一,触发器要短小精悍。超过几十行的触发器逻辑,建议改成存储过程,触发器只负责调用。这样既保留了自动触发的特性,又让逻辑可以单独测试和调试。
第二,必须加异常处理。触发器内部没有捕获的异常,会直接中断业务 DML。一个简单的EXCEPTION WHEN OTHERS THEN NULL;在某些场景下可以让业务不至于中断,但同时也会掩盖真实问题,用不用要看需求。审计类触发器我一般建议不要吞异常,而是要记录日志,然后根据业务决定是否继续抛出。
第三,命名要规范。触发器名字建议以TRG_表名_时机_事件的格式命名,比如TRG_EMP_BI_INSERT表示 EMP 表上 BEFORE INSERT 行级触发器。命名规范不是形式主义,线上排查触发器问题的时候,看到一个好名字能省下不少时间。
第四,上线前一定要做触发器链路分析。画一张表:改变 A 表会触发哪些触发器,这些触发器又会改哪些表,这些表上的触发器又会不会引发新的 DML。这个问题想清楚了再上线,能避开大部分线上事故。
第五,优先级和顺序问题。同一张表上建了多个同类型触发器时,达梦的执行顺序并不像某些数据库那样有明确的优先级控制。如果你有两个 BEFORE INSERT 触发器都修改:NEW字段,后执行的覆盖先执行的,结果可能不符合预期。遇到这种需求,最稳妥的做法是合并成同一个触发器,在里面按顺序控制逻辑。
6. 最后说点实战感受
达梦的触发器整体给我的感觉是:Oracle 风格为主,细节有差别,但整体学习曲线并不陡峭。如果你是从 Oracle 转过来,适应期会非常短;如果你是从 MySQL 转过来,建议先老老实实写完几个实操案例,把:NEW/:OLD、RAISE_APPLICATION_ERROR、自治事务这几个概念弄清楚再上生产。
从我个人的使用体验看,触发器是那种“好用但要克制”的功能。它最好的状态是隐形的,业务感知不到它的存在,数据却始终被保护着。它最坏的状态是失控的,一条简单 INSERT 背后牵出一长串触发器链路,改个字段都可能引发雪崩。
如果你正在做达梦迁移或者新项目选型,建议先把现有数据库里所有触发器的清单拉出来,逐个评估:哪些可以去掉,哪些能改成约束或存储过程,哪些必须保留。这一步做在前面,比上线后再收拾烂摊子要省心得多。如果只是刚开始接触达梦,那就按这篇文章里的几个例子,在自己测试环境里建两张表、跑一遍触发器的创建、触发、禁用、启用、删除全流程,比看十遍文档都管用。