# 数据库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]。
   表1数据库说明 
  | 数据库       | 用途                             |
  |:---|:---|
  | supabase_db | 应用数据库，存放业务表与业务数据。          |
  | _supabase | 平台元数据库，存放Supabase平台自身配置，请勿修改。 |
     
  
- **自动生成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权限配合使用，两者缺一不可。
  表3GRANT与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中逐段或整段执行。
1. **创建数据表。**
   定义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：仅插入时取默认值；如需更新时自动刷新，须另行配置触发器。
    
2. **启用行级安全（RLS）。**
   启用RLS后，表的访问须经RLS策略放行，未配置策略的操作默认拒绝。
   ```
   ALTER TABLE products ENABLE ROW LEVEL SECURITY;
   ALTER TABLE product_pricings ENABLE ROW LEVEL SECURITY;
   ```
   对产品表与价格表启用RLS，相当于给两张表都"上锁"，此后谁能查看或修改哪条产品、哪个价格，不再由表本身决定，而交由下一步的RLS策略按角色放行，即产品与价格数据不再对所有访问者敞开，而是按身份逐行把关。
   
3. **配置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]。
    
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）可上下架商品、调整价格，完成日常经营。
   
5. **刷新schema缓存** **。**
   执行以下语句可立即刷新PostgREST的schema缓存，使新建表即时可被Data API访问，无需等待周期性自动刷新。
   ```
   NOTIFY pgrst, 'reload schema';
   ```
   
 
#### SDK相关操作
- [SDK基础操作]
  应用前端通过Supabase JavaScript SDK对接后端Data API。以下示例假定已完成SDK初始化（详见[准备工作](https://support.huaweicloud.com/usermanual-rds-pg/rds_supabase_22.html)），客户端变量为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()必须搭配过滤条件，否则会删除表中所有行。
    
   
- [SDK高阶操作]
  以下场景基于[建表与权限配置]的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的定价档位。
    
   
#### 参考文档
以下为Supabase开源相关文档，仅供参考。
- [Supabase Database概览](https://supabase.com/docs/guides/database/overview)
- [Supabase JavaScript SDK --- select](https://supabase.com/docs/reference/javascript/select)
- [Supabase JavaScript SDK --- insert](https://supabase.com/docs/reference/javascript/insert)
- [Supabase JavaScript SDK --- update](https://supabase.com/docs/reference/javascript/update)
- [Supabase JavaScript SDK --- upsert](https://supabase.com/docs/reference/javascript/upsert)
- [Supabase JavaScript SDK --- delete](https://supabase.com/docs/reference/javascript/delete)
- [Postgres REST API管理](https://supabase.github.io/postgres-meta/#/)
 
