必须用BEFORE INSERT/UPDATE行级触发器校验业务规则,因AFTER无法阻止写入、语句级无NEW字段;失败须RAISE EXCEPTION中断事务,末尾必须RETURN NEW/OLD,严禁查权限表或调外部API。

BEFORE INSERT/UPDATE 触发器里怎么安全校验业务规则
必须用 BEFORE 时机,且只在行级触发器中操作;AFTER 阶段无法阻止写入,FOR EACH STATEMENT 则拿不到 NEW 字段。校验失败要用 RAISE EXCEPTION 中断事务,不能靠 RETURN NULL(它只对 BEFORE ROW 有效,但语义模糊,易被误读)。
常见错误现象:INSERT INTO orders VALUES (1, 'pending', 100) 成功写入,但业务要求 status 必须是枚举值之一,触发器却没拦住。
- 触发器函数开头必须声明
RETURNS TRIGGER,结尾必须写RETURN NEW(INSERT/UPDATE)或RETURN OLD(DELETE) - 校验逻辑应封装为独立
FUNCTION(返回BOOLEAN或抛错),避免把复杂 SQL 塞进触发器主体 - 禁止在触发器里查权限表、调外部 API、写文件——这些会拖慢主事务,且事务回滚时副作用不可控
触发器里访问 NEW/OLD 字段报 record "new" has no field "xxx"
这是运行时错误,不是语法错:说明你写了 NEW.field_name,但当前触发事件是 DELETE,NEW 根本不存在。PostgreSQL 不做空值兜底,必须显式判断 TG_OP。
使用场景:同一触发器函数复用在 INSERT/UPDATE/DELETE 上,但字段可用性不同。
-
INSERT:只有NEW可用,OLD是NULL -
DELETE:只有OLD可用,NEW是NULL -
UPDATE:两者都可用,但未修改字段的NEW.x和OLD.x值相同 - 安全写法是用
IF TG_OP = 'UPDATE' THEN ... END IF;包裹对OLD的访问,别写COALESCE(NEW.status, OLD.status)这类无条件表达式
如何让触发器拿到真实业务用户而非数据库角色
current_user 返回的是登录角色(如 app_rw),不是业务系统的 user_id;硬编码或依赖 inet_client_addr() 在 pgbouncer 后失效。唯一稳的方式是应用层主动传参。
实操建议(PostgreSQL 14+):
- 应用连接建立后立即执行:
SET app.user_id = 'u123'; SET app.client_ip = '2001:db8::1'; - 触发器函数中用
current_setting('app.user_id', true)读取,第二个参数true表示允许返回NULL而不报错 - 禁止用
session_user或USER()——前者不可控,后者在 PL/pgSQL 中语法非法 - 校验逻辑里用该值比对行上
owner_id字段,例如:IF NEW.owner_id != current_setting('app.user_id', true) THEN RAISE EXCEPTION 'owner_id mismatch'; END IF;
为什么不能在 BEFORE 触发器里 SELECT 权限配置表
看似灵活,实则高危:每次 INSERT/UPDATE 都要查 user_role_mapping 这类配置表,会引入额外 I/O、锁竞争,尤其在高并发场景下可能锁死权限表,导致整个库写入阻塞。
性能影响明显:压测显示,单次触发器内带一次 SELECT 的 INSERT,TPS 下降 40% 以上;若配置表本身没索引或数据量大,延迟直接升到百毫秒级。
- 真正需要动态权限的场景,应改用应用层预检 + 行级策略(RLS),而非触发器内查表
- 触发器内的校验必须是轻量、确定、无副作用的,比如字段格式检查、状态机流转(
status IN ('draft', 'confirmed'))、数值范围(amount > 0) - 如果真要关联校验,把配置缓存到
JSONB字段或用pg_prepared_statements预编译查询,但依然不如纯内存判断快
最常被忽略的一点:触发器函数末尾漏写 RETURN NEW,会导致 INSERT/UPDATE 静默失败——数据没写进去,也不报错,只返回成功,排查时得翻 pg_stat_statements 看执行耗时是否异常归零。

















