Skip to content

第 08 章 业务层 Service 与数据层 CRUD ​

学习目标 ​

学完本章,你应该能:

  1. 理解 Service 层和 CRUD 层的职责分工
  2. 掌握 CRUD 的函数命名规范
  3. 能编写 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 层示例 ​

python
# 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 层示例 ​

python
# 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

五、完整调用链 ​

一个"创建用户"的完整链路:

python
# 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:

python
@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() 生成分页数据:

python
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)