Menu

Supabase 数据库操作指南

提示

  • 以下文档不是 Supabase 数据库操作的完整指南,而是 Nexty.dev 中应用到的一些操作方法,可以帮助你更快地理解模板提供的功能。
  • AI 对 Supabase 非常熟悉,你可以大胆使用 AI 来完成各种数据库操作。

Nexty.dev 采用完整的 Supabase 数据库操作架构,提供类型安全的数据库访问、完善的权限控制和统一的操作模式。通过结合 TypeScript 类型系统、Zod 数据验证和 RLS(行级安全)策略,确保数据操作的安全性和可维护性。

基础 CRUD 操作

查询操作(SELECT)

简单查询

const { data: users, error } = await supabase
  .from('users')
  .select('*') // 或者指定返回字段 .select('email, role')
  .eq('role', 'admin');

关联查询

const { data: posts, error } = await supabase
  .from('posts')
  .select(`
    *,
    tags (*)
  `)
  .eq('status', 'published');

分页查询

const { data, error, count } = await supabase
  .from('posts')
  .select('*', { count: 'exact' }) // { count: 'exact' } -> 带总量统计
  .range(0, 9)
  .order('created_at', { ascending: false });

插入数据

const { data: newPost, error } = await supabase
  .from('posts')
  .insert({
    title: 'New Post',
    slug: 'new-post',
    content: 'Post content...',
    language: 'en',
    status: 'draft',
    author_id: user.id
  })
  .select("id")
  .single();

更新数据

const { data: updatedPost, error } = await supabase
  .from('posts')
  .update({
    title: 'Updated Title',
    status: 'published'
  })
  .eq('id', postId)
  .select("id")
  .single();

删除数据

const { error } = await supabase
  .from('posts')
  .delete()
  .eq('id', postId);

高级查询技巧

条件查询

let query = supabase
  .from('posts')
  .select('*');
 
if (filter) {
  // 对 title, slug, description 进行模糊查询
  const filterValue = `%${filter}%`;
  query = query.or(
    `title.ilike.${filterValue},slug.ilike.${filterValue},description.ilike.${filterValue}`
  );
}
 
if (status) {
  query = query.eq('status', status);
}
 
const { data, error } = await query
  .order('is_pinned', { ascending: false })
  .order('created_at', { ascending: false })
  .range(from, to);

聚合查询

// 统计 status 字段为 published 的数据的数量
const { count, error } = await supabase
  .from('posts')
  .select('*', { count: 'exact', head: true })
  .eq('status', 'published');

事务处理与 RPC

遇到数据库原子性操作、复杂操作可以利用 Supabase 的 RPC(Remote Procedure Call,远程过程调用)函数来完成。

RPC 是直接在数据库中定义的 PostgreSQL 函数,可以通过 Supabase 客户端直接调用,提供比普通 SQL 查询更强大的功能。

const { error: usageError } = await supabaseAdmin.rpc('upsert_and_set_subscription_credits', {
  p_user_id: userId,
  p_credits_to_set: creditsToGrant
});

RPC 函数的优势:

  • 原子性操作:确保复杂的多步操作要么全部成功,要么全部失败
  • 更好的性能:在数据库层面执行,减少网络往返
  • 权限控制:通过 SECURITY DEFINER 实现细粒度权限控制
  • 复杂业务逻辑:支持条件判断、循环、异常处理等
  • 数据一致性:避免竞态条件和并发问题

使用场景:

  • 积分扣除系统
  • 订单处理流程
  • 批量数据更新
  • 复杂的计算逻辑
  • 需要事务保证的操作

在 Nexty.dev 源码中,你全局搜索 .rpc 可以看到 RPC 函数在用户付费后增加 credits 的方法里应用,它们的函数逻辑在 data 文件夹下对应的数据表定义文件里。

权限控制与安全

管理员权限检查

涉及敏感操作,需要先判断是管理员身份,然后使用 Service Role Key 操作数据库

import { createClient as createAdminClient } from "@supabase/supabase-js";
import { isAdmin } from '@/lib/supabase/isAdmin';
 
// 判断管理员身份
if (!(await isAdmin())) {
  return actionResponse.forbidden("Admin privileges required.");
}
 
// 管理员使用 Service Role Key 操作数据库
const supabaseAdmin = createAdminClient<Database>(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY! // 仅服务端使用
);

RLS 策略

