上一篇介绍了 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 即可获得聚合结果,无需自建后端接口、无需明细传至前端再统计。

 


原文地址: https://www.cveoy.top/t/topic/qHMT 著作权归作者所有。请勿转载和采集!

免费AI点我,无需注册和登录