LW14: 第 4 天 使用声明式 Schemas 简化数据库管理 了解更多

Supabase LogoSupabase Logo

返回 博客

使用声明式 Schemas 简化数据库管理

2025 年 4 月 3 日 • 7 分钟阅读

Qiao Han avatarQiao HanEngineering 使用声明式 Schemas 简化数据库管理

今天我们发布了声明式 schemas,以简化管理和维护复杂的数据库 schemas。 使用声明式 schemas,您可以以清晰、集中和版本控制的方式定义数据库结构。

什么是声明式 Schemas?#

声明式 schemas 将数据库的最终期望状态存储在 .sql 文件中,这些文件可以与项目一起保存并进行版本控制。 例如,这是一个经典的 products 表的声明式 schema:

1
create table "products" (
2
 "id" serial primary key,
3
 "name" text not null,
4
 "description" text,
5
 "price" numeric(10,2) not null,
6
 "created_at" timestamp default now()
7
);
89
alter table "products"
10
enable row level security;

与直接更改数据库 schema 相比,声明式 schemas 提供了许多好处:

声明式 Schemas vs 迁移#

最佳实践是使用 Migrations 来跟踪和应用数据库的更改。 每次进行更改时,您都会创建一个包含所有更改的新文件,从而保持更改的版本控制和可重现性。

但是,随着数据库 schemas 复杂性的增加,使用版本化迁移进行开发变得越来越困难,因为没有一个地方可以查看整个数据库 schema。

例如,在 Supabase,我们有一个复杂且经常更新的 projects 表。 这是一个启用了 RLS 的部分示例:

1
create table private.projects (
2
 id       bigint  not null,
3
 name      text   not null,
4
 organization_id bigint  not null,
5
 inserted_at   timestamp not null,
6
 updated_at   timestamp not null
7
);
89
alter table private.projects
10
enable row level security;
1112
create policy projects_insert
13
 on private.projects
14
 for insert
15
 to authenticated
16
with check auth.can_write(project_id);
1718
create policy projects_select
19
 on private.projects
20
 for select
21
 to authenticated
22
using auth.can_read(project_id);
2324
-- Users can only view the projects that they have access to
25
create view public.projects as select
26
 projects.id,
27
 projects.name,
28
 projects.organization_id,
29
 projects.inserted_at,
30
 projects.updated_at
31
from private.projects
32
where auth.can_read(projects.id);

projects 表是在一个私有 schema 中创建的,公开视图用于读取。 基于属性的访问控制 (ABAC) 在 RLS 策略之上实现,以确保查询仅返回用户有权访问的项目。

由于默认情况下 Postgres 视图不可更新,因此我们定义了触发器函数,以便在 Supabase 用户创建新项目时将写入级联到基础表。 这使得开发更容易,因为可以使用常规 PostgREST 调用插入 projects 视图,同时调用基础表上的相应 RLS 策略。

1
-- Triggers to update views from PostgREST: select('projects').insert({ ... })
2
create function public.public_projects_on_insert() returns trigger
3
as $$
4
begin
5
 insert into private.projects(
6
  name,
7
  organization_id,
8
  inserted_at,
9
  updated_at
10
 ) values (
11
  NEW.name,
12
  NEW.organization_id,
13
  coalesce(NEW.inserted_at, now()),
14
  coalesce(NEW.updated_at, now())
15
 ) returning * into NEW;
16
 return NEW;
17
end
18
$$ language plpgsql;
1920
create trigger public_projects_on_insert
21
 instead of insert
22
 on public.projects
23
 for each row
24
execute function public.public_projects_on_insert();

这种复杂性降低了开发速度,因为对表的更改可能会破坏其他视图或函数。 早在 2022 年初,添加新列的简单更改涉及以下步骤。

  1. 在我们的迁移文件中或通过查询我们的数据库找到 projects 表的最新 schema。
  2. 在新的迁移文件中编写 alter table 语句。
  3. 复制并更新 projects 视图定义以包含新列。
  4. 复制并更新触发器函数定义以包含新列。
  5. 添加新的 pgTAP 测试并验证现有测试是否通过。
  6. 提交新的迁移文件以进行审查,该文件至少有数百行。

这个过程很繁琐,并且多个工程师同时处理 projects 表令人沮丧。 合并 PR 会导致合并冲突,必须通过重复步骤 1-5 来解决。

在生产环境中使用声明式 Schemas#

采用声明式 schemas 为我们的工程师在更新数据库 schemas 时提供了一个单一管理界面。 我们只需要在一个地方更改 schema 定义,而无需在迁移文件中手动复制受影响的 postgres 实体。

然后,我们使用 schema 差异工具(例如 migra)来确定生成迁移文件时对视图和函数进行的必要更新。

例如,将新的 metadata 列添加到 projects 表现在变成单行差异。

1
--- a/supabase/schemas/projects.sql
2
+++ b/supabase/schemas/projects.sql
3
@@ -2,6 +2,7 @@ create table private.projects (
4
  id       bigint  not null,
5
  name      text   not null,
6
  organization_id bigint  not null,
7
+ metadata    jsonb,
8
  inserted_at   timestamp not null,
9
  updated_at   timestamp not null
10
 );

同样的流程也适用于视图、数据库函数、RLS 策略、角色授权、自定义类型和约束。 虽然仍然需要对生成的迁移文件进行手动审查,但它已将我们的开发时间从数小时缩短到数分钟。 在其他 PR 引入的合并冲突上进行 rebase 也容易得多。

开始使用声明式 Schemas#

声明式 schemas 今天在 Supabase 上可用。

我们将过去 2 年在内部使用的一组相同工具添加到了 Supabase CLI。 无论您是刚开始使用迁移,还是已经厌倦了管理数百个迁移文件,都可以尝试使用声明式 schemas,因为它可能会简化您的开发过程。

查看我们关于 Postgres Language Server 的博客文章,了解使用声明式 schemas 进行开发时更好的工具和 IDE 集成。

Launch Week 14 2025 年 3 月 31 日 - 4 月 04 日

第 1 天 -Supabase UI Library第 2 天 -Supabase Edge Functions: Deploy from the Dashboard + Deno 2.1第 3 天 -Realtime: Broadcast from Database第 4 天 -Declarative Schemas for Simpler Database Management

内容概要: