web后端开发概念: VO 和 PO

29 阅读4分钟

📚 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,不影响数据库结构。