理解 Java 后端:Entity 和 VO 的作用与区别

        这篇笔记整理了 Java 后端开发中两个重要的概念:实体类(Entity) 和 值对象(Value Object,也叫 DTO)。它们都用来存储数据,但在后端架构中扮演着不同的角色。理解它们的区别,有助于我们更好地构建后端应用。

1. 实体类(Entity):核心概念与持久化

实体类在领域驱动设计(DDD)中扮演着至关重要的角色,它代表着业务领域中的核心概念。在持久层框架(如 JPA 或 MyBatis)中,实体类更是与数据库表结构紧密关联,负责数据的持久化和业务逻辑的处理

  • 核心职责:
  • 数据持久化映射: 实体类的属性直接映射到数据库表的列,负责将应用程序的状态持久化到数据存储中。
  • 代表业务实体: 实体类是对真实世界业务概念的抽象, 实体类封装了应用程序的核心业务数据和行为。例如,在一个电商系统中,User、Product、Order 都是典型的实体。
  • 承载业务行为: 实体类可以封装与自身状态相关的业务逻辑,确保业务规则的完整性。
  • 具备唯一标识: 实体对象通过唯一的标识符(通常对应数据库表的主键)来区分彼此。
  • 关键特征:
  • 持久性: 实体对象的生命周期超越应用程序的运行周期,数据需要被持久化存储。
  • 唯一性: 每个实体对象都有一个唯一的身份标识。
  • 状态可变: 实体对象的状态在其生命周期内可能会发生改变。
  • 业务行为: 实体类可以包含业务行为方法,操作自身状态。

代码示例 (简化版):

// SysDictData.java
public class SysDictData {
private Long dictCode;  // 字典编码,主键
private Integer dictSort; // 字典排序
private String dictLabel;  // 字典标签
private String dictValue;  // 字典值
private String dictType;   // 字典类型

// 构造函数(Constructor),getter 和 setter 方法
public Long getDictCode() { return dictCode; }
public void setDictCode(Long dictCode) { this.dictCode = dictCode; }

 public Integer getDictSort() { return dictSort; }
public void setDictSort(Integer dictSort) { this.dictSort = dictSort; }

public String getDictLabel() { return dictLabel; }
public void setDictLabel(String dictLabel) { this.dictLabel = dictLabel; }

public String getDictValue() { return dictValue; }
public void setDictValue(String dictValue) { this.dictValue = dictValue; }

 public String getDictType() { return dictType; }
public void setDictType(String dictType) { this.dictType = dictType; }}

说明:这个 SysDictData 类表示字典数据,包含字典编码、排序、标签、值和类型等属性。

2. 值对象 (VO) / DTO:数据传输与展示

值对象(VO)或数据传输对象(DTO)的主要职责是在不同的应用程序层之间传递数据,特别是在服务层和表示层(例如 Controller)之间。它们专注于封装需要展示给前端的数据,而不涉及持久化或核心业务逻辑。

  • 核心职责:
  • 数据传输: 用于在后端和前端之间传输数据。
  • 数据展示: 根据前端特定的展示需求,组织和封装数据。
  • 解耦前后端: 隔离后端实体类的变化对前端的影响。
  • 特点:
  • 通常不具备唯一的标识符: VO 的重点在于数据的组合和展示,而不是唯一性。
  • 生命周期通常较短: VO 对象通常在一次请求响应中创建和销毁。
  • 关注数据的呈现方式: VO 可能会对数据进行格式化、转换或聚合,以便更好地展示。
  • 不可变性(推荐): 通常建议 VO 对象是不可变的,一旦创建,其状态就不应该被修改。
  • 不包含业务行为: VO/DTO 主要关注数据的承载,通常不包含复杂的业务逻辑。

代码示例 (简化版):

// SysDictDataVO.java
public class SysDictDataVO {
private String dictLabel; // 字典标签,只保留了前端需要的标签
private String dictValue; // 字典值,只保留了前端需要的值

// 构造函数(Constructor),getter 和 setter 方法
 public String getDictLabel() {
    return dictLabel;
 }
 public void setDictLabel(String dictLabel) {
    this.dictLabel = dictLabel;
 }
 public String getDictValue() {
    return dictValue;
 }
 public void setDictValue(String dictValue) {
    this.dictValue = dictValue;
 }
 Use code with caution.
}

说明:这个 `SysDictDataVO` 类表示一个更简化的字典数据,它只包含前端需要的 `dictLabel` 和 `dictValue` 两个属性。它也不包含 JPA 注解,重点在于数据的承载。

