[转载]SpringBoot中Entity、DTO、VO的通俗理解与实战

最近在学Java,SpringBoot, MyBatis,那DTO,VO,ENTIGY,MAPPER,DAO,这些东东为什么要出现就要学习了。看到两个讲得好的,转载。

https://www.cnblogs.com/tlnshuju/p/19441908

刚接触SpringBoot开发时,你是不是也有过这样的困惑:明明数据库表对应一个实体类就够了,为什么还要搞出DTO、VO这些"花里胡哨"的东西?直接把数据库实体类传到前端不行吗?

答案是:不行。就像我们寄快递时,不会把家里的保险箱直接寄出去,而是会把里面的东西拿出来装在快递盒里(选需要的东西、去掉敏感信息)。Entity、DTO、VO就是这套"数据包装"的不同容器,各自有明确的使用场景。今天咱们用最直白的案例,把这些概念讲清楚。

一、Entity:和数据库"一对一"的"原始数据袋"

定义:Entity(实体类)是数据库表的"镜像",字段和数据库表一一对应,它的使命就是和数据库打交道——存数据、取数据。

核心作用:作为持久层(Repository层)操作的载体,负责数据在Java对象和数据库表之间的映射。

实战案例:用户表对应的Entity

假设数据库有张user表,结构如下:

字段名 类型 说明
id bigint 主键
username varchar(50) 用户名
password varchar(100) 加密密码
email varchar(100) 邮箱
create_time datetime 创建时间
对应的Entity类:
import jakarta.persistence.*;
import lombok.Data;
import java.time.LocalDateTime;
// 与数据库表user映射
@Entity
@Table(name = "user")
@Data // Lombok注解,自动生成getter、setter等
public class UserEntity {
// 主键
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// 用户名,对应表中username字段
private String username;
// 密码(加密存储),对应表中password字段
private String password;
// 邮箱,对应表中email字段
private String email;
// 创建时间,对应表中create_time字段
@Column(name = "create_time")
private LocalDateTime createTime;
}

注意:Entity只应该在DAO层(数据访问层)和Service层内部使用,绝对不能直接传到前端!因为它包含敏感信息(如password)和前端可能不需要的字段(如createTime)。

二、DTO:服务间数据传输的"快递盒"

定义:DTO(Data Transfer Object,数据传输对象)用于不同服务之间、或服务内部不同层之间传递数据。它就像快递盒,只装对方需要的数据,不多不少。

核心作用:1. 减少数据传输量(只传必要字段);2. 隔离不同服务的实体结构(避免一个服务改实体影响另一个服务)。

实战案例:用户注册的DTO

用户注册时,前端需要传用户名、密码、邮箱,但不需要传id(数据库自增)和createTime(后端生成)。这时候就需要一个UserRegisterDTO

import lombok.Data;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Email;
@Data
public class UserRegisterDTO {
// 用户名不能为空
@NotBlank(message = "用户名不能为空")
private String username;
// 密码不能为空
@NotBlank(message = "密码不能为空")
private String password;
// 邮箱格式要正确
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
}

Service层接收DTO并转换为Entity:

@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public void register(UserRegisterDTO registerDTO) {
// 1\. DTO转换为Entity
UserEntity userEntity = new UserEntity();
userEntity.setUsername(registerDTO.getUsername());
// 密码加密(实际开发中一定要加密!)
userEntity.setPassword(encryptPassword(registerDTO.getPassword()));
userEntity.setEmail(registerDTO.getEmail());
userEntity.setCreateTime(LocalDateTime.now());
// 2\. 保存到数据库
userRepository.save(userEntity);
}
// 密码加密方法(示例)
private String encryptPassword(String rawPassword) {
return new BCryptPasswordEncoder().encode(rawPassword);
}
}

三、VO:给前端展示的"最终商品"

定义:VO(View Object,视图对象)是专门给前端页面展示用的数据对象。它就像商店里的商品展示,只展示用户想看到的信息,隐藏内部细节。

