重构合同文件管理逻辑,增加错误处理和日志记录 新增ContractBalance实体、Repository和VO类 完善Voable接口文档和实现规范 更新项目架构文档和数据库设计 修复SmbFileService的连接问题 移动合同相关TabSkin类到contract包 添加合同文件重建任务的WebSocket支持
460 lines
14 KiB
Markdown
460 lines
14 KiB
Markdown
# 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 基础接口结构
|
||
```java
|
||
@Repository
|
||
public interface ContractBalanceRepository extends MyRepository<ContractBalance, Integer> {
|
||
// 自定义查询方法
|
||
}
|
||
```
|
||
|
||
### 2.2 ⚠️ 重要错误示例 - 避免这些实现方式
|
||
|
||
**❌ 错误示例1:直接继承JpaRepository**
|
||
```java
|
||
// ContractFileRepository, ContractItemRepository, VendorFileRepository, ProjectQuotationRepository
|
||
@Repository
|
||
public interface ContractFileRepository
|
||
extends JpaRepository<ContractFile, Integer>, JpaSpecificationExecutor<ContractFile> {
|
||
// 错误:不应该直接继承JpaRepository
|
||
List<ContractFile> findByContractId(Integer contractId);
|
||
}
|
||
```
|
||
|
||
**❌ 错误示例2:继承多个冗余接口**
|
||
```java
|
||
// PurchaseOrderRepository
|
||
public interface PurchaseOrderRepository extends
|
||
CrudRepository<PurchaseOrder, Integer>,
|
||
PagingAndSortingRepository<PurchaseOrder, Integer>,
|
||
JpaRepository<PurchaseOrder, Integer>,
|
||
JpaSpecificationExecutor<PurchaseOrder> {
|
||
// 错误:冗余接口继承,应该只继承MyRepository
|
||
}
|
||
```
|
||
|
||
**❌ 错误示例3:缺少@Repository注解**
|
||
```java
|
||
// CompanyFileRepository, ProjectFileRepository
|
||
public interface CompanyFileRepository extends MyRepository<CompanyFile, Integer> {
|
||
// 错误:缺少@Repository注解
|
||
List<CompanyFile> findByCompany(Company company);
|
||
}
|
||
```
|
||
|
||
**❌ 错误示例4:缺少JavaDoc注释**
|
||
```java
|
||
// 多个Repository存在此问题
|
||
public interface VendorFileRepository extends MyRepository<VendorFile, Integer> {
|
||
// 错误:缺少JavaDoc注释说明用途和方法
|
||
List<VendorFile> findAllByVendorId(int vendorId);
|
||
}
|
||
```
|
||
|
||
**✅ 正确实现示例**
|
||
```java
|
||
/**
|
||
* 合同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` - 直接继承JpaRepository
|
||
- `ContractItemRepository` - 直接继承JpaRepository
|
||
- `VendorFileRepository` - 直接继承JpaRepository
|
||
- `ProjectQuotationRepository` - 直接继承JpaRepository
|
||
- `PurchaseOrderRepository` - 继承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`
|
||
|
||
#### 特殊查询类型
|
||
```java
|
||
// 根据业务ID查找
|
||
ContractBalance findByGuid(UUID guid);
|
||
|
||
// 根据合同ID查找(带分页支持)
|
||
Page<ContractBalance> findByContractId(Integer contractId, Pageable pageable);
|
||
|
||
// 根据多个条件组合查询
|
||
List<ContractBalance> findByContractIdAndRefId(Integer contractId, String refId);
|
||
```
|
||
|
||
### 3.2 自定义查询实现
|
||
当方法命名无法满足需求时,使用`@Query`注解:
|
||
|
||
```java
|
||
@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 分页查询
|
||
```java
|
||
// 在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 基础统计
|
||
```java
|
||
@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 复杂聚合
|
||
```java
|
||
@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层的缓存需求:
|
||
|
||
```java
|
||
// 在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 异常处理
|
||
```java
|
||
@Transactional
|
||
public ContractBalance saveWithRetry(ContractBalance balance) {
|
||
try {
|
||
return repository.save(balance);
|
||
} catch (DataIntegrityViolationException e) {
|
||
// 处理数据完整性异常
|
||
throw new BusinessException("数据重复或违反约束", e);
|
||
}
|
||
}
|
||
```
|
||
|
||
## 8. 性能优化
|
||
|
||
### 8.1 懒加载优化
|
||
```java
|
||
// 在查询时指定抓取策略
|
||
@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 批量操作优化
|
||
```java
|
||
// 批量插入
|
||
@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 日志记录
|
||
```java
|
||
@Repository
|
||
@Slf4j
|
||
public class ContractBalanceRepository {
|
||
|
||
@Query("...")
|
||
public List<ContractBalance> findComplexQuery(...) {
|
||
log.debug("执行复杂查询: {}", jpql);
|
||
// 执行查询
|
||
}
|
||
}
|
||
```
|
||
|
||
## 10. 测试策略
|
||
|
||
### 10.1 Repository测试
|
||
```java
|
||
@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 性能测试
|
||
```java
|
||
@Test
|
||
@RepeatedTest(100)
|
||
@Timeout(value = 5, unit = TimeUnit.SECONDS)
|
||
void performanceTest() {
|
||
// 性能测试实现
|
||
}
|
||
```
|
||
|
||
## 11. 最佳实践总结
|
||
|
||
### 11.1 设计原则
|
||
1. **单一职责**:每个Repository只负责一个实体类型
|
||
2. **接口隔离**:根据客户端需求设计专用查询方法
|
||
3. **命名清晰**:方法名应能直观表达查询意图
|
||
4. **性能优先**:合理使用索引和查询优化
|
||
|
||
### 11.2 代码规范
|
||
1. **注解完整**:正确使用`@Repository`、`@Query`、`@Param`
|
||
2. **参数校验**:对传入参数进行null和有效性检查
|
||
3. **异常处理**:捕获并处理数据库访问异常
|
||
4. **日志记录**:记录关键查询操作和性能指标
|
||
|
||
### 11.3 维护性
|
||
1. **版本兼容**:考虑数据库模式变更的影响
|
||
2. **向后兼容**:新增方法不影响现有功能
|
||
3. **文档完整**:复杂查询添加JavaDoc说明
|
||
4. **测试覆盖**:确保关键查询逻辑有测试覆盖
|
||
|
||
## 12. 常见问题和解决方案
|
||
|
||
### 12.1 N+1查询问题
|
||
**问题**:查询列表时触发N+1次关联查询
|
||
**解决**:使用`JOIN FETCH`或`@EntityGraph`
|
||
|
||
### 12.2 性能瓶颈
|
||
**问题**:复杂查询导致响应慢
|
||
**解决**:
|
||
1. 添加数据库索引
|
||
2. 优化查询语句
|
||
3. 使用分页限制结果集
|
||
4. 考虑使用缓存
|
||
|
||
### 12.3 事务死锁
|
||
**问题**:并发操作导致事务死锁
|
||
**解决**:
|
||
1. 合理设计事务边界
|
||
2. 调整事务隔离级别
|
||
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 常见错误检查
|
||
在提交代码前,特别检查是否犯了以下常见错误:
|
||
|
||
#### ❌ 继承层次错误
|
||
```java
|
||
// 错误示例 - 不要这样做
|
||
public interface SomeRepository extends JpaRepository<Entity, Integer>
|
||
public interface AnotherRepository extends CrudRepository<Entity, Integer>
|
||
|
||
// 正确做法 - 统一使用MyRepository
|
||
public interface SomeRepository extends MyRepository<Entity, Integer>
|
||
```
|
||
|
||
#### ❌ 方法命名不规范
|
||
```java
|
||
// 错误示例
|
||
List<Entity> getDataByCondition(String condition) // 使用get前缀而非find
|
||
|
||
// 正确做法
|
||
List<Entity> findByCondition(String condition) // 使用find前缀
|
||
```
|
||
|
||
#### ❌ 缺少必要文档
|
||
```java
|
||
// 错误示例 - 无注释
|
||
List<Entity> findByStatus(String status);
|
||
|
||
// 正确做法 - 完整注释
|
||
/**
|
||
* 根据状态查询实体列表
|
||
*
|
||
* @param status 状态值
|
||
* @return 符合状态的实体列表
|
||
*/
|
||
List<Entity> findByStatus(String status);
|
||
```
|
||
|
||
### 14.3 架构一致性验证
|
||
确保新实现的Repository与项目中其他Repository保持一致:
|
||
|
||
1. **继承模式统一**:所有Repository都必须继承MyRepository
|
||
2. **注解使用统一**:统一使用@Repository注解
|
||
3. **命名约定统一**:遵循Spring Data JPA命名规范
|
||
4. **文档风格统一**:保持JavaDoc注释风格一致
|
||
|
||
### 14.4 后续维护注意事项
|
||
- 新增查询方法时遵循现有命名规范
|
||
- 修改现有方法时保持向后兼容性
|
||
- 定期审查Repository方法的性能和合理性
|
||
- 及时更新相关文档和测试用例
|
||
|
||
---
|
||
|
||
*本文档基于ContractBalanceRepository实现经验总结,结合项目实际情况分析,其他Repository实现应参考此文档规范。特别注意避免文档中提到的常见错误。* |