3. Entity 与 VO 的主要区别



4. 为什么要区分 Entity 和 VO?

  • 关注点分离: 让不同的类专注于不同的职责,提高代码的可读性和可维护性。

  • 解耦前后端: 使用 VO 可以隔离后端 Entity 的变化对前端的影响,使得前后端可以独立演进。

  • 安全性: VO 可以避免将后端敏感数据直接暴露给前端。

  • 性能: VO 可以只包含前端需要的字段,减少数据传输量。

  • 灵活性: VO 可以根据不同的前端展示需求进行定制。

5. Service 层:Entity 到 VO 的转换

服务层在架构中扮演着关键的转换角色。从数据访问层获取的 Entity 对象,需要根据表示层的需求转换为相应的 VO/DTO 对象,这个过程不仅仅是数据复制,更包含了数据转换和格式化。

实现方式:

  • Bean 映射工具(Bean Mapping): 使用 org.apache.commons.beanutils.BeanUtils 或更强大的 MapStruct 等工具可以简化属性复制过程。MapStruct 通过编译时代码生成,避免了反射的性能损耗,是更为高效的选择。

  • 手动属性复制: 对于简单的对象或需要精细控制转换逻辑的情况,也可以手动编写代码进行属性复制。

Apache Commons BeanUtils 是什么?

Apache Commons BeanUtils 是一个 Java 库,它提供了一系列用于操作 JavaBean 的工具方法。

  • getProperty() 和 setProperty(): 可以动态地根据属性名来读取和设置 Bean 属性的值。 通常用于动态 Bean 操作,或者用于通用工具类。

  • setNestedProperty(): 用于设置嵌套属性的值, 例如 person.address.city, 这个是设置了 person 对象的 address 属性 的 city 属性。

  • populate(): 可以从一个 Map 对象中读取值并设置到 Bean 的对应属性上。

  • getPropertyDescriptors(): 可以获取一个 Bean 的所有属性的元数据信息。

  • describe(): 用于获取 Bean 对象的属性值, 用于 Debug, 或者生成一些报告。

  • cloneBean(): 可以克隆一个 Bean 对象, 用于生成对象的副本。

  • copyProperty(): 用于复制单个属性。

示例:使用 org.apache.commons.beanutils.BeanUtils进行转换

 import  org.apache.commons.beanutils.BeanUtils;
 import java.util.List;
 import java.util.ArrayList;
 import org.springframework.stereotype.Service;
 import org.springframework.beans.factory.annotation.Autowired;

  @Service
 public class SysDictDataServiceImpl implements SysDictDataService {

      @Autowired
     private SysDictDataMapper dictDataMapper; // 假设你有一个 SysDictDataMapper 用于数据访问

     @Override
      public SysDictDataVO getDictDataByCode(Long dictCode) {
        // 1. 调用 Mapper 从数据库获取 Entity 对象
        SysDictData dictData = dictDataMapper.selectByPrimaryKey(dictCode);
        if (dictData == null) {
            return null;
        }

        // 2. 创建一个 VO 对象
       SysDictDataVO dictDataVO = new SysDictDataVO();
       // 3. 将 Entity 对象的属性值复制到 VO 对象中
        BeanUtils.copyProperties(dictData, dictDataVO);

         return dictDataVO;
     }

     @Override
    public List<SysDictDataVO> getDictDataByType(String dictType) {
        // 1. 调用 Mapper 从数据库获取 Entity 对象列表
        SysDictData query = new SysDictData();
       query.setDictType(dictType);
        List<SysDictData> dictDataList = dictDataMapper.selectDictDataByType(query);

        // 2. 创建一个 VO 对象列表
        List<SysDictDataVO> dictDataVOList = new ArrayList<>();

        // 3. 遍历 Entity 对象列表,并将每个 Entity 对象转换为 VO 对象
        for (SysDictData dictData : dictDataList) {
           SysDictDataVO dictDataVO = new SysDictDataVO();
            BeanUtils.copyProperties(dictData, dictDataVO); // 使用 BeanUtils 复制属性
           dictDataVOList.add(dictDataVO); // 将 VO 对象添加到列表
        }
        return dictDataVOList;
    }
 }

