Files
contract-manager/docs/task/server模块service缓存调整为Vo对象/DESIGN_entity_vo_conversion.md
songqq 49413ad473 refactor(service): 统一Service缓存为VO对象并优化关联实体处理
重构Service类实现,将QueryService泛型参数调整为VO类型,确保缓存VO对象而非实体。优化关联实体处理逻辑,减少重复代码。修改findById方法返回VO对象,新增getById方法获取实体。更新相关调用点以适配新接口。

调整WebSocket处理、控制器及Service实现,确保数据类型一致性。完善文档记录重构过程及发现的问题。为后续优化提供基础架构支持。
2025-09-29 19:31:51 +08:00

339 lines
9.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 实体-VO转换机制与缓存策略设计
## 1. 整体架构与设计原则
### 1.1 核心架构
```mermaid
flowchart TD
A[客户端/调用方] -->|请求VO数据| B[Service层]
B -->|查询| C[Repository层]
C -->|返回实体| B
B -->|实体转VO| D[缓存层]
D -->|返回缓存VO| B
B -->|返回VO| A
subgraph 实体-VO转换机制
E[实体类<br/>实现Voable接口] -->|toVo| F[VO类]
G[Service层<br/>实现VoableService接口] -->|updateByVo| E
end
subgraph 缓存策略
H[查询方法<br/>@Cacheable] --> D
I[保存/删除方法<br/>@CacheEvict/@Caching] -->|清除缓存| D
end
```
### 1.2 设计原则
1. **职责分离**实体类负责数据持久化VO类负责数据传输和展示
2. **转换封装**实体到VO的转换逻辑封装在实体类内部
3. **双向转换**支持实体转VO和VO更新实体两种转换方向
4. **关联处理**关联实体在VO中通常只保留ID引用
5. **缓存一致性**:更新/删除操作需同步清理相关缓存
6. **类型安全**:使用泛型确保类型安全
## 2. 核心接口定义
### 2.1 Voable接口
```java
public interface Voable<T> {
T toVo();
}
```
- **作用**定义实体类到VO类的转换方法
- **泛型参数**T - 目标VO类型
- **核心方法**`toVo()` - 将实体对象转换为VO对象
### 2.2 QueryService接口
```java
public interface QueryService<Vo> {
Vo findById(Integer id);
Page<Vo> findAll(JsonNode paramsNode, Pageable pageable);
// 其他查询方法...
}
```
- **作用**定义返回VO对象的查询服务接口
- **泛型参数**Vo - 返回的VO类型
- **核心方法**
- `findById(Integer id)` - 根据ID查询单个VO对象
- `findAll(JsonNode paramsNode, Pageable pageable)` - 分页查询VO对象列表
### 2.3 VoableService接口
```java
public interface VoableService<M, Vo> {
void updateByVo(M model, Vo vo);
}
```
- **作用**定义VO对象到实体对象的更新方法
- **泛型参数**
- M - 目标实体类型
- Vo - 源VO类型
- **核心方法**`updateByVo(M model, Vo vo)` - 将VO对象的属性更新到实体对象
## 3. 实体-VO转换实现规范
### 3.1 实体类实现
实体类需同时实现`BasedEntity``IdentityEntity``NamedEntity``Voable<T>`接口,其中`Voable<T>`的泛型参数为对应的VO类型。
**示例实现**
```java
@Getter
@Setter
@jakarta.persistence.Entity
@Table(name = "EMPLOYEE", schema = "supplier_ms")
public class Employee implements BasedEntity, IdentityEntity, NamedEntity, Serializable, Voable<EmployeeVo> {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID", nullable = false)
private Integer id;
// 其他实体字段...
@JoinColumn(name = "DEPARTMENT_ID")
@ManyToOne(fetch = FetchType.LAZY)
@ToString.Exclude
@JsonIgnoreProperties({ "leader" })
private Department department;
// 其他关联字段...
@Override
public EmployeeVo toVo() {
EmployeeVo vo = new EmployeeVo();
// 设置基本字段
vo.setId(id);
vo.setName(name);
// ...其他字段设置
// 处理关联实体只保留ID
if (getDepartment() != null) {
vo.setDepartmentId(getDepartment().getId());
}
return vo;
}
}
```
### 3.2 VO类设计
VO类通常包含实体的核心字段实现`IdentityEntity``NamedEntity`等标识接口,并使用`@Data`注解简化代码。
**示例实现**
```java
@Data
public class EmployeeVo implements IdentityEntity, NamedEntity {
private Integer id;
private String account;
private String name;
private String alias;
private String code;
private Integer departmentId; // 关联实体只保留ID
private String phone;
private String email;
// 其他需要传输的字段...
}
```
### 3.3 Service类实现
Service类需同时实现`IEntityService<T>``QueryService<Vo>``VoableService<T, Vo>`三个接口,并配置适当的缓存注解。
**示例实现**
```java
@Service
@CacheConfig(cacheNames = "employee")
public class EmployeeService
implements IEntityService<Employee>, QueryService<EmployeeVo>, VoableService<Employee, EmployeeVo> {
@Autowired
private EmployeeRepository employeeRepository;
// 实现QueryService接口的查询方法返回VO对象并缓存
@Cacheable(key = "#p0")
public EmployeeVo findById(Integer id) {
return employeeRepository.findById(id).map(Employee::toVo).orElse(null);
}
@Override
public Page<EmployeeVo> findAll(JsonNode paramsNode, Pageable pageable) {
// 构建查询条件
Specification<Employee> spec = buildSpecification(paramsNode);
// 查询并转换为VO对象
return findAll(spec, pageable).map(Employee::toVo);
}
// 实现IEntityService接口的方法操作实体
@Caching(evict = {
@CacheEvict(key = "#p0.id"),
@CacheEvict(key = "'name-'+#p0.name")
})
public Employee save(Employee employee) {
return employeeRepository.save(employee);
}
// 实现VoableService接口的方法更新实体
@Override
public void updateByVo(Employee entity, EmployeeVo vo) {
// 更新基本字段
entity.setName(vo.getName());
entity.setAccount(vo.getAccount());
// ...其他字段更新
// 处理关联实体
if (vo.getDepartmentId() != null) {
if(entity.getDepartment()==null || !entity.getDepartment().getId().equals(vo.getDepartmentId())){
Department department = SpringApp.getBean(DepartmentService.class).getById(vo.getDepartmentId());
entity.setDepartment(department);
}
} else {
entity.setDepartment(null);
}
}
}
```
## 4. 缓存策略规范
### 4.1 缓存配置
- 使用`@CacheConfig(cacheNames = "xxx")`在类级别定义缓存名称
- 缓存名称应与实体类型对应,如`employee``contract`
### 4.2 查询缓存
- 使用`@Cacheable(key = "#p0")`缓存单对象查询结果
- 缓存键设计:
- 基于ID`key = "#p0"`
- 基于业务键:`key = "'name-'+#p0"``key = "'code-'+#p0"`
- 注意findAll方法不应缓存以避免缓存过大
### 4.3 缓存清理
- 使用`@Caching(evict = {...})`在保存/删除操作时清理相关缓存
- 清理策略:
- 清理基于ID的缓存`@CacheEvict(key = "#p0.id")`
- 清理基于业务键的缓存:`@CacheEvict(key = "'name-'+#p0.name")`
- 批量清理:`@CacheEvict(allEntries = true)`(谨慎使用)
## 5. 关联实体处理
### 5.1 实体转VO时的关联处理
- 对于`@ManyToOne`关联在VO中只保留关联实体的ID不加载完整关联对象
- 对于`@OneToMany``@ManyToMany`关联通常在VO中不直接包含关联集合而是通过单独的查询获取
- 避免在转换过程中触发懒加载导致的性能问题
### 5.2 VO更新实体时的关联处理
- 根据VO中的关联ID查找关联实体
- 比较现有关联和新关联,仅在不同时进行更新
- 使用`SpringApp.getBean(ServiceClass.class)`获取相关Service进行关联实体查询
## 6. 最佳实践与注意事项
1. **避免循环引用**在实体和VO的转换中注意避免循环引用导致的堆栈溢出
2. **延迟加载处理**:处理懒加载关联时,确保在事务内完成转换,避免`LazyInitializationException`
3. **缓存键唯一性**:确保缓存键全局唯一,避免不同实体类型之间的缓存冲突
4. **缓存粒度控制**:合理设计缓存粒度,避免缓存过大或频繁失效
5. **版本控制**VO对象应包含version字段用于并发控制
6. **批量操作缓存处理**:批量操作时需特别注意缓存清理策略,确保缓存一致性
7. **继承体系处理**对于有继承关系的实体类需特别注意toVo方法的实现和缓存策略
8. **单元测试覆盖**:确保转换逻辑和缓存策略有充分的单元测试覆盖
## 7. 输入输出示例
#### 输入输出示例
**实体转VO**
输入:
```java
// Employee实体对象
Employee employee = new Employee();
employee.setId(1);
employee.setName("张三");
employee.setCode("EMP001");
Department dept = new Department();
dept.setId(101);
employee.setDepartment(dept);
// 调用toVo方法
EmployeeVo vo = employee.toVo();
```
输出:
```java
// EmployeeVo对象
{
"id": 1,
"name": "张三",
"code": "EMP001",
"departmentId": 101,
// 其他字段...
}
```
**VO更新实体**
输入:
```java
// 现有Employee实体对象
Employee employee = employeeRepository.findById(1).orElse(null);
// EmployeeVo对象包含更新信息
EmployeeVo vo = new EmployeeVo();
vo.setId(1);
vo.setName("李四");
vo.setDepartmentId(102);
// 调用updateByVo方法更新实体
employeeService.updateByVo(employee, vo);
```
输出:
```java
// 更新后的Employee实体对象
{
"id": 1,
"name": "李四",
"department": {"id": 102, /* 其他部门信息 */},
// 其他字段保持不变或更新
}
```
**缓存使用**
输入:
```java
// 首次查询,会从数据库获取并缓存
EmployeeVo vo1 = employeeService.findById(1);
// 再次查询,会从缓存获取
EmployeeVo vo2 = employeeService.findById(1);
// 更新操作,会清除缓存
Employee employee = employeeRepository.findById(1).orElse(null);
employee.setName("王五");
employeeService.save(employee);
// 再次查询,会从数据库重新获取并缓存
EmployeeVo vo3 = employeeService.findById(1);
```
输出:
```java
// vo1和vo2是同一个对象或内容相同的不同对象取决于缓存实现
// vo3是更新后的新对象
```