通过 Services 层统一封装数据查询逻辑

1. 为什么不应该在页面中直接查询数据库

即便 Supabase 提供了便捷的查询能力,也不建议在页面组件中直接执行数据查询,主要存在以下问题:

  • 查询逻辑分散(多个页面重复编写)
  • 查询字段不一致(有的使用 select *,有的只选择部分字段)
  • 后期维护困难(需要全局搜索和逐一修改)
  • 复用性差(列表页与详情页逻辑重复)

2. 引入 Services 层

核心思想:

页面只负责“使用数据”,不负责“如何获取数据”

通过引入 Services 层,可以将数据查询逻辑进行集中管理,从而提升整体可维护性。

Services 层主要承担以下职责:

  • 统一查询入口
    抽离通用的查询逻辑,避免在多个页面中重复实现

  • 封装基础查询条件(Base Query)
    提前约束数据范围,例如:

    • 过滤已删除数据
    • 仅查询已发布内容
  • 明确查询字段(避免数据泄漏)
    统一定义 select 字段,避免使用 select *

  • 按场景拆分查询逻辑

    • detail.ts:用于详情查询(单条数据)
    • list.ts:用于列表查询(支持分页、排序)
    • writers.ts:用于数据写入与更新

通过这种方式,可以让数据访问层结构更加清晰,同时保证查询条件的一致性。


3. 具体实现以 lessons/list.ts 为例

1. 抽离通用查询逻辑,统一封装查询条件

将通用查询逻辑提取到独立文件中,作为“基础查询(Base Query)”,确保所有相关查询共享一致的约束条件。

// services/lessons/queries.ts
export const publishedLessonsByCourseSlugQuery = (
  supabase: TypedSupabaseClient,
  courseSlug: string,
) => {
  return supabase
    .from("lessons")
    .select("*,courses!inner (slug)")
    .eq("courses.slug", courseSlug)
    .eq("is_published", true);
};

2. 在具体场景中补充查询能力(分页 / 排序)

在 list.ts 中基于基础查询进行扩展,补充分页、排序等与“列表场景”相关的逻辑。

// services/lessons/list.ts
export const listLessonsByCourse = async (
  supabase: TypedSupabaseClient,
  courseSlug: string,
  params: ListLessonsParams = {},
) => {
  const { page = 1, pageSize = LESSON_PAGE_SIZE } = params;

  const from = (page - 1) * pageSize;
  const to = from + pageSize;

  const query = publishedLessonsByCourseSlugQuery(supabase, courseSlug)
    .order("sort_order", { ascending: true })
    .range(from, to);

  ...
}

这种方式可以做到:

  • 查询条件统一(不会遗漏 is_published 等约束)
  • 列表逻辑集中(分页、排序只在一处维护)
  • 查询能力可组合(Base Query + 场景扩展)

3. 页面加载时使用并行查询

在页面层,通过 Promise.all 并行获取数据,提升整体加载性能。

const coursePromise = getCourseBySlug(supabase, courseSlug);
const lessonsPromise = listLessonsByCourse(supabase, courseSlug, {
  pageSize: LESSON_PAGE_SIZE,
});
const lessonPromise = getLessonBySlug(supabase, courseSlug, lessonSlug);

const [course, lessonsResult, lesson] = await Promise.all([
  coursePromise,
  lessonsPromise,
  lessonPromise,
]);