refactor(service): 统一Service缓存为VO对象并优化关联实体处理
重构Service类实现,将QueryService泛型参数调整为VO类型,确保缓存VO对象而非实体。优化关联实体处理逻辑,减少重复代码。修改findById方法返回VO对象,新增getById方法获取实体。更新相关调用点以适配新接口。 调整WebSocket处理、控制器及Service实现,确保数据类型一致性。完善文档记录重构过程及发现的问题。为后续优化提供基础架构支持。
This commit is contained in:
@@ -0,0 +1,339 @@
|
||||
# 实体-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是更新后的新对象
|
||||
```
|
||||
Reference in New Issue
Block a user