跳转到主要内容

13 言出法随:函数、触发器与存储过程

数据库端逻辑最危险的误解,是把“PostgreSQL 能做”当成“应该放进 PostgreSQL”。函数、触发器和过程都能执行复杂逻辑,但三者不是更高级的 应用框架。它们首先是不同的数据库对象,各有调用方式、事务语义、规划 承诺、权限边界和可观测性。

本章只追问一个工程问题:

一条规则由谁负责,才能在所有写入口下保持正确,同时仍然能够测试、 发布、观测和回退?

答案不是“全部放应用”或“全部放数据库”。更可靠的分层是:

单列与单行合法域
  └─ NOT NULL / CHECK / 类型

表间引用与可声明关系
  └─ UNIQUE / FOREIGN KEY / EXCLUDE

旧行到新行的数据库状态跃迁
  └─ BEFORE ROW trigger(确实无法声明时)

事务最终点的跨表断言
  └─ deferred constraint trigger(知道并发边界时)

应用可调用的窄数据库命令
  └─ SECURITY INVOKER / SECURITY DEFINER function

需要分批提交的数据库维护动作
  └─ top-level CALL + procedure

跨系统工作流、重试策略与调度
  └─ 应用、outbox、worker 与平台

越靠上越声明式、越容易由 PostgreSQL 自动维护;越靠下越需要显式协议。 触发器不是把跨系统工作流藏起来的捷径,过程也不是调度器。

本章完成后

你应当能够:

  • 先用约束、普通 SQL 和事务表达规则,再判断是否真的需要例程;
  • 区分 SQL function、PL/pgSQL function、trigger function 与 procedure;
  • 设计标量、复合、集合返回和多态函数,并控制重载歧义;
  • VOLATILESTABLEIMMUTABLE 当成给优化器的承诺;
  • 正确声明 STRICTPARALLEL SAFE/RESTRICTED/UNSAFECOSTROWS
  • 使用稳定 SQLSTATE、DETAILHINT 定义机器可消费的错误合同;
  • 理解 EXCEPTION 块为什么形成子事务,以及它不能替代正常控制流;
  • 区分行级、语句级、BEFOREAFTERINSTEAD OF 触发器;
  • 用 transition table 对批量变更做一次集合处理;
  • 解释 deferred constraint trigger 检查的是事务最终状态,而不是 任意并发历史;
  • 识别递归、触发顺序、每行放大与隐藏 I/O;
  • 准确说明 function 与 procedure 的调用和事务控制边界;
  • 让批处理可重入、可续跑、可限批,而不把过程误当作 scheduler;
  • 安全编写 SECURITY DEFINER:NOLOGIN owner、固定 search_path、 全限定对象名、撤销 PUBLIC EXECUTE、输入收窄与最小授权;
  • pg_procpg_trigger、ACL、SQLSTATE 和函数统计中取得证据;
  • 在 Pigsty L1 中交付声明、SQL 变更、测试证据、观察窗口和回退入口。

贯穿实验:订单状态护栏

本章不使用只展示语法的零散对象,而是维护一个完整的 shop_ch13 实验:

规则 实现 为什么
金额为正、状态属于有限集合 CHECK 单行、可声明、目录可见
created → paid/canceled/expired 等跃迁 BEFORE ROW trigger 必须比较 OLDNEW
paid 时捕获金额等于订单金额 deferred constraint trigger 两张表在提交点同时成立
应用取消订单、捕获支付 SECURITY DEFINER function 应用没有底表 DML,只调用窄命令
多行更新写审计 AFTER STATEMENT + transition tables 三行更新只产生一条 statement audit
过期五张陈旧订单 SECURITY INVOKER procedure 顶层 CALL2/2/1 三批提交
邮件、HTTP、消息消费、定时启动 不放触发器或过程 属于外部系统和平台

夹具刻意让不同机制叠在同一事务里:

pg36_app
  │ EXECUTE only
capture_payment(...)
  ├─ lock order
  ├─ insert captured payment
  └─ update order: created -> paid
       ├─ BEFORE ROW validates edge + version
       ├─ AFTER STATEMENT writes row history + statement audit
       └─ deferred constraint triggers validate final payment total
          COMMIT or SQLSTATE P3614

