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

第 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.4 直连路径运行,同时把适用范围限制为 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.4 是本次实际验证版本, 不是暗示 14–17 会自动通过所有环境差异。

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

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


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

最后更新于