Java 里的 DAO、DTO、POJO 都是什么

PO、DTO、VO、BO、DO、POJO 的差异不在写法——它们都是一堆字段加 getter/setter——而在「活在哪一层」。这篇顺带说清 Controller / Service / DAO 是哪三层,以及 DAO、Mapper、Repository 为什么是同一个角色的三个名字。

翻开一个 Java 后端项目,最先劝退人的经常不是业务逻辑,而是满屏的 UserPOUserDTOUserVOUserBO——打开一看,字段几乎一样,都是一堆属性加 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 / EntityPersistent ObjectDAO 层和数据库表一一对应,字段名跟列名对得上
DTOData Transfer Object跨进程、跨层传输接口的出入参,RPC 传的就是它
VOView Object返回给前端只放页面要展示的字段
BOBusiness ObjectService 内部聚合多个 PO 的业务对象
DODomain Object看团队阿里规范里指 Domain Object(约等于 PO),但很多团队用它指 Data Object
POJOPlain 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 这一类:

java
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 里放 PageResponseEntity 这类框架对象,消费方不一定有那个依赖。

四、DAO 和 PO 不是一类东西

如果对 PO 的理解是「对应一张表」,那很容易顺着问:DAO 说是数据操作层,它和 Mapper 是一回事吗?

先把最容易混的这一对分开:

PO 是名词,DAO 是动词的容器。 PO 描述一行数据长什么样,只有字段没有行为;DAO 描述对这张表能做什么操作,只有方法不存数据。一个是数据,一个是操作数据的人。

至于 DAO 和 Mapper——是同一个角色的两个叫法,Mapper 是 MyBatis 给它起的名字。

4.1 为什么会有两个名字

DAO 这个名字来自 2001 年 Sun 的 J2EE 设计模式,比 MyBatis 早很多年。当年 Spring 加 iBatis 的写法是接口加实现类:

java
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 模式)。

所以在老项目里看到 UserDaoUserDaoImpl,就知道那是 MyBatis 3 之前的写法。

4.2 现在实际长什么样

java
@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 才自己写:

java
@Mapper
public interface UserMapper extends BaseMapper<User> { }

4.3 还会遇到 Repository

JPA / Spring Data 那套叫 Repository。名字不同不只是习惯问题,概念上有点差别:

DAO / MapperRepository
面向领域(聚合根)
一个接口对应一张表一个聚合,可能跨好几张表
对上层的伪装一组 SQL 操作「一个集合」

不过在九成的项目里,这个区别只停留在命名上,实际写法没差。进新项目先看别人怎么命名,跟着来就行,别自己另起一套。

五、一页对照

User.java          ← PO,一行数据
UserMapper.java    ← DAO,对这张表的操作
UserMapper.xml     ← SQL
UserService.java   ← 业务逻辑,调 Mapper
UserDTO.java       ← 跨进程传的,别把 User 直接传出去
UserVO.java        ← 给前端的,只放页面要展示的字段

这堆缩写没有一个是语法特性,全是约定。它们唯一的作用是让「这个对象允许被谁看到」这件事从口头约定变成类型系统里的事实。所以团队里叫什么名字不重要,重要的是别让一个类跨越太多层


这篇是从一次 Dubbo 学习的对话里拆出来的,服务发现、协议、序列化那部分在另一篇 Dubbo 笔记里。

评论

评论加载中……

登录后再评论

注册要用邮箱收个验证码,只为确认邮箱能收信,不会拿去做别的。账号设置