后端开发 DTO-Entity-VO 转换模式详解
目录
🧩 一、数据流走向(总览)
⚙️ 二、DTO → Entity:在业务层中完成
⚙️ 三、Entity → VO:返回时的转换
🧠 四、关键点总结
✨ 五、推荐实践(让代码更干净)
1. 使用工具类简化拷贝
2. 把转换逻辑封装成“转换器类”
✅ 六、一句话总结
在现代 Spring Boot Web 开发中,为了实现前后端分离、保证数据安全和明确分层,我们通常会使用三种类型的 Java 对象来处理数据:DTO、Entity 和 VO。
这个模式的核心思想是:在不同的应用层之间,使用专门的对象进行数据传输。
🧩 一、数据流走向(总览)
下面是-个标准 Web 请求的数据流转过程:
前端 JSON 请求体↓
DTO(数据传输对象) ← 【控制器层 Controller】(用于接收输入)↓
Entity(实体对象) ← 【业务层 Service】(转换为 Entity,用于保存到数据库)↓
数据库↓
Entity(实体对象) ← 【业务层 Service】(从数据库取出)↓
VO(视图对象) ← 【控制器层 Controller/Service】(转换为 VO,用于封装返回前端)↓
前端 JSON 响应体
⚙️ 二、DTO → Entity:在业务层中完成
DTO (Data Transfer Object):数据传输对象。它的唯一职责是接收前端传来的数据。
当 Controller 接收到前端的 JSON 请求时,Spring MVC 会自动将其封装为 DTO 对象。
// Controller
@PostMapping("/register")
public ApiResponse<Long> register(@Valid @RequestBody UserRegisterRequest dto) {// 此时,dto 对象就是前端传来的 JSON 数据// 然后我们把 dto 传给业务层Long userId = userService.register(dto);return ApiResponse.success("注册成功", userId);
}
Entity (实体):数据库实体对象。它严格对应数据库中的表结构。
这一步转换发生在 Service (业务) 层。Service 层负责核心业务逻辑,它接收 DTO,然后将其转换为 Entity,并补充业务所需的其他字段(比如默认权限、初始积分、加密密码等)。
// ServiceImpl
public Long register(UserRegisterRequest dto) {// 1. 手动将 DTO 字段拷贝给 EntityUser user = new User();user.setUsername(dto.getUsername());// 业务逻辑:密码需要加密user.setPassword(passwordEncoder.encode(dto.getPassword()));// 业务逻辑:昵称默认为用户名user.setNickname(StringUtils.hasText(dto.getNickname()) ? dto.getNickname() : dto.getUsername());user.setPhone(dto.getPhone());user.setEmail(dto.getEmail());// 2. 也可以用工具类来简化拷贝// BeanUtils.copyProperties(dto, user);// (注意:使用工具类后,仍需手动处理密码加密等特殊逻辑)// 3. 补充前端不传的业务字段user.setRole("user");user.setStatus(1);user.setCreditScore(100);// 4. 通过 MyBatis-Plus 将 Entity 存入数据库save(user);return user.getId();
}
✅ 这一步的作用:
-
把前端的请求参数(DTO)转换成数据库对应的实体对象(Entity)。
-
在转换过程中执行业务逻辑(如加密、设置默认值)。
⚙️ 三、Entity → VO:返回时的转换
VO (View Object):视图对象。它专门用于封装后端需要返回给前端的数据。
我们通常不会直接把 Entity 返回给前端,因为 Entity 里可能包含敏感字段(比如 password、salt)或者前端不需要的字段(比如 is_deleted、update_time)。
VO 的作用就是只挑选前端需要的字段。
示例:
// 专门用于前端展示的 UserVO
public class UserVO {private Long id;private String username;private String nickname;private String avatar;private String email;
}
转换可以在 Service 层完成,也可以在 Controller 层完成(推荐在 Service 层)。
// ServiceImpl 中...
// 假设我们从数据库查到了 user (Entity)
User user = getById(userId);// 1. 转换为 VO
UserVO vo = new UserVO();
vo.setId(user.getId());
vo.setUsername(user.getUsername());
vo.setNickname(user.getNickname());
vo.setAvatar(user.getAvatar());
vo.setEmail(user.getEmail());// 2. 或者同样用工具类
// BeanUtils.copyProperties(user, vo);// 3. 在 Controller 返回 VO
return ApiResponse.success("查询成功", vo);
前端最终拿到的 JSON 就会非常干净:
{"success": true,"message": "查询成功","data": {"id": 1001,"username": "tom","nickname": "Tom","avatar": "avatar/default.png","email": "tom@demo.com"}
}
🧠 四、关键点总结
| 转换方向 | 发生位置 | 意义 |
|---|---|---|
| DTO → Entity | 在 Service 层 | 把前端请求数据转换为数据库对象(用于保存) |
| Entity → VO | 在 Service 或 Controller 层 | 把数据库对象转换为前端可展示的数据(用于返回) |
✨ 五、推荐实践(让代码更干净)
1. 使用工具类简化拷贝
手动 set 和 get 非常繁琐且容易出错。
-
Spring 自带:
BeanUtils.copyProperties(source, target); -
常用增强库:
-
MapStruct(推荐):在编译时自动生成类型安全的转换代码,性能极高。
-
ModelMapper:在运行时通过反射自动映射,非常灵活但性能略慢。
-
2. 把转换逻辑封装成“转换器类”
当转换逻辑变多时,可以创建一个专门的 Convert 类(或接口),让 Service 层保持干净。
public class UserConvert {// 使用 MapStruct 的示例// @Mapper(componentModel = "spring")// public interface UserConvert {// UserConvert INSTANCE = Mappers.getMapper(UserConvert.class);// User toEntity(UserRegisterRequest dto);// UserVO toVO(User user);// }// 手动封装的示例public static User toEntity(UserRegisterRequest dto) {User user = new User();BeanUtils.copyProperties(dto, user);// ... 其他特殊处理return user;}public static UserVO toVO(User user) {UserVO vo = new UserVO();BeanUtils.copyProperties(user, vo);return vo;}
}
然后你的 Service 代码就可以简化为:
// ServiceImpl
User user = UserConvert.toEntity(request);
// ... 处理加密等 ...
save(user);
return UserConvert.toVO(user);
✅ 六、一句话总结
| 环节 | 作用 | 代码位置 |
|---|---|---|
| DTO | 接收前端输入 | Controller 入参 |
| Entity | 持久化到数据库 | Service + Mapper |
| VO | 返回前端展示 | Controller 出参 |
| DTO→Entity | 在 Service 中转换 | 业务逻辑处理时 |
| Entity→VO | 在 Service 或 Controller 中转换 | 返回结果时 |
