存储过程不适合驱动多级审批状态流转,因其缺乏事件感知、易引发并发冲突、无法跨库调用且难以动态更新规则;它仅应承担校验、预处理和轻量聚合三类确定性任务。

不能用存储过程驱动多级审批状态流转——它不是干这个的。真正该由存储过程承担的,只有校验、预处理和轻量聚合;状态变更主逻辑必须收口到应用层或独立工作流服务中。
为什么存储过程不适合做审批状态机
存储过程缺乏事件感知能力,无法监听「某级审批完成」这类业务信号;强行在 UPDATE 后调用存储过程去更新下一级状态,会立刻撞上事务隔离和并发冲突:两个审批人同时点「通过」,可能都读到旧的 CurrentStage = 2,然后都试图设为 3,最终只有一条生效。
- MySQL/PostgreSQL 中,存储过程内无法发起跨库 HTTP 请求或发邮件,
SEND_NOTIFICATION()类函数直接报错 - SQL Server 虽支持
xp_cmdshell或 CLR 外部调用,但稳定性差、权限收紧严,生产环境基本禁用 - 审批规则常需动态加载(比如 HR 流程临时加 CFO 审核),硬编码在存储过程中等于每次改规则都要 DBA 发版
存储过程能且仅能做的三件事
把存储过程当成「数据库侧的守门员」,而不是「流程引擎」。它只在数据落库前/后做确定性检查和整理:
-
校验状态跃迁合法性:比如
OLD.status = 'pending' AND NEW.status = 'approved_by_mgr'才允许更新,否则SIGNAL SQLSTATE '45000' -
补全冗余字段:自动写入
updated_at、last_approver_id,避免应用层漏传 -
聚合明细表生成主表状态:从
approval_steps表查出当前最高通过级,写回AuditFlow.CurrentStage,但不触发下一步动作
示例(PostgreSQL):
CREATE OR REPLACE FUNCTION update_auditflow_stage() RETURNS TRIGGER AS $$ DECLARE max_approved INT; BEGIN SELECT COALESCE(MAX(level), 0) INTO max_approved FROM approval_steps WHERE request_id = NEW.id AND status = 'approved'; <p>UPDATE AuditFlow SET CurrentStage = max_approved WHERE FlowNo = NEW.id;</p><p>RETURN NEW; END; $$ LANGUAGE plpgsql;
容易被忽略的性能陷阱
哪怕只做校验和聚合,存储过程也极易拖慢审批写入性能:
- 在触发器里查
users.role判断审批人权限?别查——把approver_role字段冗余进approval_steps表,否则一次更新锁住用户表 - 用子查询计算「是否全部通过」?改成
COUNT(*) FILTER (WHERE status = 'approved')配合索引,比NOT EXISTS (SELECT 1 FROM ... WHERE status != 'approved')快 3 倍以上 - 对
AuditFlowDetail做ORDER BY ApproverLevel DESC LIMIT 1查最新审批?必须在(request_id, ApproverLevel)上建联合索引,否则全表扫描
真要封装审批逻辑,用视图而非存储过程
业务系统查「哪些单据卡在二级审批」,不要让每个应用都写一遍 JOIN + CASE,而是建一个视图:
CREATE VIEW pending_approval_summary AS
SELECT
f.FlowNo,
f.BusType,
CASE
WHEN d1.ApproveResult = 'P' THEN 'pending_level1'
WHEN d2.ApproveResult = 'P' AND d1.ApproveResult = 'Y' THEN 'pending_level2'
ELSE 'other'
END AS current_pending_stage
FROM AuditFlow f
LEFT JOIN AuditFlowDetail d1 ON f.FlowNo = d1.FlowNo AND d1.ApproverLevel = 1
LEFT JOIN AuditFlowDetail d2 ON f.FlowNo = d2.FlowNo AND d2.ApproverLevel = 2;这样应用层只需 SELECT * FROM pending_approval_summary WHERE current_pending_stage = 'pending_level2',既免去重复逻辑,又不引入存储过程的事务耦合风险。
最麻烦的从来不是写 SQL,而是让不同团队对「审批完成」达成一致定义——有人认为最后一级点通过才算,有人认为任意一级拒绝就终止。这种语义分歧,没法靠存储过程解决,得靠流程配置中心统一管理规则元数据。


