核心作用:定制前端需要的展示数据,比如字段别名、组合字段等。

实战案例:用户详情的VO

前端展示用户详情时,需要id、用户名、邮箱,但不需要password和createTime。这时候用UserVO

import lombok.Data;
@Data
public class UserVO {
private Long id;
private String username;
private String email;
// 可以增加前端需要的组合字段,比如"用户标签"
private String userTag;
}

Service层查询Entity并转换为VO:

@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public UserVO getUserDetail(Long userId) {
// 1\. 从数据库查询Entity
UserEntity userEntity = userRepository.findById(userId)
.orElseThrow(() -> new RuntimeException("用户不存在"));
// 2\. Entity转换为VO
UserVO userVO = new UserVO();
userVO.setId(userEntity.getId());
userVO.setUsername(userEntity.getUsername());
userVO.setEmail(userEntity.getEmail());
// 定制前端需要的组合字段
userVO.setUserTag("普通用户");
return userVO;
}
}

Controller层返回VO给前端:

@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{userId}")
public UserVO getUserDetail(@PathVariable Long userId) {
return userService.getUserDetail(userId);
}
}

四、三者转换的"小技巧"

手动转换(setter/getter)虽然直观,但字段多了很麻烦。实际开发中推荐用工具类:

  • ModelMapper:自动映射同名字段,支持自定义映射规则。

  • MapStruct:编译期生成映射代码,性能更好,需要写接口。

ModelMapper示例:

// 1\. 引入依赖
<dependency>
  <groupId>org.modelmapper</groupId>
    <artifactId>modelmapper</artifactId>
      <version>3.2.0</version>
        </dependency>
          // 2\. 配置Bean
          @Configuration
          public class ModelMapperConfig {
          @Bean
          public ModelMapper modelMapper() {
          return new ModelMapper();
          }
          }
          // 3\. 服务中使用
          @Service
          public class UserService {
          @Autowired
          private ModelMapper modelMapper;
          public UserVO getUserDetail(Long userId) {
          UserEntity userEntity = userRepository.findById(userId).orElseThrow(...);
          // 自动转换Entity到VO
          return modelMapper.map(userEntity, UserVO.class);
          }
          }

五、总结:一句话分清三者

对象类型 使用场景 核心目的
Entity DAO层 ↔ 数据库 和数据库表一一对应,负责数据持久化
DTO 服务间/层间数据传输 减少传输量,隔离服务依赖
VO Service层 ↔ 前端 定制前端展示数据,隐藏敏感信息
记住:边界清晰是代码整洁的关键。用对Entity、DTO、VO,能让你的SpringBoot项目结构更清晰,维护性大大提升。

https://www.toutiao.com/article/7604056370148622902/
做SpringBoot开发也有几年了,刚入门的时候,被VO、DTO、BO、DO、PO这些缩写搞得头大。对着网上的资料看,要么说得太抽象,要么全是理论套话,实操起来还是分不清什么时候该用哪个,甚至会乱混用——比如把数据库字段直接返回给前端,或者把前端传过来的参数直接丢给Service层,到后面需求迭代,改一处代码牵一发而动全身,排查bug能查到怀疑人生。

其实这些对象没有那么玄乎,核心就是“分层解耦”,把不同层的职责拆分开,让代码更清晰、更好维护。今天就用最接地气的话,结合我实际开发中遇到的场景,跟大家聊聊这些常用对象的区别和使用方法,没有复杂的理论,全是实操干货,新手也能一看就懂,避开那些我踩过的坑。

先声明一句:不同公司、不同项目的规范可能略有差异,但核心逻辑是一致的。本文分享的是最通用、最易落地的用法,适合绝大多数SpringBoot单体项目和简单微服务项目,不用追求过度设计,实用就好。

先搞懂核心:为什么需要这么多“O”?
很多新手会问:直接用一个实体类贯穿所有层不行吗?比如定义一个User类,既有数据库字段,又有前端展示的字段,还有业务计算的字段,多简单。

