深扒Java中POJO、VO、DO、DTO、PO、BO、AO、DAO的概念和区别以及如何应用
作者:流华追梦
一. 前言
POJO、VO、DO、DTO、PO、BO、AO 和 DAO 等术语被广泛应用于各种编程语言中。尽管这些术语是非常常见的,但是很多程序员依然无法清楚地理解它们之间的区别和关系。本文将深入探讨这些术语的含义和用途,帮助程序员更好地理解它们之间的差异和联系。
二. 概念
2.1. 概要介绍
- POJO(Plain Ordinary Java Object):简单 Java 对象。
- VO(View Object or Value Object):视图对象或值对象,用于展示层。
- DTO(Data Transfer Object):数据传输对象,泛指用于展示层与服务层之间的数据传输对象。
- PO(Persistent Object):持久化对象,它跟持久层(通常是关系型数据库)的数据结构形成一一对应的映射关系。
- BO(Business Object):业务对象。封装基本业务操作。
- DO(Data Object or Domain Object):数据对象或领域对象,就是从现实世界中抽象出来的有形或无形的业务实体。
- AO(Application Object):应用对象。
- DAO(Data Access Object):数据访问对象。主要封装对数据库的访问。
2.2. POJO(Plain Old/Ordinary Java Object)
简单 Java 对象,表示一个个简单的 Java 对象,POJO 是 DO、DTO、BO、VO 的统称,禁止命名成 xxxPOJO。
使用 POJO 名称是为了避免和 EJB 混淆起来,而且简称比较直接。其中有一些属性及其 getter、setter 方法的类,没有业务逻辑。可以说 POJO 是一个JavaBean,但 JavaBean 不一定是 POJO。
2.3. VO(View/Value Object)
VO 对象全称为 View Object(视图对象)或者 Value Object(值对象),主要用于前端界面显示的数据,是与前端进行交互的对象,但这里是不用 PO 传递数据的,因为 PO 包括数据库表中的所有字段,对于前端来说我们只需要显示一部分字段就可以了,例如我们的用户表 user 中的 password(密码)字段、phone(电话)字段、insert_time(插入时间)字段是没有必要也不能显示在前端界面的。遵循 JavaBean 规范,拥有 get 和 set 方法。
2.4. DTO(Data Transfer Object)
数据传输对象是在传递给前端时使用的,如一张表有100个字段,那么对应的 PO 就有100个属性,但是我们的前端界面只需要显示10个字段,所以我们没必要把所有字段的 PO 对象传递到客户端,我们只需要把只有这10个属性的 DTO 对象传递到客户端,不会暴露服务端的表结构,到达客户端后,如果这个对象用于界面表示,那么它的身份就是 VO 对象。
DTO 和 VO 概念相似,通常情况下字段也基本一致。但有所不同,DTO 表示一个数据传输对象,是在服务端用于不同服务或不同层之间的数据传输,例如,Dao 层到 Service 层,Service 层到 Web 层;而 VO 是在客户端浏览器显示的表现对象,用于在浏览器界面数据的显示。
数据传输对象比较特殊,之所以将 DTO 绘制在 视图层 和 业务逻辑层 之间,是因为它有两种存在形式:
- 前端:它是以 Json 字串的形式存在。
- 后端:它是以对象的形式存在。
微服务之间 DTO 对象的模型鉴定形式:
- 服务(模块)与服务(模块)之间相对独立,我们可以将数据传输对象命名为 DTO。
- 服务(模块)与服务(模块)之间不是独立,每一个都不是一个完整的业务模块,拆分可能仅仅是因为计算复杂度或者性能问题考虑拆分的问题,那么就不能将对象命名为 DTO,只能是 BO。
2.5. PO(Persistent Object)
PO 对象全称为 Persistent Object(持久化对象),用于表示数据库中的一条记录映射成的对象,类中应该都是基本数据类型和 String,而不是更复杂的类型,因为要和数据库表字段对应。PO 仅仅用于表示数据,不对数据进行操作,拥有 get 和 set 方法。对象类中的属性对应数据库表中的字段,有多少个字段就有多少个属性,完全匹配。遵循 JavaBean 规范,拥有 get 和 set 方法。如下图所示:
2.6. BO(Business Object)
BO 是实际的业务对象,会参与业务逻辑的处理操作,里面可能会包含多个类,用于表示一个业务对象。例如:用户可以拥有宠物,在这里把用户对应一个 PO、宠物对应一个 PO,那么建立一个对应的 BO 对象来处理用户和宠物的关系,每个 BO 都包含用户 PO 和宠物 PO,而处理逻辑时针对 BO 去处理。遵循 JavaBean 规范,拥有 get 和 set 方法。注:User 和 Pet 都是 PO 对象,但会放进 BO 中,形成一个复杂的业务对象。
2.7. DO(Data/Domain Object)
DO 对象全称为 Data Object(数据对象)或者 Domain Object(领域对象),也有一种叫法是 Entity Object,一般不推荐使用 Entity Object 这个叫法。该对象与数据库表结构一一对应,通过Dao 层向上传输数据对象,属性和 PO 中的基本一致,用到的不多。
DO 用到的不多,有一种情况是微服务之间互相传输对象数据的时候,我们会将该数据抽取出来。
比如商品下单以后,大量的 JSON 数据会传输到后台,我们会在后台调用很多模块,比如 OMS/订单管理模块,又或是 ERP 模块,又或是 WMS/库存物流模块等,会调用很多其他微服务模块。不同服务之间传输对象,就可以将对象定义为 DO 或者 TO(Transfer Object),不同叫法而已。然后两个模块之间 RPC 调用,传输的对象要被两个模块共享,一种是将每一个模块涉及的各种实体对象单独抽出来放在一个 Entity 模块下;还一种是将这些微服务模块之间的交互传输对象定义在 Common 模块中,作为 DO 或 TO 来进行微服务之间的数据传输。
2.8. AO(Application Object)
AO 对象全称为 Application Object(应用对象),在 Web 层与 Service 层之间抽象的复用对象模型,极为贴近展示层,复用度不高。举一个很简单的例子:控制层(Controller) 在业务逻辑层(Service)查询一条或多条数据,这个数据的传输过程的运载就是 AO 完成。在正常的业务逻辑中一般都有很多种类型的数据,例如 整形、字符型、集合、类 等,我们把它统称为 AO。
2.9. DAO(Data Access Object)
DAO 对象全称为 Data Access Object(数据访问对象),是主要封装对数据库的访问。通过它可以把 POJO 持久化为 PO,用 PO 组装出 VO 和 DTO。DAO 一般在持久层,完全封装数据库操作,对外暴露的方法的使得上层不需要关注数据库的相关信息,只需要插入、删除、更新、查询即可。
例如 UserDao 封装的就是对 user 表的增删改查操作:
三. 区别和应用
3.1. VO 与 DTO 的区别
大家可能会有个疑问:既然 DTO 是展示层与服务层之间传递数据的对象,为什么还需要一个VO 呢?对于绝大部分的应用场景来说,DTO 和 VO 的属性值基本是一致的,而且他们通常都是POJO,因此没必要多此一举,但不要忘记这是实现层面的思维,对于设计层面来说,概念上还是应该存在 VO 和 DTO,因为两者有着本质的区别。
DTO 代表服务层需要接收的数据和返回的数据,而 VO 代表展示层需要显示的数据。用一个例子来说明可能会比较容易理解:例如服务层有一个 getUser 的方法返回一个系统用户,其中有一个属性是 gender(性别),对于服务层来说,它只从语义上定义:1-男性,2-女性,0-未指定,而对于展示层来说,它可能需要用“帅哥”代表男性,用“美女”代表女性,用“秘密”代表未指定。说到这里,可能你还会反驳,在服务层直接就返回“帅哥美女”不就行了吗?对于大部分应用来说,这不是问题,但设想一下,如果需求允许客户可以定制风格,而不同风格对于“性别”的表现方式不一样,又或者这个服务同时供多个客户端使用(不同门户),而不同的客户端对于表现层的要求有所不同,那么,问题就来了。
再者,回到设计层面上分析,从职责单一原则来看,服务层只负责业务,与具体的表现形式无关,因此,它返回的 DTO,不应该出现与表现形式的耦合。理论归理论,这到底还是分析设计层面的思维,是否在实现层面必须这样做呢?一刀切的做法往往会得不偿失,下面我马上会分析应用中如何做出正确的选择。
3.2. VO 与 DTO 的应用
上面只是用了一个简单的例子来说明 VO 与 DTO 在概念上的区别,本节将会告诉你如何在应用中做出正确的选择。在以下场景中,我们可以考虑把 VO 与 DTO 二合为一(注意,是实现层面):
- 需求非常清晰稳定,而且客户端很明确只有一个的时候,没有必要把 VO 和 DTO 区分开来,这时候 VO 可以退隐,用一个 DTO 即可,为什么是 VO 退隐而不是 DTO?回到设计层面,服务层的职责依然不应该与展示层耦合,所以,对于前面的例子,你很容易理解,DTO 对于“性别”来说,依然不能用“帅哥美女”,这个转换应该依赖于页面的脚本(如 JavaScript)或其他机制(JSTL、EL、CSS);
- 即使客户端可以进行定制,或者存在多个不同的客户端,如果客户端能够用某种技术(脚本或其他机制)实现转换,同样可以让 VO 退隐。
以下场景需要优先考虑 VO、DTO 并存:
- 上述场景的反面场景;
- 因为某种技术原因,比如某个框架(如 Flex)提供自动把 POJO 转换为 UI 中某些 Field 时,可以考虑在实现层面定义出 VO,这个权衡完全取决于使用框架的自动转换能力带来的开发和维护效率提升,与设计多一个 VO 所多做的事情带来的开发和维护效率的下降之间的比对。
- 如果页面出现一个“大视图”,而组成这个大视图的所有数据需要调用多个服务,返回多个DTO 来组装(当然,这同样可以通过服务层提供一次性返回一个大视图的 DTO 来取代,但在服务层提供一个这样的方法是否合适,需要在设计层面进行权衡)。
3.3. DTO 与 DO 的区别
首先是概念上的区别,DTO 是展示层和服务层之间的数据传输对象(可以认为是两者之间的协议),而 DO 是对现实世界各种业务角色的抽象,这就引出了两者在数据上的区别,例如 UserInfo和 User,对于一个 getUser 方法来说,本质上它永远不应该返回用户的密码,因此 UserInfo 至少比 User 少一个 password 的数据。而在领域驱动设计中,DO 不是简单的 POJO,它具有领域业务逻辑。
3.4. DTO 与 DO 的应用
从上一节的例子中,细心的读者可能会发现问题:既然 getUser 方法返回的 UserInfo 不应该包含 password,那么就不应该存在 password 这个属性定义,但如果同时有一个 createUser 的方法,传入的 UserInfo 需要包含用户的 password,怎么办?在设计层面,展示层向服务层传递的DTO 与服务层返回给展示层的 DTO 在概念上是不同的,但在实现层面,我们通常很少会这样做(定义两个 UserInfo,甚至更多),因为这样做并不见得很明智,我们完全可以设计一个完全兼容的 DTO,在服务层接收数据的时候,不该有展示层设置的属性(如订单的总价应该由其单价、数量、折扣等决定),无论展示层是否设置,服务层都一概忽略,而在服务层返回数据时,不该返回的数据(如用户密码),就不设置对应的属性。
对于 DO 来说,还有一点需要说明:为什么不在服务层中直接返回 DO 呢?这样可以省去 DTO 的编码和转换工作,原因如下:
- 两者在本质上的区别可能导致彼此并不一一对应,一个 DTO 可能对应多个 DO,反之亦然,甚至两者存在多对多的关系。
- DO 具有一些不应该让展示层知道的数据。
- DO 具有业务方法,如果直接把 DO 传递给展示层,展示层的代码就可以绕过服务层直接调用它不应该访问的操作,对于基于 AOP 拦截服务层来进行访问控制的机制来说,这问题尤为突出,而在展示层调用 DO 的业务方法也会因为事务的问题,让事务难以控制。
- 对于某些 ORM 框架(如 Hibernate)来说,通常会使用“延迟加载”技术,如果直接把 DO 暴露给展示层,对于大部分情况,展示层不在事务范围之内(Open session in view 在大部分情况下不是一种值得推崇的设计),如果其尝试在 Session 关闭的情况下获取一个未加载的关联对象,会出现运行时异常(对于 Hibernate 来说,就是 LazyInitiliaztionException)。
- 从设计层面来说,展示层依赖于服务层,服务层依赖于领域层,如果把 DO 暴露出去,就会导致展示层直接依赖于领域层,这虽然依然是单向依赖,但这种跨层依赖会导致不必要的耦合。
对于 DTO 来说,也有一点必须进行说明,就是 DTO 应该是一个“扁平的二维对象”,举个例子来说:如果 User 会关联若干个其他实体(例如 Address、Account、Region 等),那么 getUser() 返回的 UserInfo,是否就需要把其关联的对象的 DTO 都一并返回呢?如果这样的话,必然导致数据传输量的大增,对于分布式应用来说,由于涉及数据在网络上的传输、序列化和反序列化,这种设计更不可接受。如果 getUser 除了要返回 User 的基本信息外,还需要返回一个 AccountId、AccountName、RegionId、RegionName,那么,请把这些属性定义到 UserInfo 中,把一个“立体”的对象树“压扁”成一个“扁平的二维对象”,笔者目前参与的项目是一个分布式系统,该系统不管三七二十一,把一个对象的所有关联对象都转换为相同结构的 DTO 对象树并返回,导致性能非常的慢。
3.5. DO 与 PO 的区别
DO 和 PO 在绝大部分情况下是一一对应的,PO 是只含有 get/set 方法的 POJO,但某些场景还是能反映出两者在概念上存在本质的区别:
- DO 在某些场景下不需要进行显式的持久化,例如利用策略模式设计的商品折扣策略,会衍生出折扣策略的接口和不同折扣策略实现类,这些折扣策略实现类可以算是 DO,但它们只驻留在静态内存,不需要持久化到持久层,因此,这类 DO 是不存在对应的 PO 的。
- 同样的道理,某些场景下,PO 也没有对应的 DO,例如老师 Teacher 和学生 Student 存在多对多的关系,在关系数据库中,这种关系需要表现为一个中间表,也就对应有一个TeacherAndStudentPO 的 PO,但这个 PO 在业务领域没有任何现实的意义,它完全不能与任何 DO 对应上。这里要特别声明,并不是所有多对多关系都没有业务含义,这跟具体业务场景有关,例如:两个 PO 之间的关系会影响具体业务,并且这种关系存在多种类型,那么这种多对多关系也应该表现为一个 DO,又如:“角色”与“资源”之间存在多对多关系,而这种关系很明显会表现为一个 DO——“权限”。
- 某些情况下,为了某种持久化策略或者性能的考虑,一个 PO 可能对应多个 DO,反之亦然。例如客户 Customer 有其联系信息 Contacts,这里是两个一对一关系的 DO,但可能出于性能的考虑(极端情况,权作举例),为了减少数据库的连接查询操作,把 Customer 和Contacts 两个 DO 数据合并到一张数据表中。反过来,如果一本图书 Book,有一个属性是封面 Cover,但该属性是一副图片的二进制数据,而某些查询操作不希望把 Cover 一并加载,从而减轻磁盘 IO 开销,同时假设 ORM 框架不支持属性级别的延迟加载,那么就需要考虑把 Cover 独立到一张数据表中去,这样就形成一个 DO 对应一个 PO 的情况。
- PO 的某些属性值对于 DO 没有任何意义,这些属性值可能是为了解决某些持久化策略而存在的数据,例如为了实现“乐观锁”,PO 存在一个 version 的属性,这个 version 对于 DO 来说是没有任何业务意义的,它不应该在 DO 中存在。同理,DO 中也可能存在不需要持久化的属性。
3.6. DO 与 PO 的应用
由于 ORM 框架的功能非常强大而大行其道,而且 JavaEE 也推出了 JPA 规范,现在的业务应用开发,基本上不需要区分 DO 与 PO,PO 完全可以通过 JPA、Hibernate Annotations/hbm隐藏在 DO 之中。虽然如此,但有些问题我们还必须注意:
- 对于 DO 中不需要持久化的属性,需要通过 ORM 显式的声明,如:在 JPA 中,可以利用@Transient 声明。
- 对于 PO 中为了某种持久化策略而存在的属性,例如 version,由于 DO、PO 合并了,必须在 DO 中声明,但由于这个属性对 DO 是没有任何业务意义的,需要让该属性对外隐藏起来,最常见的做法是把该属性的 get/set 方法私有化,甚至不提供 get/set 方法,但对于Hibernate 来说,这需要特别注意,由于 Hibernate 从数据库读取数据转换为 DO 时,是利用反射机制先调用 DO 的空参数构造函数构造 DO 实例,然后再利用 JavaBean 的规范反射出set 方法来为每个属性设值,如果不显式声明 set 方法,或把 set 方法设置为 private,都会导致 Hibernate 无法初始化 DO,从而出现运行时异常,可行的做法是把属性的 set 方法设置为 protected。
- 对于一个 DO 对应多个 PO,或者一个 PO 对应多个 DO 的场景,以及属性级别的延迟加载,Hibernate 都提供了很好的支持,请参考 Hibnate 的相关资料。
四. 实例讲解
有一个博客系统,数据库中存储了很多篇博客。我们会做如下设计:
- 数据库表:表中的博客包括编号、博客标题、博客内容、博客标签、博客分类、博客状态、创建时间、修改时间等。
- PO:包括编号、博客标题、博客内容、博客标签、博客分类、博客状态、创建时间、修改时间等。(与数据库表中的字段一样)。
- VO:在客户端浏览器展示的页面数据,博客标题、博客内容、博客标签、博客分类、创建时间、上一篇博客 URL、下一篇博客 URL。
- DTO:在服务端数据传输的对象,编号、博客标题、博客内容、博客标签、博客分类、创建时间、上一篇博客编号、下一篇博客编号。
- DAO:数据库增删改查的方法,例如新增博客、删除博客、查询所有博客、更新博客。
- BO:基本业务操作,如管理分类、管理标签、修改博客状态等,是我们常说的 Service 层操作。
- 用户发出请求(可能是填写表单),表单的数据在展示层被匹配为 VO。
- 展示层把 VO 转换为服务层对应方法所要求的 DTO,传送给服务层。
- 服务层首先根据 DTO 的数据构建或重建一个 DO,调用 DO 的业务方法完成具体业务。
- 服务层把 DO 转换为持久层对应的 PO(可以使用 ORM 工具,也可以不用),调用持久层的持久化方法,把 PO 传递给它,完成持久化操作。
- 对于一个逆向操作,如读取数据,也是用类似的方式转换和传递。
五. 总结
到目前为止,相信大家都已经比较清晰的了解了 VO、DTO、DO、PO、BO 等的概念、区别和实际应用了。通过上面的详细分析,我们还可以总结出一个原则:分析设计层面和实现层面完全是两个独立的层面,即使实现层面通过某种技术手段可以把两个完全独立的概念合二为一,在分析设计层面,我们仍然(至少在头脑中)需要把概念上独立的东西清晰的区分开来,这个原则对于做好分析设计非常重要(工具越先进,往往会让我们越麻木)。
到此这篇关于Java中POJO、VO、DO、DTO、PO、BO、AO、DAO的概念和区别以及如何应用的文章就介绍到这了,更多相关POJO、VO、DO、DTO、PO、BO、AO、DAO应用内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!