四、数据传输、数据模型与Dozer
数据传输是程序员实现各种功能时刻需要考虑的问题,从数据模型的建立,到数据模型的转换,从数据的合法性验证,到数据类型的转化,我们要时刻小心,精心设计与组织。数据模型与数据传输可简单可复杂,完全取决于设计者的经验与意图,当然,项目的规模也是我们应该考虑的因素,一个小型项目实在没必要将问题复杂化。
我们首先考虑数据从视图(View)传输到数据库(DB)的数据模型变化过程。用户从界面输入数据,此时,数据毫无结构可言,是零散的、无组织的,数据提交到控制器后,一方面考虑到OOP的严谨性,另一方面考虑到数据的封装,会将零散的无组织的数据封装成Value Object(简称VO)对象,VO就是JavaBean,并传送到业务类中,在数据模型比较复杂的情况下,业务类的方法参数和返回值都应该是VO或VO的集合,VO转换成PO(Persistence Object)后传送到DAO,DAO调用Hibernate API持久化数据。简单来说,业务类对外暴露的数据模型是VO,DAO对外暴露的数据模型是PO。
数据从数据库传递到视图层的数据模型变化恰恰相反,Hibernate将数据库中的记录转换成PO,PO传递给业务类后转换成VO,VO被传送到视图层进行显示处理。
以上转换过程如下图:
为什么同时需要PO和VO?前面说过,这不是必须的,如果项目比较小,直接使用PO就行了。但PO有如下缺点:
v PO反应了数据库的物理模型,向外暴露PO存在一定的风险;
v PO过于僵化,无法适应变化莫测的业务需求;
v PO在持久化状态下与数据库同步,可能导致数据意外修改;
v PO将导致程序缺乏健壮性。
而VO没有PO的缺点,相对而言,VO更加灵活,能适应各种需求的变化,下面是PO和VO的区别:
v VO是用new关键字创建,由GC回收的。PO则是向数据库中添加新数据时创建,删除数据库中数据时削除的。并且它只能存活在一个数据库连接中,断开连接即被销毁;
v VO是值对象,精确点讲它是业务对象,是存活在业务层的,是业务逻辑使用的,它存活的目的就是为数据提供一个生存的地方。PO则是有状态的,每个属性代表其当前的状态。它是物理数据的对象表示。使用它,可以使我们的程序与物理数据解耦,并且可以简化对象数据与物理数据之间的转换;
v VO的属性是根据当前业务的不同而不同的,也就是说,它的每一个属性都一一对应当前业务逻辑所需要的数据的名称。PO的属性是跟数据库表的字段一一对应的;
v PO一般只有一个,但对应的VO可能有多个。
我们举一个简单的例子来说明VO与PO的区别:比如要实现用户注册与登陆的功能,在物理模型中创建一个用户表,字段分别为用户ID(标识列)、用户名、密码、注册日期等等,定义PO时应该有这四个字段的映射属性。现在,我们来实现用户注册这一功能,为了接收用户输入的注册信息,必须定义VO,注册用户需要输入的信息有:用户名、密码1、密码2,这正好是VO的属性。而实现用户登陆功能时,用户需要输入的信息只有用户名和密码,所以,该VO的属性只有两个:用户名、密码。可以看出,PO侧重于物理模型,而VO则更关注用户的实际需求。如下图所示:
因为数据传输是双向的,所以VO和PO之间存在相互转换的问题,这会给编程带来麻烦,我们总是要通过getter方法取出数据,再通过setter方法给对方属性赋值,在属性很多的情况下,让人深感繁琐,而Dozer工具能简化这个问题。
Dozer(http://dozer.sourceforge.net/)是一个JavaBean映射工具,能实现对象属性值之间的相互赋值,Dozer支持简单类型映射、复合类型映射、双向映射以及递归映射,默认情况下,Dozer能实现类型相同、名字相同的属性之间的赋值,如果属性名不同,则必须在xml(dozerBeanMapping.xml)配置文件中指定,如果不必为JavaBean的每个属性赋值,也可以在xml中指定。Xml的定义可以参考http://dozer.sourceforge.net/dtd/dozerbeanmapping.dtd,典型的结构如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mappings PUBLIC "-//DOZER//DTD MAPPINGS//EN"
"http://dozer.sourceforge.net/dtd/dozerbeanmapping.dtd">
<mappings>
<configuration>
<stop-on-errors>false</stop-on-errors>
<date-format>MM/dd/yyyy HH:mm</date-format>
<wildcard>true</wildcard>
</configuration>
<mapping>
<class-a>com.denny_blue.dozerdemo.Book</class-a>
<class-b>com.denny_blue.dozerdemo.CookBook</class-b>
<field>
<a>name</a>
<b>bookName</b>
</field>
<field>
<a>author</a>
<b>author</b>
</field>
</mapping>
</mappings>
<class-a>指定所要复制的源对象,<class-b>复制的目标对象,<a>源对象的属性名, <b>目标对象的属性名。wildcard默认为true,在此时默认对所有属性进行map,如果为false,则只对在xml文件中配置的属性进行map。
其中,Book和CookBook类分别定义如下:
public class Book{
public Book(){
}
public void setAuthor(String author) {
this.author = author;
}
public String getAuthor() {
return (this.author);
}
public void setName(String name){
this.name=name;
}
public String getName(){
return this.name;
}
}
public class CookBook {
private String bookName;
private String author;
public CookBook(){}
public String getBookName() {
return (this.bookName);
}
public void setBookName(String bookName) {
this.bookName = bookName;
}
public String getAuthor() {
return (this.author);
}
public void setAuthor(String author) {
this.author = author;
}
}
以下是测试代码:
Book book1=new Book();
book1.setAuthor("dennis");
book1.setName("dozer demo");
DozerBeanMapper mapper=new DozerBeanMapper();
book2=(Book)mapper.map(book1,com.denny_blue.dozerdemo.Book.class);
CookBook cookBook=new CookBook();
List myMappingFiles = new ArrayList();
myMappingFiles.add("dozerBeanMapping.xml");
mapper.setMappingFiles(myMappingFiles);
cookBook=(CookBook)mapper.map(book1,CookBook.class);
System.out.println("cookBook's name:"+ cookBook.getBookName()+" cookBook's author:"+
cookBook.getAuthor());
}
通过mapper.setMappingFiles()设置映射文件,可以添加多个配置文件,也可以把所有的映射写在一个配置文件里面。这里介绍的只是最基本的使用方法,为了实现Dozer的模块化应用,我专门写了一个VoPoConverter类简化Dozer的调用。
package com.aptech.util;
import java.util.ArrayList;
import java.util.List;
import org.dozer.DozerBeanMapper;
import org.dozer.Mapper;
/**
* VO和PO相互转换的类
*/
public class VoPoConverter {
/**
* VO和PO之间相互转换,将源对象的同名属性复制目标对象中
* 前提:源对象和目标对象都必须存在
* @param src 源对象
* @param desc 目标对象
*/
public static void copyProperties(Object src, Object desc){
if(src == null) return;
Mapper mapper = new DozerBeanMapper();
mapper.map(src, desc);
}
/**
* VO和PO之间相互转换,先创建对象,再将源对象的同名属性复制目标对象中
* @param <T> 目标类型
* @param src 源对象
* @param descType 目标类型
* @return
*/
public static <T> T copyProperties(Object src, Class<T> descType){
if(src == null) return null;
Mapper mapper = new DozerBeanMapper();
return mapper.map(src, descType);
}
/**
* 将源集合转换为目标集合,注意:目标集合是新建的
* @param <T>
* @param srcList 源集合
* @param descType 目标集合中元素的类型
* @return
*/
public static <T> List<T> copyList(List srcList, Class<T> descType){
if(srcList == null) return null;
List<T> descList = new ArrayList<T>();
for(Object obj : srcList){
T t = VoPoConverter.copyProperties(obj, descType);
descList.add(t);
}
return descList;
}
}
类名的意思虽然叫VO与PO转换器,实际上可以应用在任何场合。如果要配合xml配置文件,该类还需要做一些修改。
——作者:李赞红 (lifenote@21cn.com),转载请保留版权!