重构合同文件管理逻辑,增加错误处理和日志记录 新增ContractBalance实体、Repository和VO类 完善Voable接口文档和实现规范 更新项目架构文档和数据库设计 修复SmbFileService的连接问题 移动合同相关TabSkin类到contract包 添加合同文件重建任务的WebSocket支持
14 KiB
14 KiB
Server模块 Repository 实现经验总结
1. Repository 基本架构
1.1 接口设计原则
- Repository接口必须继承
MyRepository<T, ID>基类 - 使用
@Repository注解标记为Spring组件 - 遵循接口隔离原则,按业务功能组织方法
1.2 包结构规范
com.ecep.contract.ds.{business}.repository.{EntityName}Repository.java
示例:ContractBalanceRepository位于com.ecep.contract.ds.contract.repository
2. 接口继承层次
2.1 基础接口结构
@Repository
public interface ContractBalanceRepository extends MyRepository<ContractBalance, Integer> {
// 自定义查询方法
}
2.2 ⚠️ 重要错误示例 - 避免这些实现方式
❌ 错误示例1:直接继承JpaRepository
// ContractFileRepository, ContractItemRepository, VendorFileRepository, ProjectQuotationRepository
@Repository
public interface ContractFileRepository
extends JpaRepository<ContractFile, Integer>, JpaSpecificationExecutor<ContractFile> {
// 错误:不应该直接继承JpaRepository
List<ContractFile> findByContractId(Integer contractId);
}
❌ 错误示例2:继承多个冗余接口
// PurchaseOrderRepository
public interface PurchaseOrderRepository extends
CrudRepository<PurchaseOrder, Integer>,
PagingAndSortingRepository<PurchaseOrder, Integer>,
JpaRepository<PurchaseOrder, Integer>,
JpaSpecificationExecutor<PurchaseOrder> {
// 错误:冗余接口继承,应该只继承MyRepository
}
❌ 错误示例3:缺少@Repository注解
// CompanyFileRepository, ProjectFileRepository
public interface CompanyFileRepository extends MyRepository<CompanyFile, Integer> {
// 错误:缺少@Repository注解
List<CompanyFile> findByCompany(Company company);
}
❌ 错误示例4:缺少JavaDoc注释
// 多个Repository存在此问题
public interface VendorFileRepository extends MyRepository<VendorFile, Integer> {
// 错误:缺少JavaDoc注释说明用途和方法
List<VendorFile> findAllByVendorId(int vendorId);
}
✅ 正确实现示例
/**
* 合同Repository - 提供合同相关的数据库访问操作
*/
@Repository
public interface ContractRepository extends MyRepository<Contract, Integer> {
// 正确:统一继承MyRepository,有完整的JavaDoc
/**
* 根据合同代码查询合同
* @param code 合同代码
* @return 合同 Optional
*/
Optional<Contract> findByCode(String code);
/**
* 根据状态查询合同列表
* @param status 合同状态
* @return 合同列表
*/
List<Contract> findByStatus(ContractStatus status);
}
2.3 当前项目错误统计
基于对全项目22个Repository的全面分析,发现以下问题分布:
🔴 继承层次错误(5个):
ContractFileRepository- 直接继承JpaRepositoryContractItemRepository- 直接继承JpaRepositoryVendorFileRepository- 直接继承JpaRepositoryProjectQuotationRepository- 直接继承JpaRepositoryPurchaseOrderRepository- 继承4个冗余接口
🟡 注解缺失(2个):
CompanyFileRepository- 缺少@Repository注解ProjectFileRepository- 缺少@Repository注解
🟡 文档注释不完整(4个):
CompanyFileRepository- 缺少JavaDoc注释ProjectFileRepository- 缺少JavaDoc注释VendorFileRepository- 缺少JavaDoc注释ProjectQuotationRepository- 缺少JavaDoc注释
✅ 正确实现(17个):
- contract: ContractRepository, ContractBalanceRepository, SalesOrderRepository, ContractTypeRepository, ContractKindRepository, ContractInvoiceRepository
- company: CompanyRepository
- customer: CompanyCustomerRepository, CustomerCatalogRepository
- project: ProjectRepository
- vendor: VendorRepository
2.5 MyRepository基类能力
MyRepository接口继承结构:
JpaRepository提供CRUD操作- 继承
JpaSpecificationExecutor提供条件查询能力 - 继承
QueryByExampleExecutor提供示例查询能力
3. 查询方法设计
3.1 方法命名规范
使用Spring Data JPA的查询方法命名约定:
基本查询方法
findBy{属性名}- 根据属性查找findBy{属性名}And{属性名}- 多条件AND查询findBy{属性名}Or{属性名}- 多条件OR查询findBy{属性名}OrderBy{排序属性}- 带排序查询
关联对象查询
findBy{关联对象属性名}.{关联对象属性}- 嵌套属性查询- 支持多级嵌套,如:
findByContractCompanyName
特殊查询类型
// 根据业务ID查找
ContractBalance findByGuid(UUID guid);
// 根据合同ID查找(带分页支持)
Page<ContractBalance> findByContractId(Integer contractId, Pageable pageable);
// 根据多个条件组合查询
List<ContractBalance> findByContractIdAndRefId(Integer contractId, String refId);
3.2 自定义查询实现
当方法命名无法满足需求时,使用@Query注解:
@Query("SELECT cb FROM ContractBalance cb WHERE cb.contract.id = :contractId " +
"AND cb.guid = :guid AND cb.setupDate >= :startDate")
Page<ContractBalance> findByContractAndGuidAndDateRange(
@Param("contractId") Integer contractId,
@Param("guid") UUID guid,
@Param("startDate") LocalDateTime startDate,
Pageable pageable
);
4. 分页和排序支持
4.1 分页查询
// 在Repository接口中定义
Page<ContractBalance> findByContractId(Integer contractId, Pageable pageable);
// 在Service中调用
Page<ContractBalance> result = repository.findByContractId(contractId,
PageRequest.of(page, size, Sort.by("setupDate").descending()));
4.2 排序规范
- 默认按主键ID降序排列
- 业务相关字段按创建时间降序
- 支持多字段排序:
Sort.by("field1").ascending().and(Sort.by("field2").descending())
5. 统计和聚合查询
5.1 基础统计
@Query("SELECT COUNT(cb) FROM ContractBalance cb WHERE cb.contract.id = :contractId")
Long countByContractId(@Param("contractId") Integer contractId);
@Query("SELECT SUM(cb.balanceAmount) FROM ContractBalance cb WHERE cb.contract.id = :contractId")
BigDecimal sumBalanceByContractId(@Param("contractId") Integer contractId);
5.2 复杂聚合
@Query("SELECT new com.ecep.contract.vo.ContractBalanceStatsVO(" +
"cb.contract.id, COUNT(cb), SUM(cb.balanceAmount)) " +
"FROM ContractBalance cb GROUP BY cb.contract.id")
List<ContractBalanceStatsVO> getBalanceStatsByContract();
6. 缓存策略
6.1 缓存注解使用
虽然Repository本身不直接使用缓存,但需要考虑Service层的缓存需求:
// 在Service层配合使用
@Cacheable(key = "#p0")
ContractBalance findById(Integer id);
@CacheEvict(key = "#p0.id")
ContractBalance save(ContractBalance contractBalance);
6.2 缓存键设计原则
- 单一记录:使用主键作为键,如
"balance-" + id - 按业务维度缓存:使用业务标识,如
"contract-" + contractId - 避免缓存键冲突
7. 事务管理
7.1 事务传播级别
- 查询方法:默认
REQUIRED - 写操作:明确指定
@Transactional(propagation = Propagation.REQUIRED)
7.2 异常处理
@Transactional
public ContractBalance saveWithRetry(ContractBalance balance) {
try {
return repository.save(balance);
} catch (DataIntegrityViolationException e) {
// 处理数据完整性异常
throw new BusinessException("数据重复或违反约束", e);
}
}
8. 性能优化
8.1 懒加载优化
// 在查询时指定抓取策略
@Query("SELECT cb FROM ContractBalance cb LEFT JOIN FETCH cb.contract " +
"LEFT JOIN FETCH cb.employee WHERE cb.id = :id")
Optional<ContractBalance> findByIdWithRelations(@Param("id") Integer id);
8.2 批量操作优化
// 批量插入
@Modifying
@Query("DELETE FROM ContractBalance cb WHERE cb.contract.id IN :contractIds")
void deleteByContractIds(@Param("contractIds") List<Integer> contractIds);
9. 错误处理和调试
9.1 常见异常类型
DataAccessException- 数据访问异常DataIntegrityViolationException- 数据完整性异常EmptyResultDataAccessException- 空结果异常
9.2 日志记录
@Repository
@Slf4j
public class ContractBalanceRepository {
@Query("...")
public List<ContractBalance> findComplexQuery(...) {
log.debug("执行复杂查询: {}", jpql);
// 执行查询
}
}
10. 测试策略
10.1 Repository测试
@DataJpaTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class ContractBalanceRepositoryTest {
@Autowired
private TestEntityManager em;
@Autowired
private ContractBalanceRepository repository;
@Test
void shouldFindByContractId() {
// 测试数据准备
ContractBalance balance = createTestBalance();
// 执行测试
Page<ContractBalance> result = repository.findByContractId(
balance.getContract().getId(),
PageRequest.of(0, 10)
);
// 断言验证
assertThat(result.getContent()).hasSize(1);
assertThat(result.getContent().get(0)).isEqualTo(balance);
}
}
10.2 性能测试
@Test
@RepeatedTest(100)
@Timeout(value = 5, unit = TimeUnit.SECONDS)
void performanceTest() {
// 性能测试实现
}
11. 最佳实践总结
11.1 设计原则
- 单一职责:每个Repository只负责一个实体类型
- 接口隔离:根据客户端需求设计专用查询方法
- 命名清晰:方法名应能直观表达查询意图
- 性能优先:合理使用索引和查询优化
11.2 代码规范
- 注解完整:正确使用
@Repository、@Query、@Param - 参数校验:对传入参数进行null和有效性检查
- 异常处理:捕获并处理数据库访问异常
- 日志记录:记录关键查询操作和性能指标
11.3 维护性
- 版本兼容:考虑数据库模式变更的影响
- 向后兼容:新增方法不影响现有功能
- 文档完整:复杂查询添加JavaDoc说明
- 测试覆盖:确保关键查询逻辑有测试覆盖
12. 常见问题和解决方案
12.1 N+1查询问题
问题:查询列表时触发N+1次关联查询
解决:使用JOIN FETCH或@EntityGraph
12.2 性能瓶颈
问题:复杂查询导致响应慢 解决:
- 添加数据库索引
- 优化查询语句
- 使用分页限制结果集
- 考虑使用缓存
12.3 事务死锁
问题:并发操作导致事务死锁 解决:
- 合理设计事务边界
- 调整事务隔离级别
- 实现重试机制
13. 扩展建议
13.1 动态查询支持
考虑使用Querydsl或Specification支持动态查询构建。
13.2 读写分离
实现主从数据库分离,提升查询性能。
13.3 分布式缓存
结合Redis等分布式缓存,提升查询性能。
14. 实施检查清单
14.1 Repository实现后验证步骤
在每个Repository实现完成后,请按以下清单进行验证:
✅ 基础架构检查
- Repository接口继承了
MyRepository<T, ID> - 使用了
@Repository注解 - 包路径符合规范:
com.ecep.contract.ds.{business}.repository - 类名符合命名规范:
{EntityName}Repository
✅ 方法设计检查
- 遵循Spring Data JPA命名约定
- 包含必要的业务查询方法
- 复杂查询使用
@Query注解 - 分页方法支持
Pageable参数
✅ 文档和注释检查
- 类级别JavaDoc注释完整
- 关键方法有JavaDoc说明
- 参数和返回值有明确说明
✅ 性能和质量检查
- 避免N+1查询问题
- 适当使用索引提示
- 包含必要的单元测试
- 异常处理考虑周全
14.2 常见错误检查
在提交代码前,特别检查是否犯了以下常见错误:
❌ 继承层次错误
// 错误示例 - 不要这样做
public interface SomeRepository extends JpaRepository<Entity, Integer>
public interface AnotherRepository extends CrudRepository<Entity, Integer>
// 正确做法 - 统一使用MyRepository
public interface SomeRepository extends MyRepository<Entity, Integer>
❌ 方法命名不规范
// 错误示例
List<Entity> getDataByCondition(String condition) // 使用get前缀而非find
// 正确做法
List<Entity> findByCondition(String condition) // 使用find前缀
❌ 缺少必要文档
// 错误示例 - 无注释
List<Entity> findByStatus(String status);
// 正确做法 - 完整注释
/**
* 根据状态查询实体列表
*
* @param status 状态值
* @return 符合状态的实体列表
*/
List<Entity> findByStatus(String status);
14.3 架构一致性验证
确保新实现的Repository与项目中其他Repository保持一致:
- 继承模式统一:所有Repository都必须继承MyRepository
- 注解使用统一:统一使用@Repository注解
- 命名约定统一:遵循Spring Data JPA命名规范
- 文档风格统一:保持JavaDoc注释风格一致
14.4 后续维护注意事项
- 新增查询方法时遵循现有命名规范
- 修改现有方法时保持向后兼容性
- 定期审查Repository方法的性能和合理性
- 及时更新相关文档和测试用例
本文档基于ContractBalanceRepository实现经验总结,结合项目实际情况分析,其他Repository实现应参考此文档规范。特别注意避免文档中提到的常见错误。