PostgREST 函数即 RPC:用 /rpc 端点把 PostgreSQL 函数暴露为 REST API 完整指南
【免费下载链接】postgrestREST API for any Postgres database项目地址: https://gitcode.com/GitHub_Trending/po/postgrest
导读
PostgREST 将 PostgreSQL 数据库函数直接映射为 HTTP 端点:只要函数位于暴露的 schema(默认public)中、且当前激活的数据库角色拥有执行权限,就可以通过/rpc/<function_name>统一前缀调用,支持POST传 JSON 参数、GET传查询字符串参数,还能对返回表类型的函数继续施加筛选、排序、分页与资源嵌套。本文以官方参考文档 docs/references/api/functions.rst 为主线,结合仓库内 CallPlan.hs、QueryBuilder.hs、Routine.hs 等源码,系统讲解 RPC 调用的全部形态:参数传递、JSON/XML/二进制原始体、数组与 variadic 参数、表值函数筛选与内联、标量返回值、未类型化函数与重载函数的边界条件。读完你将能够把任意复杂度的数据库逻辑(读、写、报错乃至 DDL)封装成可复用的 REST 资源。
为什么是/rpc前缀
PostgreSQL 允许表或视图与函数同名,例如同时存在CREATE TABLE users(...)和CREATE FUNCTION users(...)。如果函数直接挂在根路径,PostgREST 就无法区分这次请求是命中表资源还是函数资源,产生路由冲突。为此 PostgREST 把所有函数调用统一收敛到/rpc前缀之下,从根本上消除了歧义。
函数能做的事情与 PostgreSQL 本身的能力一致,包括:
- 读取数据(
SELECT); - 修改数据(
INSERT/UPDATE/DELETE,见后文update_data示例); - 抛出自定义错误(配合
RAISE,错误会按 errors.rst 的规则映射为 HTTP 错误响应); - 甚至执行 DDL 操作。
需要注意一个明确的边界:存储过程(Stored Procedure,即CREATE PROCEDURE)不受支持,请使用CREATE FUNCTION。该限制在文档中以 warning 形式声明,仓库中 schema cache 也只查询函数(见 SchemaCache.hs 的decodeFuncs实现)。
从源码结构看,请求在 ApiRequest.hs 中把["rpc", pName]解析为ResourceRoutine pName,随后在 Plan.hs 的callPlan中决定以"键参数(KeyParams)"还是"单位置参数(OnePosParam)"方式生成调用计划,最终由 QueryBuilder.hs 的callPlanToQuery拼出实际的 SQL 调用。
重要:创建或修改函数后必须刷新 PostgREST 的 schema cache(参见 schema_reloading 相关说明),否则新函数不可见、签名变更不会生效。
用 POST 调用函数:JSON 对象即参数表
向/rpc/<function>发送POST请求时,请求体是一个 JSON 对象,对象的每个键值对会映射为函数的一个参数(参数名即键名)。
先在数据库中创建如下函数:
CREATE FUNCTION add_them(a integer, b integer) RETURNS integer AS $$ SELECT a + b; $$ LANGUAGE SQL IMMUTABLE;客户端调用:
curl "http://localhost:3000/rpc/add_them" \ -X POST -H "Content-Type: application/json" \ -d '{ "a": 1, "b": 2 }'响应:
3几点事实需要明确:
- 标识符大小写:PostgreSQL 默认把所有未加引号的标识符转换为小写。如果函数名或参数名带大写字母,必须用双引号定义,例如
CREATE FUNCTION "someFunc"("someParam" text) ...,调用时也需按原大小写传参。 - POST 语义不限于"读":
POST可以调用任何函数,包括带副作用的函数。而GET仅对不修改数据库的函数开放,判断依据是函数的 volatility(IMMUTABLE/STABLE允许 GET,VOLATILE不允许),详见 access_mode 相关配置。
用 GET 调用函数:查询字符串即参数表
若函数不修改数据库(volatility 为STABLE或IMMUTABLE),它也会在GET方法下运行:
curl "http://localhost:3000/rpc/add_them?a=1&b=2"GET场景下参数名与查询参数名一一对应:?a=1&b=2等价于 POST 的{ "a": 1, "b": 2 }。
带默认值的参数可以省略。例如:
CREATE FUNCTION greet_user(username TEXT DEFAULT 'guest') RETURNS TEXT AS $$ SELECT 'Hello ' || username || '!'; $$ LANGUAGE SQL IMMUTABLE;curl -i "http://localhost:3000/rpc/greet_user"响应:
HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 "Hello guest!"需要注意的是,省略参数仅对"定义了默认值"的参数成立。从 QueryBuilder.hs 的注释与实现看,未提供且无默认值的参数目前会被编译为NULL传入,因此请确保你的函数对缺省参数有 DEFAULT 声明,或者逻辑上容忍NULL。
传 JSON 数组给函数(json / jsonb 参数)
如果需要一次性向函数传递多条数据(对象数组),可以定义一个参数类型为json或jsonb的函数,客户端把整个数组嵌在对应参数名之下。这样可以在一次curl请求内完成原本需要多次请求的批量操作。
CREATE FUNCTION update_data(p_json jsonb) RETURNS void AS $$ DECLARE json_item json; BEGIN FOR json_item IN SELECT jsonb_array_elements(p_json) LOOP UPDATE data_table SET data_text_column = (json_item->>'data_text')::text WHERE data_int_column = (json_item->>'data_int')::integer; END LOOP; END; $$ LANGUAGE SQL IMMUTABLE;注意上例中LANGUAGE SQL与PL/pgSQL风格的DECLARE/BEGIN/END混用并不正确,实际生产中请为含DECLARE块的过程体使用LANGUAGE plpgsql;文档此处以演示批量更新思路为主。对应的 POST 请求:
curl "http://localhost:3000/rpc/update_data" \ -X POST -H "Content-Type: application/json" \ -d '{ "p_json": [ { "data_text": "one", "data_int": "1" }, { "data_text": "two", "data_int": "2" } ] }'这个模式适合需要把多条业务记录在一次事务性调用中落库的场景,可以减少网络往返次数。
单个未命名 JSON 参数:请求体整体传给函数
如果想让整个 JSON 请求体作为唯一参数传给函数,可以定义一个有且仅有一个、且未命名的json/jsonb参数。此时必须在请求中携带Content-Type: application/json。
CREATE FUNCTION mult_them(json) RETURNS int AS $$ SELECT ($1->>'x')::int * ($1->>'y')::int $$ LANGUAGE SQL;curl "http://localhost:3000/rpc/mult_them" \ -X POST -H "Content-Type: application/json" \ -d '{ "x": 4, "y": 2 }'响应:
8此处$1即整个 JSON 请求体。从源码看,这种"单未命名参数"形态对应 CallPlan.hs 中的OnePosParam(位置参数),并由 QueryBuilder.hs 的singleParameter分支生成调用。
一个重要的重载回退规则:如果存在一组重载函数,其中某个重载恰好只有一个未命名的json/jsonb参数,那么当 POST 请求中的参数无法匹配到任何其他重载时,PostgREST 会回退调用这个"单 JSON 参数"版本。这让单 JSON 参数函数成为"万能兜底"入口。
单个未命名参数传原始数据:bytea / text / xml
如果函数的唯一参数未命名,且类型是bytea、text或xml,POST 请求体就会以原始字节流形式整体作为该参数传入,而不再要求 JSON 包装。此时需要设置对应的Content-Type:
| 参数类型 | 必须的 Content-Type | 典型用途 |
|---|---|---|
xml | text/xml | 直接提交 XML 文档 |
bytea | application/octet-stream | 上传二进制文件 |
text | text/plain | 提交纯文本 |
例如上传二进制文件到bytea字段:
CREATE TABLE files(blob bytea); CREATE FUNCTION upload_binary(bytea) RETURNS void AS $$ INSERT INTO files(blob) VALUES ($1); $$ LANGUAGE SQL;curl "http://localhost:3000/rpc/upload_binary" \ -X POST -H "Content-Type: application/octet-stream" \ --data-binary "@file_name.ext"响应:
HTTP/1.1 200 OK [ ... ]--data-binary "@file_name.ext"会把本地文件的原始字节读入请求体,配合函数内的INSERT完成文件入库。同类思路也支持把 XML 文档、纯文本整段送入函数处理。
数组参数:POST 传 JSON 数组,GET 传数组字面量
PostgREST 支持直接调用带数组参数的函数。先定义一个数组参数函数:
create function plus_one(arr int[]) returns int[] as $$ SELECT array_agg(n + 1) FROM unnest($1) AS n; $$ language sql;POST:参数值直接写 JSON 数组:
curl "http://localhost:3000/rpc/plus_one" \ -X POST -H "Content-Type: application/json" \ -d '{"arr": [1,2,3,4]}'响应:
[2,3,4,5]GET:使用 PostgreSQL 的数组字面量语法{1,2,3,4},注意花括号必须 URL 编码({→%7B,}→%7D):
curl "http://localhost:3000/rpc/plus_one?arr=%7B1,2,3,4%7D"PostgreSQL 10 之前的兼容性说明:在旧版本上,POST 载荷中传 PostgreSQL 原生数组需要先加引号再写数组字面量:
curl "http://localhost:3000/rpc/plus_one" \ -X POST -H "Content-Type: application/json" \ -d '{ "arr": "{1,2,3,4}" }'这类旧版本上官方建议优先改用 JSON 参数类型来接收数组,避免字面量解析带来的兼容问题。
变参(VARIADIC)函数
VARIADIC函数允许在调用端以"不定数量实参"的形式传值。
create function plus_one(variadic v int[]) returns int[] as $$ SELECT array_agg(n + 1) FROM unnest($1) AS n; $$ language sql;- POST + JSON:把数组作为
v参数传入:
curl "http://localhost:3000/rpc/plus_one" \ -X POST -H "Content-Type: application/json" \ -d '{"v": [1,2,3,4]}'响应:
[2,3,4,5]- GET:重复同一个参数名,PostgREST 会将其合并为数组:
curl "http://localhost:3000/rpc/plus_one?v=1&v=2&v=3&v=4"- POST + form-urlencoded:同样重复参数名:
curl "http://localhost:3000/rpc/plus_one" \ -X POST -H "Content-Type: application/x-www-form-urlencoded" \ -d 'v=1&v=2&v=3&v=4'源码层面,重复参数合并逻辑位于 CallPlan.hs:toRpcParams会先检测函数是否含 variadic 参数(pdHasVariadic),是则用mergeParams把同名参数累积成Variadic [v],否则直接转为Fixed v映射;最终在 QueryBuilder.hs 中为 variadic 参数拼出VARIADIC name := ...并以内建数组编码器编码。
表值函数(Table-Valued Functions)
返回表类型(RETURNS SETOF <table>或RETURNS TABLE(...))的函数,返回结果可以当作表资源使用,支持与表、视图完全一致的读过滤器:
- 水平过滤(
?col=eq.val/gt./like.等操作符); - 垂直过滤(
?select=col1,col2); - 计数(
Prefer: count=exact); - 分页(
Range头或limit/offset参数); - 排序(
order); - 资源嵌套:如果返回的表类型与其他表存在外键关系,还能用
select=...:related(*)做资源嵌入。
例如:
CREATE FUNCTION best_films_2017() RETURNS SETOF films ..带嵌入与字段选择的调用:
curl "http://localhost:3000/rpc/best_films_2017?select=title,director:directors(*)"带过滤、排序的调用:
curl "http://localhost:3000/rpc/best_films_2017?rating=gt.8&order=title.desc"从实现看,表值函数返回类型在 Routine.hs 中被建模为SetOf (Composite qi),funcTableName(Routine.hs)把复合类型还原为对应表名,从而让 PostgREST 复用整套表查询的 read plan 管线(ReadPlan.hs 中的select、where_、order、range_等字段)。
函数内联(Function Inlining)
当函数满足 PostgreSQL 的 SQL 函数内联规则(LANGUAGE SQL、STABLE、函数体是简单 SELECT 等条件,见 PostgreSQL Wiki 的 Inlining conditions for table functions)时,PostgREST 施加的筛选、排序和 limit 会被下推进函数体内部执行,而不是先物化函数结果再过滤——这是表值函数查询性能的关键。
例如:
create function getallprojects() returns setof projects language sql stable as $$ select * from projects; $$;用Accept: application/vnd.pgrst.plan头获取执行计划(explain_plan相关说明见 PlanSpec 测试):
curl "http://localhost:3000/rpc/getallprojects?id=eq.1" \ -H "Accept: application/vnd.pgrst.plan"执行计划:
Aggregate (cost=8.18..8.20 rows=1 width=112) -> Index Scan using projects_pkey on projects (cost=0.15..8.17 rows=1 width=40) Index Cond: (id = 1)计划里没有 "Function Scan" 节点,说明函数已被内联,WHERE id = 1直接下推到projects表的主键索引扫描——这正是 SQL 函数优于普通 PL/pgSQL 函数的重要理由:它让数据库优化器看到函数内部的真实表结构。
表值函数的水平过滤
表值函数支持对已选择与未选择列进行水平过滤。下面的调用在select中只返回id, client_id,却用未选择的name列做过滤:
curl "http://localhost:3000/rpc/getallprojects?select=id,client_id&name=like.OSX"[ { "id": 4, "client_id": 2 } ]实现上,Plan.hs 的getFilterFieldNames会从 read plan 中提取所有过滤列,QueryBuilder.hs 在拼 SELECT 列时把filterFields与returnings合并去重,确保过滤列即使不在 select 中也会被查询出来供 WHERE 使用。
标量函数:返回格式自动识别
PostgREST 会依据 schema cache 中记录的返回类型自动区分"标量函数"和"表值函数",并据此决定响应形态:
- 标量函数(
RETURNS int等):直接返回裸 JSON 值(数字、字符串、布尔);
curl "http://localhost:3000/rpc/add_them?a=1&b=2"3- 表值函数:返回 JSON 数组:
curl "http://localhost:3000/rpc/best_films_2017"[ { "title": "Okja", "rating": 7.4}, { "title": "Call me by your name", "rating": 8}, { "title": "Blade Runner 2049", "rating": 8.1} ]类型判定逻辑集中在 Routine.hs:funcReturnsScalar(Single (Scalar _))、funcReturnsSetOfScalar(SetOf (Scalar _))、funcReturnsSingleComposite、funcReturnsVoid分别处理单值、标量集合、单条复合记录、void等情形;QueryBuilder.hs 在拼 SQL 时若判定为标量则取pgrst_call.pgrst_scalar一列。如需手动控制返回格式(如强制二进制),参见 media_type_handlers.rst。
未类型化函数(record / SETOF record)
返回record或SETOF record的函数也可调用,适合快速原型验证:
create function projects_setof_record() returns setof record as $$ select * from projects; $$ language sql;curl "http://localhost:3000/rpc/projects_setof_record"[{"id":1,"name":"Windows 7","client_id":1}, {"id":2,"name":"Windows 10","client_id":1}, {"id":3,"name":"IOS","client_id":2}]但要注意边界:对record返回类型无法使用垂直过滤(select)与水平过滤(WHERE),因为 PostgREST 不知道记录里有哪些列。因此官方建议仅用于快速测试,生产函数应始终声明明确的返回类型(具体表类型或TABLE(...)结构),这样才能获得筛选、排序、分页与嵌入能力。
重载函数
PostgreSQL 允许同名函数按参数个数/类型重载,PostgREST 支持按"本次请求提供的参数"自动选择重载版本:
CREATE FUNCTION rental_duration(customer_id integer) .. CREATE FUNCTION rental_duration(customer_id integer, from_date date) ..只传必填参数:
curl "http://localhost:3000/rpc/rental_duration?customer_id=232"补充可选参数(命中第二版重载):
curl "http://localhost:3000/rpc/rental_duration?customer_id=232&from_date=2018-07-01"实现上,schema cache 用RoutineMap = HashMap QualifiedIdentifier [Routine]保存每个函数名的全部重载(Routine.hs),并按参数个数升序排序(Routine.hs 的Ord Routine实例),以支持"最少参数优先匹配"的查找策略。
一个必须避开的限制:参数名相同但类型不同的重载不受支持(例如func(a int)与func(a text)同时存在),因为参数名相同意味着无法通过请求键区分该选哪个版本,匹配会变得不明确。
源码视角:一次 RPC 调用的完整链路
把以上行为串联起来,一次/rpc请求在仓库中的处理路径大致是:
- 路由识别:
ApiRequest.hs将["rpc", pName]解析为ResourceRoutine pName(ApiRequest.hs)。 - 参数规整:
toRpcParams(CallPlan.hs)把查询参数/表单参数转换为HashMap Text RpcParamValue,variadic 参数在此合并;JSON 请求体则保持为JsonArgs (Maybe LBS.ByteString)。 - 生成调用计划:
callPlan(Plan.hs)依据pdParams判断是KeyParams(具名参数name := value)还是OnePosParam(唯一未命名参数,位置传参)。 - 拼装 SQL:
callPlanToQuery(QueryBuilder.hs)把参数按name := 'value'::type形式编码(variadic 前缀VARIADIC),标量/标量集合用子查询包一层pgrst_scalar,表值函数则作为FROM pgrst_call的表源继续接 read plan 的 WHERE/ORDER/LIMIT/SELECT。 - 执行与格式化:最终由 Statements.hs 的
mainCall组装语句,Query.hs 统一执行,响应按返回类型序列化为 JSON。
以上每一环都有对应的测试佐证,例如test/spec/Feature/Query/RpcSpec.hs覆盖了 RPC 的 limit/offset(RpcSpec.hs)、可限制过程结果(RpcSpec.hs)以及 bit/char 类型参数(RpcSpec.hs)等场景。
实战要点速查
| 场景 | 方法 | 关键要求 |
|---|---|---|
| 普通多参数调用 | POST JSON 对象 或 GET 查询串 | 参数名与函数参数一致 |
| 批量写多行 | POST,参数为json/jsonb,值传对象数组 | 参数名包裹整个数组 |
| 请求体整体作为参数 | POST,唯一未命名json/jsonb参数 | Content-Type: application/json;可作重载兜底 |
| 传二进制文件 | POST,唯一未命名bytea参数 | Content-Type: application/octet-stream |
| 传 XML / 纯文本 | POST,唯一未命名xml/text参数 | Content-Type: text/xml/text/plain |
| 数组参数 | POST JSON 数组;GET 用 URL 编码的数组字面量 | 花括号需编码为%7B%7D |
| 变参函数 | POST 数组 / GET 重复参数名 / form 重复参数名 | 参数需声明为VARIADIC |
| 表值函数 | 复用表的筛选、排序、分页、嵌入 | 返回类型必须是具体表类型 |
| 标量函数 | 响应为裸 JSON 值 | 返回类型为标量 |
| 未类型化函数 | 可调用但无过滤能力 | 仅限快速测试 |
| 重载函数 | 按提供的参数个数自动选版本 | 不允许"同名不同类型"重载 |
最后再提醒一遍:任何函数定义或签名变更后,请先刷新 PostgREST 的 schema cache 再验证;生产环境建议为所有函数声明严格的返回类型,把数据库逻辑全部收敛到/rpc这一条 REST 边界之内。
【免费下载链接】postgrestREST API for any Postgres database项目地址: https://gitcode.com/GitHub_Trending/po/postgrest
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考