华为云开发者联盟公众号文章

如何使用RDS Supabase后端BaaS能力:数据库函数(Database Function)

欢迎体验~

华为云开发者联盟

公众号 · 4 篇文章


上一篇介绍了 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 $$DECLARE  min_price numeric;BEGIN  SELECT min(price) INTO min_price  FROM product_pricings  WHERE 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.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 bigintLANGUAGE plpgsqlSECURITY DEFINERSET search_path = publicAS $$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. 确认后保存,函数即创建。

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 $$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 triggerLANGUAGE plpgsqlAS $$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,仅运维可见。

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

图片

免费体验

图片

把这类逻辑封装成数据库函数:一次定义,在数据库内执行,前端以 .rpc() 直接调用,补齐单表读写之外的能力。

免费公测 Region :华北-北京四、华东-上海一、华南-广州-友好用户环境、华南-广州、西南-贵阳一、中东-利雅得、中国-香港  已开放局点。其他局点暂处邀测阶段,可提交申请。


图片

欢迎关注、点赞、分享、留言

发表更多观点

一起交流,共同进步!

图片图片图片


图片

图片戳“阅读原文”了解云数据库RDS Supabase用户指南






前往微信阅读全文

内容来自公众号,可前往微信查看原文。

查看作者的更多文章 →订阅更新、RSS 与阅读指南 →