上一篇介绍了 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操作步骤
-
登录Dashboard,左侧选SQL Editor。
-
粘贴CREATE FUNCTION语句,点Run执行即创建。
-
测试:运行 select get_product_count_by_category('计算'); 确认返回;可存为Snippet复用;若RPC未识别,执行Part 06刷新缓存。
方式二:用图形界面创建(Database > Functions)
Database > Functions 用图形表单创建,函数体在表单内SQL框编写,适合快速创建简单函数。
-
登录RDS Supabase Dashboard,左侧选择Database > Functions。
-
点击Create function(新建函数)按钮,打开创建表单。
-
在表单填写:函数名、所属schema(默认public)、参数(名+类型,可增删)、返回类型(void/标量/SETOF/TABLE)、语言(sql/plpgsql)、是否SECURITY DEFINER、函数体。
-
确认后保存,函数即创建。
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 即可获得聚合结果,无需自建后端接口、无需明细传至前端再统计。