触发器中禁止使用WAITFOR或事务内阻塞操作,应仅做状态预置与消息投递;审批逻辑须移出数据库,通过外部服务或Service Broker处理;需防递归触发、避免字符串拼接状态、设计原子性表结构,并在并发场景下保障乐观锁或条件更新。

触发器里不能用 WAITFOR 或事务内阻塞操作
SQL Server 触发器运行在语句事务上下文中,一旦触发,必须快速完成。试图在 AFTER INSERT 或 INSTEAD OF UPDATE 里调用 WAITFOR DELAY 等待审批人响应,会直接卡死当前事务,导致锁表、阻塞其他会话——这不是“自动流转”,是主动制造死锁。
真正可行的路径是:触发器只做「状态预置 + 消息投递」,把流转逻辑移出数据库。常见做法包括:
- 在触发器中向消息表(如
ApprovalQueue)插入一条待处理记录,含request_id、next_approver_id、step_order等字段 - 由外部服务(如 .NET Worker Service、Python Celery Task)轮询该表,或通过 SQL Server 的
Service Broker接收通知后执行审批逻辑 - 审批动作(如用户点击“同意”)走普通业务 API,更新主表状态并触发下一级插入
多级审批状态更新必须避免递归触发器死循环
如果在 UPDATE ApprovalRequest SET status = 'Approved' 后,触发器又去 UPDATE ApprovalRequest SET next_approver_id = ...,而这个 UPDATE 又再次触发同一个触发器,就会无限递归——SQL Server 默认限制嵌套层级为 32,超限报错 Msg 217, Level 16, State 1: Maximum stored procedure, function, trigger, or view nesting level exceeded。
安全写法是显式关闭递归,并用条件过滤:
CREATE TRIGGER tr_ApprovalRequest_StatusChange
ON ApprovalRequest
AFTER UPDATE
AS
BEGIN
IF NOT EXISTS (SELECT * FROM inserted i JOIN deleted d ON i.id = d.id WHERE i.status != d.status)
RETURN;
<p>-- 关键:禁止自身再触发
IF TRIGGER_NESTLEVEL() > 1 RETURN;</p><p>-- 只处理从 Pending → Approved 的跃迁
UPDATE ar
SET next_approver_id = a.next_approver,
status = CASE
WHEN a.is_final = 1 THEN 'Completed'
ELSE 'Pending'
END
FROM ApprovalRequest ar
INNER JOIN inserted i ON ar.id = i.id
INNER JOIN ApprovalStep a ON ar.current_step = a.step_order
WHERE i.status = 'Approved' AND ar.status = 'Pending';
END;状态字段设计要支持原子性判断,别用字符串拼接
有人喜欢把审批链存成 'user1;user2;user3',然后在触发器里用 CHARINDEX 判断当前到谁了——这不可靠。一旦中间审批人被替换、顺序调整,字符串解析极易出错,且无法加索引加速查询。
正确结构至少包含三张表:
-
ApprovalRequest:主申请单,含status('Draft'/'Pending'/'Rejected'/'Completed')、current_step(int)、last_updated_by -
ApprovalStep:审批步骤定义,含step_order、approver_role_id、is_mandatory、is_final -
ApprovalLog:每次操作留痕,含request_id、step_order、approver_id、action('Approve'/'Reject')、created_at
触发器查 ApprovalStep 找下一步时,靠 JOIN 和 WHERE step_order = current_step + 1,稳定、可索引、易测试。
测试触发器必须覆盖并发更新场景
两个审批人几乎同时点“同意”,可能都读到 current_step = 2,都算出下一步是 3,然后都去更新同一行——最终只有一个人成功,另一个人触发唯一约束或丢失更新。这不是触发器写得不对,而是业务逻辑没处理乐观并发。
应对方式:
- 在
ApprovalRequest加row_version列(timestamp或rowversion),UPDATE 时带上WHERE row_version = @old_version,检查@@ROWCOUNT = 0则重试 - 或改用
UPDATE ... FROM ... WHERE ... AND status = 'Pending',利用 WHERE 条件天然排他 - 绝不要在触发器里做“先 SELECT 再 UPDATE”的两阶段操作
真实审批流的复杂度不在触发器本身,而在状态跃迁规则、异常分支(驳回重提、跳过某级、多人会签)和幂等性保障——这些全压给触发器,迟早失控。把触发器当信号灯,不是发动机。


















