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

9.6 KiB
Raw Blame History

实体-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 设计原则

  1. 职责分离实体类负责数据持久化VO类负责数据传输和展示
  2. 转换封装实体到VO的转换逻辑封装在实体类内部
  3. 双向转换支持实体转VO和VO更新实体两种转换方向
  4. 关联处理关联实体在VO中通常只保留ID引用
  5. 缓存一致性:更新/删除操作需同步清理相关缓存
  6. 类型安全:使用泛型确保类型安全

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 实体类实现

实体类需同时实现BasedEntityIdentityEntityNamedEntityVoable<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类通常包含实体的核心字段实现IdentityEntityNamedEntity等标识接口,并使用@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")在类级别定义缓存名称
  • 缓存名称应与实体类型对应,如employeecontract

4.2 查询缓存

  • 使用@Cacheable(key = "#p0")缓存单对象查询结果
  • 缓存键设计:
    • 基于IDkey = "#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

输入:

// 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是更新后的新对象