# 实体-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是更新后的新对象
```