RPC(数据库函数调用)
PostgREST 不仅自动暴露表/视图为 REST 接口,还支持通过 /rpc/{函数名} 端点调用 PostgreSQL 函数(Stored Function)。这让你可以把复杂业务逻辑下推到数据库层,前端通过 HTTP 一次调用即可完成多表事务、聚合计算、敏感操作等。
何时使用 RPC
REST 的 CRUD 适合简单读写,但以下场景更适合用 RPC:
| 场景 | 说明 | 示例 |
|---|---|---|
| 数据聚合 | 统计、排行榜、报表 | 「我的总订单金额」「热销 TOP 10」 |
| 多表事务 | 一次操作涉及多个表的原子写入 | 下单同时减库存、写流水 |
| 复杂校验 | 简单 RLS 表达不了的业务规则 | 「每天最多创建 3 条」「限时秒杀剩余数」 |
| 敏感操作 | 需要绕过 RLS 但要受限的特权动作 | 「重置密码」「发放优惠券」 |
| 服务端计算 | 客户端不应承载的运算 | 价格计算、加密 |
调用方式
PostgREST RPC 通过 POST 请求调用,参数作为 JSON Body:
POST https://<envId>.api.tcloudbasegateway.com/v1/rdb/rest/rpc/<函数名>
Authorization: Bearer <Token>
Content-Type: application/json
{ "参数1": "值1", "参数2": 123 }
返回值由函数定义决定(标量、JSON、行集等)。
Cloudbase PostgREST 当前不强制检查 GRANT EXECUTE 权限——所有角色(包括 anon)在网关层面都可以调用任何 /rpc/{函数名} 端点。
实际的数据隔离完全依赖:
SECURITY INVOKER函数:底层表的 GRANT + RLS 仍然约束调用者,可天然保证安全SECURITY DEFINER函数:必须在函数体内自行校验调用者角色(如current_setting('request.jwt.claims', true)::json->>'role'),否则等于把创建者的全部权限暴露给所有人
务必在每个 SECURITY DEFINER 函数中写显式的角色校验,否则 RPC 端点会成为绕过 RLS 的"后门"。下文所有 DEFINER 示例都会演示这一点。
创建 RPC 函数
简单示例:公开计算函数
CREATE OR REPLACE FUNCTION public.rpc_add_numbers(a int, b int)
RETURNS int
LANGUAGE sql
SECURITY INVOKER
AS $$
SELECT a + b;
$$;
调用:
curl -X POST 'https://<envId>.api.tcloudbasegateway.com/v1/rdb/rest/rpc/rpc_add_numbers' \
-H "Authorization: Bearer <Publishable Key>" \
-H "Content-Type: application/json" \
-d '{"a": 1, "b": 2}'
# 响应: 3
读取当前用户身份
CREATE OR REPLACE FUNCTION public.rpc_whoami()
RETURNS json
LANGUAGE sql
SECURITY INVOKER
AS $$
SELECT json_build_object(
'role', current_setting('request.jwt.claims', true)::json->>'role',
'sub', current_setting('request.jwt.claims', true)::json->>'sub',
'aud', current_setting('request.jwt.claims', true)::json->>'aud'
);
$$;
复杂业务:原子下单 + 库存扣减
CREATE OR REPLACE FUNCTION public.rpc_place_order(
p_product_id int,
p_quantity int
)
RETURNS json
LANGUAGE plpgsql
SECURITY INVOKER
AS $$
DECLARE
v_product public.products%ROWTYPE;
v_buyer_id text := current_setting('request.jwt.claims', true)::json->>'sub';
v_order_id int;
BEGIN
SELECT * INTO v_product FROM public.products WHERE id = p_product_id FOR UPDATE;
IF NOT FOUND OR v_product.stock < p_quantity THEN
RAISE EXCEPTION '商品不存在或库存不足';
END IF;
UPDATE public.products SET stock = stock - p_quantity WHERE id = p_product_id;
INSERT INTO public.orders (product_id, buyer_id, quantity, total_price)
VALUES (p_product_id, v_buyer_id, p_quantity, v_product.price * p_quantity)
RETURNING id INTO v_order_id;
RETURN json_build_object('order_id', v_order_id, 'status', 'pending');
END;
$$;
整个流程在一个数据库事务内完成,扣库存与建订单要么都成功要么都回滚,无需前端协调。
SECURITY INVOKER vs SECURITY DEFINER
| 模式 | 谁的身份执行 | RLS 是否生效 | 适用场景 |
|---|---|---|---|
SECURITY INVOKER(默认) | 调用者 | ✅ 受调用者的 RLS 约束 | 大多数场景:让数据访问遵循 RLS |
SECURITY DEFINER | 函数创建者 | ❌ 绕过 RLS | 受控的越权操作 |
INVOKER(默认,受 RLS 约束)
CREATE OR REPLACE FUNCTION public.rpc_get_my_todos()
RETURNS SETOF public.todos
LANGUAGE sql
SECURITY INVOKER
AS $$
SELECT * FROM public.todos; -- 不写 WHERE 也只返回自己的:RLS 自动过滤
$$;
调用时:用户 A 调只看到 A 的,用户 B 调只看到 B 的,匿名 anon 调返回空。
DEFINER(绕过 RLS,全局可见)
CREATE OR REPLACE FUNCTION public.rpc_get_all_todo_count()
RETURNS bigint
LANGUAGE sql
SECURITY DEFINER -- ⭐ 以创建者身份执行,绕过 RLS
AS $$
SELECT count(*) FROM public.todos;
$$;
任何角色(含匿名)调用都能拿到全表行数——但只能拿到这个数字,不返回具体行。
DEFINER 安全约束
SECURITY DEFINER 是把双刃剑,建议遵循以下原则:
-
函数体内主动校验角色 / 用户,决定哪些角色可调
-- 仅允许 service_role 调用的 DEFINER 函数CREATE OR REPLACE FUNCTION public.admin_only_action()RETURNS jsonLANGUAGE plpgsqlSECURITY DEFINERAS $$BEGINIF current_setting('request.jwt.claims', true)::json->>'role' <> 'service_role' THENRAISE EXCEPTION 'Permission denied';END IF;-- 实际逻辑 ...RETURN json_build_object('ok', true);END;$$; -
只暴露最小必要数据:返回脱敏后的摘要、计数、布尔值,避免直接吐出表行
-
明确
search_path:ALTER FUNCTION xxx SET search_path = public, pg_temp;防止被恶意 schema 劫持
权限控制
GRANT EXECUTE 与表级权限的关系
PostgreSQL 内置规则:
-- 默认所有角色都能 EXECUTE 函数(PUBLIC 含义)
-- 如需限制,先 REVOKE 再 GRANT:
REVOKE EXECUTE ON FUNCTION public.rpc_admin_only() FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_only() TO service_role;
Cloudbase PostgREST 当前不强制检查 GRANT EXECUTE 权限——所有角色(包括 anon)在网关层面都可以调用任何 /rpc/{函数名} 端点。
实际的数据隔离完全依赖:
- 函数内的 RLS 约束(INVOKER 模式下)
- 表级 GRANT(即使函数能调用,没有表权限也读不到数据)
- 函数体内主动的角色校验(DEFINER 模式必须自检)
不要把"客户端能不能调用某 RPC"作为安全防线;安全防线一定要落到表级 GRANT + RLS 或函数体内的显式校验上。
推荐范式
-- INVOKER 函数:完全依赖底层表的 GRANT + RLS
CREATE FUNCTION public.rpc_user_action()
RETURNS json LANGUAGE sql SECURITY INVOKER
AS $$ ... $$;
-- DEFINER 函数:函数内显式校验
CREATE FUNCTION public.rpc_privileged_action()
RETURNS json LANGUAGE plpgsql SECURITY DEFINER
SET search_path = public, pg_temp
AS $$
BEGIN
IF NOT (current_setting('request.jwt.claims', true)::json->>'role' = 'service_role') THEN
RAISE EXCEPTION 'forbidden';
END IF;
-- ... 业务逻辑
END;
$$;
调用错误对照
| 错误现象 | 原因 | 排查 |
|---|---|---|
404 Not Found | 函数不存在或网关 metadata 未刷新 | 确认函数已创建;切换 schema 后等待几秒再调 |
argument missing | 参数名拼错或类型不符 | 与 SQL 中的 argname 完全匹配;JSON 数字与 int / numeric 自动转换 |
permission denied for table | INVOKER 函数中操作的表,调用者没有表级 GRANT | GRANT 给对应角色;或改 DEFINER 并自检 |
| RLS 拒绝 | INVOKER 函数读不到数据 | 检查 RLS Policy;或对该函数改 DEFINER(注意做角色校验) |
| 返回空数组 / 0 | RLS 自动过滤导致看不到数据 | 这是预期行为,非错误 |
SDK 调用示例
import cloudbase from '@cloudbase/js-sdk';
const app = cloudbase.init({ env: '<envId>' });
const auth = app.auth;
await auth.signInAnonymously();
const db = app.rdb();
// 调用 RPC
const { data, error } = await db.rpc('rpc_place_order', {
p_product_id: 1,
p_quantity: 2,
});
// 仅获取条数(head + count)
const { count } = await db.rpc('search_articles', { keyword: '云开发' }, {
count: 'exact',
head: true,
});
// 对返回 SETOF 表数据的 RPC 应用过滤 / 排序 / 分页
const { data: articles } = await db
.rpc('search_articles', { keyword: '云开发' })
.eq('status', 'published')
.order('published_at', { ascending: false })
.limit(5);
完整 SDK 用法见 JS SDK - RPC 调用。
下一步
- 架构与权限模型 — 完整数据库能力
- RLS 权限模式库 — 各种 RLS 范式
- 实战教程:电商小程序 — 完整示例