我刚入门的时候也这么干过,结果踩了大坑。举个例子:有一个用户列表接口,前端需要展示用户的id、姓名、手机号(脱敏)、角色名称,而数据库里存的是id、name、phone、role_id、password、create_time等字段。如果直接把数据库对应的实体类(比如User)返回给前端,会把password(密码)、create_time(创建时间)这些前端用不上的字段也返回过去,既浪费带宽,又有安全隐患;更麻烦的是,前端需要手机号脱敏(比如138****1234),如果在数据库实体类里加脱敏逻辑,那这个类就既承担了“对应数据库”的职责,又承担了“前端展示”的职责,后续如果前端要改脱敏规则,或者数据库要加字段,都得动这个类,极易出错。

而VO、DTO、BO、DO、PO的出现,就是为了解决这个问题——让每个对象只做自己的事,各层之间通过这些对象传递数据,互不干扰。简单说:分层解耦,职责单一,后续维护更省心。

逐个拆解:每个“O”到底是什么?怎么用?
下面逐个说明,重点讲“含义+使用场景+实操示例”,结合SpringBoot常用的分层(Controller、Service、Dao),让大家知道在哪个层该用哪个对象。

  1. PO(Persistent Object):持久化对象,和数据库表一一对应
    核心定位:PO是数据库表的“镜像”,字段和数据库表的列完全一致,没有任何业务逻辑,只负责承载数据库中的数据,说白了就是“用来存数据、取数据的载体”。

使用场景:仅在Dao层(数据访问层)使用,比如MyBatis的Mapper接口、XML映射文件中,用来接收数据库查询结果,或者作为插入/更新数据库的参数。

实操注意:

字段名和数据库列名保持一致(或者通过@Column注解映射),不添加任何多余字段;
只提供getter、setter方法,不写任何业务逻辑(比如计算、判断);
不要在Controller、Service层直接使用PO,避免数据库字段泄露或业务逻辑入侵。
示例(以用户表user为例):

// PO类,和数据库user表一一对应
public class UserPO {
// 对应数据库user表的id列
private Long id;
// 对应数据库user表的name列
private String name;
// 对应数据库user表的phone列
private String phone;
// 对应数据库user表的password列(加密存储)
private String password;
// 对应数据库user表的role_id列
private Long roleId;
// 对应数据库user表的create_time列
private LocalDateTime createTime;

// 只有getter、setter,无其他业务逻辑
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
// 其他getter、setter省略...

}

  1. DO(Domain Object):领域对象,等价于PO?
    这里要特别说明:很多人会把DO和PO搞混,甚至认为两者是同一个东西——在绝大多数SpringBoot单体项目中,DO和PO确实可以混用,因为项目规模不大,不需要过度拆分。

严格来说,两者的区别在于:PO更侧重“持久化”(和数据库的映射),而DO更侧重“领域模型”(承载领域内的基础数据,可能包含极简单的领域逻辑)。但在实际开发中,除非是大型项目、领域模型复杂,否则不需要单独定义DO,直接用PO代替DO即可,避免冗余。

我的实操建议:中小规模SpringBoot项目,不用区分DO和PO,直接用PO作为持久化对象和领域基础对象,减少类的数量,降低维护成本。如果是大型项目,领域逻辑复杂,可以拆分DO和PO(比如DO包含部分领域逻辑,PO仅负责数据库映射)。

  1. BO(Business Object):业务对象,承载业务逻辑
    核心定位:BO是Service层(业务逻辑层)的核心对象,用来承载业务逻辑、进行业务计算,它的数据可以来自一个或多个PO(/DO),是“业务处理的载体”。

使用场景:仅在Service层内部使用,或者Service层之间传递数据,用来封装业务处理过程中的数据和逻辑,Controller层不直接操作BO。

实操注意:

