上一篇介绍了 Database Data API——建表即自动生成增删改查接口,解决单表读写,但聚合统计、跨表事务、字段自动维护等逻辑已超出其能力边界。本篇推进到数据库函数(Database Function):把业务逻辑封装进数据库,前端通过 .rpc() 调用,由触发器自动响应数据变更。
上一篇中,小张基于 Data API 完成了产品目录的建表与基础读写。但新需求随之而来:按分类统计产品数与价格区间、上架新品同时写产品表与定价表、产品修改后自动刷新更新时间。这些多表关联、跨表事务、自动触发的逻辑,已超出单表 CRUD 的范围,若在应用层实现,又要反复对齐字段。RDS Supabase 的数据库函数把这些逻辑下沉到数据库内完成,前端一次 RPC 即可拿到结果,无需自建后端。
1
为什么需要数据库函数
建表、增删改查、行级安全已由 Data API 解决,但聚合统计、跨表事务、流程控制与自动触发这类逻辑,Data API 的单表接口难以覆盖。数据库函数(即存储过程)用 SQL 或 PL/pgSQL 把这类复杂逻辑封装成可复用过程,写入数据库,供应用调用——它不替代 CRUD,而是承接 CRUD 覆盖不到的部分。
2
核心概念速览
数据库函数
保存在PostgreSQL中、可被调用的程序,接收输入参数、在数据库内执行、返回结果,可调用PostgreSQL全部能力(JSONB、窗口函数、事务等)。
函数语言
返回类型
安全模式
函数按执行权限归属分两种安全模式,直接决定是否受RLS约束。
SECURITY DEFINER 默认授予 PUBLIC 执行权限,放在 public 模式的此类函数可被 anon、authenticated 调用,是公开的高权限入口,须收口(见 Part 3)。
调用方式
支持RPC与触发器两种调用方式。
返回trigger的函数专供触发器、不能经RPC;普通函数可经RPC、不能绑定为触发器。
3
创建函数:两种方式
创建函数有两种方式,可混用:
简单函数用图形界面,复杂逻辑、权限收口、触发器函数用SQL Editor。本章示例基于上一篇已创建的products与product_pricings两张表(建表DDL见该篇)。
方式一:用SQL Editor编写SQL创建(主推·完整可控)
基本语法
-- SQL——函数定义骨架CREATE OR REPLACE FUNCTION 函数名(参数名 类型, ...)RETURNS 返回类型LANGUAGE sql -- 或 plpgsqlAS $$-- 函数体$$;
• CREATE OR REPLACE 便于迭代;$$ 标记函数体;LANGUAGE 选 sql 或 plpgsql。
SQL函数:封装查询
按分类统计产品数量,用sql函数封装。
-- SQL——按分类统计产品数量CREATE OR REPLACE FUNCTION get_product_count_by_category(cat text)RETURNS bigintLANGUAGE sqlAS $$SELECT count(*) FROM products WHERE category = cat;$$;
PL/pgSQL函数:流程控制
查询某产品最低价,无定价时返回0而非空值,需条件处理,用plpgsql。
-- SQL——查询产品最低价,无定价返回0CREATE OR REPLACE FUNCTION get_min_price(pid bigint)RETURNS numericLANGUAGE plpgsqlAS $$DECLAREmin_price numeric;BEGINSELECT min(price) INTO min_priceFROM product_pricingsWHERE product_id = pid;RETURN COALESCE(min_price, 0); -- 无定价时返回0END;$$;
• 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 sqlAS $$SELECT p.id, p.name, p.category, pr.tier, pr.priceFROM products pLEFT 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 bigintLANGUAGE plpgsqlSECURITY DEFINERSET search_path = publicAS $$DECLAREnew_id bigint;BEGININSERT 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. 确认后保存,函数即创建。
4
调用函数: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()等链式方法用法与表查询一致。
5
触发器函数:数据变更自动执行
触发器函数在数据变更时由数据库自动执行。上一篇提到updated_at仅在插入时取默认值、修改时不会自动刷新——若需自动刷新,触发器是典型做法。
字段自动维护
产品修改时自动刷新updated_at,避免应用每次手动传值。先定义返回trigger的函数:
-- SQL——触发器函数:刷新updated_atCREATE OR REPLACE FUNCTION set_updated_at()RETURNS triggerLANGUAGE plpgsqlAS $$BEGINNEW.updated_at = now(); -- 将新行updated_at置为当前时间RETURN NEW; -- 返回新行,使其继续写入END;$$;
再将其绑定到目标表的UPDATE:
-- SQL——为products绑定更新触发器CREATE TRIGGER trg_products_updated_atBEFORE UPDATE ON products -- 更新前执行,改写updated_atFOR 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_atBEFORE UPDATE ON product_pricingsFOR EACH ROWEXECUTE 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 triggerLANGUAGE plpgsqlAS $$BEGINIF 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_auditAFTER UPDATE ON product_pricings -- 更新后记录,确保新值已落库FOR EACH ROWEXECUTE FUNCTION log_price_change();
💡要点:AFTER确保数据已写入再审计;IS DISTINCT FROM可安全比较含NULL的值;审计表可单独配RLS,仅运维可见。
6
管理与维护
修改与删除函数
修改用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 可视化创建触发器(选目标表、时机、事件、绑定函数),并可启用、禁用或删除。
7
综合案例:产品概览接口
运营需要一个"产品概览"接口:按分类汇总产品数、最低价与最高价。这属于多表关联聚合,单表 CRUD 无法一次返回,用数据库函数封装最合适。
-- SQL——产品概览:按分类聚合产品数与价格区间CREATE OR REPLACE FUNCTION get_category_summary()RETURNS TABLE (category text,product_count bigint,min_price numeric,max_price numeric)LANGUAGE sqlAS $$SELECT p.category,count(DISTINCT p.id),min(pr.price),max(pr.price)FROM products pLEFT JOIN product_pricings pr ON pr.product_id = p.idGROUP BY p.categoryORDER 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 即可获得聚合结果,无需自建后端接口、无需明细传至前端再统计。
免费体验
把这类逻辑封装成数据库函数:一次定义,在数据库内执行,前端以 .rpc() 直接调用,补齐单表读写之外的能力。
欢迎关注、点赞、分享、留言
发表更多观点
一起交流,共同进步!
戳“阅读原文”了解云数据库RDS Supabase用户指南