数据库函数
Postgres 原生支持 SQL 函数。这些函数存储在您的数据库中,可以通过 API 调用 来使用。
快速演示
开始使用
Supabase 提供了多种创建数据库函数的方式。您可以使用仪表盘或直接通过 SQL 语句创建。我们提供了内置的 SQL 编辑器,您也可以 连接 到数据库后自行执行 SQL 查询。
- 进入 "SQL 编辑器" 版块
- 点击 "新建查询"
- 输入创建或替换数据库函数的 SQL 语句
- 点击 "运行" 或使用快捷键 cmd+enter (Windows 用户使用 ctrl+enter)
简单函数
让我们创建一个基本的数据库函数,返回字符串"hello world"。
123456create or replace function hello_world() -- 1returns text -- 2language sql -- 3as $$ -- 4 select 'hello world'; -- 5$$; --6
显示/隐藏详情
最基本的函数包含以下部分:
create or replace function hello_world()
: 函数声明,其中hello_world
是函数名。可以使用create
创建新函数,或使用replace
替换现有函数。也可以同时使用create or replace
来处理两种情况。returns text
: 函数返回的数据类型。如果不返回任何内容,可以使用returns void
。language sql
: 函数体内使用的语言。也可以是过程语言:plpgsql
、plpython
等。as $$
: 函数包装器。$$
符号内的所有内容都是函数体的一部分。select 'hello world';
: 简单的函数体。如果没有后续语句,函数体内最后的select
语句将被返回。$$;
: 函数包装器的结束符号。
创建函数后,我们有几种"执行"函数的方式 - 可以直接在数据库中使用SQL,或使用客户端库之一。
1select hello_world();
返回数据集
数据库函数也可以从表或视图中返回数据集。
例如,如果我们有一个包含星球大战数据的数据库:
行星
12345| id | name || --- | -------- || 1 | Tatooine || 2 | Alderaan || 3 | Kashyyyk |
人物
123456| id | name | planet_id || --- | ---------------- | --------- || 1 | Anakin Skywalker | 1 || 2 | Luke Skywalker | 1 || 3 | Princess Leia | 2 || 4 | Chewbacca | 3 |
我们可以创建一个返回所有行星的函数:
123456create or replace function get_planets()returns setof planetslanguage sqlas $$ select * from planets;$$;
由于这个函数返回的是表集合,我们还可以应用过滤器和选择器。例如,如果我们只想获取第一个行星:
123select *from get_planets()where id = 1;
参数传递
让我们创建一个函数来向 planets
表中插入新行星并返回新ID。注意这次我们使用的是 plpgsql
语言。
1234567891011121314create or replace function add_planet(name text)returns bigintlanguage plpgsqlas $$declare new_row bigint;begin insert into planets(name) values (add_planet.name) returning id into new_row; return new_row;end;$$;
同样地,您可以在数据库内部使用 select
查询执行此函数,也可以通过客户端库执行:
1select * from add_planet('Jakku');
建议
数据库函数 vs 边缘函数
对于数据密集型操作,请使用数据库函数。这些函数在数据库内部执行,可以通过REST和GraphQL API远程调用。
对于需要低延迟的用例,请使用边缘函数,它们全球分布式部署且可以用TypeScript编写。
安全性:definer
vs invoker
Postgres允许您指定函数是作为调用函数的用户(invoker
)执行,还是作为函数创建者(definer
)执行。例如:
123456789create function hello_world()returns textlanguage plpgsqlsecurity definer set search_path = ''as $$begin select 'hello world';end;$$;
最佳实践是使用security invoker
(这也是默认设置)。如果使用security definer
,则必须设置search_path
。如果使用空搜索路径(search_path = ''
),则必须在函数体中明确指定每个关系的模式(例如from public.table
)。这样可以限制潜在损害,特别是当您允许访问执行函数的用户本不应访问的模式时。
函数权限
默认情况下,数据库函数可以被任何角色执行。有两种主要方式来限制这一点:
-
逐个函数限制。专门撤销您想要保护的函数的执行权限。需要同时撤销
public
和您要限制的角色的执行权限:12revoke execute on function public.hello_world from public;revoke execute on function public.hello_world from anon; -
默认限制函数执行。当您希望特定角色可以执行某个函数时,专门授予访问权限。
要限制所有现有函数,需要同时撤销
public
和您想限制的角色的执行权限:12revoke execute on all functions in schema public from public;revoke execute on all functions in schema public from anon, authenticated;要限制所有新函数,需要修改
public
和您想限制的角色的默认权限:12alter default privileges in schema public revoke execute on functions from public;alter default privileges in schema public revoke execute on functions from anon, authenticated;然后您可以针对特定函数向特定角色重新授予权限:
1grant execute on function public.hello_world to authenticated;
调试函数
您可以添加日志来帮助调试函数,这对于复杂函数特别推荐。
适合记录日志的目标包括:
- (非敏感)变量的值
- 查询返回的结果
通用日志记录
要在仪表板的Postgres日志中创建自定义日志,可以使用raise
关键字。默认情况下,系统支持3种严重级别:
log
(日志)warning
(警告)exception
(错误级别)
123456789101112131415161718create function logging_example( log_message text, warning_message text, error_message text)returns voidlanguage plpgsqlas $$begin raise log '日志消息: %', log_message; raise warning '警告消息: %', warning_message; -- 立即终止函数并回滚事务 raise exception '错误消息: %', error_message;end;$$;select logging_example('日志信息', '警告信息', '错误信息');
错误处理
您可以使用 raise exception
关键字创建自定义错误。
常见的模式是在变量不满足条件时抛出错误:
123456789101112131415create or replace function error_if_null(some_val text)returns textlanguage plpgsqlas $$begin -- 如果 some_val 为 null 则报错 if some_val is null then raise exception 'some_val 不应为 NULL'; end if; -- 如果 some_val 不为 null 则返回它 return some_val;end;$$;select error_if_null(null);
值检查很常见,因此 Postgres 提供了一个简写方式:assert
关键字。它使用以下格式:
12-- 当条件为 false 时抛出错误assert <某个条件>, '错误信息';
下面是一个示例:
12345678910111213141516171819202122create function assert_example(name text)returns uuidlanguage plpgsqlas $$declare student_id uuid;begin -- 将用户ID保存到 user_id 变量中 select id into student_id from attendance_table where student = name; -- 如果 student_id 为 null 则抛出错误 assert student_id is not null, 'assert_example() 错误:未找到学生'; -- 否则返回用户ID return student_id;end;$$;select assert_example('Harry Potter');
错误信息也可以通过 exception
关键字捕获并修改:
12345678910111213create function error_example()returns voidlanguage plpgsqlas $$begin -- 失败:无法从不存在的数据表读取 select * from table_that_does_not_exist; exception when others then raise exception '函数 <function name> 中发生错误: %', sqlerrm;end;$$;
高级日志记录
对于更复杂的函数或疑难调试场景,可以尝试记录以下内容:
- 格式化变量
- 单行数据
- 函数调用的开始和结束
123456789101112131415161718192021222324252627282930313233343536373839404142create or replace function advanced_example(num int default 10)returns textlanguage plpgsqlas $$declare var1 int := 20; var2 text;begin -- 记录函数调用开始 raise log '记录函数调用开始时间: (%)', (select now()); -- 记录来自SELECT查询的变量 select col_1 into var1 from some_table limit 1; raise log '记录变量值 (%)', var1; -- 也可以不通过变量,直接将查询结果输出到日志 raise log '记录单值查询结果(%)', (select col_1 from some_table limit 1); -- 必要时可以记录整行数据为JSON格式 raise log '记录整行数据为JSON格式 (%)', (select to_jsonb(some_table.*) from some_table limit 1); -- 使用INSERT或UPDATE时,新值可以通过RETURNING返回 -- 使用DELETE时,被删除的值可以返回 -- 这三种操作都使用"RETURNING value(s) INTO variable(s)"语法 insert into some_table (col_2) values ('new val') returning col_2 into var2; raise log '记录INSERT操作的值 (%)', var2; return var1 || ',' || var2;exception -- 在此处处理异常(如需要) when others then raise exception '函数<advanced_example>执行出错: %', sqlerrm;end;$$;select advanced_example();
资源
- 官方客户端库: JavaScript 和 Flutter
- 社区客户端库: github.com/supabase-community
- Postgres 官方文档: 第9章 函数与操作符
- Postgres 参考: CREATE FUNCTION