传统的DAO设计
数据访问对象(DAO)是一个集成层设计模式,如Core J2EE Design Pattern 图书所归纳。它将持久性存储访问和操作代码封装到一个单独的层中。本文的上下文中所提到的持久存储器是一个RDBMS。
这一模式在业务逻辑层和持久存储层之间引入了一个抽象层,如图1所示。业务对象通过数据访问对象来访问RDBMS(数据源)。抽象层改善了应用 程序代码并引入了灵活性。理论上,当数据源改变时,比如更换数据库供应商或是数据库的类型时,仅需改变数据访问对象,从而把对业务对象的影响降到最低。

图1. 应用程序结构,包括DAO之前和之后的部分
讲解了DAO设计模式的基础知识,下面将编写一些代码。下面的例子来自于一个公司域模型。简而言之,这家公司有几位员工工作在不同的部门,如销售部、市场部以及人力资源部。为了简单起见,我们将集中讨论一个称作“雇员”的实体。
针对接口编程
DAO设计模式带来的灵活性首先要归功于一个对象设计的最佳实践:针对接口编程(P2I)。这一原则规定实体必须实现一个供调用程序而不是实体自身使用的接口。因此,可以轻松替换成不同的实现而对客户端代码只产生很小的影响。
我们将据此使用findBySalaryRange()行为定义Employee DAO接口,IEmployeeDAO。业务组件将通过这个接口与DAO交互:
import
java.util.Map;
public
interface
IEmployeeDAO
...
{
//SQLStringthatwillbeexecuted
publicStringFIND_BY_SAL_RNG="SELECTEMP_NO,EMP_NAME,"
+"SALARYFROMEMPWHERESALARY>=?ANDSALARY<=?";
//Returnsthelistofemployeeswhofallintothegivensalary
//range.Theinputparameteristheimmutablemapobject
//obtainedfromtheHttpServletRequest.Thisisanearly
//refactoringbasedon"IntroduceParameterObject"
publicListfindBySalaryRange(MapsalaryMap);
}
提供DAO实现类
接口已经定义,现在必须提供Employee DAO的具体实现,EmployeeDAOImpl:
import
java.sql.Connection;
import
java.sql.PreparedStatement;
import
java.sql.ResultSet;
import
java.util.List;
import
java.util.ArrayList;
import
java.util.Map;
import
com.bea.dev2dev.to.EmployeeTO;