BO可以包含多个PO(/DO)的数据,比如“订单业务对象”可以包含订单PO、用户PO、商品PO的数据;
BO中可以包含业务逻辑方法(比如计算订单总价、判断用户是否有权限);
BO不直接和数据库交互,数据来自Dao层查询的PO,经过Service层处理后封装成BO。
示例(用户业务对象,结合用户PO和角色PO):

// BO类,承载用户相关业务逻辑
public class UserBO {
// 来自UserPO的基础数据
private Long userId;
private String userName;
private String phone;
// 来自RolePO的数据(关联查询得到)
private Long roleId;
private String roleName;
// 业务计算字段(非数据库字段)
private boolean isVip; // 是否为VIP用户

// 业务逻辑方法:判断是否为VIP用户(示例)
public void judgeVip(LocalDateTime registerTime) {
    // 业务规则:注册时间超过3年即为VIP
    this.isVip = registerTime.plusYears(3).isBefore(LocalDateTime.now());
}

// getter、setter省略...

}
使用场景示例:Service层查询用户信息时,先通过Dao层查询UserPO和RolePO,然后将两者的数据封装到UserBO中,调用judgeVip方法判断是否为VIP,再将BO用于后续业务处理。

  1. DTO(Data Transfer Object):数据传输对象,用于层与层之间传递数据
    核心定位:DTO是“数据传输的工具”,用来在不同层之间传递数据,解决各层对象字段不一致的问题,比如Controller层和Service层之间、Service层和Dao层之间,或者微服务之间的远程调用(比如Feign调用)。

这里要注意:DTO和BO的区别——BO侧重“业务逻辑”,DTO侧重“数据传输”,不含任何业务逻辑,只负责承载要传输的数据。

使用场景:

前端→Controller:前端传递的请求参数(比如新增用户时,前端传name、phone、roleId,不需要传id、createTime),用DTO接收;
Service→Controller:Service层处理完业务后,将数据封装成DTO,返回给Controller层;
微服务之间调用:比如服务A调用服务B的接口,传递的数据用DTO封装,避免暴露自身的PO/BO。
实操注意:

DTO的字段根据“传输需求”定义,不需要和PO/BO完全一致,只包含需要传输的字段;
不含任何业务逻辑,只提供getter、setter方法;
常用的有RequestDTO(前端请求参数)和ResponseDTO(后端响应数据),可以分开定义,更清晰。
示例(用户相关DTO):

// 1. RequestDTO:接收前端新增用户的请求参数
public class UserAddRequestDTO {
// 前端只需要传name、phone、roleId,其他字段由后端生成
private String name;
private String phone;
private Long roleId;

// getter、setter省略...

}

// 2. ResponseDTO:Service层处理完,返回给Controller的响应数据
public class UserResponseDTO {
// 前端需要展示的字段:id、name、phone(脱敏)、roleName
private Long id;
private String name;
private String phone; // 这里会在Service层处理成脱敏格式
private String roleName;

// getter、setter省略...

}
使用场景示例:前端提交新增用户的表单(name、phone、roleId),Controller层用UserAddRequestDTO接收,然后将DTO转换成BO/PO,交给Service层处理;Service层处理完成后,将PO/BO的数据转换成UserResponseDTO,返回给Controller层,再由Controller层返回给前端。

  1. VO(View Object):视图对象,专门给前端展示用
    核心定位:VO是“前端专属的数据载体”,字段完全对应前端页面需要展示的内容,比如数据脱敏、字段重命名、组合字段等,只为前端展示服务,不参与后端任何业务逻辑。

这里很多人会把VO和ResponseDTO搞混,其实两者的区别很简单:

ResponseDTO:侧重“层与层之间的传输”,比如Service→Controller,可能包含前端不需要的字段(但实际开发中会尽量和前端需求对齐);
VO:侧重“前端视图展示”,完全贴合前端页面的需求,比如前端需要“用户性别(男/女)”,而数据库存的是0/1,VO中就直接存“男/女”,不需要前端再做转换。
实操建议:中小规模项目中,VO和ResponseDTO可以混用,因为前端需求和传输需求基本一致,没必要单独定义两个类,增加冗余;如果前端需求和传输需求差异较大(比如前端需要大量组合字段、格式化字段),可以单独定义VO,由Controller层将ResponseDTO转换成VO后返回给前端。

