PO(persistant object) 持久对象
在 o/r 映射的时候出现的概念,如果没有 o/r 映射,没有这个概念存在了。通常对应数据模型 ( 数据库 ), 本身还有部分业务逻辑的处理。可以看成是与数据库中的表相映射的 java 对象。最简单的 PO 就是对应数据库中某个表中的一条记录,多个记录可以用 PO 的集合。 PO 中应该不包含任何对数据库的操作。
DO(Domain Object)领域对象
就是从现实世界中抽象出来的有形或无形的业务实体。一般和数据中的表结构对应。
TO(Transfer Object) ,数据传输对象
在应用程序不同 tie( 关系 ) 之间传输的对象
DTO(Data Transfer Object)数据传输对象
这个概念来源于J2EE的设计模式,原来的目的是为了EJB的分布式应用提供粗粒度的数据实体,以减少分布式调用的次数,从而提高分布式调用的性能和降低网络负载,但在这里,我泛指用于展示层与服务层之间的数据传输对象。
VO(view object) 值对象
视图对象,用于展示层,它的作用是把某个指定页面(或组件)的所有数据封装起来。
BO(business object) 业务对象
从业务模型的角度看 , 见 UML 元件领域模型中的领域对象。封装业务逻辑的 java 对象 , 通过调用 DAO 方法 , 结合 PO,VO 进行业务操作。 business object: 业务对象 主要作用是把业务逻辑封装为一个对象。这个对象可以包括一个或多个其它的对象。 比如一个简历,有教育经历、工作经历、社会关系等等。 我们可以把教育经历对应一个 PO ,工作经历对应一个 PO ,社会关系对应一个 PO 。 建立一个对应简历的 BO 对象处理简历,每个 BO 包含这些 PO 。 这样处理业务逻辑时,我们就可以针对 BO 去处理。
POJO(plain ordinary java object) 简单无规则 java 对象
纯的传统意义的 java 对象。就是说在一些 Object/Relation Mapping 工具中,能够做到维护数据库表记录的 persisent object 完全是一个符合 Java Bean 规范的纯 Java 对象,没有增加别的属性和方法。我的理解就是最基本的 Java Bean ,只有属性字段及 setter 和 getter 方法!。
DAO(data access object) 数据访问对象
是一个 sun 的一个标准 j2ee 设计模式, 这个模式中有个接口就是 DAO ,它负持久层的操作。为业务层提供接口。此对象用于访问数据库。通常和 PO 结合使用, DAO 中包含了各种数据库的操作方法。通过它的方法 , 结合 PO 对数据库进行相关的操作。夹在业务逻辑与数据库资源中间。配合 VO, 提供数据库的 CRUD 操作
在Web开发中,我们会接触到很多领域模型中的概念,其中大部分和实体相关的概念都有缩写,一般以O(Object)结尾。其中比较常见的由DO、DTO、VO、DAO等。我们也经常有把一个实体对象转换为另外一个实体对象的操作。本文主要是介绍一种作者在实践中总结的一种自认为比较优雅的转换方式。欢迎拍砖。
什么是DO、DTO和VO
在Java中 VO、 PO、DO、DTO、 BO、 QO、DAO、POJO的概念中介绍过Java中的各种模型概念。
在这里简单再总结一下:
在日常的项目开发中,
VO
对应于页面上需要显示的数据(表单),DO
对应于数据库中存储的数据(数据表),DTO
对应于除二者之外需要进行传递的数据。
很多人可能对VO和DTO并不是那么熟悉,相反对DO却比较熟悉,那是因为在很多项目中由于种种原因我们只使用了DO,原因可能有以下几种:
1、项目太小,对于一种业务实体,封装成一个DO就够了。
2、并不熟悉DTO、VO,更不知道他们之间的区别。
3、了解DO\DTO\VO之间的区别,但是懒得用。
那么,这里,博主再啰嗦一下为什么要引入这么多概念,为什么我要建议大家在自己的项目中使用这些实体对象。
为什么不能只用一个DO
我们来看这样一个例子。假如我们的项目中由User这样一个实体。我们在创建User表的时候一般包含一下属性:
针对这个实体,我们通常需要创建一个DO类,用来封装这个User实体。
public class UserDO {
private Integer id; //唯一主键
private Date createdTime; //创建时间
private Date updatedTime; //最后更新时间
private String name; //姓名
private Integer age; //年龄
private String gender; //性别
private String address; //住址
private String password; //密码
private String nickName; //昵称
private Date birthday; //生日
private String politicalStatus; //政治面貌,1表示群众,2表示团员,3表示党员,4表示其他,100表示未知
private Integer companyId; //公司的ID
private Integer status; //数据状态,1表示可用,0表示不可用
//setter and getter
}
然后,在代码中,从DAO一直到前端展示,我们都通过这个UserDO类的对象来进行数据传输。这样做会有什么问题嘛?
不需要的字段也会传递到前端页面。
- 如password、createdTime、updatedTime和status这几个字段我们可能在前端根本不需要展示,但是这些字段有可能也会被传递到前端(除非我们在SQL查询的时候没有查询出对应的字段)。这不仅使数据的传输量增大,还可能有安全性问题。
某些字段需要转换,但是无法支持。
- 对于上面例子中的政治面貌字段,我们在数据库中存储的是数字,但是在前端页面我要展示的是中文描述。这种情况只能在前端通过
if/else
的方式来分情况展示。
- 对于上面例子中的政治面貌字段,我们在数据库中存储的是数字,但是在前端页面我要展示的是中文描述。这种情况只能在前端通过
某些字段要展示,但是并不希望出现在数据库中
- 在User表中我们只保存了这个用户的companyId,需要同时查询company表来查询出该公司的更多详细信息。对于User对象,如果我们想在前端同时展示他所属的公司,希望通过一次查询全都查出来怎么办?有几个简单的方案,第一个是让UserDO中包含一个Company类的属性,通过这个属性来传递。另外一种是把我们希望传到前端的Company的属性也写到UserDO中。但是,如果真的这么做了,那UserDO还能被称作DO了吗?
还有很多问题,这这里就不详细介绍了。
可见,使用一个DO从头用到尾(从数据库到前端页面)并不是一种好的设计。
如何正确的使用DO、DTO、VO
对于上面的例子,我们可以将他设计成以下类。由于模型并不复杂,这里只需要再引入VO就可以了。
UserDO已经和数据库字段一一对应了,这里不需要修改。
public class UserDO {
private Integer id; //唯一主键
private Date createdTime; //创建时间
private Date updatedTime; //最后更新时间
private String name; //姓名
private Integer age; //年龄
private String gender; //性别
private String address; //住址
private String password; //密码
private String nickName; //昵称
private Date birthday; //生日
private String education; //学历
private String politicalStatus; //政治面貌,1表示群众,2表示团员,3表示党员,4表示其他,100表示未知
private Integer companyId; //公司的ID
private Integer status; //数据状态,1表示可用,0表示不可用
//setter and getter
}
引入UserVO,用于封装传递到前端需要展示的字段。
public class UserVO {
private Integer id; //唯一主键
private String name; //姓名
private Integer age; //年龄
private String gender; //性别
private String address; //住址
private String nickName; //昵称
private Date birthday; //生日
private String education; //学历
private String politicalStatus; //政治面貌,群众、团员、党员等
private Company company; //公司
//setter and getter
}
UserVO中只包含了展示所需要的字段,并不需要展示的字段在这里不需要包含。
在引入了这个类之后,我们就可在进行数据库查询的时候使用UserDO,然后再需要传递到前端的时候把DO转换成VO。
总结
看到这里,你可能已经发现,UserDO和UserVO中包含了大量的相同字段。难道真的有必要再单独设计个VO嘛?我可以明确告诉你的是,当你的系统越来越大,表中的字段越来越多的时候,使用DO\DTO\VO等概念进行分层处理是绝对有好处的。至于如何进行有效的在不同的实体类间进行转换是我接下来要介绍的。
优雅的将DO转换成VO
Dozer 是一个对象转换工具。
Dozer可以在JavaBean到JavaBean之间进行递归数据复制,并且这些JavaBean可以是不同的复杂的类型。
所有的mapping,Dozer将会很直接的将名称相同的fields进行复制,如果field名不同,或者有特别的对应要求,则可以在xml中进行定义。
前面我们介绍的DO\DTO\VO不就是JavaBean嘛。正好可以使用Dozer进行转换呀。
除了使用Dozer,当然你还由其他选择:
典型的解决方案就是手动拷贝,弊端很明显,代码中充斥大量Set 和Get方法,真正的业务被埋藏值与值的拷贝之中。
另一种方案就是使用BeanUtil,但BeanUtil不够很好的灵活性,又时候还不得不手动拷贝。Dozer可以灵活的对对象进行转换,且使用简单。
Dozer 支持的转换类型
Primitive
基本数据类型 , 后面带 Wrapper
是包装类 Complex Type
是复杂类型
• Primitive to Primitive Wrapper
• Primitive to Custom Wrapper
• Primitive Wrapper to Primitive Wrapper
• Primitive to Primitive
• Complex Type to Complex Type
• String to Primitive
• String to Primitive Wrapper
• String to Complex Type if the Complex Type contains a String constructor
• String 到复杂类型 , 如果复杂类型包含一个 String 类型的构造器
• String to Map
• Collection to Collection
• Collection to Array
• Map to Complex Type
• Map to Custom Map Type
• Enum to Enum
• Each of these can be mapped to one another: java.util.Date, java.sql.Date, java.sql.Time, java.sql.Timestamp, java.util.Calendar, java.util.GregorianCalendar
• String to any of the supported Date/Calendar Objects.
• Objects containing a toString() method that produces a long representing time in (ms) to any supported Date/Calendar object.
在普通Java项目中使用Dozer
在pom.xml中增加依赖
<dependency>
<groupId>net.sf.dozer</groupId>
<artifactId>dozer</artifactId>
<version>5.5.1</version>
</dependency>
使用Dozer进行类转换
public class Main {
public static void main(String[] args) {
DozerBeanMapper mapper = new DozerBeanMapper();
UserDO userDO = new UserDO();
userDO.setName("hollis");
userDO.setAddress("hz");
userDO.setAge(25);
userDO.setCompanyId(1);
userDO.setBirthday(new Date());
userDO.setGender("male");
userDO.setEducation("1");
userDO.setNickName("hollis");
userDO.setPoliticalStatus("3");
UserVO userVO = (UserVO) mapper.map(userDO, UserVO.class);
System.out.println(userVO);
}
}
特殊的字段转换
在使用mapper进行转换前,设置一个或多个mapping文件
List myMappingFiles = new ArrayList();
myMappingFiles.add("dozer-mapping.xml");
mapper.setMappingFiles(myMappingFiles);
配置dozer-mapping.xml文件
数据类型不一致,或者名称不相同或者有级联关系等情况下,可以通过文件dozer-mapping.xml
来进行定制Bean的转换
<?xml version="1.0" encoding="UTF-8"?>
<mappings xmlns="http://dozer.sourceforge.net" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://dozer.sourceforge.net
http://dozer.sourceforge.net/schema/beanmapping.xsd">
<mapping>
<class-a>com.hollis.lab.UserDO</class-a>
<class-b>com.hollis.lab.UserVO</class-b>
</mapping>
</mappings>
在JavaWeb项目中使用Dozer
在pom.xml中增加依赖
<dependency>
<groupId>net.sf.dozer</groupId>
<artifactId>dozer</artifactId>
<version>5.5.1</version>
</dependency>
使用Spring集成dozer
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="baseMapper" class="org.dozer.spring.DozerBeanMapperFactoryBean">
<property name="mappingFiles">
<list>
<value>classpath:mapping/dozer-mapping.xml</value>
</list>
</property>
</bean>
</beans>
使用baseMapper进行Bean的转换
@Autowired
private Mapper baseMapper;
private UserVO doToVo(UserDO userDO){
if(userDO == null) return null;
UserVO vo = baseMapper.map(userDO, UserVO.getClass());
if(userDO.getCompanyId != null) getCompany(vo);
return vo;
}
通过以上的代码加配置,我们就实现了从DO转换到VO的部分操作,之所以说是部分操作,是因为我们在dozer-mapping.xml
并没有做多余的配置,只是使用dozer将DO中和VO中共有的属性转换了过来。对于其他的类型不同或者名称不同等的转换可以参考官网例子通过设置dozer-mapping.xml
文件来实现。
上面还有一个getCompany()
没有实现。这个方法其实就是通过companyId查询出company实体然后在赋值给UserVO中的company属性。
在使用了dozer之后,我们可以把UserDO中的部分属性赋值到UserVO中,其实,转化的过程是通过调用UserDO中的getter方法和UserVO中的setter方法来实现的。读者可以做个实验,对于UserVO中的部分属性不写Setter方法看看还能不能把属性值转换过来,博主已经测试过了,是不能的。