如何使用RDS Supabase后端BaaS能力:数据库函数(Database Function)
上一篇介绍了 Database Data API——建表即自动生成增删改查接口,解决单表读写,但聚合统计、跨表事务、字段自动维护等逻辑已超出其能力边界。本篇推进到数据库函数(Database Function):把业务逻辑封装进数据库,前端通过 .rpc() 调用,由触发器自动响应数据变更。
上一篇中,小张基于 Data API 完成了产品目录的建表与基础读写。但新需求随之而来:按分类统计产品数与价格区间、上架新品同时写产品表与定价表、产品修改后自动刷新更新时间。这些多表关联、跨表事务、自动触发的逻辑,已超出单表 CRUD 的范围,若在应用层实现,又要反复对齐字段。RDS Supabase 的数据库函数把这些逻辑下沉到数据库内完成,前端一次 RPC 即可拿到结果,无需自建后端。
Part 01 — 为什么需要数据库函数
建表、增删改查、行级安全已由 Data API 解决,但聚合统计、跨表事务、流程控制与自动触发这类逻辑,Data API 的单表接口难以覆盖。数据库函数(即存储过程)用 SQL 或 PL/pgSQL 把这类复杂逻辑封装成可复用过程,写入数据库,供应用调用——它不替代 CRUD,而是承接 CRUD 覆盖不到的部分。
|
场景 |
选型 |
说明 |
|
单表读写 |
仍用CRUD |
Data API已自动生成,无需封装 |
|
多表关联、聚合统计 |
用函数 |
封装复杂SQL,免手写JOIN |
|
跨表事务(多表同增同改) |
用函数 |
一个事务完成,保证一致性 |
|
业务校验、流程控制 |
用函数 |
分支、循环、异常处理 |
|
数据变更自动处理 |
用触发器函数 |
自动刷新updated_at、写审计日志、级联校验 |
|
对外仅开放特定操作 |
用SECURITY DEFINER函数 |
不授底层表权限,仅开放受控函数入口 |
|
计算列/派生字段 |
用函数 |
计算下沉数据库 |
Part 02 — 核心概念速览
数据库函数
保存在PostgreSQL中、可被调用的程序,接收输入参数、在数据库内执行、返回结果,可调用PostgreSQL全部能力(JSONB、窗口函数、事务等)。
函数语言
|
语言 |
特点 |
适用 |
|
sql |
单条SQL即可,无流程控制,简单高效 |
纯查询、计算、关联封装 |
|
plpgsql |
支持变量、分支、循环、异常处理、事务控制 |
多步骤逻辑、条件判断、批量处理 |
返回类型
|
返回类型 |
说明 |
典型场景 |
|
void |
无返回值 |
仅执行写入/更新,如批量维护 |
|
标量(bigint、numeric、text等) |
返回单个值 |
计数、最值、计算结果 |
|
SETOF |
返回某表的若干行 |
返回符合条件的数据集合 |
|
TABLE(字段 类型, …) |
返回自定义结构的多行 |
多表关联、计算列的集合结果 |
|
trigger |
专供触发器调用 |
自动维护字段、审计、校验 |
安全模式
函数按执行权限归属分两种安全模式,直接决定是否受RLS约束。
|
模式 |
执行权限 |
是否受RLS约束 |
适用 |
|
SECURITY INVOKER(默认) |
调用者 |
受约束 |
只处理调用者本就有权访问的数据 |
|
SECURITY DEFINER |
函数属主(通常绕过RLS) |
绕过RLS |
需以高权限完成特定操作,且要收口权限 |
SECURITY DEFINER 默认授予 PUBLIC 执行权限,放在 public 模式的此类函数可被 anon、authenticated 调用,是公开的高权限入口,须收口(见 Part 03)。
调用方式
支持RPC与触发器两种调用方式。
|
方式 |
触发时机 |
调用方 |
|
RPC |
应用主动调用 |
前端/服务端经SDK的.rpc()传参获取结果 |
|
触发器 |
数据变更时自动执行 |
绑定到表的INSERT/UPDATE/DELETE,无需应用介入 |
返回trigger的函数专供触发器、不能经RPC;普通函数可经RPC、不能绑定为触发器。
Part 03 — 创建函数:两种方式
创建函数有两种方式,可混用:
|
维度 |
方式一:SQL Editor |
方式二:Database > Functions |
|
创建方式 |
手写CREATE FUNCTION语句执行 |
图形表单填写后保存 |
|
适合场景 |
任意复杂度,含多步骤PL/pgSQL、权限收口、触发器 |
结构清晰的简单函数,快速创建 |
|
语法完整度 |
支持PostgreSQL全部语法与选项 |
覆盖常用字段,复杂选项受限 |
|
修改迭代 |
改SQL重跑即可,可存Snippet复用 |
表单可编辑,复杂修改仍建议回SQL Editor |
|
权限收口(REVOKE/GRANT) |
可直接执行 |
不提供图形入口,仍需SQL |
|
触发器函数与绑定 |
可定义trigger函数并建触发器 |
建议用SQL Editor或Triggers界面 |
|
上手难度 |
需熟悉SQL |
可视化,上手快 |
简单函数用图形界面,复杂逻辑、权限收口、触发器函数用SQL Editor。本章示例基于上一篇已创建的products与product_pricings两张表(建表DDL见该篇)。
方式一:用SQL Editor编写SQL创建(主推·完整可控)
基本语法
-- SQL——函数定义骨架
CREATE OR REPLACE FUNCTION 函数名(参数名 类型, ...)
RETURNS 返回类型
LANGUAGE sql -- 或 plpgsql
AS $$
-- 函数体
$$;
CREATE OR REPLACE 便于迭代;$$ 标记函数体;LANGUAGE 选 sql 或 plpgsql。
SQL函数:封装查询
按分类统计产品数量,用sql函数封装。
-- SQL——按分类统计产品数量
CREATE OR REPLACE FUNCTION get_product_count_by_category(cat text)
RETURNS bigint
LANGUAGE sql
AS $$
SELECT count(*) FROM products WHERE category = cat;
$$;
PL/pgSQL函数:流程控制
查询某产品最低价,无定价时返回0而非空值,需条件处理,用plpgsql。
-- SQL——查询产品最低价,无定价返回0
CREATE OR REPLACE FUNCTION get_min_price(pid bigint)
RETURNS numeric
LANGUAGE plpgsql
AS $$
DECLARE
min_price numeric;
BEGIN
SELECT min(price) INTO min_price
FROM product_pricings
WHERE product_id = pid;
RETURN COALESCE(min_price, 0); -- 无定价时返回0
END;
$$;
DECLARE声明变量,INTO把查询结果写入变量。
参数名避免与列名同名(本例用pid而非product_id),否则WHERE product_id = product_id变成列与自身比较,产生歧义。
COALESCE将NULL替换为0,避免空值传到前端。
返回集合:SETOF与TABLE
一次取出产品及其定价(多表关联),前端无需手写JOIN,用TABLE(...)返回自定义结构。
-- SQL——产品及其定价联表查询
CREATE OR REPLACE FUNCTION get_products_with_pricing()
RETURNS TABLE (
id bigint,
name text,
category text,
tier text,
price numeric
)
LANGUAGE sql
AS $$
SELECT p.id, p.name, p.category, pr.tier, pr.price
FROM products p
LEFT JOIN product_pricings pr ON pr.product_id = p.id;
$$;
安全模式与权限收口(SECURITY DEFINER)
示例:上架新品须同时写产品表与定价表,要求一个事务内完成且不直接开放这两张表的写权限。用SECURITY DEFINER封装:函数以属主权限执行,调用方仅需EXECUTE权限即可完成受控操作。
-- SQL——上架新品(产品+定价一个事务完成,SECURITY DEFINER收口权限)
CREATE OR REPLACE FUNCTION create_product_with_price(
p_name text,
p_category text,
p_tier text,
p_price numeric
)
RETURNS bigint
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
new_id bigint;
BEGIN
INSERT INTO products (name, category) VALUES (p_name, p_category)
RETURNING id INTO new_id;
INSERT INTO product_pricings (product_id, tier, price) VALUES (new_id, p_tier, p_price);
RETURN new_id;
END;
$$;
-- 收口权限:移除PUBLIC默认执行权,仅认证用户可调用
REVOKE ALL ON FUNCTION create_product_with_price(text, text, text, numeric) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION create_product_with_price(text, text, text, numeric) TO authenticated;
SECURITY DEFINER的收口要点(Part 02安全模式提及,此处落实):
- SECURITY DEFINER + SET search_path = public:锁定模式搜索路径,防止同名对象被劫持。
- 两步INSERT在同一事务,要么全部成功、要么整体回滚,保证产品与定价一致。
- REVOKE ... FROM PUBLIC 收回默认执行权,否则anon也能调用。
- 仅向authenticated开放EXECUTE,调用方无需拥有两张表的INSERT权限即可上架。
- 函数体内可再用auth.uid()校验调用者身份。
创建或修改函数后须刷新schema缓存才对RPC可见(见Part 06),首次可能延迟数秒。
SQL Editor操作步骤
1. 登录Dashboard,左侧选SQL Editor。
2. 粘贴CREATE FUNCTION语句,点Run执行即创建。
3. 测试:运行 select get_product_count_by_category('计算'); 确认返回;可存为Snippet复用;若RPC未识别,执行Part 06刷新缓存。
方式二:用图形界面创建(Database > Functions)
Database > Functions 用图形表单创建,函数体在表单内SQL框编写,适合快速创建简单函数。
1. 登录RDS Supabase Dashboard,左侧选择Database > Functions。
2. 点击Create function(新建函数)按钮,打开创建表单。
3. 在表单填写:函数名、所属schema(默认public)、参数(名+类型,可增删)、返回类型(void/标量/SETOF/TABLE)、语言(sql/plpgsql)、是否SECURITY DEFINER、函数体。
4. 确认后保存,函数即创建。
Part 04 — 调用函数:RPC方式
已暴露模式(如public)下的函数即RPC接口,前端用Supabase SDK的rpc()调用。
调用权限同CRUD:未登录以anon身份执行(受RLS约束),登录后以authenticated执行。
无参调用
获取全部产品及其定价清单。无参调用第二参数可省略,返回值结构与RETURNS TABLE一一对应。
// JavaScript——无参调用,取产品定价清单
const { data, error } = await supabase.rpc('get_products_with_pricing')
if (error) { console.error(error); return }
// 返回示例:[{ id: 1, name: '云服务器 ECS', category: '计算', tier: '包月', price: 99.00 }, ...]
带参调用
查询id为1的产品的最低价。rpc()第二参数为对象,键名对应参数名,按命名参数绑定,无需关心位置。
// JavaScript——按命名参数传参
const { data, error } = await supabase.rpc('get_min_price', { pid: 1 })
if (error) { console.error(error); return }
// 返回示例:函数返回的标量,如0或99.00
结果过滤与分页
返回集合的函数结果支持与表相同的链式过滤、排序、分页。从产品定价清单中只取计算类、按名称排序的前10条:
// JavaScript——对返回集合链式过滤、排序、分页
const { data, error } = await supabase
.rpc('get_products_with_pricing')
.eq('category', '计算')
.order('name')
.limit(10)
eq()/.order()/.limit()等链式方法用法与表查询一致。
Part 05 — 触发器函数:数据变更自动执行
触发器函数在数据变更时由数据库自动执行。上一篇提到updated_at仅在插入时取默认值、修改时不会自动刷新——若需自动刷新,触发器是典型做法。
字段自动维护
产品修改时自动刷新updated_at,避免应用每次手动传值。先定义返回trigger的函数:
-- SQL——触发器函数:刷新updated_at
CREATE OR REPLACE FUNCTION set_updated_at()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
NEW.updated_at = now(); -- 将新行updated_at置为当前时间
RETURN NEW; -- 返回新行,使其继续写入
END;
$$;
再将其绑定到目标表的UPDATE:
-- SQL——为products绑定更新触发器
CREATE TRIGGER trg_products_updated_at
BEFORE UPDATE ON products -- 更新前执行,改写updated_at
FOR EACH ROW -- 每行触发
EXECUTE FUNCTION set_updated_at();
此后products的任何UPDATE都自动刷新updated_at。products已有该列可直接绑定;product_pricings无此列,需先新增该列再绑定同一触发器:
-- SQL——为product_pricings补列并绑定同一触发器
ALTER TABLE product_pricings ADD COLUMN updated_at TIMESTAMPTZ DEFAULT now();
CREATE TRIGGER trg_pricings_updated_at
BEFORE UPDATE ON product_pricings
FOR EACH ROW
EXECUTE FUNCTION set_updated_at();
注意:BEFORE返回NEW使修改生效、返回NULL则取消该行操作。
变更审计
记录价格变更前后的值便于追溯。先建审计表,再以触发器在更新时写入。
-- SQL——建审计表+触发器:记录价格变更
CREATE TABLE product_price_audit (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
product_id BIGINT NOT NULL,
old_price NUMERIC(10,2),
new_price NUMERIC(10,2),
changed_at TIMESTAMPTZ DEFAULT now()
);
CREATE OR REPLACE FUNCTION log_price_change()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF NEW.price IS DISTINCT FROM OLD.price THEN -- 仅价格变化才记录
INSERT INTO product_price_audit (product_id, old_price, new_price)
VALUES (NEW.product_id, OLD.price, NEW.price);
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_pricing_audit
AFTER UPDATE ON product_pricings -- 更新后记录,确保新值已落库
FOR EACH ROW
EXECUTE FUNCTION log_price_change();
要点:AFTER确保数据已写入再审计;IS DISTINCT FROM可安全比较含NULL的值;审计表可单独配RLS,仅运维可见。
Part 06 — 管理与维护
|
入口 |
主要用途 |
|
SQL Editor |
编写/执行/测试/迭代函数与触发器SQL |
|
Database > Functions |
图形创建函数、查看函数定义 |
|
Database > Triggers |
可视化创建、启用、禁用、删除触发器 |
修改与删除函数
修改用CREATE OR REPLACE FUNCTION覆写,后须刷新schema缓存。删除用DROP FUNCTION,须带参数类型区分同名重载:
-- SQL——删除函数(须带参数类型)
DROP FUNCTION get_min_price(bigint);
刷新schema缓存
须刷新PostgREST的schema缓存,函数才对RPC可见(首次可能延迟数秒):
-- SQL——刷新PostgREST schema缓存
NOTIFY pgrst, 'reload schema';
管理触发器
Database > Triggers 可视化创建触发器(选目标表、时机、事件、绑定函数),并可启用、禁用或删除。
Part 07 — 综合案例:产品概览接口
运营需要一个"产品概览"接口:按分类汇总产品数、最低价与最高价。这属于多表关联聚合,单表 CRUD 无法一次返回,用数据库函数封装最合适。
-- SQL——产品概览:按分类聚合产品数与价格区间
CREATE OR REPLACE FUNCTION get_category_summary()
RETURNS TABLE (
category text,
product_count bigint,
min_price numeric,
max_price numeric
)
LANGUAGE sql
AS $$
SELECT p.category,
count(DISTINCT p.id),
min(pr.price),
max(pr.price)
FROM products p
LEFT JOIN product_pricings pr ON pr.product_id = p.id
GROUP BY p.category
ORDER BY p.category;
$$;
刷新schema缓存后由SDK调用:
// JavaScript——调用产品概览接口
const { data, error } = await supabase.rpc('get_category_summary')
if (error) { console.error(error); return }
// 返回示例:
// [
// { category: '计算', product_count: 2, min_price: 99.00, max_price: 588.00 },
// { category: '存储', product_count: 1, min_price: 45.00, max_price: 45.00 }
// ]
本案例将"多表关联+聚合+排序"封装进数据库,前端一次 RPC 即可获得聚合结果,无需自建后端接口、无需明细传至前端再统计。
浙公网安备 33010602011771号