跳到主要内容

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/{函数名} 端点。

实际的数据隔离完全依赖

  1. SECURITY INVOKER 函数:底层表的 GRANT + RLS 仍然约束调用者,可天然保证安全
  2. 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 是把双刃剑,建议遵循以下原则:

  1. 函数体内主动校验角色 / 用户,决定哪些角色可调

    -- 仅允许 service_role 调用的 DEFINER 函数
    CREATE OR REPLACE FUNCTION public.admin_only_action()
    RETURNS json
    LANGUAGE plpgsql
    SECURITY DEFINER
    AS $$
    BEGIN
    IF current_setting('request.jwt.claims', true)::json->>'role' <> 'service_role' THEN
    RAISE EXCEPTION 'Permission denied';
    END IF;
    -- 实际逻辑 ...
    RETURN json_build_object('ok', true);
    END;
    $$;
  2. 只暴露最小必要数据:返回脱敏后的摘要、计数、布尔值,避免直接吐出表行

  3. 明确 search_pathALTER 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 当前行为

Cloudbase PostgREST 当前不强制检查 GRANT EXECUTE 权限——所有角色(包括 anon)在网关层面都可以调用任何 /rpc/{函数名} 端点。

实际的数据隔离完全依赖:

  1. 函数内的 RLS 约束(INVOKER 模式下)
  2. 表级 GRANT(即使函数能调用,没有表权限也读不到数据)
  3. 函数体内主动的角色校验(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 tableINVOKER 函数中操作的表,调用者没有表级 GRANTGRANT 给对应角色;或改 DEFINER 并自检
RLS 拒绝INVOKER 函数读不到数据检查 RLS Policy;或对该函数改 DEFINER(注意做角色校验)
返回空数组 / 0RLS 自动过滤导致看不到数据这是预期行为,非错误

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 调用

下一步