这条链路同时说明两个事实:

  1. SECURITY DEFINER 不是绕开约束;提升后的命令仍然经过触发器和提交点 验证;
  2. 触发器只能参与当前 PostgreSQL 事务,不能证明外部副作用已经完成。

实验入口由 ch13 实验合同 统一说明:

正式实验在 PostgreSQL 18.6 直连路径运行,同时把适用范围限制为 PostgreSQL 14–18。它没有经过 PgBouncer,因此不能声称 pooler 路径已验证。

快速运行

沿用前章的受控管理 service:

export PGSERVICEFILE=/path/to/pg_service.conf
export PGSERVICE=pg36-admin

PG36_EVIDENCE_DIR="$PWD/evidence/ch13" \
  ./static/labs/ch13/task.sh all

all 会:

  1. 验证 ch04-v1 模型与 ch05 业务 checksum;
  2. 精确重建 shop_ch13
  3. 采集 pg_procpg_trigger 和 ACL;
  4. 穷举七个状态的 49 个有序对,证明恰好六条合法边;
  5. pg36_app 调用成功命令;
  6. 验证七个失败 case、六类 SQLSTATE;
  7. 证明异常子事务、transition table 与函数计数;
  8. 证明显式事务里的过程以 2D000 失败;
  9. 顶层调用过程取得 2/2/1,重跑取得 0;
  10. 拒绝错误 token、错误 target 和活跃 worker 下的 reset;
  11. 精确复位,再完整重建和复验第二遍。

成功摘要为:

status=ok
business=orders:13/payments:1/history:10/audit:6
boundary=check+before-row+deferred-constraint+security-definer
failure=42501/P3613/P3614/P3616/P3618/2D000
transaction=exception-subtransaction+commit-time-check+procedure-batches
observability=transition-table+function-stats+sqlstate
release=1.1-proposal
release_candidate_checksum=32377d82a7ce958aa50b0077ebe99c47d27672223c3c77fd9f91072d3745de9d

计时和生成的 identity 值不是 golden。验收比较状态分布、权限矩阵、 SQLSTATE、批次关系和 canonical proposal checksum。

失败合同

SQLSTATE 含义 谁产生 预期结果
42501 应用直接写底表 PostgreSQL ACL 没有任何业务变化
P3613 非法状态边 BEFORE trigger 行、历史和审计一起回滚
P3614 支付最终状态不一致 deferred trigger 到提交点拒绝整个事务
P3616 乐观版本不匹配 command function 调用方重新读取后决定是否重试
P3618 支付命令前置条件不成立 command function 不插支付、不改订单
2D000 显式事务块内试图结束事务 procedure runtime 该显式事务失败

自定义 P36xx 只属于本书实验合同;真实项目必须建立自己的错误注册表, 避免不同模块复用同一码位。调用方匹配 SQLSTATE,而不是匹配可能被翻译、 改写或补充上下文的 message。

学习路径

13.1 先决定逻辑放在哪里

先建立决策算法。如果跳过这一节,后面的语法很容易变成“看到锤子,到处 找钉子”。

13.2 SQL 与 PL/pgSQL 函数

函数是查询表达式的一部分,因此必须同时理解类型系统、优化器承诺和 调用者事务。

13.3 触发器与约束触发器

触发器要从“自动执行”还原为“写语句执行计划中隐藏的一段同步代码”。

13.4 过程、任务与事务控制

过程最独特的能力是受限的事务控制,不是“函数的加强版”。

13.5 安全、测试与观测

例程一旦成为权限边界,就必须按 API 和安全敏感代码来发布,而不是当成 一段随手粘贴的 SQL。

13.6 实战:为订单状态建立数据库端护栏

最后把决策、对象、失败、证据、声明和回退压成一份可评审交付物。

版本与证据边界

本章使用 PostgreSQL 14–18 共有的核心能力;anycompatible 多态类型族从 14 开始,因此实验下限设为 14。PostgreSQL 18.6 是本次实际验证版本, 不是暗示 14–17 会自动通过所有环境差异。

权威语义以以下文档为准:

本章会明确区分“官方定义”“本章设计选择”和“本地实验观察”。只有第三类 结论能够由当前 evidence 目录证明。


上一章:一气呵成:从数据库契约到后端服务 · 返回上卷导读 · 下一章:博采众长:内核分支与扩展生态 · 查看全书目录 · 查看索引中心