其实不止DO,VO,DTO,还有 BO,PO 等等一大堆"欧",目前暂时先用这个三个吧,记录一下,下次重构项目时候按照这个梳理。
📦 项目结构示例
text
com.example.project
├── user/
│ ├── controller/
│ ├── service/
│ ├── dao/
│ ├── domain/
│ │ ├── form/ ← 请求参数接收和校验对象
│ │ │ ├── InsertUserForm.java
│ │ │ ├── LoginForm.java
│ │ ├── dto/ ← 用于 Service 层业务传输
│ │ │ ├── InsertUserDTO.java
│ │ ├── vo/ ← 给前端展示的数据模型
│ │ │ ├── UserVO.java
│ │ ├── do/ ← 数据库映射对象
│ │ │ ├── UserDO.java
│ │ └── convert/ ← 数据对象之间的转换工具类
│ │ ├── UserConvert.java
🧭 请求流程总览
以 /user/create
为例:
text
[HTTP Request]
↓
[Controller 层]
| [InsertUserForm] ← 接收参数(校验注解)
↓ [InsertUserDTO] ← Controller 和 Service 层之间的传输对象
[Service 层]
| [UserDO] ← DO,映射数据库字段,用于存储
↓ 或者[UserQueryDTO] ← 对于查询参数等的场景,DTO不需要转成DO,直接传入DAO层即可
[DAO 层]
↓
[数据库]
↓
[DAO 层]
↓
Service
| [UserDO] ← 查询结果
| 或者[UserSummaryVO] ← 查询返回部分字段、统计字段、拼接字段...
↓ 或者[UserSummaryDTO] ← 查询返回部分字段、统计字段、拼接字段...
Controller
↓ [UserVO] ← Controller 转换成展示结构
[HTTP Response]
🧩 各层对象职责
1⃣ Controller 层:接收请求 + 参数校验
- 类位置:
user/controller/UserController.java
- 使用对象:
InsertUserForm
(位于user/domain/form/
)
java
@PostMapping("/create")
public R createUser(@Valid @RequestBody InsertUserForm form) {
InsertUserDTO dto = UserConvert.INSTANCE.formToDTO(form);
userService.insertUser(dto);
return R.ok();
}
转换发生:
InsertUserForm
→InsertUserDTO
- 利用
UserConvert
做数据映射
目的:
- 接收并校验前端数据(如用户名非空、邮箱格式)
- 提供处理后的数据给业务逻辑层使用(比如有的前端传来的字段,需要处理后才能给业务使用)
2⃣ Service 层:执行业务逻辑 + 转换为持久对象
- 类位置:
user/service/UserServiceImpl.java
- 使用对象:
InsertUserDTO
→UserDO
(位于user/domain/do/
)- 如果是其他操作,直接用
xxxDTO
(查询参数等的场景,DTO不需要转成DO,直接传入DAO层即可) - 例如:
UserQueryDTO
- 如果是其他操作,直接用
java
public void insertUser(InsertUserDTO dto) {
UserDO userDO = UserConvert.INSTANCE.dtoToDO(dto);
userDao.insert(userDO);
}
转换发生:
DTO
→DO
- 可设置一些默认值(注册时间、状态字段)
目的:
- 业务逻辑干净,不污染
Controller
层结构 DO
是数据库映射对象,与表结构一一对应
3⃣ DAO 层:操作数据库
- 类位置:
user/dao/UserDao.java
- 使用对象:
UserDO
- 或者返回结果映射到
UserSummaryVO
(查询返回部分字段、统计字段、拼接字段...) - 或者返回结果映射到
UserSummaryDTO
(查询返回部分字段、统计字段、拼接字段...)
- 或者返回结果映射到
假如创建完用户后要返回用户详情:
java
UserVO vo = UserConvert.INSTANCE.doToVO(userDO);
return R.ok(vo);
转换发生:
DO
→VO
VO
可格式化时间、显示标签文案等,仅做展示
DAO 执行数据库语句后,返回结果可能映射到 DO,DTO,VO:
情况一:DAO 返回的是 DO(数据库映射对象)
需要在接下来的 Service 层,转换为业务对象(DTO)或展示对象(VO)
例如 DAO 接收到数据库返回的 UserDO
,接下来在 Service 层:
java
UserDO userDO = userDao.findById(id);
UserDTO dto = UserConvert.INSTANCE.doToDTO(userDO); // 用于业务逻辑处理(脱敏、格式化等等)
UserVO vo = UserConvert.INSTANCE.doToVO(userDO); // 用于返回前端展示
情况二:DAO 直接返回 VO(展示对象)
无需转换(直接返回),可直接用作 Controller 响应数据。
例如 DAO 接收到数据库返回的 UserListVO
,途径 Service 层,一路回传到 Controller 层后:
java
List<UserListVO> voList = userDao.queryUserList(); // VO 结构已拼接好字段
return R.ok(voList); // 直接响应前端
- 适合场景:
- 只查部分字段
- SQL 已完成业务拼接或展示字段格式处理(如联表查询)
⚠️ 注意:这种设计要小心 VO 被 "SQL 耦合污染",不建议太复杂的展示结构直接从 DAO 出。
情况三:DAO 返回 DTO(中间业务对象)
例如 DAO 接收到数据库返回的 UserDTO
,可以在 Service 层或 Controller 层(就 Service 层吧)转为 VO,用于展示:
java
List<UserDTO> dtoList = userDao.selectActiveUsers();
List<UserVO> voList = dtoList.stream()
.map(UserConvert::dtoToVO)
.collect(Collectors.toList());
return R.ok(voList);
这种做法适用于复杂业务逻辑之后还要做展示格式转换的情况。DTO 保持业务干净,VO 负责展示语义。
转换方法(convert/ 包)
Java 业内时间搞出来这么多实体类,还经常要转来转去,那肯定是封装一下对象间转换工具类比较好了,不然太折磨人了(好吧,现在这么多实体类已经很折磨人了)
梳理一下每个对象的转换类要实现哪些方法:
- Form → DTO(前端请求参数 → 业务传输对象)
- DTO → DO (业务参数 → 数据库持久对象)
- DO → VO (数据库对象 → 展示对象)
- DTO → VO (业务结果 → 展示结构)
而且啊:每一种明显职责不一致的转换都单独写方法,尤其字段不完全对得上、或者需要格式化的场景。
比如 UserConvert.java
(使用 MapStruct 可以自动生成转换实现,无需手写 Getter/Setter 代码):
java
@Mapper
public interface UserConvert {
UserConvert INSTANCE = Mappers.getMapper(UserConvert.class);
// form -> dto
InsertUserDTO formToDTO(InsertUserForm form);
// dto -> do
UserDO dtoToDO(InsertUserDTO dto);
// do -> vo
UserVO doToVO(UserDO userDO);
// dto -> vo(有些业务逻辑后直接转展示结构)
UserVO dtoToVO(UserDTO dto);
}
✅ 总结
层级 | 对象类型 | 作用 | 转换工具 |
---|---|---|---|
Controller | Form | 接收 + 校验 | UserConvert.formToDTO() |
Service | DTO → DO | 业务处理 → 数据落库 | UserConvert.dtoToDO() |
DAO | DO | 与数据库交互 | - |
Controller | DO/DTO → VO | 结果展示 | UserConvert.doToVO() |
Controller | VO → R | 响应封装 | R.ok(vo) |