PostgREST 事务模型全解析:访问模式、隔离级别与事务级设置实战指南
【免费下载链接】postgrestREST API for any Postgres database项目地址: https://gitcode.com/GitHub_Trending/po/postgrest
本文以 PostgREST 官方参考文档 transactions.rst 为骨架,结合仓库源码中 MainTx.hs、PreQuery.hs 与 Config.hs 的实现细节,系统讲解 PostgREST 每个 HTTP 请求背后的事务生命周期:访问模式(Access Mode)如何强制执行 HTTP 语义、隔离级别如何按角色或函数定制、事务级设置(Transaction-Scoped Settings)如何让你在数据库中读取请求信息与改写 HTTP 响应,以及db-pre-request、db-tx-end等配置的实战用法。读完本文,你将能利用 GUC 在数据库函数中读取请求头、Cookie、JWT 声明,动态注入响应头与状态码,并为测试场景安全地控制事务回滚。
从一次请求看事务生命周期
在 用户角色模拟(user impersonation)完成之后,每一个对 API 资源的请求都会运行在一个数据库事务内。PostgREST 官方文档给出的事务序列如下:
START TRANSACTION; -- <Access Mode> <Isolation Level> -- <Transaction-scoped settings> -- <Main Query> END; -- <Transaction End>这一序列在源码中可以逐段对应。核心事务执行器位于 MainTx.hs:mainTx通过SQL.transactionNoRetry isoLvl txMode开启事务,其中隔离级别由planIsoLvl计算、访问模式由planTxMode计算;随后依次执行:
- 事务级设置:
SQL.statement mempty $ SQL.dynamicallyParameterized mqTxVars ...,即由 PreQuery.hs 的txVarQuery生成的SELECT set_config(...)语句; - pre-request 函数(若配置了
db-pre-request); - 主查询
mqMain; - 事务结束:默认 COMMIT,或根据
Prefer: tx=rollback与db-tx-end配置回滚。
Access Mode:用只读事务强制 HTTP 语义
访问模式决定事务能否修改数据库,只有两个值:READ ONLY和READ WRITE。PostgREST 利用"在 READ ONLY 事务中无法修改数据库"这一事实,来强制执行 GET 与 HEAD 请求的 HTTP 语义。
官方文档给出了一个直观的验证示例:创建一个会修改序列的视图——
CREATE SEQUENCE callcounter_count START 1; CREATE VIEW callcounter AS SELECT nextval('callcounter_count');对callcounter发起 GET 请求,会因nextval()在只读事务中被禁止而报错:
curl "http://localhost:3000/callcounter"HTTP/1.1 405 Method Not Allowed {"code":"25006","details":null,"hint":null,"message":"cannot execute nextval() in a read-only transaction"}错误码25006(read_only_sql_transaction)在 Error.hs 中被映射为 HTTP 405,正是访问模式被用于强制 HTTP 语义的源码级证据。
表与视图的访问模式
对 表与视图,访问模式完全由 HTTP 方法决定:
| HTTP Method | Access Mode |
|---|---|
| GET, HEAD | READ ONLY |
| POST, PATCH, PUT, DELETE | READ WRITE |
函数的访问模式
对 函数,除了 HTTP 方法,还要看函数的易变性(volatility)声明:
| HTTP Method | VOLATILE | STABLE | IMMUTABLE |
|---|---|---|---|
| GET, HEAD | READ ONLY | READ ONLY | READ ONLY |
| POST | READ WRITE | READ ONLY | READ ONLY |
两个重要的注意事项:
- volatility 只是一种"承诺":PostgreSQL 允许你把一个修改数据库的函数标记为
IMMUTABLE或STABLE而不会报错,但在 PostgREST 下,由于事务是 READ ONLY,该函数会在运行时失败。 - OPTIONS 请求 不会开启事务,因此与访问模式无关。
Isolation Level:默认 READ COMMITTED,可按角色或函数定制
每个事务默认使用 PostgreSQL 的默认隔离级别READ COMMITTED。除非你为被模拟的角色或某个函数修改了default_transaction_isolation。
按角色修改(例如让webuser的所有查询都使用可重复读):
ALTER ROLE webuser SET default_transaction_isolation TO 'repeatable read';按函数调用修改(例如某个函数调用时使用串行化):
CREATE OR REPLACE FUNCTION myfunc() RETURNS text as $$ SELECT 'hello'; $$ LANGUAGE SQL SET default_transaction_isolation TO 'serializable';从源码看,隔离级别的解析逻辑位于 MainTx.hs 的planIsoLvl:它先从configRoleIsoLvl(按角色存储的隔离级别表)中按当前角色查找,默认回退到SQL.ReadCommitted;如果计划是函数调用(CallReadPlan),则优先采用函数自身的隔离级别设置pdIsoLvl。值得注意的是,Config/Database.hs 在读取角色设置时会专门过滤default_transaction_isolation键,单独提取后用于构建configRoleIsoLvl,其余设置则作为普通角色设置应用。
Transaction-Scoped Settings:数据库与 HTTP 之间的桥梁
PostgREST 使用与事务生命周期绑定的设置(GUC),这些设置有两个用途:获取 HTTP 请求的信息,或修改 HTTP 响应。
- 读取请求设置:使用
request.前缀,通过current_setting获取:
-- request settings use the request. prefix. SELECT current_setting('request.<setting>', true);- 写入响应设置:使用
response.前缀,通过set_config设置:
-- response settings use the response. prefix. SELECT set_config('response.<setting>', 'value1', true);这些set_config调用正是 PreQuery.hs 中txVarQuery生成的语句,其底层实现位于 SqlFragment.hs:setConfigWithConstantName生成set_config('key', value, true),而请求头、Cookie 则通过setConfigWithConstantNameJSON以 JSON 数组形式写入。
请求头、Cookie 与 JWT 声明
PostgREST 将请求头、Cookie 和 JWT 声明以 JSON 形式存储,可这样读取:
-- 获取请求中发送的所有请求头 SELECT current_setting('request.headers', true)::json; -- 获取单个请求头,可使用 JSON 箭头运算符 SELECT current_setting('request.headers', true)::json->>'user-agent'; -- 获取某个 Cookie 中 sessionId 的值 SELECT current_setting('request.cookies', true)::json->>'sessionId'; -- 获取 JWT 中 email 声明的值 SELECT current_setting('request.jwt.claims', true)::json->>'email';需要注意的关键行为:
- 请求头名称会被小写化:例如请求发送
User-Agent: x,只能通过current_setting('request.headers', true)::json->>'user-agent'获取。 request.jwt.claims中的role默认为db-anon-role配置的值。- 设置不会在事务提交后变为 NULL,而是被设置为空字符串
''。这是 PostgreSQL 的预期行为(详见社区讨论)。要规避这种不一致,可以创建包装函数:
CREATE FUNCTION my_current_setting(text) RETURNS text LANGUAGE SQL AS $$ SELECT nullif(current_setting($1, true), ''); $$;从源码看,这些 GUC 的设置顺序(PreQuery.hs)依次为:search_path、角色设置(roleSettingsSql)、role、JWT 声明(claimsSql,且会把role插入到 claims 中)、request.method、request.path、request.headers、request.cookies、时区(timezoneSql,由Prefer: timezone控制)、函数设置与db-app-settings中的应用设置。
请求路径与方法
路径和方法以text存储:
SELECT current_setting('request.path', true); SELECT current_setting('request.method', true);对应源码中methodSql与pathSql分别写入request.method和request.path。
请求角色与搜索路径
由于用户角色模拟,PostgREST 会设置标准的role,有多种读取方式:
SELECT current_role; SELECT current_user; SELECT current_setting('role', true);此外,PostgREST 还会基于db-schemas和db-extra-search-path设置search_path。源码中searchPathSql将当前 schema(iSchema)与configDbExtraSearchPath拼接后写入search_path(PreQuery.hs)。
响应头:动态注入缓存、Set-Cookie 等
可以设置response.headers来为 HTTP 响应添加请求头。例如为响应添加两天的缓存头:
-- tell client to cache response for two days SELECT set_config('response.headers', '[{"Cache-Control": "public"}, {"Cache-Control": "max-age=259200"}]', true);HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 Cache-Control: no-cache, no-store, must-revalidate关键细节:response.headers必须设置为单键对象的数组,而不是多键对象。因为Cache-Control、Set-Cookie这类请求头需要重复出现才能设置多个值,而 JSON 对象不允许重复键。这也是为什么示例中是[{"Cache-Control": "public"}, {"Cache-Control": "max-age=259200"}]而不是{"Cache-Control": "public", "Cache-Control": "max-age=259200"}。
另外需要注意:PostgREST 自带的Content-Type、Location等请求头也可以被这种方式覆盖。但无论Content-Type被覆盖成什么,响应内容仍会被转换为 JSON,除非使用 自定义媒体类型(custom media)。
这些响应头在事务执行结束后,通过rsGucHeaders字段(MainTx.hs)从结果集中解码并应用到 HTTP 响应。
响应状态码:自定义 HTTP 状态
可以设置response.status来覆盖 PostgREST 默认提供的状态码。例如下面的函数会把默认的 200 替换成 418:
create or replace function teapot() returns json as $$ begin perform set_config('response.status', '418', true); return json_build_object('message', 'The requested entity body is short and stout.', 'hint', 'Tip it over and pour it out.'); end; $$ language plpgsql;curl "http://localhost:3000/rpc/teapot" -iHTTP/1.1 418 I'm a teapot { "message" : "The requested entity body is short and stout.", "hint" : "Tip it over and pour it out." }如果状态码是标准的,PostgREST 会补全状态消息(本例中的I'm a teapot)。源码中该状态通过rsGucStatus :: Maybe Text(MainTx.hs)从数据库结果中携带回响应层。
模拟角色的设置(Impersonated Role Settings)
PostgreSQL 会应用连接角色(authenticator)的设置;此外,PostgREST 还会把被模拟角色的设置作为事务级设置应用,从而实现更细粒度的角色控制。
例如用statement_timeout限制语句执行时间(默认禁用):
ALTER ROLE authenticator SET statement_timeout TO '10s'; ALTER ROLE anonymous SET statement_timeout TO '1s';以上设置的效果:所有用户获得 10 秒的全局语句超时,匿名用户获得 1 秒的超时。源码中对应roleSettingsSql = setConfigWithDynamicName <$> HM.toList (fromMaybe mempty $ HM.lookup authRole configRoleSettings)(PreQuery.hs),即按当前模拟角色从其设置表中取出对应项,以动态 GUC 名写入事务。
需要特权的设置(Settings with privileged context):上下文需要特权的设置默认不会被应用,以免产生权限错误。从 PostgreSQL 15 开始,可以为这些设置授予权限:
GRANT SET ON PARAMETER <setting> TO <authenticator>;对应的源码逻辑见 PreQuery.hs 中的注释:为保证GRANT SET ON PARAMETER <superuser_setting> TO authenticator生效,角色设置必须在模拟角色之前设置,否则该 GRANT 就必须授予被模拟角色(见 PostgREST issue #3045 的相关讨论)。
提升的函数设置(Hoisted Function Settings)
PostgREST 可以把函数的设置"提升"为事务级设置,从而使函数设置覆盖模拟角色与连接角色的设置。
CREATE OR REPLACE FUNCTION myfunc() RETURNS void as $$ SELECT pg_sleep(3); -- simulating some long-running process $$ LANGUAGE SQL SET statement_timeout TO '4s';当调用上述函数时,语句超时会是 4 秒。只有db-hoisted-tx-settings中列出的设置才会被提升,其默认白名单在 Config.hs 中定义:
defaultHoistedAllowList = ["statement_timeout","plan_filter.statement_cost_limit","default_transaction_isolation"]注意:这个"提升"(hoist)机制与 Plan.hs 中用于查询计划的HoistedAgg(聚合字段提升)是完全不同的概念,后者仅与 SQL 查询构造相关。函数设置的提升对应 PreQuery.hs 中的funcSettingsSql,只有当计划是函数调用(CallReadPlan)时才生效。
Main Query:全部走预处理语句
主查询由请求表、视图或函数生成。所有生成的查询都使用预处理语句(受db-prepared-statements配置控制)。在 MainTx.hs 中,主查询通过SQL.dynamicallyParameterized mqMain ... configDbPreparedStatements执行,其结果被解码为ResultSet,其中包含表总数、查询总数、Location头、响应体、GUC 响应头与状态码等字段。
Transaction End:默认提交,可配置回滚
如果事务没有失败,它总是以 COMMIT 结束。除非将db-tx-end配置为无论如何都 ROLLBACK,或在特定条件下通过Prefer: tx=rollback回滚。这在测试场景中非常有用。
db-tx-end的四种取值及含义见 Config.hs 中的配置注释:
| 取值 | 行为 |
|---|---|
commit(默认) | 事务总是提交,不可被覆盖 |
commit-allow-override | 事务提交,但可通过Prefer: tx=rollback头覆盖 |
rollback | 事务总是回滚,不可被覆盖 |
rollback-allow-override | 事务回滚,但可通过Prefer: tx=commit头覆盖 |
配置示例(postgrest.conf):
# 事务总是提交(默认) # db-tx-end = "commit" # 事务回滚但允许用 Prefer 头覆盖(适合测试) # db-tx-end = "rollback-allow-override"源码解析逻辑位于 Config.hs 的parseTxEnd,任何其他取值都会报错 "Invalid transaction termination. Check your configuration."。运行时行为在 MainTx.hs 的optionalRollback中实现:当Prefer: tx=rollback或(配置了全部回滚且未请求 commit)时,先执行SET CONSTRAINTS ALL IMMEDIATE,再通过SQL.condemn强制事务回滚。PreferTransaction的两种取值(Commit/Rollback)在 Preferences.hs 中定义。
Aborting Transactions:失败即回滚
任何数据库失败(如约束冲突)都会导致事务回滚。也可以在函数内部RAISE一个错误来触发回滚。
Pre-Request:主查询前的拦截钩子
Pre-request 是一个在事务级设置设置完成之后、主查询执行之前运行的函数,通过db-pre-request配置启用:
# postgrest.conf # db-pre-request = "stored_proc_name"它提供了修改设置或抛出异常来阻止请求完成的机会。源码中,PreQuery.hs 的preReqQuery生成select <func>()语句,在 MainTx.hs 中通过whenJust mqPreReq于主查询之前执行。
实战:通过 pre-request 设置请求头
官方文档示例——为所有来自 IE 6/7 浏览器的请求添加缓存头:
create or replace function custom_headers() returns void as $$ declare user_agent text := current_setting('request.headers', true)::json->>'user-agent'; begin if user_agent similar to '%MSIE (6.0|7.0)%' then perform set_config('response.headers', '[{"Cache-Control": "no-cache, no-store, must-revalidate"}]', false); end if; end; $$ language plpgsql; -- set this function on postgrest.conf -- db-pre-request = custom_headers注意这里set_config的第三个参数传的是false(会话级)而不是true(事务级)。然后对表或视图发起 GET 请求,即可看到注入的缓存头:
curl "http://localhost:3000/people" -i \ -H "User-Agent: Mozilla/4.01 (compatible; MSIE 6.0; Windows NT 5.1)"测试事务行为的推荐实践
结合db-tx-end与Prefer: tx=rollback,可以在不产生数据持久化影响的前提下测试 API 行为。官方在测试中大量使用这一机制——例如仓库中的 RollbackSpec.hs 就是专门验证tx=rollback场景的测试用例。把db-tx-end设为rollback-allow-override,配合Prefer: tx=commit,可以在默认回滚的测试环境中对个别需要持久化的场景放行;反之,commit-allow-override适合默认提交的生产环境,仅在特定请求上通过Prefer: tx=rollback验证事务行为。
小结
PostgREST 把数据库事务与 HTTP 请求深度绑定:访问模式将 GET/HEAD 强制为只读以维护 HTTP 语义,隔离级别可按角色与函数灵活定制,事务级 GUC 成为数据库感知 HTTP 请求、改写 HTTP 响应的桥梁,而 pre-request 与db-tx-end则为请求前拦截与测试提供了强大的控制力。理解这一模型,是编写安全、高效、可测试的 PostgREST 应用(尤其是复杂数据库函数与响应定制场景)的关键前提。相关配置项完整清单可查阅 postgrest.cabal 与 Config.hs,其中包含db-pre-request、db-tx-end、db-hoisted-tx-settings、db-prepared-statements、db-schemas、db-extra-search-path、db-anon-role等配置的默认值与解析逻辑。
【免费下载链接】postgrestREST API for any Postgres database项目地址: https://gitcode.com/GitHub_Trending/po/postgrest
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考