示例(用户VO,贴合前端展示需求):

// VO类,专门给前端展示用
public class UserVO {
// 前端展示:用户ID(和数据库一致)
private Long userId;
// 前端展示:用户姓名(和数据库一致)
private String userName;
// 前端展示:脱敏手机号(数据库是完整手机号,VO中是脱敏后的数据)
private String phone; // 格式:138****1234
// 前端展示:角色名称(数据库存的是role_id,VO中是角色名称)
private String roleName;
// 前端展示:注册时间(格式化后,比如2023-01-01 12:00:00)
private String createTime;
// 前端展示:用户状态(数据库存0/1,VO中存“正常/禁用”)
private String status;

// getter、setter省略...

}
使用场景示例:Service层返回UserResponseDTO(包含完整phone、时间戳格式的createTime、0/1格式的status),Controller层将其转换成UserVO,处理phone脱敏、时间格式化、status转换,然后返回给前端,前端直接拿VO的字段展示即可,不需要做任何数据处理。

关键总结:各层对象的使用流程(SpringBoot实操版)
结合SpringBoot的分层架构(Controller→Service→Dao),给大家梳理一下这些对象的完整使用流程,看完就知道什么时候该用哪个了:

前端发起请求:前端提交表单(比如新增用户),传递name、phone、roleId等参数,Controller层用【RequestDTO】接收;
Controller→Service:Controller层将RequestDTO转换成【BO】(或直接传递DTO,根据项目规模),调用Service层的业务方法;
Service→Dao:Service层将BO(或DTO)转换成【PO】,调用Dao层(Mapper)的方法,查询/操作数据库;
Dao→Service:Dao层查询数据库,返回【PO】给Service层;
Service层处理业务:Service层将PO的数据封装到【BO】中,执行业务逻辑(比如判断VIP、数据校验);
Service→Controller:Service层将BO转换成【ResponseDTO】(或【VO】),返回给Controller层;
Controller→前端:Controller层将ResponseDTO(或VO)返回给前端,前端接收数据并展示。
简化版(中小规模项目,最常用):前端请求→RequestDTO→Service→PO(操作数据库)→Service处理→ResponseDTO(VO)→前端展示。

避坑提醒:这些错误千万别犯!
结合我自己踩过的坑,给大家提几个实操中最容易犯的错误,避开这些,能少走很多弯路:

不要用PO直接返回给前端:会泄露数据库敏感字段(比如password、createTime),也会返回前端用不上的字段,增加带宽消耗;
不要在DTO/VO中写业务逻辑:DTO/VO只负责数据传输和展示,业务逻辑必须放在Service层(BO中),否则会导致职责混乱,后续难以维护;
不要过度设计:中小规模项目,不用强行拆分DO和PO、VO和ResponseDTO,避免类的数量过多,增加维护成本,实用优先;
字段命名要规范:PO的字段和数据库一致,DTO/VO的字段和前端一致,避免出现字段不匹配、歧义的问题(比如前端要userId,后端返回id,导致前端渲染失败);
转换逻辑要统一:DTO→PO、PO→BO、BO→VO的转换逻辑,建议单独写一个转换工具类(比如UserConvert),不要散落在Controller、Service层,方便后续修改。
最后总结
其实VO、DTO、BO、DO、PO这些对象,核心就是“分层解耦、职责单一”,没有那么复杂。记住一句话:什么层用什么对象,什么对象做什么事,不越界、不混用,代码就会清晰很多。

对于新手来说,不用一开始就死记硬背各个对象的定义,先在项目中尝试使用RequestDTO、ResponseDTO、PO这三个最常用的,熟悉之后,再根据项目规模,考虑是否拆分BO、VO、DO。实操多了,自然就懂了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容