重构Service类实现,将QueryService泛型参数调整为VO类型,确保缓存VO对象而非实体。优化关联实体处理逻辑,减少重复代码。修改findById方法返回VO对象,新增getById方法获取实体。更新相关调用点以适配新接口。 调整WebSocket处理、控制器及Service实现,确保数据类型一致性。完善文档记录重构过程及发现的问题。为后续优化提供基础架构支持。
9.6 KiB
9.6 KiB
实体-VO转换机制与缓存策略设计
1. 整体架构与设计原则
1.1 核心架构
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 设计原则
- 职责分离:实体类负责数据持久化,VO类负责数据传输和展示
- 转换封装:实体到VO的转换逻辑封装在实体类内部
- 双向转换:支持实体转VO和VO更新实体两种转换方向
- 关联处理:关联实体在VO中通常只保留ID引用
- 缓存一致性:更新/删除操作需同步清理相关缓存
- 类型安全:使用泛型确保类型安全
2. 核心接口定义
2.1 Voable接口
public interface Voable<T> {
T toVo();
}
- 作用:定义实体类到VO类的转换方法
- 泛型参数:T - 目标VO类型
- 核心方法:
toVo()- 将实体对象转换为VO对象
2.2 QueryService接口
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接口
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类型。
示例实现:
@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注解简化代码。
示例实现:
@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>三个接口,并配置适当的缓存注解。
示例实现:
@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"
- 基于ID:
- 注意:findAll方法不应缓存,以避免缓存过大
4.3 缓存清理
- 使用
@Caching(evict = {...})在保存/删除操作时清理相关缓存 - 清理策略:
- 清理基于ID的缓存:
@CacheEvict(key = "#p0.id") - 清理基于业务键的缓存:
@CacheEvict(key = "'name-'+#p0.name") - 批量清理:
@CacheEvict(allEntries = true)(谨慎使用)
- 清理基于ID的缓存:
5. 关联实体处理
5.1 实体转VO时的关联处理
- 对于
@ManyToOne关联:在VO中只保留关联实体的ID,不加载完整关联对象 - 对于
@OneToMany和@ManyToMany关联:通常在VO中不直接包含关联集合,而是通过单独的查询获取 - 避免在转换过程中触发懒加载导致的性能问题
5.2 VO更新实体时的关联处理
- 根据VO中的关联ID查找关联实体
- 比较现有关联和新关联,仅在不同时进行更新
- 使用
SpringApp.getBean(ServiceClass.class)获取相关Service进行关联实体查询
6. 最佳实践与注意事项
- 避免循环引用:在实体和VO的转换中注意避免循环引用导致的堆栈溢出
- 延迟加载处理:处理懒加载关联时,确保在事务内完成转换,避免
LazyInitializationException - 缓存键唯一性:确保缓存键全局唯一,避免不同实体类型之间的缓存冲突
- 缓存粒度控制:合理设计缓存粒度,避免缓存过大或频繁失效
- 版本控制:VO对象应包含version字段,用于并发控制
- 批量操作缓存处理:批量操作时需特别注意缓存清理策略,确保缓存一致性
- 继承体系处理:对于有继承关系的实体类,需特别注意toVo方法的实现和缓存策略
- 单元测试覆盖:确保转换逻辑和缓存策略有充分的单元测试覆盖
7. 输入输出示例
输入输出示例
实体转VO:
输入:
// 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();
输出:
// EmployeeVo对象
{
"id": 1,
"name": "张三",
"code": "EMP001",
"departmentId": 101,
// 其他字段...
}
VO更新实体:
输入:
// 现有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);
输出:
// 更新后的Employee实体对象
{
"id": 1,
"name": "李四",
"department": {"id": 102, /* 其他部门信息 */},
// 其他字段保持不变或更新
}
缓存使用:
输入:
// 首次查询,会从数据库获取并缓存
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);
输出:
// vo1和vo2是同一个对象或内容相同的不同对象(取决于缓存实现)
// vo3是更新后的新对象