通过 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,
]);