You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
19 KiB
19 KiB
ADR 010: Repository 与 Service 层事务管理职责划分
状态: 已采纳
日期: 2026-02-04
决策者: 开发团队
相关问题: Project Repository 测试失败、事务管理混乱
背景
在实施 Project Repository 单元测试时,发现了严重的架构设计问题:
问题 1: Repository 层错误地管理事务
# ❌ 错误:Repository 调用 commit()
class ProjectRepository:
async def create(self, project: Project) -> Project:
self.session.add(project)
await self.session.commit() # 违反单一职责原则
await self.session.refresh(project)
return project
问题:
- 违反单一职责原则(SRP)
- Repository 应该只负责数据访问,不应该管理事务
- 导致测试无法通过(与 conftest 的事务回滚机制冲突)
- 无法在一个事务中调用多个 Repository 方法
问题 2: 测试事务冲突
# ❌ 错误:fixture 中创建数据导致事务冲突
@pytest.fixture
async def sample_project(db_session: AsyncSession):
project = Project(...)
db_session.add(project)
await db_session.commit() # 与 conftest 的事务管理冲突
return project
错误信息:
InterfaceError: cannot use Connection.transaction() in a manually started transaction
RuntimeError: Task got Future attached to a different loop
问题 3: 职责不清晰
- Repository、Service、API 层的事务管理职责不明确
- 不同开发人员采用不同的模式
- 代码维护困难,容易出错
决策
制定明确的 Repository 和 Service 层职责划分规范,所有代码必须遵守。
架构原则
1. Repository 层职责
定位: 数据访问层(Data Access Layer)
1.1 应该做什么 ✅
-
数据访问逻辑
async def get_by_id(self, project_id: str) -> Optional[Project]: statement = select(Project).where(Project.id == UUID(project_id)) result = await self.session.exec(statement) return result.first() -
查询构建
async def get_by_user( self, user_id: str, filters: dict, page: int, page_size: int ) -> List[Project]: conditions = [Project.owner_id == UUID(user_id)] # 构建复杂查询... statement = select(Project).where(and_(*conditions)) result = await self.session.exec(statement) return list(result.all()) -
数据转换
async def create(self, project: Project) -> Project: self.session.add(project) await self.session.flush() # ✅ 使用 flush,不是 commit return project -
使用 flush() 确保数据可见性
async def update(self, project_id: str, data: dict) -> Optional[Project]: project = await self.get_by_id(project_id) if not project: return None for key, value in data.items(): setattr(project, key, value) self.session.add(project) await self.session.flush() # ✅ flush 使数据在当前事务内可见 return project
1.2 不应该做什么 ❌
-
调用 commit() 管理事务
# ❌ 错误 await self.session.commit() -
调用 refresh() 刷新对象
# ❌ 错误 await self.session.refresh(project)原因: refresh() 会触发数据库查询,应该由调用方决定是否需要
-
业务逻辑判断
# ❌ 错误:业务逻辑应该在 Service 层 if project.status == ProjectStatus.ARCHIVED: raise BusinessException("不能修改已归档的项目") -
调用其他 Repository
# ❌ 错误:跨 Repository 调用应该在 Service 层 folder = await self.folder_repo.get_by_id(folder_id)
1.3 标准模板
class ProjectRepository:
"""项目数据访问层"""
def __init__(self, session: AsyncSession):
self.session = session
async def create(self, project: Project) -> Project:
"""创建项目"""
self.session.add(project)
await self.session.flush() # ✅ 仅 flush
return project
async def get_by_id(self, project_id: str) -> Optional[Project]:
"""查询项目"""
statement = select(Project).where(Project.id == UUID(project_id))
result = await self.session.exec(statement)
return result.first()
async def update(self, project_id: str, data: dict) -> Optional[Project]:
"""更新项目"""
project = await self.get_by_id(project_id)
if not project:
return None
for key, value in data.items():
setattr(project, key, value)
self.session.add(project)
await self.session.flush() # ✅ 仅 flush
return project
async def delete(self, project_id: str) -> bool:
"""删除项目"""
project = await self.get_by_id(project_id)
if not project:
return False
await self.session.delete(project)
await self.session.flush() # ✅ 仅 flush
return True
2. Service 层职责
定位: 业务逻辑层(Business Logic Layer)
2.1 应该做什么 ✅
-
事务管理
class ProjectService: async def create_project(self, data: ProjectCreate) -> Project: try: # 业务逻辑 project = Project(**data.dict()) created = await self.repo.create(project) # ✅ Service 层负责提交事务 await self.session.commit() return created except Exception as e: # ✅ Service 层负责回滚 await self.session.rollback() raise -
业务逻辑编排
async def move_project_to_folder( self, project_id: str, folder_id: str, user_id: str ) -> Project: try: # 1. 权限检查 if not await self.check_permission(user_id, project_id): raise PermissionDenied() # 2. 验证文件夹存在 folder = await self.folder_repo.get_by_id(folder_id) if not folder: raise FolderNotFound() # 3. 移动项目 project = await self.repo.move_to_folder(project_id, folder_id) # 4. 记录操作日志 await self.log_repo.create_log(...) # ✅ 统一提交事务 await self.session.commit() return project except Exception as e: await self.session.rollback() raise -
调用多个 Repository
async def clone_project_with_resources( self, source_id: str, new_name: str ) -> Project: try: # 调用多个 Repository source = await self.project_repo.get_by_id(source_id) new_project = await self.project_repo.clone(source, new_name) resources = await self.resource_repo.get_by_project(source_id) for resource in resources: await self.resource_repo.clone(resource, new_project.id) # ✅ 所有操作在一个事务中 await self.session.commit() return new_project except Exception as e: await self.session.rollback() raise -
异常处理和转换
async def update_project( self, project_id: str, data: ProjectUpdate ) -> Project: try: project = await self.repo.update(project_id, data.dict()) if not project: raise ProjectNotFound(project_id) await self.session.commit() return project except IntegrityError as e: await self.session.rollback() raise DuplicateProjectName() from e except Exception as e: await self.session.rollback() raise
2.2 标准模板
class ProjectService:
"""项目业务逻辑层"""
def __init__(
self,
session: AsyncSession,
project_repo: ProjectRepository,
folder_repo: FolderRepository
):
self.session = session
self.project_repo = project_repo
self.folder_repo = folder_repo
async def create_project(self, data: ProjectCreate) -> Project:
"""创建项目"""
try:
# 1. 业务验证
if await self.project_repo.exists_by_name(
data.name,
data.folder_id,
data.owner_id
):
raise DuplicateProjectName()
# 2. 创建项目
project = Project(**data.dict())
created = await self.project_repo.create(project)
# 3. ✅ 提交事务
await self.session.commit()
return created
except Exception as e:
# 4. ✅ 回滚事务
await self.session.rollback()
raise
async def update_project(
self,
project_id: str,
data: ProjectUpdate
) -> Project:
"""更新项目"""
try:
# 1. 权限检查
# 2. 业务验证
# 3. 更新数据
project = await self.project_repo.update(project_id, data.dict())
if not project:
raise ProjectNotFound()
# 4. ✅ 提交事务
await self.session.commit()
return project
except Exception as e:
await self.session.rollback()
raise
async def delete_project(self, project_id: str) -> bool:
"""删除项目"""
try:
# 1. 权限检查
# 2. 业务验证(是否有关联资源等)
# 3. 删除数据
success = await self.project_repo.delete(project_id)
# 4. ✅ 提交事务
await self.session.commit()
return success
except Exception as e:
await self.session.rollback()
raise
3. API 层职责
定位: 接口层(Presentation Layer)
3.1 应该做什么 ✅
-
请求验证
@router.post("/projects", response_model=ProjectResponse) async def create_project( data: ProjectCreate, # ✅ Pydantic 自动验证 current_user: User = Depends(get_current_user), service: ProjectService = Depends(get_project_service) ): # 调用 Service 层 project = await service.create_project(data) return project -
依赖注入
def get_project_service( session: AsyncSession = Depends(get_session) ) -> ProjectService: project_repo = ProjectRepository(session) folder_repo = FolderRepository(session) return ProjectService(session, project_repo, folder_repo) -
响应格式化
@router.get("/projects/{project_id}", response_model=ProjectResponse) async def get_project( project_id: str, service: ProjectService = Depends(get_project_service) ): project = await service.get_project(project_id) if not project: raise HTTPException(status_code=404, detail="项目不存在") return project
3.2 不应该做什么 ❌
-
直接调用 Repository
# ❌ 错误:API 层不应该直接调用 Repository @router.post("/projects") async def create_project( data: ProjectCreate, repo: ProjectRepository = Depends(get_repo) ): project = await repo.create(Project(**data.dict())) await repo.session.commit() # 错误! return project -
业务逻辑
# ❌ 错误:业务逻辑应该在 Service 层 @router.post("/projects") async def create_project(data: ProjectCreate, ...): if await repo.exists_by_name(data.name): raise HTTPException(400, "项目名称已存在") # ...
4. 测试最佳实践
4.1 Repository 单元测试
@pytest.mark.asyncio
class TestProjectRepository:
"""Repository 单元测试"""
async def test_create_project(self, db_session):
"""测试创建项目"""
repo = ProjectRepository(db_session)
# ✅ 在测试方法内创建数据,不使用 fixture
project = Project(
name="测试项目",
type=ProjectType.MINE,
owner_type="user",
owner_id=UUID("00000000-0000-0000-0000-000000000001"),
status=ProjectStatus.ACTIVE
)
created = await repo.create(project)
assert created.id is not None
assert created.name == "测试项目"
# ✅ 不需要手动 commit,conftest 会自动回滚
关键点:
- ✅ 使用
db_sessionfixture(由 conftest 管理事务) - ✅ 在测试方法内直接创建数据
- ✅ 依赖 conftest 的自动回滚机制
- ❌ 不在 fixture 中创建需要 flush/commit 的数据
- ❌ 不在测试中手动 commit
4.2 Service 单元测试
@pytest.mark.asyncio
class TestProjectService:
"""Service 单元测试"""
async def test_create_project(self, db_session):
"""测试创建项目"""
repo = ProjectRepository(db_session)
service = ProjectService(db_session, repo)
data = ProjectCreate(
name="测试项目",
type="mine",
owner_id="00000000-0000-0000-0000-000000000001"
)
# ✅ Service 会调用 commit()
project = await service.create_project(data)
assert project.id is not None
# ✅ conftest 会在测试结束后回滚
4.3 错误的测试模式 ❌
# ❌ 错误:在 fixture 中创建数据
@pytest.fixture
async def sample_project(db_session):
project = Project(...)
db_session.add(project)
await db_session.commit() # 与 conftest 冲突!
return project
# ❌ 错误:在测试中手动 commit
async def test_create(db_session):
repo = ProjectRepository(db_session)
project = await repo.create(Project(...))
await db_session.commit() # 不需要!
后果
优点
- ✅ 职责清晰: Repository、Service、API 各司其职
- ✅ 易于测试: Repository 测试不需要管理事务
- ✅ 事务一致性: Service 层统一管理事务,避免部分提交
- ✅ 代码复用: Repository 方法可以在一个事务中多次调用
- ✅ 易于维护: 架构清晰,新人容易理解
缺点
- ⚠️ 代码量增加: 需要明确的 Service 层
- ⚠️ 学习成本: 开发人员需要理解分层架构
风险
- 开发人员可能忘记在 Service 层调用
commit() - 可能在 Repository 层添加业务逻辑
- 需要在 Code Review 中严格检查
实施计划
阶段 1: 规范制定(已完成)
- 创建 ADR 文档
- 定义 Repository 层职责
- 定义 Service 层职责
- 定义测试最佳实践
阶段 2: 现有代码修复(进行中)
- 修复 ProjectRepository(已完成)
- 修复 ProjectRepository 测试(14/14 通过)
- 检查 FolderRepository
- 检查其他 Repository
- 更新 Service 层添加事务管理
阶段 3: 工具和流程
- 创建 Repository 模板
- 创建 Service 模板
- 添加 pre-commit hook 检查
- 更新 CI/CD 流程
阶段 4: 团队培训
- 团队分享会
- 更新开发文档
- Code Review Checklist
迁移指南
修复现有 Repository
-
查找所有
commit()调用grep -r "await.*session.commit()" server/app/repositories/ -
替换为
flush()# 修改前 await self.session.commit() await self.session.refresh(obj) # 修改后 await self.session.flush() -
移除
refresh()调用# 修改前 await self.session.refresh(project) return project # 修改后 return project # 调用方决定是否需要 refresh
更新 Service 层
-
添加事务管理
async def create_project(self, data: ProjectCreate) -> Project: try: project = await self.repo.create(Project(**data.dict())) await self.session.commit() # ✅ 添加 return project except Exception as e: await self.session.rollback() # ✅ 添加 raise -
统一异常处理
try: # 业务逻辑 await self.session.commit() except IntegrityError as e: await self.session.rollback() raise DuplicateError() from e except Exception as e: await self.session.rollback() raise
修复测试
-
移除 fixture 中的 commit
# 修改前 @pytest.fixture async def sample_project(db_session): project = Project(...) db_session.add(project) await db_session.commit() # ❌ 移除 return project # 修改后:在测试方法内创建 async def test_create(self, db_session): repo = ProjectRepository(db_session) project = Project(...) created = await repo.create(project) # 不需要 commit -
使用字符串比较 UUID
# 避免对象比较问题 assert str(found.id) == str(created.id)
Code Review Checklist
Repository 层
- 没有调用
commit() - 没有调用
refresh() - 使用
flush()确保数据可见性 - 没有业务逻辑判断
- 没有调用其他 Repository
Service 层
- 所有方法都有
try-except块 - 成功时调用
commit() - 异常时调用
rollback() - 业务逻辑清晰
- 异常处理完整
测试
- 使用
db_sessionfixture - 在测试方法内创建数据
- 不手动 commit
- 不在 fixture 中创建需要 flush 的数据
参考资料
- Martin Fowler - Patterns of Enterprise Application Architecture
- Repository Pattern
- Unit of Work Pattern
- SQLAlchemy Session Basics
相关 ADR
- ADR 007: 数据库迁移最佳实践
相关文档
- Changelog:
docs/server/changelogs/2026-02-04-project-repository-transaction-management-fix.md - 测试指南:
server/tests/README.md
变更历史
- 2026-02-04: 初始版本,基于 Project Repository 测试修复总结