第 08 章 业务层 Service 与数据层 CRUD
学习目标
学完本章,你应该能:
- 理解 Service 层和 CRUD 层的职责分工
- 掌握 CRUD 的函数命名规范
- 能编写 Service 和 CRUD 代码,完成一次完整的增删改查
一、两层各自的职责
回顾三层架构,Service 和 CRUD 是中间的两层:
| 层 | 职责 | 比喻 |
|---|---|---|
| Service 层 | 业务逻辑、编排数据操作、事务控制 | 业务主管:决定"做什么、按什么顺序做" |
| CRUD 层 | 数据库增删改查的封装 | 数据搬运工:执行"具体的取数/存数" |
核心原则(官方反复强调):
- 业务逻辑写在 Service,不要下沉到 CRUD
- 查询细节写在 CRUD,不要散落到 API 层
三层职责分工图
二、CRUD 层:函数命名规范
fba 的 CRUD 层基于 sqlalchemy-crud-plus(官方自研的异步 CRUD SDK),并约定了一套严格的函数命名规范:
| 函数名 | 用途 |
|---|---|
get() | 获取/查询详情 |
get_by_xxx() | 通过 xxx 字段查询详情 |
get_select() | 获取查询列表的表达式 |
get_list() | 获取列表 |
get_all() | 获取所有 |
get_with_join() | 连接查询(多表 join) |
get_with_relation() | 关系查询(ORM 关系) |
get_children() | 子查询 |
create() | 创建 |
update() | 更新 |
delete() | 删除 |
这套命名很"望文生义",看到函数名就知道它是干嘛的,这就是规范的威力。
CRUD 函数分类图
三、CRUD 层示例
# backend/app/admin/crud/crud_user.py
from backend.app.admin.model import User
from backend.common.crud import BaseCRUD # 示意,实际来自 sqlalchemy-crud-plus
class UserCRUD(BaseCRUD[User]):
"""用户数据操作"""
async def get_by_username(self, db, username: str) -> User | None:
"""通过用户名查询用户"""
return await self.get(db, username=username)
user_dao = UserCRUD(User) # 实例化,供 Service 调用要点:
- 每个模型对应一个 CRUD 类,继承
BaseCRUD - 通用增删改查已经由基类提供,只需写特殊查询(如
get_by_username) - 最后实例化一个对象(如
user_dao),供 Service 调用
四、Service 层示例
# backend/app/admin/service/user_service.py
from backend.app.admin.crud.crud_user import user_dao
class UserService:
"""用户业务逻辑"""
async def get_user_detail(self, db, user_id: int):
"""获取用户详情(这里可以加业务逻辑)"""
user = await user_dao.get(db, user_id)
if not user:
# 业务判断:用户不存在时的处理
raise NotFoundError(msg='用户不存在')
# 这里可以加更多业务逻辑
return user
user_service = UserService()要点:
- Service 调用 CRUD 层(
user_dao)完成数据操作 - 业务逻辑(判断、校验、编排多个操作)写在 Service
五、完整调用链
一个"创建用户"的完整链路:
# 1. API 层(api/user.py)
@router.post('', summary='创建用户')
async def create_user(obj: CreateUserParam, db: CurrentSession):
return await user_service.create_user(db, obj)
# 2. Service 层(service/user_service.py)
class UserService:
async def create_user(self, db, obj: CreateUserParam):
# 业务逻辑:如密码加密、用户名去重检查等
hashed_pwd = hash_password(obj.password)
user = await user_dao.create(db, obj) # 调用 CRUD
return user
# 3. CRUD 层(crud/crud_user.py)
class UserCRUD(BaseCRUD[User]):
pass # create 方法已由基类提供调用链时序图
三层各司其职,边界清晰。
六、分页(列表查询的必备能力)
实际项目中,列表数据往往成千上万条,不能一次全返回,需要分页。fba 基于 fastapi-pagination 封装了统一分页能力,支持两种模式:
| 分页类型 | 适用场景 | 依赖 |
|---|---|---|
| 普通分页 | 后台管理列表页(上一页/下一页) | DependsPagination |
| 游标分页 | 无限滚动、会话列表 | DependsCursorPagination |
普通分页用法
① 接口层:添加 DependsPagination 依赖,返回 PageData:
@router.get('', summary='分页获取用户', dependencies=[DependsPagination])
async def get_users_paginated(
db: CurrentSession,
query: Annotated[UserQueryParam, Query()],
) -> ResponseSchemaModel[PageData[GetUserDetail]]:
page_data = await user_service.get_list(db=db, **query.model_dump())
return response_base.success(data=page_data)② Service 层:CRUD 获取 Select 对象后,用 paging_data() 生成分页数据:
user_select = await user_dao.get_select(username=username, ...)
return await paging_data(db, user_select)分页参数说明
DependsPagination 默认参数:
page:页码,默认 1(最小 1)size:每页数量,默认 20(最大 200)
关键点:
page、size由DependsPagination自动提供,不要在你的查询参数模型里重复定义。
返回字段:items(当前页数据)、total(总条数)、page(页码)、size(每页数量)、total_pages(总页数)。
分页流程
七、什么是 DAO
你可能看到文档里 CRUD 和 DAO 混着出现,简单解释:
- DAO(Data Access Object,数据访问对象):一个更通用的设计模式概念,指"封装数据访问逻辑的对象"。
- CRUD:增删改查的简称,在 fba 里 CRUD 层就是 DAO 的一种实现。
所以"CRUD 层"和"DAO 层"在 fba 里基本指同一个东西,不用纠结。
八、常见疑问
Q1:CRUD 类为什么能免写 create/update/delete? 因为这些通用操作由 BaseCRUD 基类提供了,你只需继承就能用,只有特殊查询才需要自己写。
Q2:业务逻辑放 Service,那"校验用户名不重复"算业务逻辑吗? 算。这类业务规则(判断、校验)都应该放 Service,而不是 CRUD。CRUD 只提供"按用户名查"这个能力(get_by_username),Service 来决定"查到了要不要报错"。
本章小结
关键要点回顾:
- Service 写业务逻辑,CRUD 写数据库操作,职责分离
- CRUD 命名规范:get / get_by_xxx / get_list / create / update / delete 等
- CRUD 类继承 BaseCRUD,通用操作免写,只写特殊查询
- 每层实例化一个对象(dao、service),逐层调用
- 调用链:API → Service → CRUD → 数据库
- 分页:
DependsPagination(普通分页)/DependsCursorPagination(游标分页),Service 层用paging_data()生成分页数据
下一步: 数据拿到了,如何统一地返回给前端?第 09 章讲统一响应和异常处理。
对应官方文档:
CRUD(backend/reference/CRUD.html)、分页(backend/reference/pagination.html)