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