📚 VO 和 PO 的概念
PO (Persistent Object) - 持久化对象
定义: 与数据库表结构一一对应的 Java 对象
特点:
- 直接映射数据库表的字段
- 包含数据库的所有列
- 通常包含数据库相关的注解(如果使用 ORM)
- 字段名通常与数据库列名对应
用途:
- 从数据库读取数据
- 向数据库写入数据
- 在 DAO/Repository 层使用
示例:
// 数据库表: battery
// 列: project_id, ts, value, device, iter
@Data
public class Battery { // PO 类
private String projectId; // 对应 project_id
private Long ts; // 对应 ts
private BigDecimal value; // 对应 value
private String device; // 对应 device
private Integer iter; // 对应 iter
}
VO (Value Object / View Object) - 值对象/视图对象
定义: 用于前后端数据传输的 Java 对象
特点:
- 面向业务需求设计
- 可能包含多个 PO 的数据
- 可能包含计算字段
- 字段名更符合业务语义
- 不包含数据库相关信息
用途:
- API 接口返回数据
- 前端展示数据
- 在 Controller/Service 层使用
示例:
// API 返回给前端的数据结构
@Data
public class ItersPeakRssVO { // VO 类
private Integer iter; // 迭代次数
private String device; // 设备名称
private Long rss; // RSS 内存值(KB)
}
🔄 PO 和 VO 的关系
数据流转过程
数据库 → PO → Service 转换 → VO → Controller → 前端
↑ ↓
└──────────────── 写入数据 ←──────────────────┘
为什么要分开?
1. 解耦数据库和业务逻辑
// 数据库表可能有很多字段
@Data
public class MemoryDetailPO {
private String projectId;
private Long timestamp;
private String deviceId;
private Integer iterationNumber;
private Long rssValue;
private String internalFlag; // 内部使用
private Timestamp createdAt; // 创建时间
private Timestamp updatedAt; // 更新时间
}
// 但前端只需要部分字段
@Data
public class MemoryDetailVO {
private Integer iter; // 重命名更友好
private String device; // 简化字段名
private Long rss; // 只返回需要的数据
}
2. 数据聚合和计算
// 可能需要从多个 PO 聚合数据
public MemorySummaryVO buildSummary(List<MemoryPO> memories, List<DevicePO> devices) {
MemorySummaryVO vo = new MemorySummaryVO();
vo.setTotalRss(memories.stream().mapToLong(MemoryPO::getRss).sum());
vo.setDeviceCount(devices.size());
vo.setAvgRss(vo.getTotalRss() / vo.getDeviceCount());
return vo;
}
3. 安全性
// PO 可能包含敏感信息
@Data
public class UserPO {
private String username;
private String password; // 敏感信息
private String internalId; // 内部ID
}
// VO 只返回安全的信息
@Data
public class UserVO {
private String username;
// 不包含密码和内部ID
}
🎯 在本项目中的应用
场景 1: 简单查询
数据库表: memory_peak_rss
CREATE TABLE memory_peak_rss (
project_id VARCHAR(255),
iter INT,
device VARCHAR(255),
rss BIGINT
);
PO 类:
@Data
public class MemoryPeakRssPO {
private String projectId;
private Integer iter;
private String device;
private Long rss;
}
VO 类:
@Data
public class ItersPeakRssVO {
private Integer iter;
private String device;
private Long rss;
// 注意:不包含 projectId,因为前端不需要
}
转换过程:
@Service
public class MemoryService {
public List<ItersPeakRssVO> getItersPeakRss(String projectId) {
// 1. 从数据库查询 PO
List<MemoryPeakRssPO> poList = memoryDorisService.queryItersPeakRss(projectId);
// 2. 转换 PO → VO
return poList.stream()
.map(po -> {
ItersPeakRssVO vo = new ItersPeakRssVO();
vo.setIter(po.getIter());
vo.setDevice(po.getDevice());
vo.setRss(po.getRss());
// 不设置 projectId
return vo;
})
.collect(Collectors.toList());
}
}
场景 2: 复杂数据结构
VO 类(已存在):
@Data
public class RoundRssVO {
private Integer iter;
private String device;
private Integer round;
private Long rss;
// 动态分类字段 - 这是业务需求,不是数据库字段
private Map<String, Long> categories;
private Map<String, Integer> categoryCounts;
}
可能的数据库表:
-- 方案 1: 扁平化存储
CREATE TABLE memory_round_rss (
project_id VARCHAR(255),
iter INT,
device VARCHAR(255),
round INT,
rss BIGINT,
category VARCHAR(255),
category_rss BIGINT,
category_count INT
);
-- 一个 round 可能有多行数据,每行代表一个分类
PO 类:
@Data
public class MemoryRoundRssPO {
private String projectId;
private Integer iter;
private String device;
private Integer round;
private Long rss;
private String category;
private Long categoryRss;
private Integer categoryCount;
}
转换逻辑:
public List<RoundRssVO> getRoundRss(String projectId) {
// 1. 查询所有数据(多行)
List<MemoryRoundRssPO> poList = memoryDorisService.queryRoundRss(projectId);
// 2. 按 (iter, device, round) 分组
Map<String, List<MemoryRoundRssPO>> grouped = poList.stream()
.collect(Collectors.groupingBy(po ->
po.getIter() + "_" + po.getDevice() + "_" + po.getRound()
));
// 3. 转换为 VO
return grouped.values().stream()
.map(group -> {
RoundRssVO vo = new RoundRssVO();
MemoryRoundRssPO first = group.get(0);
vo.setIter(first.getIter());
vo.setDevice(first.getDevice());
vo.setRound(first.getRound());
vo.setRss(first.getRss());
// 聚合分类数据到 Map
Map<String, Long> categories = new HashMap<>();
Map<String, Integer> counts = new HashMap<>();
for (MemoryRoundRssPO po : group) {
categories.put(po.getCategory(), po.getCategoryRss());
counts.put(po.getCategory(), po.getCategoryCount());
}
vo.setCategories(categories);
vo.setCategoryCounts(counts);
return vo;
})
.collect(Collectors.toList());
}
📊 对比总结
| 特性 | PO (Persistent Object) | VO (Value Object) |
|---|---|---|
| 用途 | 数据库映射 | 数据传输 |
| 位置 | DAO/Repository 层 | Controller/Service 层 |
| 设计依据 | 数据库表结构 | 业务需求 |
| 字段来源 | 数据库列 | 业务逻辑 |
| 是否包含敏感信息 | 可能包含 | 不包含 |
| 是否可以聚合 | 否 | 是 |
| 是否可以计算 | 否 | 是 |
| 命名风格 | 数据库风格 | 业务风格 |
💡 实际例子对比
功耗接口中的例子
PO 类:
// 对应数据库表 battery
@Data
public class Battery {
private String projectId; // 数据库字段
private Long ts; // 数据库字段
private BigDecimal value; // 数据库字段
private String device; // 数据库字段
}
VO 类:
// 返回给前端的数据
@Data
public class CombinationVO {
private List<List<String>> startup; // 聚合后的启动数据
private List<List<String>> userInput; // 聚合后的输入数据
private List<List<String>> battery; // 聚合后的电池数据
private List<List<String>> instrsPerSec; // 聚合后的指令数据
}
🎓 最佳实践
1. 命名规范
// PO 类命名
Battery.java // 直接用表名
MemoryPeakRss.java // 或加 PO 后缀
MemoryPeakRssPO.java
// VO 类命名
BatteryVO.java // 加 VO 后缀
ItersPeakRssVO.java
2. 包结构
model/
├── po/ # PO 类
│ ├── powerConsumption/
│ │ └── Battery.java
│ └── memory/
│ └── MemoryPeakRss.java
└── vo/ # VO 类
├── powerconsumption/
│ └── CombinationVO.java
└── coldstartMemory/
└── ItersPeakRssVO.java
3. 转换工具类(可选)
public class MemoryConverter {
public static ItersPeakRssVO toVO(MemoryPeakRssPO po) {
ItersPeakRssVO vo = new ItersPeakRssVO();
vo.setIter(po.getIter());
vo.setDevice(po.getDevice());
vo.setRss(po.getRss());
return vo;
}
public static List<ItersPeakRssVO> toVOList(List<MemoryPeakRssPO> poList) {
return poList.stream()
.map(MemoryConverter::toVO)
.collect(Collectors.toList());
}
}
总结:
- PO = 数据库的镜像,用于数据持久化
- VO = 业务的视图,用于数据展示
- 转换 = 在 Service 层完成,隔离数据库和业务逻辑
这样设计的好处是:数据库结构变化时,只需要修改 PO 和转换逻辑,不影响 API 接口;业务需求变化时,只需要修改 VO,不影响数据库结构。