Java 里的 DAO、DTO、POJO 都是什么
PO、DTO、VO、BO、DO、POJO 的差异不在写法——它们都是一堆字段加 getter/setter——而在「活在哪一层」。这篇顺带说清 Controller / Service / DAO 是哪三层,以及 DAO、Mapper、Repository 为什么是同一个角色的三个名字。
翻开一个 Java 后端项目,最先劝退人的经常不是业务逻辑,而是满屏的 UserPO、UserDTO、UserVO、UserBO——打开一看,字段几乎一样,都是一堆属性加 getter/setter。于是很自然地想问:这不是同一个东西换了四个名字吗?
某种意义上是的。它们的差异从来不在写法,而在Java的分层架构上。所以得先把分层说清楚。
一、三层架构是哪三层
Controller → Service → DAO(Data Access Object,数据访问对象)。
| 层 | 职责 | 不该出现在这一层的东西 |
|---|---|---|
| Controller | 收请求、参数校验、组装响应 | 业务逻辑、SQL |
| Service | 业务逻辑、事务边界(@Transactional 加在这一层) | HttpServletRequest、拼 SQL |
| DAO | 只跟数据库打交道,一个方法对应一次数据操作 | 业务判断、跨表编排 |
现实里 DAO 这一层的名字会变:MyBatis 项目叫 Mapper(接口加 XML),JPA / Spring Data 项目叫 Repository。名字不同,角色是一个,第四节展开。
另外在稍大的项目里还常见第四层 Manager,放跨 Service 的通用逻辑。它存在的理由很实际:两个 Service 互相调用容易形成循环依赖,把公共部分下沉到 Manager 就断开了。
二、那堆 O具体是什么
| 名字 | 全称 | 活在哪 | 干嘛的 |
|---|---|---|---|
| PO / Entity | Persistent Object | DAO 层 | 和数据库表一一对应,字段名跟列名对得上 |
| DTO | Data Transfer Object | 跨进程、跨层传输 | 接口的出入参,RPC 传的就是它 |
| VO | View Object | 返回给前端 | 只放页面要展示的字段 |
| BO | Business Object | Service 内部 | 聚合多个 PO 的业务对象 |
| DO | Domain Object | 看团队 | 阿里规范里指 Domain Object(约等于 PO),但很多团队用它指 Data Object |
| POJO | Plain Old Java Object | 不属于任何一层 | 上面所有的统称 |
POJO 不是某一层。这个词是 2000 年提出来的,反的是当年 EJB 那种必须继承框架基类、实现一堆接口的重量级对象。它的意思就是「一个不依赖任何框架的普通 Java 类」。所以 PO、DTO、VO 全都是 POJO。
所以一条完整的链路通常是:
数据库 → PO(DAO) → BO(Service) → DTO(跨进程,api jar) → VO(Controller,给前端)小项目没必要搞这么全,但至少要分出 PO 和 DTO 两层:一个对数据库负责,一个对接口契约负责。中间的转换用 MapStruct,它在编译期生成代码,比 BeanUtils.copyProperties 快,而且类型对不上时编译就报错,不会等到运行时才发现某个字段一直是 null。
三、DTO 既是出参又是入参?
DTO 不是一个具体的类,是一类角色。凡是跨进程传的数据对象都算 DTO。同一个方法的入参和返回值是两个不同的类,只是都属于 DTO 这一类:
public interface UserService {
UserDTO getUser(UserQueryDTO query);
// ↑ 出参 ↑ 入参
// 两个完全不同的类
}实践中的命名会把方向说清楚:
| 命名 | 方向 | 内容 |
|---|---|---|
UserCreateDTO / UserQueryDTO / UserCreateReq | 入参 | 只放调用方需要提供的字段 |
UserDTO / UserResp | 出参 | 只放要返回的字段 |
注意
RPC 项目里这些 DTO 放在 api jar 里,提供方和消费方共享,所以字段增删要格外克制:改一个 DTO 字段,所有依赖这个 jar 的服务都得重新编译发版。加字段是安全的(老消费方拿到 null),删字段和改类型是破坏性变更。
另外 DTO 必须 implements Serializable 并显式写 serialVersionUID,字段只用基本类型和 JDK 集合。别在 DTO 里放 Page、ResponseEntity 这类框架对象,消费方不一定有那个依赖。
四、DAO 和 PO 不是一类东西
如果对 PO 的理解是「对应一张表」,那很容易顺着问:DAO 说是数据操作层,它和 Mapper 是一回事吗?
先把最容易混的这一对分开:
PO 是名词,DAO 是动词的容器。 PO 描述一行数据长什么样,只有字段没有行为;DAO 描述对这张表能做什么操作,只有方法不存数据。一个是数据,一个是操作数据的人。
至于 DAO 和 Mapper——是同一个角色的两个叫法,Mapper 是 MyBatis 给它起的名字。
4.1 为什么会有两个名字
DAO 这个名字来自 2001 年 Sun 的 J2EE 设计模式,比 MyBatis 早很多年。当年 Spring 加 iBatis 的写法是接口加实现类:
public interface UserDao {
User selectById(Long id);
}
@Repository
public class UserDaoImpl extends SqlSessionDaoSupport implements UserDao {
public User selectById(Long id) {
return getSqlSession().selectOne("UserMapper.selectById", id);
}
}那个 Impl 类纯粹是样板代码,每个方法都在重复「拼 statement id、传参、调 sqlSession」。MyBatis 3 引入了接口绑定,框架用动态代理自动生成实现,方法名对上 XML 里的 id 就行,Impl 类直接消失。既然只剩接口,就顺势改名叫 Mapper(取自 Data Mapper 模式)。
所以在老项目里看到 UserDao 加 UserDaoImpl,就知道那是 MyBatis 3 之前的写法。
4.2 现在实际长什么样
@Mapper
public interface UserMapper {
User selectById(Long id);
List<User> selectByStatus(@Param("status") Integer status);
int insert(User user);
int updateById(User user);
}配一个 UserMapper.xml 写 SQL。国内公司大量使用 MyBatis-Plus,那就是继承 BaseMapper<User>,单表增删改查全部白送,只有复杂 SQL 才自己写:
@Mapper
public interface UserMapper extends BaseMapper<User> { }4.3 还会遇到 Repository
JPA / Spring Data 那套叫 Repository。名字不同不只是习惯问题,概念上有点差别:
| DAO / Mapper | Repository | |
|---|---|---|
| 面向 | 表 | 领域(聚合根) |
| 一个接口对应 | 一张表 | 一个聚合,可能跨好几张表 |
| 对上层的伪装 | 一组 SQL 操作 | 「一个集合」 |
不过在九成的项目里,这个区别只停留在命名上,实际写法没差。进新项目先看别人怎么命名,跟着来就行,别自己另起一套。
五、一页对照
User.java ← PO,一行数据
UserMapper.java ← DAO,对这张表的操作
UserMapper.xml ← SQL
UserService.java ← 业务逻辑,调 Mapper
UserDTO.java ← 跨进程传的,别把 User 直接传出去
UserVO.java ← 给前端的,只放页面要展示的字段这堆缩写没有一个是语法特性,全是约定。它们唯一的作用是让「这个对象允许被谁看到」这件事从口头约定变成类型系统里的事实。所以团队里叫什么名字不重要,重要的是别让一个类跨越太多层。
这篇是从一次 Dubbo 学习的对话里拆出来的,服务发现、协议、序列化那部分在另一篇 Dubbo 笔记里。
评论
评论加载中……