# ADR 010: Repository 与 Service 层事务管理职责划分 **状态**: 已采纳 **日期**: 2026-02-04 **决策者**: 开发团队 **相关问题**: Project Repository 测试失败、事务管理混乱 ## 背景 在实施 Project Repository 单元测试时,发现了严重的架构设计问题: ### 问题 1: Repository 层错误地管理事务 ```python # ❌ 错误: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: 测试事务冲突 ```python # ❌ 错误: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 应该做什么 ✅ 1. **数据访问逻辑** ```python 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() ``` 2. **查询构建** ```python 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()) ``` 3. **数据转换** ```python async def create(self, project: Project) -> Project: self.session.add(project) await self.session.flush() # ✅ 使用 flush,不是 commit return project ``` 4. **使用 flush() 确保数据可见性** ```python 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 不应该做什么 ❌ 1. **调用 commit() 管理事务** ```python # ❌ 错误 await self.session.commit() ``` 2. **调用 refresh() 刷新对象** ```python # ❌ 错误 await self.session.refresh(project) ``` **原因**: refresh() 会触发数据库查询,应该由调用方决定是否需要 3. **业务逻辑判断** ```python # ❌ 错误:业务逻辑应该在 Service 层 if project.status == ProjectStatus.ARCHIVED: raise BusinessException("不能修改已归档的项目") ``` 4. **调用其他 Repository** ```python # ❌ 错误:跨 Repository 调用应该在 Service 层 folder = await self.folder_repo.get_by_id(folder_id) ``` #### 1.3 标准模板 ```python 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 应该做什么 ✅ 1. **事务管理** ```python 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 ``` 2. **业务逻辑编排** ```python 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 ``` 3. **调用多个 Repository** ```python 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 ``` 4. **异常处理和转换** ```python 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 标准模板 ```python 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 应该做什么 ✅ 1. **请求验证** ```python @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 ``` 2. **依赖注入** ```python 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) ``` 3. **响应格式化** ```python @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 不应该做什么 ❌ 1. **直接调用 Repository** ```python # ❌ 错误: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 ``` 2. **业务逻辑** ```python # ❌ 错误:业务逻辑应该在 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 单元测试 ```python @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_session` fixture(由 conftest 管理事务) - ✅ 在测试方法内直接创建数据 - ✅ 依赖 conftest 的自动回滚机制 - ❌ 不在 fixture 中创建需要 flush/commit 的数据 - ❌ 不在测试中手动 commit #### 4.2 Service 单元测试 ```python @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 错误的测试模式 ❌ ```python # ❌ 错误:在 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() # 不需要! ``` ## 后果 ### 优点 1. ✅ **职责清晰**: Repository、Service、API 各司其职 2. ✅ **易于测试**: Repository 测试不需要管理事务 3. ✅ **事务一致性**: Service 层统一管理事务,避免部分提交 4. ✅ **代码复用**: Repository 方法可以在一个事务中多次调用 5. ✅ **易于维护**: 架构清晰,新人容易理解 ### 缺点 1. ⚠️ **代码量增加**: 需要明确的 Service 层 2. ⚠️ **学习成本**: 开发人员需要理解分层架构 ### 风险 1. 开发人员可能忘记在 Service 层调用 `commit()` 2. 可能在 Repository 层添加业务逻辑 3. 需要在 Code Review 中严格检查 ## 实施计划 ### 阶段 1: 规范制定(已完成) - [x] 创建 ADR 文档 - [x] 定义 Repository 层职责 - [x] 定义 Service 层职责 - [x] 定义测试最佳实践 ### 阶段 2: 现有代码修复(进行中) - [x] 修复 ProjectRepository(已完成) - [x] 修复 ProjectRepository 测试(14/14 通过) - [ ] 检查 FolderRepository - [ ] 检查其他 Repository - [ ] 更新 Service 层添加事务管理 ### 阶段 3: 工具和流程 - [ ] 创建 Repository 模板 - [ ] 创建 Service 模板 - [ ] 添加 pre-commit hook 检查 - [ ] 更新 CI/CD 流程 ### 阶段 4: 团队培训 - [ ] 团队分享会 - [ ] 更新开发文档 - [ ] Code Review Checklist ## 迁移指南 ### 修复现有 Repository 1. **查找所有 `commit()` 调用** ```bash grep -r "await.*session.commit()" server/app/repositories/ ``` 2. **替换为 `flush()`** ```python # 修改前 await self.session.commit() await self.session.refresh(obj) # 修改后 await self.session.flush() ``` 3. **移除 `refresh()` 调用** ```python # 修改前 await self.session.refresh(project) return project # 修改后 return project # 调用方决定是否需要 refresh ``` ### 更新 Service 层 1. **添加事务管理** ```python 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 ``` 2. **统一异常处理** ```python 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 ``` ### 修复测试 1. **移除 fixture 中的 commit** ```python # 修改前 @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 ``` 2. **使用字符串比较 UUID** ```python # 避免对象比较问题 assert str(found.id) == str(created.id) ``` ## Code Review Checklist ### Repository 层 - [ ] 没有调用 `commit()` - [ ] 没有调用 `refresh()` - [ ] 使用 `flush()` 确保数据可见性 - [ ] 没有业务逻辑判断 - [ ] 没有调用其他 Repository ### Service 层 - [ ] 所有方法都有 `try-except` 块 - [ ] 成功时调用 `commit()` - [ ] 异常时调用 `rollback()` - [ ] 业务逻辑清晰 - [ ] 异常处理完整 ### 测试 - [ ] 使用 `db_session` fixture - [ ] 在测试方法内创建数据 - [ ] 不手动 commit - [ ] 不在 fixture 中创建需要 flush 的数据 ## 参考资料 - [Martin Fowler - Patterns of Enterprise Application Architecture](https://martinfowler.com/eaaCatalog/) - [Repository Pattern](https://martinfowler.com/eaaCatalog/repository.html) - [Unit of Work Pattern](https://martinfowler.com/eaaCatalog/unitOfWork.html) - [SQLAlchemy Session Basics](https://docs.sqlalchemy.org/en/20/orm/session_basics.html) ## 相关 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 测试修复总结