public
class
EmployeeDAOImpl
implements
IEmployeeDAO
...
{
publicListfindBySalaryRange(MapsalaryMap)
...{
Connectionconn=null;
PreparedStatementpstmt=null;
ResultSetrs=null;
ListempList=newArrayList();
//TransferObjectforinter-tierdatatransfer
EmployeeTOtempEmpTO=null;
try...{
//DBUtil-helperclassesthatretrieveconnectionfrompool
conn=DBUtil.getConnection();
pstmt=conn.prepareStatement(FIND_BY_SAL_RNG);
pstmt.setDouble(1,Double.valueOf((String)
salaryMap.get("MIN_SALARY"));
pstmt.setDouble(2,Double.valueOf((String)
salaryMap.get("MIN_SALARY"));
rs=pstmt.executeQuery();
inttmpEmpNo=0;
StringtmpEmpName="";
doubletmpSalary=0.0D;
while(rs.next())...{
tmpEmpNo=rs.getInt("EMP_NO");
tmpEmpName=rs.getString("EMP_NAME");
tmpSalary=rs.getDouble("SALARY");
tempEmpTO=newEmployeeTO(tmpEmpNo,
tmpEmpName,
tmpSalary);
empList.add(tempEmpTO);
}//endwhile
}//endtry
catch(SQLExceptionsqle)...{
thrownewDBException(sqle);
}//endcatch
finally...{
try...{
if(rs!=null)...{
rs.close();
}
}
catch(SQLExceptionsqle)...{
thrownewDBException(sqle);
}
try...{
if(pstmt!=null)...{
pstmt.close();
}
}
catch(SQLExceptionsqle)...{
thrownewDBException(sqle);
}
try...{
if(conn!=null)...{
conn.close();
}
}
catch(SQLExceptionsqle)...{
thrownewDBException(sqle);
}
}//endoffinallyblock
returnempList;
}//endmethodfindBySalaryRange
}
- 它们封装了所有与JDBC API的交互。如果使用像Kodo或者Hibernate的O/R映射方案,则DAO类可以将这些产品的私有API打包。
- 它们将检索到的数据打包到一个与JDBC API无关的传输对象中,然后将其返回给业务层作进一步处理。
- 它们实质上是无状态的。唯一的目的是访问并更改业务对象的持久数据。
- 在这个过程中,它们像SQLException一样捕获任何底层JDBC API或数据库报告的错误(例如,数据库不可用、错误的SQL句法)。DAO对象再次使用一个与JDBC无关的自定义运行时异常类DBException,通知业务对象这些错误。
- 它们像Connection和PreparedStatement对象那样,将数据库资源释放回池中,并在使用完ResultSet游标之后,将其所占用的内存释放。
因此,DAO层将底层的数据访问API抽象化,为业务层提供了一致的数据访问API。
构建DAO工厂
DAO工厂是典型的工厂设计模式实现,用于为业务对象创建和提供具体的DAO实现。业务对象使用DAO接口,而不用了解实现类的具体情况。DAO工厂带来的依赖反转(dependency inversion)提供了极大的灵活性。只要DAO接口建立的约定未改变,那么很容易改变DAO实现(例如,从straight JDBC实现到基于Kodo的O/R映射),同时又不影响客户的业务对象:
public
class
DAOFactory
...
{
privatestaticDAOFactorydaoFac;

static...{
daoFac=newDAOFactory();
}

privateDAOFactory()...{}

publicDAOFactorygetInstance()...{
returndaoFac;
}

publicIEmployeeDAOgetEmployeeDAO()...{
returnnewEmployeeDAOImpl();
}
}
与业务组件的协作
现在该了解DAO怎样适应更复杂的情形。如前几节所述,DAO与业务层组件协作获取和更改持久业务数据。下面的清单展示了业务服务组件及其与DAO层的交互:
public
class
EmployeeBusinessServiceImpl
implements

IEmployeeBusinessService
...
{

publicListgetEmployeesWithinSalaryRange(MapsalaryMap)...{
IEmployeeDAOempDAO=DAOFactory.getInstance()
.getEmployeeDAO();
ListempList=empDAO.findBySalaryRange(salaryMap);
returnempList;
}
}
问题
DAO设计模式也有缺点:
- 代码重复:从EmployeeDAOImpl清单可以清楚地看到,对于基于JDBC的传统数据库访问,代码重复(如上面的粗体字所示)是一个主要的问题。一遍又一遍地写着同样的代码,明显违背了基本的面向对象设计的代码重用原则。它将对项目成本、时间安排和工作产生明显的副面影响。
- 耦合:DAO代码与JDBC接口和核心collection耦合得非常紧密。从每个DAO类的导入声明的数量可以明显地看出这种耦合。
- 资源耗损:依据EmployeeDAOImpl类的设计,所有DAO方法必须释放对所获得的连接、声明、结果集等数据库资源的控制。这是危险的主张,因为一个编程新手可能很容易漏掉那些约束。结果造成资源耗尽,导致系统停机。
- 错误处理:JDBC 驱动程序通过抛出SQLException来报告所有的错误情况。SQLException是检查到的异常,所以开发人员被迫去处理它,即使不可能从这类 导致代码混乱的大多数异常中恢复过来。而且,从SQLException对象获得的错误代码和消息特定于数据库厂商,所以不可能写出可移植的DAO错误发 送代码。
- 脆弱的代码:在基于JDBC的DAO中,两个常用的任务是设置声明对象的绑定变量和使用结果集检索数据。如果SQL where子句中的列数目或者位置更改了,就不得不对代码执行更改、测试、重新部署这个严格的循环过程。
让我们看看如何能够减少这些问题并保留DAO的大多数优点。
进入Spring DAO
先识别代码中发生变化的部分,然后将这一部分代码分离出来或者封装起来,就能解决以上所列出的问题。Spring的设计者们已经完全做到了这一 点,他们发布了一个超级简洁、健壮的、高度可伸缩的JDBC框架。固定部分(像检索连接、准备声明对象、执行查询和释放数据库资源)已经被一次性地写好, 所以该框架的一部分内容有助于消除在传统的基于JDBC的DAO中出现的缺点。
图2显示的是Spring JDBC框架的主要组成部分。业务服务对象通过适当的接口继续使用DAO实现类。JdbcDaoSupport是JDBC数据访问对象的超类。它与特定的数据源相关联。Spring Inversion of Control (IOC)容器或BeanFactory负责获得相应数据源的配置详细信息,并将其与JdbcDaoSupport相关联。这个类最重要的功能就是使子类可以使用JdbcTemplate对象。

图2. Spring JDBC框架的主要组件
JdbcTemplate是Spring JDBC框架中最重要的类。引用文献中的话:“它简化了JDBC的使用,有助于避免常见的错误。它执行核心JDBC工作流,保留应用代码以提供SQL和提 取结果。”这个类通过执行下面的样板任务来帮助分离JDBC DAO代码的静态部分:
- 从数据源检索连接。
- 准备合适的声明对象。
- 执行SQL CRUD操作。
- 遍历结果集,然后将结果填入标准的collection对象。
- 处理SQLException异常并将其转换成更加特定于错误的异常层次结构。
利用Spring DAO重新编写
既然已基本理解了Spring JDBC框架,现在要重新编写已有的代码。下面将逐步讲述如何解决前几节中提到的问题。
第一步:修改DAO实现类- 现在从JdbcDaoSupport扩展出EmployeeDAOImpl以获得JdbcTemplate。
import
org.springframework.jdbc.core.support.JdbcDaoSupport;
import
org.springframework.jdbc.core.JdbcTemplate;
public
class
EmployeeDAOImpl
extends
JdbcDaoSupport
implements
IEmployeeDAO
...
{

publicListfindBySalaryRange(MapsalaryMap)...{

DoubledblParams[]=...{Double.valueOf((String)
salaryMap.get("MIN_SALARY"))
,Double.valueOf((String)
salaryMap.get("MAX_SALARY"))
};
//ThegetJdbcTemplatemethodofJdbcDaoSupportreturnsan
//instanceofJdbcTemplateinitializedwithadatasourcebythe
//SpringBeanFactory
JdbcTemplatedaoTmplt=this.getJdbcTemplate();
returndaoTmplt.queryForList(FIND_BY_SAL_RNG,dblParams);
}
}
从简化的代码可以明显看出,JdbcTemplate鼓励重用,这大大削减了DAO实现中的代码。JDBC和collection包之间的紧密 耦合已经消除。由于JdbcTemplate方法可确保在使用数据库资源后将其按正确的次序释放,所以JDBC的资源耗损不再是一个问题。
另外,使用Spring DAO时,不必处理异常。JdbcTemplate类会处理SQLException,并根据SQL错误代码或错误状态将其转换成特定于Spring异常 的层次结构。例如,试图向主键列插入重复值时,将引发DataIntegrityViolationException。然而,如果无法从这一错误中恢 复,就无需处理该异常。因为Spring DAO的根异常类DataAccessException是运行时异常类,所以可以这样做。值得注意的是Spring DAO异常独立于数据访问实现。如果实现是由O/R映射解决方案提供,就会抛出同样的异常。
第二步:修改业务服务- 现在业务服务实现了一个新方法setDao(),Spring容器使用该方法传递DAO实现类的引用。该过程称为“设置方法注入(setter injection)”,通过第三步中的配置文件告知Spring容器该过程。注意,不再需要使用DAOFactory,因为Spring BeanFactory提供了这项功能:
public
class
EmployeeBusinessServiceImpl
implements
IEmployeeBusinessService
...
{
IEmployeeDAOempDAO;

publicListgetEmployeesWithinSalaryRange(MapsalaryMap)...{
ListempList=empDAO.findBySalaryRange(salaryMap);
returnempList;
}
publicvoidsetDao(IEmployeeDAOempDAO)...{
this.empDAO=empDAO;
}
}
第三步:配置Bean Factory- Spring bean factory需要一个配置文件进行初始化并启动Spring框架。这个配置文件包含所有业务服务和带Spring bean容器的DAO实现类。除此之外,它还包含用于初始化数据源和JdbcDaoSupport的信息:
<?
xmlversion="1.0"encoding="UTF-8"
?>

<!
DOCTYPEbeansPUBLIC"-//SPRING//DTDBEAN//EN"
"http://www.springframework.org/dtd/spring-beans.dtd"
>

<
beans
>
<!--
ConfigureDatasource
-->
<
bean
id
="FIREBIRD_DATASOURCE"
class
="org.springframework.jndi.JndiObjectFactoryBean"
>
<
property
name
="jndiEnvironment"
>
<
props
>
<
prop
key
="java.naming.factory.initial"
>
weblogic.jndi.WLInitialContextFactory
</
prop
>
<
prop
key
="java.naming.provider.url"
>
t3://localhost:7001
</
prop
>
</
props
>
</
property
>
<
property
name
="jndiName"
>
<
value
>
jdbc/DBPool
</
value
>
</
property
>
</
bean
>

<!--
ConfigureDAO
-->
<
bean
id
="EMP_DAO"
class
="com.bea.dev2dev.dao.EmployeeDAOImpl"
>
<
property
name
="dataSource"
>
<
ref
bean
="FIREBIRD_DATASOURCE"
></
ref
>
</
property
>
</
bean
>

<!--
ConfigureBusinessService
-->
<
bean
id
="EMP_BUSINESS"
class
="com.bea.dev2dev.sampleapp.business.EmployeeBusinessServiceImpl"
>
<
property
name
="dao"
>
<
ref
bean
="EMP_DAO"
></
ref
>
</
property
>
</
bean
>
</
beans
>
第四步:测试- 最后是编写JUnit测试类。依照Spring的方式,需要在容器外部进行测试。然而,从第三步中的配置文件可以清楚地看到,我们一直在使用WebLogic Server连接池。
package
com.bea.dev2dev.business;
import
java.util.
*
;
import
junit.framework.
*
;
import
org.springframework.context.ApplicationContext;
import
org.springframework.context.support.FileSystemXmlApplicationContext;

public
class
EmployeeBusinessServiceImplTest
extends
TestCase
...
{
privateIEmployeeBusinessServiceempBusiness;
privateMapsalaryMap;
ListexpResult;

protectedvoidsetUp()throwsException...{
initSpringFramework();
initSalaryMap();
initExpectedResult();
}
privatevoidinitExpectedResult()...{
expResult=newArrayList();
MaptempMap=newHashMap();
tempMap.put("EMP_NO",newInteger(1));
tempMap.put("EMP_NAME","John");
tempMap.put("SALARY",newDouble(46.11));
expResult.add(tempMap);
}
privatevoidinitSalaryMap()...{
salaryMap=newHashMap();
salaryMap.put("MIN_SALARY","1");
salaryMap.put("MAX_SALARY","50");
}
privatevoidinitSpringFramework()...{
ApplicationContextac=newFileSystemXmlApplicationContext
("C:/SpringConfig/Spring-Config.xml");
empBusiness=
(IEmployeeBusinessService)ac.getBean("EMP_BUSINESS");
}
protectedvoidtearDown()throwsException...{
}

/***//**
*TestofgetEmployeesWithinSalaryRangemethod,
*ofclass
*com.bea.dev2dev.business.EmployeeBusinessServiceImpl.
*/
publicvoidtestGetEmployeesWithinSalaryRange()...{
Listresult=empBusiness.getEmployeesWithinSalaryRange
(salaryMap);
assertEquals(expResult,result);
}
}
使用绑定变量
到目前为止,我们搜索了工资介于最低值和最高值之间的雇员。假设在某种情形下,业务用户想要颠倒这一范围。DAO代码很脆弱,将不得不通过更改 来满足要求的变化。这个问题在于使用了静态的位置绑定变量(用“?”表示)。Spring DAO通过支持命名的绑定变量来挽救这个情况。修改的IEmployeeDAO清单引入了命名的绑定变量(用“:<somename>”表 示)。注意查询中的变化,如下所示:
import
java.util.Map;
public
interface
IEmployeeDAO
...
{
//SQLStringthatwillbeexecuted
publicStringFIND_BY_SAL_RNG="SELECTEMP_NO,EMP_NAME,"
+"SALARYFROMEMPWHERESALARY>=:maxANDSALARY<=:min";
//Returnsthelistofemployeesfallingintothegivensalaryrange
//Theinputparameteristheimmutablemapobjectobtainedfrom
//theHttpServletRequest.Thisisanearlyrefactoringbasedon
//-"IntroduceParameterObject"
publicListfindBySalaryRange(MapsalaryMap);
}
import
org.springframework.jdbc.core.namedparam.NamedParameterJdbcDaoSupport;
import
org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
public
class
EmployeeDAOImpl
extends
NamedParameterJdbcDaoSupport
implements
IEmployeeDAO
...
{

publicListfindBySalaryRange(MapsalaryMap)...{
NamedParameterJdbcTemplatetmplt=
this.getNamedParameterJdbcTemplate();
returntmplt.queryForList(IEmployeeDAO.FIND_BY_SAL_RNG
,salaryMap);
}
}
最后,如果数据库不支持自动类型转换,需要如下所示,对JUnit测试类中的initSalaryMap()方法稍做修改。
private
void
initSalaryMap()
...
{
salaryMap=newHashMap();
salaryMap.put("MIN_SALARY",newDouble(1));
salaryMap.put("MAX_SALARY",newDouble(50));
}
Spring DAO回调函数
至此,已经说明为了解决传统DAO设计中存在的问题,如何封装和概括JdbcTemplate类中JDBC代码的静态部分。现在了解一下有关变 量的问题,如设置绑定变量、结果集遍历等。虽然Spring DAO已经拥有这些问题的一般化解决方案,但在某些基于SQL的情况下,可能仍需要设置绑定变量。
在尝试向Spring DAO转换的过程中,介绍了由于业务服务及其客户机之间的约定遭到破坏而导致的隐蔽运行时错误。这个错误的来源可以追溯到原始的DAO。 dbcTemplate.queryForList()方法不再返回EmployeeTO实例列表。而是返回一个map表(每个map是结果集的一行)。
如您目前所知,JdbcTemplate基于模板方法设计模式,该模式利用JDBC API定义SQL执行工作流。必须改变这个工作流以修复被破坏的约定。第一个选择是在子类中更改或扩展工作流。您可以遍历 JdbcTemplate.queryForList()返回的列表,用EmployeeTO实例替换map对象。然而,这会导致我们一直竭力避免的静态 代码与动态代码的混合。第二个选择是将代码插入JdbcTemplate提供的各种工作流修改钩子(hook)。明智的做法是在一个不同的类中封装传输对 象填充代码,然后通过钩子链接它。填充逻辑的任何修改将不会改变DAO。
编写一个类,使其实现在Spring框架特定的接口中定义的方法,就可以实现第二个选择。这些方法称为回调函数,通过JdbcTemplate向框架注册。当发生相应的事件(例如,遍历结果集并填充独立于框架的传输对象)时,框架将调用这些方法。
第一步:传输对象
下面是您可能感兴趣的传输对象。注意,以下所示的传输对象是固定的:
package
com.bea.dev2dev.to;

public
final
class
EmployeeTO
implements
Serializable
...
{
privateintempNo;
privateStringempName;
privatedoublesalary;

/***//**CreatesanewinstanceofEmployeeTO*/
publicEmployeeTO(intempNo,StringempName,doublesalary)...{
this.empNo=empNo;
this.empName=empName;
this.salary=salary;
}
publicStringgetEmpName()...{
returnthis.empName;
}
publicintgetEmpNo()...{
returnthis.empNo;
}
publicdoublegetSalary()...{
returnthis.salary;
}
publicbooleanequals(EmployeeTOempTO)...{
returnempTO.empNo==this.empNo;
}
}
第二步:实现回调接口
实现RowMapper接口,填充来自结果集的传输对象。下面是一个例子:
package
com.bea.dev2dev.dao.mapper;
import
com.bea.dev2dev.to.EmployeeTO;
import
java.sql.ResultSet;
import
java.sql.SQLException;
import
org.springframework.jdbc.core.RowMapper;

public
class
EmployeeTOMapper
implements
RowMapper
...
{
publicObjectmapRow(ResultSetrs,introwNum)
throwsSQLException...{
intempNo=rs.getInt(1);
StringempName=rs.getString(2);
doublesalary=rs.getDouble(3);
EmployeeTOempTo=newEmployeeTO(empNo,empName,salary);
returnempTo;
}
}
第三步:插入回调接口
执行SQL查询时,JdbcTemplate利用默认的RowMapper实现产生map列表。现在需要注册自定义回调实现来修改 JdbcTemplate的这一行为。注意现在用的是NamedParameterJdbcTemplate的query()方法,而不是 queryForList()方法:
public
class
EmployeeDAOImpl
extends
NamedParameterJdbcDaoSupport
implements
IEmployeeDAO
...
{

publicListfindBySalaryRange(MapsalaryMap)...{
NamedParameterJdbcTemplatedaoTmplt=
getNamedParameterJdbcTemplate();
returndaoTmplt.query(IEmployeeDAO.FIND_BY_SAL_RNG,salaryMap,
newEmployeeTOMapper());
}
}
第四步:修改后的JUnit类
现在要根据返回的传输对象测试这些结果。为此要对测试方法进行修改。
public
class
EmployeeBusinessServiceImplTest
extends
TestCase
...
{
privateIEmployeeBusinessServiceempBusiness;
privateMapsalaryMap;
ListexpResult;
//allmethodsnotshowninthelistingremainthe
//sameasinthepreviousexample
privatevoidinitExpectedResult()...{
expResult=newArrayList();
EmployeeTOto=newEmployeeTO(2,"John",46.11);
expResult.add(to);
}

/***//**
*TestofgetEmployeesWithinSalaryRangemethod,of
*classcom.bea.dev2dev.business.
*EmployeeBusinessServiceImpl
*/
publicvoidtestGetEmployeesWithinSalaryRange()...{
Listresult=empBusiness.
getEmployeesWithinSalaryRange(salaryMap);
assertEquals(expResult,result);
}

publicvoidassertEquals(ListexpResult,Listresult)...{
EmployeeTOexpTO=(EmployeeTO)expResult.get(0);
EmployeeTOactualTO=(EmployeeTO)result.get(0);
if(!expTO.equals(actualTO))...{
thrownewRuntimeException("**TestFailed**");
}
}
}
优势
Spring JDBC框架的优点很清楚。我们获益很多,并将DAO方法简化到只有几行代码。代码不再脆弱,这要感谢该框架对命名的参数绑定变量的“开箱即用”支持,以及在映射程序中将传输对象填充逻辑分离。
Spring JDBC的优点应该促使您向这一框架移植现有的代码。希望本文在这一方面能有所帮助。它会帮助您获得一些重构工具和知识。例如,如果您没有采用P2I Extract Interface,那么可以使用重构,从现有的DAO实现类创建接口。除此之外,查看本文的参考资料可以得到更多指导。
下载
可以下载本文用到的源代码。
结束语
在此篇文章中,我讲述了数据访问对象(DAO)设计模式的基础知识,并从正反两方面进行了讨论。引入Spring DAO或JDBC框架来克服传统DAO的不足。然后,根据Spring框架提供的“开箱即用”命名参数支持对脆弱的DAO代码进行了改进。最后,回调功能 展示了如何在指定点修改框架行为。
参考资料
- Core J2EE Patterns: Data Access Object(Sun开发人员网络)- 提供了DAO设计模式的详细描述
- Spring DAO Framework - 官方Spring DAO文档
- Spring Integration with WebLogic Server (Dev2Dev) - Spring与WebLogic Server集成一览
- WebLogic 8.1 Datasource Configuration(文档) - 逐步指导您使用Administration控制台配置数据源
- Refactoring - 重构基础知识讲解和Martin Fowler撰写的Refactoring: Improving the Design of the Existing Code一书中所有重构详细资料的目录;该站点还包含重构使用的工具列表
| 作者简介 | |
| Dhrubojyoti Kayal 是Capgemini Consulting的高级顾问。在利用企业Java技术开发和设计应用程序和产品方面,拥有5年以上的经验。 | |
本文探讨了传统的DAO设计模式及其存在的问题,并介绍如何利用Spring JDBC框架改进DAO设计,包括减少代码重复、提高代码复用性和降低耦合度。
4万+

被折叠的 条评论
为什么被折叠?