示例:手动进行转换

    @Override
      public SysDictDataVO getDictDataByCode(Long dictCode) {
       // 1. 调用 Mapper 从数据库获取 Entity 对象
       SysDictData dictData = dictDataMapper.selectByPrimaryKey(dictCode);
        if (dictData == null) {
             return null;
         }

        // 2. 创建一个 VO 对象
        SysDictDataVO dictDataVO = new SysDictDataVO();

       // 3. 将 Entity 对象的属性值手动复制到 VO 对象中
        dictDataVO.setDictLabel(dictData.getDictLabel());
        dictDataVO.setDictValue(dictData.getDictValue());

         return dictDataVO;
     }
       @Override
       public List<SysDictDataVO> getDictDataByType(String dictType) {
          // 1. 调用 Mapper 从数据库获取 Entity 对象列表
         SysDictData query = new SysDictData();
         query.setDictType(dictType);
         List<SysDictData> dictDataList = dictDataMapper.selectDictDataByType(query);

         // 2. 创建一个 VO 对象列表
          List<SysDictDataVO> dictDataVOList = new ArrayList<>();

         // 3. 遍历 Entity 对象列表,并将每个 Entity 对象转换为 VO 对象
         for (SysDictData dictData : dictDataList) {
             SysDictDataVO dictDataVO = new SysDictDataVO();
            // 将 Entity 对象的属性值手动复制到 VO 对象中
            dictDataVO.setDictLabel(dictData.getDictLabel());
             dictDataVO.setDictValue(dictData.getDictValue());
           dictDataVOList.add(dictDataVO); // 将 VO 对象添加到列表
         }
       return dictDataVOList;
    }
  • 说明:

    • @Service 注解表示这是一个服务类。

    • @Autowired 注解用于注入 SysDictDataMapper 的实例,用于数据库访问。

    • BeanUtils.copyProperties(dictData, dictDataVO) 方法会将 dictData 对象中与 dictDataVO 对象具有相同名称的属性值复制到 dictDataVO 中。

    • 这段代码展示了如何使用BeanUtils将实体对象转换成VO对象。

6. Controller 层的返回

Controller 层负责接收前端的请求,并返回响应给前端。通常使用 RESTful API,数据通常以 JSON 格式返回。

示例:

 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.http.ResponseEntity;
 import java.util.List;

 @RestController
 @RequestMapping("/api/dict")
 public class SysDictDataController {

     @Autowired
     private SysDictDataService dictDataService;

    @GetMapping("/type/{dictType}")
    public ResponseEntity<List<SysDictDataVO>> getDictByType(@PathVariable String dictType) {
         // 1. Controller 调用 Service 层的方法,获取 VO 列表
        List<SysDictDataVO> dictDataVOList = dictDataService.getDictDataByType(dictType);

        // 2. Controller 将 VO 列表作为响应返回给前端
         return ResponseEntity.ok(dictDataVOList); // 返回 HTTP 200 (OK) 状态码和 VO 列表
     }
 }

7. 数据流的简单流程

一个典型的数据流如下:

  1. 前端发送 HTTP 请求 (例如,请求指定类型的字典数据)。

  2. Controller 接收到请求,并调用 Service 层的方法。

  3. Service 层调用 Mapper 从数据库获取 Entity 对象 (或列表)。

  4. Service 层根据表示层需求创建 VO 对象 (或列表),并进行数据转换。

  5. Service 层返回 VO 对象 (或列表) 给 Controller。

  6. Controller 将 VO 对象 (或列表) 封装到 ResponseEntity 中,并作为 JSON 响应返回给前端。

  7. 前端接收到 JSON 数据并进行处理和展示。

8. 避免 VO 继承 Entity

你可能想到,如果 VO 需要的字段和 Entity 差不多,能不能让 VO 直接继承 Entity,省去一些代码呢? 答案是:不推荐。

  • 语义不符: 继承表示 "is-a" 的关系,VO 并不是一个 "是" Entity 的关系,而是 "有" Entity 的数据。

  • 紧耦合: 继承会让 VO 和 Entity 紧密耦合,一旦 Entity 发生变化,即使与前端展示无关,也可能影响到 VO。

  • 职责不清,违反单一职责原则 VO 应该专注于数据展示,不应该承担 Entity 的持久化职责。

总结

实体类和值对象是后端 Java 开发中不可或缺的数据模型组件。实体类是业务领域的核心,负责数据的持久化和业务逻辑,是业务模型的具体实现;值对象则服务于表示层,专注于数据的传输和展示,是前后端数据交流的桥梁。理解它们的本质区别、合理应用,以及遵循正确的实践方式,是构建清晰、可维护、可扩展后端应用的关键。在实际开发中,应严格遵循职责分离原则,避免 VO 继承 Entity 等反模式,充分利用 Service 层进行数据转换,并根据业务场景选择适合的数据传输方式,才能构建出稳定、高效的应用架构。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值