数据库

数据库函数


Postgres 原生支持 SQL 函数。这些函数存储在您的数据库中,可以通过 API 调用 来使用。

快速演示

开始使用

Supabase 提供了多种创建数据库函数的方式。您可以使用仪表盘或直接通过 SQL 语句创建。我们提供了内置的 SQL 编辑器,您也可以 连接 到数据库后自行执行 SQL 查询。

  1. 进入 "SQL 编辑器" 版块
  2. 点击 "新建查询"
  3. 输入创建或替换数据库函数的 SQL 语句
  4. 点击 "运行" 或使用快捷键 cmd+enter (Windows 用户使用 ctrl+enter)

简单函数

让我们创建一个基本的数据库函数,返回字符串"hello world"。

1
2
3
4
5
6
create or replace function hello_world() -- 1returns text -- 2language sql -- 3as $$ -- 4 select 'hello world'; -- 5$$; --6
显示/隐藏详情

最基本的函数包含以下部分:

  1. create or replace function hello_world(): 函数声明,其中hello_world是函数名。可以使用create创建新函数,或使用replace替换现有函数。也可以同时使用create or replace来处理两种情况。
  2. returns text: 函数返回的数据类型。如果不返回任何内容,可以使用returns void
  3. language sql: 函数体内使用的语言。也可以是过程语言:plpgsqlplpython等。
  4. as $$: 函数包装器。$$符号内的所有内容都是函数体的一部分。
  5. select 'hello world';: 简单的函数体。如果没有后续语句,函数体内最后的select语句将被返回。
  6. $$;: 函数包装器的结束符号。

创建函数后,我们有几种"执行"函数的方式 - 可以直接在数据库中使用SQL,或使用客户端库之一。

1
select hello_world();

返回数据集

数据库函数也可以从或视图中返回数据集。

例如,如果我们有一个包含星球大战数据的数据库:

行星

1
2
3
4
5
| id | name || --- | -------- || 1 | Tatooine || 2 | Alderaan || 3 | Kashyyyk |

人物

1
2
3
4
5
6
| id | name | planet_id || --- | ---------------- | --------- || 1 | Anakin Skywalker | 1 || 2 | Luke Skywalker | 1 || 3 | Princess Leia | 2 || 4 | Chewbacca | 3 |

我们可以创建一个返回所有行星的函数:

1
2
3
4
5
6
create or replace function get_planets()returns setof planetslanguage sqlas $$ select * from planets;$$;

由于这个函数返回的是表集合,我们还可以应用过滤器和选择器。例如,如果我们只想获取第一个行星:

1
2
3
select *from get_planets()where id = 1;

参数传递

让我们创建一个函数来向 planets 表中插入新行星并返回新ID。注意这次我们使用的是 plpgsql 语言。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
create 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 查询执行此函数,也可以通过客户端库执行:

1
select * from add_planet('Jakku');

建议

数据库函数 vs 边缘函数

对于数据密集型操作,请使用数据库函数。这些函数在数据库内部执行,可以通过REST和GraphQL API远程调用。

对于需要低延迟的用例,请使用边缘函数,它们全球分布式部署且可以用TypeScript编写。

安全性:definer vs invoker

Postgres允许您指定函数是作为调用函数的用户(invoker)执行,还是作为函数创建者(definer)执行。例如:

1
2
3
4
5
6
7
8
9
create 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)。这样可以限制潜在损害,特别是当您允许访问执行函数的用户本不应访问的模式时。

函数权限

默认情况下,数据库函数可以被任何角色执行。有两种主要方式来限制这一点:

  1. 逐个函数限制。专门撤销您想要保护的函数的执行权限。需要同时撤销public和您要限制的角色的执行权限:

    1
    2
    revoke execute on function public.hello_world from public;revoke execute on function public.hello_world from anon;
  2. 默认限制函数执行。当您希望特定角色可以执行某个函数时,专门授予访问权限。

    要限制所有现有函数,需要同时撤销public和您想限制的角色的执行权限:

    1
    2
    revoke execute on all functions in schema public from public;revoke execute on all functions in schema public from anon, authenticated;

    要限制所有新函数,需要修改public和您想限制的角色的默认权限:

    1
    2
    alter default privileges in schema public revoke execute on functions from public;alter default privileges in schema public revoke execute on functions from anon, authenticated;

    然后您可以针对特定函数向特定角色重新授予权限:

    1
    grant execute on function public.hello_world to authenticated;

调试函数

您可以添加日志来帮助调试函数,这对于复杂函数特别推荐。

适合记录日志的目标包括:

  • (非敏感)变量的值
  • 查询返回的结果

通用日志记录

要在仪表板的Postgres日志中创建自定义日志,可以使用raise关键字。默认情况下,系统支持3种严重级别:

  • log(日志)
  • warning(警告)
  • exception(错误级别)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
create 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 关键字创建自定义错误。

常见的模式是在变量不满足条件时抛出错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
create 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 关键字。它使用以下格式:

1
2
-- 当条件为 false 时抛出错误assert <某个条件>, '错误信息';

下面是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
create 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 关键字捕获并修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
create function error_example()returns voidlanguage plpgsqlas $$begin -- 失败:无法从不存在的数据表读取 select * from table_that_does_not_exist; exception when others then raise exception '函数 <function name> 中发生错误: %', sqlerrm;end;$$;

高级日志记录

对于更复杂的函数或疑难调试场景,可以尝试记录以下内容:

  • 格式化变量
  • 单行数据
  • 函数调用的开始和结束
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
create 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调用数据库函数

使用数据库函数调用外部API