# 实体-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[实体类
实现Voable接口] -->|toVo| F[VO类] G[Service层
实现VoableService接口] -->|updateByVo| E end subgraph 缓存策略 H[查询方法
@Cacheable] --> D I[保存/删除方法
@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 toVo(); } ``` - **作用**:定义实体类到VO类的转换方法 - **泛型参数**:T - 目标VO类型 - **核心方法**:`toVo()` - 将实体对象转换为VO对象 ### 2.2 QueryService接口 ```java public interface QueryService { Vo findById(Integer id); Page 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 { void updateByVo(M model, Vo vo); } ``` - **作用**:定义VO对象到实体对象的更新方法 - **泛型参数**: - M - 目标实体类型 - Vo - 源VO类型 - **核心方法**:`updateByVo(M model, Vo vo)` - 将VO对象的属性更新到实体对象 ## 3. 实体-VO转换实现规范 ### 3.1 实体类实现 实体类需同时实现`BasedEntity`、`IdentityEntity`、`NamedEntity`和`Voable`接口,其中`Voable`的泛型参数为对应的VO类型。 **示例实现**: ```java @Getter @Setter @jakarta.persistence.Entity @Table(name = "EMPLOYEE", schema = "supplier_ms") public class Employee implements BasedEntity, IdentityEntity, NamedEntity, Serializable, Voable { @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`、`QueryService`和`VoableService`三个接口,并配置适当的缓存注解。 **示例实现**: ```java @Service @CacheConfig(cacheNames = "employee") public class EmployeeService implements IEntityService, QueryService, VoableService { @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 findAll(JsonNode paramsNode, Pageable pageable) { // 构建查询条件 Specification 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是更新后的新对象 ```