RLS(Row Level Security,行级安全)是 PostgreSQL 的一个安全特性,允许在数据库表级别控制用户对特定行的访问权限。简单来说,它可以确保用户只能访问属于自己的数据。

在多用户的 SaaS 应用中,不同用户之间的数据必须严格隔离。RLS 提供了数据库级别的安全保障,确保即使客户端代码出现漏洞,也无法访问其他用户的数据。

以下是 RLS 策略的基本用法:

  1. 启用 RLS
-- 对表启用行级安全
ALTER TABLE public.users ENABLE ROW LEVEL SECURITY;
 
-- 强制所有用户都受 RLS 限制(包括表所有者)
ALTER TABLE public.users FORCE ROW LEVEL SECURITY;
  1. 创建策略
-- 用户只能查看自己的资料
CREATE POLICY "Allow user read their own profile"
ON public.users 
FOR SELECT
USING (auth.uid() = id);
 
-- 用户只能更新自己的资料
CREATE POLICY "Allow user update their own profile"
ON public.users 
FOR UPDATE
USING (auth.uid() = id);

在模板提供的 data 文件夹里,所有表设计都能看到创建 RLS 的语句。

提示

对于创建了类似【用户只能更新自己的资料】这样的 RLS 策略的表,管理员是无法直接通过 lib/supabase/server 导出的服务端 client 更新用户资料,只能通过先判断用户是管理员身份,然后使用 @supabase/supabase-js 来绕过 RLS 策略修改数据库。

数据库迁移与更新

数据库类型更新

模板使用 Supabase CLI 自动生成和更新 TypeScript 类型定义:

# 生成类型文件
supabase gen types typescript --project-id <your-project-id> --schema public > lib/supabase/types.ts

你需要在每次更新数据库定义后都在本地执行一遍该命令,以确保本地的类型定义是正确的。

命令执行后,lib/supabase/types.ts 文件里的数据库类型定义将更新。

提示:

  • Supabase CLI 更多使用方法请查看Supabase 集成
  • 如何找到 <your-project-id>?登录 Supabase 控制台,地址栏 /project/ 后面的那串字符就是,例如 https://supabase.com/dashboard/project/<your-project-id>

Schema变更

-- 添加新字段的示例迁移
ALTER TABLE posts ADD COLUMN view_count INTEGER DEFAULT 0;
 
-- 创建索引
CREATE INDEX IF NOT EXISTS idx_posts_status_language 
ON posts(status, language);
 
-- 更新RLS策略
CREATE POLICY "Users can view published posts" ON posts
FOR SELECT USING (status = 'published');

最佳实践

选择性字段查询

select 尽量只选择必要字段,而不是用 .select('*'),这样可以提升查询性能

const { data: users, error } = await supabase
  .from('users')
  .select('email, role')
  .eq('role', 'admin');

索引优化查询

通过创建索引优化查询性能

-- 创建复合索引
CREATE INDEX posts_language_status_idx ON posts (language, status);
CREATE INDEX posts_published_at_idx ON posts (published_at DESC);

统一返回格式

模板提供了统一返回格式的方法:

  • 对于 Server Actions 可调用 lib/action-response.ts 封装的方法
  • 对于 API 可调用 lib/api-response.ts 封装的方法
lib/action-response.ts
export type ActionResult<T = any> =
  | { success: true; data?: T, customCode?: string }
  | { success: false; error: string, customCode?: string };
 
export const actionResponse = {
  success: <T>(data?: T, customCode?: string): ActionResult<T> => {
    return { success: true, data, customCode };
  },
  error: <T>(message: string, customCode?: string): ActionResult<T> => {
    return { success: false, error: message, customCode };
  },
  
  unauthorized: <T>(message = "Unauthorized", customCode?: string): ActionResult<T> => {
    return actionResponse.error(message, customCode);
  },
  // ...others...
};

模板还提供了不同错误格式的回退处理方法,在 try...catch 代码块里,catch 捕获的错误可以调用 lib/error-utils.tsgetErrorMessage 方法进行处理,将避免不规则报错信息导致前端 toast 无法显示真实错误 message 的情况。

Supabase 数据库功能总览

在 Supabase 的 Database 目录下,可以看到所有与数据库定义相关的功能入口,包括:Tables(数据表)、Functions(RPC 函数)、Triggers(触发器)、Indexes(索引)、Policies(策略)。

supabase database overview