数据库API(Database Data API)
场景介绍
本章节介绍用户侧应用如何接入并使用华为云RDS Supabase提供的数据库服务。
- 传统方式:前端不直接连接数据库,而是自行搭建后端服务。为每张表手写数据访问代码并封装为接口,前端通过这些接口间接读写数据库;表越多,重复劳动越多;修改表时,接口也需同步维护,成本随业务增长而持续累积。
- RDS Supabase:基于托管的PostgreSQL,按表结构自动生成Data API,新建一张表即可调用(首次可能有数秒缓存延迟),后续新增表同步生成;前端只需引入Supabase JavaScript SDK直连这套API即可完成增删改查(CRUD,读操作以anon身份即可,写操作需登录态),从源头消除重复劳动,应用无需自建后端接口,也不必为每张表编写数据访问代码。
这套API由PostgREST框架生成,前端SDK直连即可读写(受RLS与登录态约束),行级安全策略(RLS)负责行级访问控制。
相关概念
华为云RDS Supabase提供托管式PostgreSQL数据库,并基于表结构自动生成Data API,开发者无需手写后端接口即可完成数据增删改查;同时通过行级安全策略(RLS)实现细粒度的数据访问控制与隔离。
- PostgreSQL数据库
华为云RDS Supabase为您托管一个独立的PostgreSQL数据库。它是完整的PostgreSQL(非简化版),您无需自行搭建或运维数据库服务器,即可使用全部SQL能力,包括JSONB、全文搜索、视图、存储过程、触发器等高级特性,并可通过SQL可视化界面查看与管理数据。
创建Supabase应用时,会在托管的PostgreSQL数据库实例上自动创建以下两个数据库,详见表1。
- 自动生成Data API
创建数据库表后,Supabase会基于表结构(Schema)自动为每张表生成对应的HTTP接口,无需手写后端代码。
这套接口由PostgREST框架生成,覆盖增删改查(CRUD)全部操作:新建一张表即获得其完整数据访问端点(首次可能有数秒缓存延迟,执行NOTIFY pgrst, 'reload schema'可立即刷新),新增表也同步生成。开发者只需关注业务逻辑,无需编写DAO层或后端接口,从源头消除重复开发。
- 数据库角色
表2列出RDS Supabase面向应用层、对外的数据库角色,应用与SDK据此选取访问身份。
表2 角色说明 角色
说明
anon
匿名用户,通过API Key访问,受RLS约束。
authenticated
已认证用户,通过JWT访问,受RLS约束。
service_role
服务端角色,绕过RLS,请勿在客户端使用。
三类角色中,anon与authenticated面向前端,均受RLS约束,区别在登录态决定的读写权限;service_role面向服务端,绕过RLS,严禁客户端使用。
- 行级安全策略(RLS)
行级安全策略(Row Level Security,RLS)是PostgreSQL内置的行级访问控制机制。对表启用RLS后,所有访问均须经策略放行:策略按“角色 × 操作”维度界定可见或可操作的行,未匹配策略的操作默认拒绝。RDS Supabase据此在共享数据库上实现租户与用户间的数据隔离,具体表现为anon与authenticated的访问均受RLS约束,service_role则绕过RLS。RLS策略须与GRANT权限配合使用。
- GRANT权限
GRANT是PostgreSQL的权限授予语句,用于将表级操作权限赋予数据库角色,决定角色能否对表执行SELECT、INSERT、UPDATE、DELETE等操作。它是表级访问控制,先于RLS生效,即角色须先经GRANT获得操作权限,再由RLS在行级把关。RDS Supabase中,authenticated默认对新建表无任何权限,须显式GRANT方可使用。
- RLS策略与GRANT权限的关系
RLS策略须与GRANT权限配合使用,两者缺一不可。
表3 GRANT与RLS权限机制对比 机制
层级
作用
GRANT
表级
决定角色能否执行SELECT、INSERT、UPDATE、DELETE操作。
RLS策略
行级
在GRANT放行后,进一步决定哪些行可见或可操作。
执行流程:请求 → GRANT检查 → RLS策略检查 → 返回结果
- 仅GRANT不设置RLS:启用RLS后默认拒绝所有行级访问,查询结果为空。
- 仅RLS不设置GRANT:角色无表级操作权限,请求直接被拒绝。
建表与权限配置
下面以一个产品定价场景为例说明:products(产品表)记录产品信息,product_pricings(价格表)记录各产品的计费档位与价格,两表以product_id关联(删除产品则其价格一并删除)。样例按业务身份划分访问边界:访客仅浏览,登录运营人员可管理,后端服务可运维。基于此,给出完整的建表、RLS策略与GRANT权限配置。
以下步骤为建表与权限配置的完整SQL,可在Supabase Dashboard控制台的SQL Editor中逐段或整段执行。
- 创建数据表。
定义products(产品表)与product_pricings(产品定价表),二者以product_id外键关联,并在删除产品时级联删除其定价。
-- products:产品表 CREATE TABLE products ( id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name TEXT NOT NULL, category TEXT, description TEXT, created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now() ); -- product_pricings:产品定价表 CREATE TABLE product_pricings ( id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, product_id BIGINT NOT NULL REFERENCES products(id) ON DELETE CASCADE, tier TEXT NOT NULL, price NUMERIC(10,2) NOT NULL, unit TEXT DEFAULT '元/月', effective_from DATE DEFAULT current_date, created_at TIMESTAMPTZ DEFAULT now() );
字段说明:
- id:主键,数据库自增。采用GENERATED BY DEFAULT以支持upsert等显式指定id的场景;官方模板默认GENERATED ALWAYS(禁止手动插入id),更为严格。
- product_id:外键,关联products(id)。ON DELETE CASCADE表示删除产品时其定价一并删除。
- price:NUMERIC(10,2)表示总有效数字10位、小数2位,整数部分最多8位。
- updated_at:仅插入时取默认值;如需更新时自动刷新,须另行配置触发器。
- 启用行级安全(RLS)。
启用RLS后,表的访问须经RLS策略放行,未配置策略的操作默认拒绝。
ALTER TABLE products ENABLE ROW LEVEL SECURITY; ALTER TABLE product_pricings ENABLE ROW LEVEL SECURITY;
对产品表与价格表启用RLS,相当于给两张表都“上锁”,此后谁能查看或修改哪条产品、哪个价格,不再由表本身决定,而交由下一步的RLS策略按角色放行,即产品与价格数据不再对所有访问者敞开,而是按身份逐行把关。
- 配置RLS策略。
按“角色 × 操作”维度配置策略:对anon(匿名用户)仅开放只读,对authenticated(已认证用户)开放增删改查。
-- anon(匿名用户):只读 CREATE POLICY "anon_select_products" ON public.products FOR SELECT TO anon USING (true); CREATE POLICY "anon_select_product_pricings" ON public.product_pricings FOR SELECT TO anon USING (true); -- authenticated(已认证用户):增、删、改、查 CREATE POLICY "authenticated_select_products" ON public.products FOR SELECT TO authenticated USING (true); CREATE POLICY "authenticated_select_product_pricings" ON public.product_pricings FOR SELECT TO authenticated USING (true); CREATE POLICY "authenticated_insert_products" ON public.products FOR INSERT TO authenticated WITH CHECK (true); CREATE POLICY "authenticated_insert_product_pricings" ON public.product_pricings FOR INSERT TO authenticated WITH CHECK (true); CREATE POLICY "authenticated_update_products" ON public.products FOR UPDATE TO authenticated USING (true) WITH CHECK (true); CREATE POLICY "authenticated_update_product_pricings" ON public.product_pricings FOR UPDATE TO authenticated USING (true) WITH CHECK (true); CREATE POLICY "authenticated_delete_products" ON public.products FOR DELETE TO authenticated USING (true); CREATE POLICY "authenticated_delete_product_pricings" ON public.product_pricings FOR DELETE TO authenticated USING (true);
这组策略为产品表与价格表分别定义各角色可做的操作。策略均放行全部行,因此限制只在“操作类型”而非“具体行”。
- 访客(anon)只能浏览产品与价格。
- 登录用户(authenticated)可新增、修改、删除产品与价格。
- service_role不在此列,因其绕过RLS,详见步骤4。
- 授予表级权限(GRANT)。
GRANT决定角色能否执行某类表级操作,RLS决定可操作哪些行,二者缺一不可。
-- anon:只读,仅授予SELECT GRANT SELECT ON TABLE public.products TO anon; GRANT SELECT ON TABLE public.product_pricings TO anon; -- authenticated:增删改查,授予全部四种操作 GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE public.products TO authenticated; GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE public.product_pricings TO authenticated; -- service_role:绕过RLS,授予全部权限 GRANT ALL ON ALL TABLES IN SCHEMA public TO service_role;
各角色对商品表与价格表的权限如下表:
表4 样例权限设计 角色
products(商品表)
product_pricings(价格表)
anon(访客)
只读
只读
authenticated(登录用户)
增删改查
增删改查
service_role(服务端)
全部(绕过RLS)
全部(绕过RLS)
从业务角度看,商品表与价格表是面向用户展示的目录数据:访客(anon)仅可浏览商品与价格,保证目录不被随意篡改;登录的运营人员(authenticated)可上下架商品、调整价格,完成日常经营。
- 刷新schema缓存。
执行以下语句可立即刷新PostgREST的schema缓存,使新建表即时可被Data API访问,无需等待周期性自动刷新。
NOTIFY pgrst, 'reload schema';
SDK相关操作
应用前端通过Supabase JavaScript SDK对接后端Data API。以下示例假定已完成SDK初始化(详见准备工作),客户端变量为supabase,且以Publishable Key(客户端密钥,旧版Anon Key)初始化、默认只读。
Data API按请求所用密钥与登录态决定以哪个数据库角色执行:客户端Publishable Key(旧版Anon Key)在未登录时对应anon角色(只读),用户登录后同一密钥对应authenticated角色(可增删改查,受RLS约束);服务端Secret Key(旧版Service Role Key)对应service_role角色(绕过RLS)。SDK以Publishable Key初始化时,未登录请求均以anon身份发起。
Publishable Key可安全用于前端,配合RLS控制可访问的行;Secret Key绕过RLS,严禁放入前端代码。查询以anon身份即可发起;插入、更新、更新插入、删除属于写操作,需以authenticated身份发起,登录凭证由用户认证流程签发,本指南不做说明。
- 获取数据(Select)
业务场景:用户在商品页浏览产品列表,或按分类筛选(如只看“计算”类产品)。查询属于读操作,未登录即可发起。
// 标准写法:解构data与error,先判错再用data const { data, error } = await supabase.from('products').select() if (error) { console.error(error); return } // data为查询结果数组,如[{ id: 1, name: '云服务器 ECS', category: '计算', ... }] // 下文示例为简洁省略错误处理,实际请务必判错 // 条件查询 + 指定字段 const { data } = await supabase .from('products') .select('id, name, category') .eq('category', '计算') - 插入数据(Insert)
业务场景:运营人员上架新商品,例如录入“云服务器ECS”、“对象存储OBS”等商品;新增价格档位(如按量、包月)则插入product_pricings,写法相同。
以下示例假定用户已登录,客户端以authenticated身份发起。
// 插入单行 await supabase.from('products').insert({ name: '云服务器 ECS', category: '计算', description: '弹性云服务器' }) // 插入多行 await supabase.from('products').insert([ { name: '云服务器 ECS', category: '计算' }, { name: '对象存储 OBS', category: '存储' } ])当前仅以Publishable Key初始化的未登录客户端为只读身份,直接发起写操作会被拒绝;要执行成功须以authenticated身份发起。
- 更新数据(Update)
业务场景:运营人员修改商品信息,如更新id为1的商品描述;如需调价,则更新价格表product_pricings中对应档位的价格。
await supabase.from('products').update({ description: '更新后的描述' }).eq('id', 1)update()必须搭配过滤条件,否则会更新表中所有行。
- 更新插入(Upsert)
业务场景:批量同步或导入商品,同一商品(按主键或指定列判重)存在则更新、不存在则新增,常见于从外部系统同步商品目录。
冲突则更新,不冲突则插入。onConflict指定判冲突的列,默认为主键;若按非主键列判冲突,该列须先建立唯一约束。
// 基于主键冲突:id=1存在则更新,不存在则插入 const { data } = await supabase .from('products') .upsert({ id: 1, name: '云服务器 ECS', category: '计算' }) .select() // 基于非主键列冲突:须先为该列建立唯一约束 // ALTER TABLE products ADD CONSTRAINT products_name_key UNIQUE (name); const { data } = await supabase .from('products') .upsert({ name: '云服务器 ECS', category: '计算' }, { onConflict: 'name' }) .select() - 删除数据(Delete) 业务场景:下架指定商品,因外键级联(ON DELETE CASCADE),该商品的价格档位一并删除。
await supabase.from('products').delete().eq('id', 1)delete()必须搭配过滤条件,否则会删除表中所有行。
以下场景基于建表与权限配置的products与product_pricings两张表,演示关联查询。
PostgREST通过外键自动识别表间关系,在select()中以关联表(字段)语法嵌套,一次即可拿到关联数据,无需手写JOIN。
- 从产品查其定价(一对多):查产品及其全部定价,子行以数组返回。
// 查产品,并带出每条产品下的全部定价档位 const { data } = await supabase .from('products') .select(` id, name, category, product_pricings(id, tier, price, unit) `) // data示例:[{ id: 1, name: '云服务器 ECS', category: '计算', product_pricings: [{ id: 1, tier: '包月', price: 99.00, unit: '元/月' }] }]
- 从定价查其产品(多对一):从定价表出发,带出其所属产品。
// 从定价表查,并带出所属产品 const { data } = await supabase .from('product_pricings') .select('id, tier, price, products(id, name, category)')
- 带过滤与排序的关联查询:查分类为“计算”的产品及其定价,按产品名排序。
const { data } = await supabase .from('products') .select('id, name, product_pricings(tier, price)') .eq('category', '计算') .order('name')
- 按子表条件反查父表(内连接):只返回存在“价格低于100”定价的产品,用“!inner”强制内连接,过滤写在嵌入列上。
// 内连接:只返回存在价格<100定价的产品 const { data } = await supabase .from('products') .select('id, name, product_pricings!inner(tier, price)') .lt('product_pricings.price', 100)去掉“!inner”则变为左连接:仍返回全部产品,但每条产品只展示价格低于100的定价档位。