[ZT]Spring數(shù)據(jù)訪問(wèn)對(duì)象(DAO)框架
摘要: J2EE應(yīng)用程序中的業(yè)務(wù)組件通常使用JDBC API訪問(wèn)和更改關(guān)系數(shù)據(jù)庫(kù)中的持久數(shù)據(jù)。這經(jīng)常導(dǎo)致持久性代碼與業(yè)務(wù)邏輯發(fā)生混合,這是一種不好的習(xí)慣。數(shù)據(jù)訪問(wèn)對(duì)象(DAO)設(shè)計(jì)模式通過(guò)把持久性邏輯分成若干數(shù)據(jù)訪問(wèn)類來(lái)解決這一問(wèn)題。
本文是一篇關(guān)于DAO設(shè)計(jì)模式的入門文章,突出講述了它的優(yōu)點(diǎn)和不足之處。另外,本文還介紹了Spring 2.0 JDBC/DAO框架并示范了它如何妥善地解決傳統(tǒng)DAO設(shè)計(jì)中的缺陷。
傳統(tǒng)的DAO設(shè)計(jì)
數(shù)據(jù)訪問(wèn)對(duì)象(DAO)是一個(gè)集成層設(shè)計(jì)模式,如Core J2EE Design Pattern 圖書(shū)所歸納。它將持久性存儲(chǔ)訪問(wèn)和操作代碼封裝到一個(gè)單獨(dú)的層中。本文的上下文中所提到的持久存儲(chǔ)器是一個(gè)RDBMS。
這一模式在業(yè)務(wù)邏輯層和持久存儲(chǔ)層之間引入了一個(gè)抽象層,如圖1所示。業(yè)務(wù)對(duì)象通過(guò)數(shù)據(jù)訪問(wèn)對(duì)象來(lái)訪問(wèn)RDBMS(數(shù)據(jù)源)。抽象層改善了應(yīng)用程序代碼并引入了靈活性。理論上,當(dāng)數(shù)據(jù)源改變時(shí),比如更換數(shù)據(jù)庫(kù)供應(yīng)商或是數(shù)據(jù)庫(kù)的類型時(shí),僅需改變數(shù)據(jù)訪問(wèn)對(duì)象,從而把對(duì)業(yè)務(wù)對(duì)象的影響降到最低。
圖1. 應(yīng)用程序結(jié)構(gòu),包括DAO之前和之后的部分
講解了DAO設(shè)計(jì)模式的基礎(chǔ)知識(shí),下面將編寫(xiě)一些代碼。下面的例子來(lái)自于一個(gè)公司域模型。簡(jiǎn)而言之,這家公司有幾位員工工作在不同的部門,如銷售部、市場(chǎng)部以及人力資源部。為了簡(jiǎn)單起見(jiàn),我們將集中討論一個(gè)稱作“雇員”的實(shí)體。
針對(duì)接口編程
DAO設(shè)計(jì)模式帶來(lái)的靈活性首先要?dú)w功于一個(gè)對(duì)象設(shè)計(jì)的最佳實(shí)踐:針對(duì)接口編程(P2I)。這一原則規(guī)定實(shí)體必須實(shí)現(xiàn)一個(gè)供調(diào)用程序而不是實(shí)體自身使用的接口。因此,可以輕松替換成不同的實(shí)現(xiàn)而對(duì)客戶端代碼只產(chǎn)生很小的影響。
我們將據(jù)此使用findBySalaryRange()行為定義Employee DAO接口,IEmployeeDAO。業(yè)務(wù)組件將通過(guò)這個(gè)接口與DAO交互:
import java.util.Map; public interface IEmployeeDAO { //SQL String that will be executed public String FIND_BY_SAL_RNG = "SELECT EMP_NO, EMP_NAME, " + "SALARY FROM EMP WHERE SALARY >= ? AND SALARY <= ?"; //Returns the list of employees who fall into the given salary //range. The input parameter is the immutable map object //obtained from the HttpServletRequest. This is an early //refactoring based on "Introduce Parameter Object" public List findBySalaryRange(Map salaryMap); }
提供DAO實(shí)現(xiàn)類
接口已經(jīng)定義,現(xiàn)在必須提供Employee DAO的具體實(shí)現(xiàn),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{ public List findBySalaryRange(Map salaryMap) { Connection conn = null; PreparedStatement pstmt = null; ResultSet rs = null; List empList = new ArrayList(); //Transfer Object for inter-tier data transfer EmployeeTO tempEmpTO = null; try{ //DBUtil - helper classes that retrieve connection from pool 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(); int tmpEmpNo = 0; String tmpEmpName = ""; double tmpSalary = 0.0D; while (rs.next()){ tmpEmpNo = rs.getInt("EMP_NO"); tmpEmpName = rs.getString("EMP_NAME"); tmpSalary = rs.getDouble("SALARY"); tempEmpTO = new EmployeeTO(tmpEmpNo, tmpEmpName, tmpSalary); empList.add(tempEmpTO); }//end while }//end try catch (SQLException sqle){ throw new DBException(sqle); }//end catch finally{ try{ if (rs != null){ rs.close(); } } catch (SQLException sqle){ throw new DBException(sqle); } try{ if (pstmt != null){ pstmt.close(); } } catch (SQLException sqle){ throw new DBException(sqle); } try{ if (conn != null){ conn.close(); } } catch (SQLException sqle){ throw new DBException(sqle); } }//end of finally block return empList; }//end method findBySalaryRange }
上面的清單說(shuō)明了DAO方法的一些要點(diǎn):
- 它們封裝了所有與JDBC API的交互。如果使用像Kodo或者Hibernate的O/R映射方案,則DAO類可以將這些產(chǎn)品的私有API打包。
- 它們將檢索到的數(shù)據(jù)打包到一個(gè)與JDBC API無(wú)關(guān)的傳輸對(duì)象中,然后將其返回給業(yè)務(wù)層作進(jìn)一步處理。
- 它們實(shí)質(zhì)上是無(wú)狀態(tài)的。唯一的目的是訪問(wèn)并更改業(yè)務(wù)對(duì)象的持久數(shù)據(jù)。
- 在這個(gè)過(guò)程中,它們像SQLException一樣捕獲任何底層JDBC API或數(shù)據(jù)庫(kù)報(bào)告的錯(cuò)誤(例如,數(shù)據(jù)庫(kù)不可用、錯(cuò)誤的SQL句法)。DAO對(duì)象再次使用一個(gè)與JDBC無(wú)關(guān)的自定義運(yùn)行時(shí)異常類DBException,通知業(yè)務(wù)對(duì)象這些錯(cuò)誤。
- 它們像Connection和PreparedStatement對(duì)象那樣,將數(shù)據(jù)庫(kù)資源釋放回池中,并在使用完ResultSet游標(biāo)之后,將其所占用的內(nèi)存釋放。
因此,DAO層將底層的數(shù)據(jù)訪問(wèn)API抽象化,為業(yè)務(wù)層提供了一致的數(shù)據(jù)訪問(wèn)API。
構(gòu)建DAO工廠
DAO工廠是典型的工廠設(shè)計(jì)模式實(shí)現(xiàn),用于為業(yè)務(wù)對(duì)象創(chuàng)建和提供具體的DAO實(shí)現(xiàn)。業(yè)務(wù)對(duì)象使用DAO接口,而不用了解實(shí)現(xiàn)類的具體情況。DAO工廠帶來(lái)的依賴反轉(zhuǎn)(dependency inversion)提供了極大的靈活性。只要DAO接口建立的約定未改變,那么很容易改變DAO實(shí)現(xiàn)(例如,從straight JDBC實(shí)現(xiàn)到基于Kodo的O/R映射),同時(shí)又不影響客戶的業(yè)務(wù)對(duì)象:
public class DAOFactory { private static DAOFactory daoFac; static{ daoFac = new DAOFactory(); } private DAOFactory(){} public DAOFactory getInstance(){ return daoFac; } public IEmployeeDAO getEmployeeDAO(){ return new EmployeeDAOImpl(); } }
與業(yè)務(wù)組件的協(xié)作
現(xiàn)在該了解DAO怎樣適應(yīng)更復(fù)雜的情形。如前幾節(jié)所述,DAO與業(yè)務(wù)層組件協(xié)作獲取和更改持久業(yè)務(wù)數(shù)據(jù)。下面的清單展示了業(yè)務(wù)服務(wù)組件及其與DAO層的交互:
public class EmployeeBusinessServiceImpl implements IEmployeeBusinessService { public List getEmployeesWithinSalaryRange(Map salaryMap){ IEmployeeDAO empDAO = DAOFactory.getInstance() .getEmployeeDAO(); List empList = empDAO.findBySalaryRange(salaryMap); return empList; } }
交互過(guò)程十分簡(jiǎn)潔,完全不依賴于任何持久性接口(包括JDBC)。
問(wèn)題
DAO設(shè)計(jì)模式也有缺點(diǎn):
- 代碼重復(fù):從EmployeeDAOImpl清單可以清楚地看到,對(duì)于基于JDBC的傳統(tǒng)數(shù)據(jù)庫(kù)訪問(wèn),代碼重復(fù)(如上面的粗體字所示)是一個(gè)主要的問(wèn)題。一遍又一遍地寫(xiě)著同樣的代碼,明顯違背了基本的面向?qū)ο笤O(shè)計(jì)的代碼重用原則。它將對(duì)項(xiàng)目成本、時(shí)間安排和工作產(chǎn)生明顯的副面影響。
- 耦合:DAO代碼與JDBC接口和核心collection耦合得非常緊密。從每個(gè)DAO類的導(dǎo)入聲明的數(shù)量可以明顯地看出這種耦合。
- 資源耗損:依據(jù)EmployeeDAOImpl類的設(shè)計(jì),所有DAO方法必須釋放對(duì)所獲得的連接、聲明、結(jié)果集等數(shù)據(jù)庫(kù)資源的控制。這是危險(xiǎn)的主張,因?yàn)橐粋€(gè)編程新手可能很容易漏掉那些約束。結(jié)果造成資源耗盡,導(dǎo)致系統(tǒng)停機(jī)。
- 錯(cuò)誤處理:JDBC 驅(qū)動(dòng)程序通過(guò)拋出SQLException來(lái)報(bào)告所有的錯(cuò)誤情況。SQLException是檢查到的異常,所以開(kāi)發(fā)人員被迫去處理它,即使不可能從這類導(dǎo)致代碼混亂的大多數(shù)異常中恢復(fù)過(guò)來(lái)。而且,從SQLException對(duì)象獲得的錯(cuò)誤代碼和消息特定于數(shù)據(jù)庫(kù)廠商,所以不可能寫(xiě)出可移植的DAO錯(cuò)誤發(fā)送代碼。
- 脆弱的代碼:在基于JDBC的DAO中,兩個(gè)常用的任務(wù)是設(shè)置聲明對(duì)象的綁定變量和使用結(jié)果集檢索數(shù)據(jù)。如果SQL where子句中的列數(shù)目或者位置更改了,就不得不對(duì)代碼執(zhí)行更改、測(cè)試、重新部署這個(gè)嚴(yán)格的循環(huán)過(guò)程。
讓我們看看如何能夠減少這些問(wèn)題并保留DAO的大多數(shù)優(yōu)點(diǎn)。
進(jìn)入Spring DAO
先識(shí)別代碼中發(fā)生變化的部分,然后將這一部分代碼分離出來(lái)或者封裝起來(lái),就能解決以上所列出的問(wèn)題。Spring的設(shè)計(jì)者們已經(jīng)完全做到了這一點(diǎn),他們發(fā)布了一個(gè)超級(jí)簡(jiǎn)潔、健壯的、高度可伸縮的JDBC框架。固定部分(像檢索連接、準(zhǔn)備聲明對(duì)象、執(zhí)行查詢和釋放數(shù)據(jù)庫(kù)資源)已經(jīng)被一次性地寫(xiě)好,所以該框架的一部分內(nèi)容有助于消除在傳統(tǒng)的基于JDBC的DAO中出現(xiàn)的缺點(diǎn)。
圖2顯示的是Spring JDBC框架的主要組成部分。業(yè)務(wù)服務(wù)對(duì)象通過(guò)適當(dāng)?shù)慕涌诶^續(xù)使用DAO實(shí)現(xiàn)類。JdbcDaoSupport是JDBC數(shù)據(jù)訪問(wèn)對(duì)象的超類。它與特定的數(shù)據(jù)源相關(guān)聯(lián)。Spring Inversion of Control (IOC)容器或BeanFactory負(fù)責(zé)獲得相應(yīng)數(shù)據(jù)源的配置詳細(xì)信息,并將其與JdbcDaoSupport相關(guān)聯(lián)。這個(gè)類最重要的功能就是使子類可以使用JdbcTemplate對(duì)象。
圖2. Spring JDBC框架的主要組件
JdbcTemplate是Spring JDBC框架中最重要的類。引用文獻(xiàn)中的話:“它簡(jiǎn)化了JDBC的使用,有助于避免常見(jiàn)的錯(cuò)誤。它執(zhí)行核心JDBC工作流,保留應(yīng)用代碼以提供SQL和提取結(jié)果。”這個(gè)類通過(guò)執(zhí)行下面的樣板任務(wù)來(lái)幫助分離JDBC DAO代碼的靜態(tài)部分:
- 從數(shù)據(jù)源檢索連接。
- 準(zhǔn)備合適的聲明對(duì)象。
- 執(zhí)行SQL CRUD操作。
- 遍歷結(jié)果集,然后將結(jié)果填入標(biāo)準(zhǔn)的collection對(duì)象。
- 處理SQLException異常并將其轉(zhuǎn)換成更加特定于錯(cuò)誤的異常層次結(jié)構(gòu)。
利用Spring DAO重新編寫(xiě)
既然已基本理解了Spring JDBC框架,現(xiàn)在要重新編寫(xiě)已有的代碼。下面將逐步講述如何解決前幾節(jié)中提到的問(wèn)題。
第一步:修改DAO實(shí)現(xiàn)類- 現(xiàn)在從JdbcDaoSupport擴(kuò)展出EmployeeDAOImpl以獲得JdbcTemplate。
import org.springframework.jdbc.core.support.JdbcDaoSupport; import org.springframework.jdbc.core.JdbcTemplate; public class EmployeeDAOImpl extends JdbcDaoSupport implements IEmployeeDAO{ public List findBySalaryRange(Map salaryMap){ Double dblParams [] = {Double.valueOf((String) salaryMap.get("MIN_SALARY")) ,Double.valueOf((String) salaryMap.get("MAX_SALARY")) }; //The getJdbcTemplate method of JdbcDaoSupport returns an //instance of JdbcTemplate initialized with a datasource by the //Spring Bean Factory JdbcTemplate daoTmplt = this.getJdbcTemplate(); return daoTmplt.queryForList(FIND_BY_SAL_RNG,dblParams); } }
在上面的清單中,傳入?yún)?shù)映射中的值存儲(chǔ)在雙字節(jié)數(shù)組中,順序與SQL字符串中的位置參數(shù)相同。queryForList()方法以包含Map(用列名作為鍵,一項(xiàng)對(duì)應(yīng)一列)的List(一項(xiàng)對(duì)應(yīng)一行)的方式返回查詢結(jié)果。稍后我會(huì)說(shuō)明如何返回傳輸對(duì)象列表。
從簡(jiǎn)化的代碼可以明顯看出,JdbcTemplate鼓勵(lì)重用,這大大削減了DAO實(shí)現(xiàn)中的代碼。JDBC和collection包之間的緊密耦合已經(jīng)消除。由于JdbcTemplate方法可確保在使用數(shù)據(jù)庫(kù)資源后將其按正確的次序釋放,所以JDBC的資源耗損不再是一個(gè)問(wèn)題。
另外,使用Spring DAO時(shí),不必處理異常。JdbcTemplate類會(huì)處理SQLException,并根據(jù)SQL錯(cuò)誤代碼或錯(cuò)誤狀態(tài)將其轉(zhuǎn)換成特定于Spring異常的層次結(jié)構(gòu)。例如,試圖向主鍵列插入重復(fù)值時(shí),將引發(fā)DataIntegrityViolationException。然而,如果無(wú)法從這一錯(cuò)誤中恢復(fù),就無(wú)需處理該異常。因?yàn)镾pring DAO的根異常類DataAccessException是運(yùn)行時(shí)異常類,所以可以這樣做。值得注意的是Spring DAO異常獨(dú)立于數(shù)據(jù)訪問(wèn)實(shí)現(xiàn)。如果實(shí)現(xiàn)是由O/R映射解決方案提供,就會(huì)拋出同樣的異常。
第二步:修改業(yè)務(wù)服務(wù)- 現(xiàn)在業(yè)務(wù)服務(wù)實(shí)現(xiàn)了一個(gè)新方法setDao(),Spring容器使用該方法傳遞DAO實(shí)現(xiàn)類的引用。該過(guò)程稱為“設(shè)置方法注入(setter injection)”,通過(guò)第三步中的配置文件告知Spring容器該過(guò)程。注意,不再需要使用DAOFactory,因?yàn)镾pring BeanFactory提供了這項(xiàng)功能:
public class EmployeeBusinessServiceImpl implements IEmployeeBusinessService { IEmployeeDAO empDAO; public List getEmployeesWithinSalaryRange(Map salaryMap){ List empList = empDAO.findBySalaryRange(salaryMap); return empList; } public void setDao(IEmployeeDAO empDAO){ this.empDAO = empDAO; } }
請(qǐng)注意P2I的靈活性;即使極大地改動(dòng)DAO實(shí)現(xiàn),業(yè)務(wù)服務(wù)實(shí)現(xiàn)也只需少量更改。這是由于業(yè)務(wù)服務(wù)現(xiàn)在由Spring容器進(jìn)行管理。
第三步:配置Bean Factory- Spring bean factory需要一個(gè)配置文件進(jìn)行初始化并啟動(dòng)Spring框架。這個(gè)配置文件包含所有業(yè)務(wù)服務(wù)和帶Spring bean容器的DAO實(shí)現(xiàn)類。除此之外,它還包含用于初始化數(shù)據(jù)源和JdbcDaoSupport的信息:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd"> <beans> <!-- Configure Datasource --> <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> <!-- Configure DAO --> <bean id="EMP_DAO" class="com.bea.dev2dev.dao.EmployeeDAOImpl"> <property name="dataSource"> <ref bean="FIREBIRD_DATASOURCE"></ref> </property> </bean> <!-- Configure Business Service --> <bean id="EMP_BUSINESS" class="com.bea.dev2dev.sampleapp.business.EmployeeBusinessServiceImpl"> <property name="dao"> <ref bean="EMP_DAO"></ref> </property> </bean> </beans>
這個(gè)Spring bean容器通過(guò)調(diào)用JdbcDaoSupport提供的setDataSource()方法,設(shè)置包含DAO實(shí)現(xiàn)的數(shù)據(jù)源對(duì)象。
第四步:測(cè)試- 最后是編寫(xiě)JUnit測(cè)試類。依照Spring的方式,需要在容器外部進(jìn)行測(cè)試。然而,從第三步中的配置文件可以清楚地看到,我們一直在使用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 { private IEmployeeBusinessService empBusiness; private Map salaryMap; List expResult; protected void setUp() throws Exception { initSpringFramework(); initSalaryMap(); initExpectedResult(); } private void initExpectedResult() { expResult = new ArrayList(); Map tempMap = new HashMap(); tempMap.put("EMP_NO",new Integer(1)); tempMap.put("EMP_NAME","John"); tempMap.put("SALARY",new Double(46.11)); expResult.add(tempMap); } private void initSalaryMap() { salaryMap = new HashMap(); salaryMap.put("MIN_SALARY","1"); salaryMap.put("MAX_SALARY","50"); } private void initSpringFramework() { ApplicationContext ac = new FileSystemXmlApplicationContext ("C:/SpringConfig/Spring-Config.xml"); empBusiness = (IEmployeeBusinessService)ac.getBean("EMP_BUSINESS"); } protected void tearDown() throws Exception { } /** * Test of getEmployeesWithinSalaryRange method, * of class * com.bea.dev2dev.business.EmployeeBusinessServiceImpl. */ public void testGetEmployeesWithinSalaryRange() { List result = empBusiness.getEmployeesWithinSalaryRange (salaryMap); assertEquals(expResult, result); } }
使用綁定變量
到目前為止,我們搜索了工資介于最低值和最高值之間的雇員。假設(shè)在某種情形下,業(yè)務(wù)用戶想要顛倒這一范圍。DAO代碼很脆弱,將不得不通過(guò)更改來(lái)滿足要求的變化。這個(gè)問(wèn)題在于使用了靜態(tài)的位置綁定變量(用“?”表示)。Spring DAO通過(guò)支持命名的綁定變量來(lái)挽救這個(gè)情況。修改的IEmployeeDAO清單引入了命名的綁定變量(用“:<some name>”表示)。注意查詢中的變化,如下所示:
import java.util.Map; public interface IEmployeeDAO { //SQL String that will be executed public String FIND_BY_SAL_RNG = "SELECT EMP_NO, EMP_NAME, " + "SALARY FROM EMP WHERE SALARY >= :max AND SALARY <= :min"; //Returns the list of employees falling into the given salary range //The input parameter is the immutable map object obtained from //the HttpServletRequest. This is an early refactoring based on //- "Introduce Parameter Object" public List findBySalaryRange(Map salaryMap); }
多數(shù)JDBC驅(qū)動(dòng)程序僅支持位置綁定變量。所以,Spring DAO在運(yùn)行時(shí)將這個(gè)查詢轉(zhuǎn)換成位置綁定、基于變量的查詢,并且設(shè)置正確的綁定變量。現(xiàn)在,為了完成這些任務(wù),需要使用NamedParameterJdbcDaoSupport類和NamedParameterJdbcTemplate類,以代替JdbcDaoSupport和JdbcTemplate。下面就是修改后的DAO實(shí)現(xiàn)類:
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcDaoSupport; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; public class EmployeeDAOImpl extends NamedParameterJdbcDaoSupport implements IEmployeeDAO{ public List findBySalaryRange(Map salaryMap){ NamedParameterJdbcTemplate tmplt = this.getNamedParameterJdbcTemplate(); return tmplt.queryForList(IEmployeeDAO.FIND_BY_SAL_RNG ,salaryMap); } }
NamedParameterJdbcDaoSupport的getNamedParameterJdbcTemplate()方法返回一個(gè) NamedParameterJdbcTemplate實(shí)例,該實(shí)例由數(shù)據(jù)源句柄進(jìn)行了預(yù)初始化。Spring Beanfactory執(zhí)行初始化任務(wù),從配置文件獲得所有的詳細(xì)信息。在執(zhí)行時(shí),一旦將命名的參數(shù)替換成位置占位符, NamedParameterJdbcTemplate就將操作委托給JdbcTemplate。可見(jiàn),使用命名的參數(shù)使得DAO方法不受底層SQL聲明任何更改的影響。
最后,如果數(shù)據(jù)庫(kù)不支持自動(dòng)類型轉(zhuǎn)換,需要如下所示,對(duì)JUnit測(cè)試類中的initSalaryMap()方法稍做修改。
private void initSalaryMap() { salaryMap = new HashMap(); salaryMap.put("MIN_SALARY",new Double(1)); salaryMap.put("MAX_SALARY",new Double(50)); }
Spring DAO回調(diào)函數(shù)
至此,已經(jīng)說(shuō)明為了解決傳統(tǒng)DAO設(shè)計(jì)中存在的問(wèn)題,如何封裝和概括JdbcTemplate類中JDBC代碼的靜態(tài)部分。現(xiàn)在了解一下有關(guān)變量的問(wèn)題,如設(shè)置綁定變量、結(jié)果集遍歷等。雖然Spring DAO已經(jīng)擁有這些問(wèn)題的一般化解決方案,但在某些基于SQL的情況下,可能仍需要設(shè)置綁定變量。
在嘗試向Spring DAO轉(zhuǎn)換的過(guò)程中,介紹了由于業(yè)務(wù)服務(wù)及其客戶機(jī)之間的約定遭到破壞而導(dǎo)致的隱蔽運(yùn)行時(shí)錯(cuò)誤。這個(gè)錯(cuò)誤的來(lái)源可以追溯到原始的DAO。 dbcTemplate.queryForList()方法不再返回EmployeeTO實(shí)例列表。而是返回一個(gè)map表(每個(gè)map是結(jié)果集的一行)。
如您目前所知,JdbcTemplate基于模板方法設(shè)計(jì)模式,該模式利用JDBC API定義SQL執(zhí)行工作流。必須改變這個(gè)工作流以修復(fù)被破壞的約定。第一個(gè)選擇是在子類中更改或擴(kuò)展工作流。您可以遍歷 JdbcTemplate.queryForList()返回的列表,用EmployeeTO實(shí)例替換map對(duì)象。然而,這會(huì)導(dǎo)致我們一直竭力避免的靜態(tài)代碼與動(dòng)態(tài)代碼的混合。第二個(gè)選擇是將代碼插入JdbcTemplate提供的各種工作流修改鉤子(hook)。明智的做法是在一個(gè)不同的類中封裝傳輸對(duì)象填充代碼,然后通過(guò)鉤子鏈接它。填充邏輯的任何修改將不會(huì)改變DAO。
編寫(xiě)一個(gè)類,使其實(shí)現(xiàn)在Spring框架特定的接口中定義的方法,就可以實(shí)現(xiàn)第二個(gè)選擇。這些方法稱為回調(diào)函數(shù),通過(guò)JdbcTemplate向框架注冊(cè)。當(dāng)發(fā)生相應(yīng)的事件(例如,遍歷結(jié)果集并填充獨(dú)立于框架的傳輸對(duì)象)時(shí),框架將調(diào)用這些方法。
第一步:傳輸對(duì)象
下面是您可能感興趣的傳輸對(duì)象。注意,以下所示的傳輸對(duì)象是固定的:
package com.bea.dev2dev.to; public final class EmployeeTO implements Serializable{ private int empNo; private String empName; private double salary; /** Creates a new instance of EmployeeTO */ public EmployeeTO(int empNo,String empName,double salary) { this.empNo = empNo; this.empName = empName; this.salary = salary; } public String getEmpName() { return this.empName; } public int getEmpNo() { return this.empNo; } public double getSalary() { return this.salary; } public boolean equals(EmployeeTO empTO){ return empTO.empNo == this.empNo; } }
第二步:實(shí)現(xiàn)回調(diào)接口
實(shí)現(xiàn)RowMapper接口,填充來(lái)自結(jié)果集的傳輸對(duì)象。下面是一個(gè)例子:
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{ public Object mapRow(ResultSet rs, int rowNum) throws SQLException{ int empNo = rs.getInt(1); String empName = rs.getString(2); double salary = rs.getDouble(3); EmployeeTO empTo = new EmployeeTO(empNo,empName,salary); return empTo; } }
注意實(shí)現(xiàn)類不應(yīng)該對(duì)提供的ResultSet對(duì)象調(diào)用next()方法。這由框架負(fù)責(zé),該類只要從結(jié)果集的當(dāng)前行提取值就行。回調(diào)實(shí)現(xiàn)拋出的任何SQLException也由Spring框架處理。
第三步:插入回調(diào)接口
執(zhí)行SQL查詢時(shí),JdbcTemplate利用默認(rèn)的RowMapper實(shí)現(xiàn)產(chǎn)生map列表。現(xiàn)在需要注冊(cè)自定義回調(diào)實(shí)現(xiàn)來(lái)修改 JdbcTemplate的這一行為。注意現(xiàn)在用的是NamedParameterJdbcTemplate的query()方法,而不是 queryForList()方法:
public class EmployeeDAOImpl extends NamedParameterJdbcDaoSupport implements IEmployeeDAO{ public List findBySalaryRange(Map salaryMap){ NamedParameterJdbcTemplate daoTmplt = getNamedParameterJdbcTemplate(); return daoTmplt.query(IEmployeeDAO.FIND_BY_SAL_RNG, salaryMap, new EmployeeTOMapper()); } }
Spring DAO框架對(duì)執(zhí)行查詢后返回的結(jié)果進(jìn)行遍歷。它在遍歷的每一步調(diào)用EmployeeTOMapper類實(shí)現(xiàn)的mapRow()方法,使用EmployeeTO傳輸對(duì)象填充最終結(jié)果的每一行。
第四步:修改后的JUnit類
現(xiàn)在要根據(jù)返回的傳輸對(duì)象測(cè)試這些結(jié)果。為此要對(duì)測(cè)試方法進(jìn)行修改。
public class EmployeeBusinessServiceImplTest extends TestCase { private IEmployeeBusinessService empBusiness; private Map salaryMap; List expResult; // all methods not shown in the listing remain the // same as in the previous example private void initExpectedResult() { expResult = new ArrayList(); EmployeeTO to = new EmployeeTO(2,"John",46.11); expResult.add(to); } /** * Test of getEmployeesWithinSalaryRange method, of * class com.bea.dev2dev.business. * EmployeeBusinessServiceImpl */ public void testGetEmployeesWithinSalaryRange() { List result = empBusiness. getEmployeesWithinSalaryRange(salaryMap); assertEquals(expResult, result); } public void assertEquals(List expResult, List result){ EmployeeTO expTO = (EmployeeTO) expResult.get(0); EmployeeTO actualTO = (EmployeeTO) result.get(0); if(!expTO.equals(actualTO)){ throw new RuntimeException("** Test Failed **"); } } }
優(yōu)勢(shì)
Spring JDBC框架的優(yōu)點(diǎn)很清楚。我們獲益很多,并將DAO方法簡(jiǎn)化到只有幾行代碼。代碼不再脆弱,這要感謝該框架對(duì)命名的參數(shù)綁定變量的“開(kāi)箱即用”支持,以及在映射程序中將傳輸對(duì)象填充邏輯分離。
Spring JDBC的優(yōu)點(diǎn)應(yīng)該促使您向這一框架移植現(xiàn)有的代碼。希望本文在這一方面能有所幫助。它會(huì)幫助您獲得一些重構(gòu)工具和知識(shí)。例如,如果您沒(méi)有采用P2I Extract Interface,那么可以使用重構(gòu),從現(xiàn)有的DAO實(shí)現(xiàn)類創(chuàng)建接口。除此之外,查看本文的參考資料可以得到更多指導(dǎo)。
下載
可以下載本文用到的源代碼。
結(jié)束語(yǔ)
在此篇文章中,我講述了數(shù)據(jù)訪問(wèn)對(duì)象(DAO)設(shè)計(jì)模式的基礎(chǔ)知識(shí),并從正反兩方面進(jìn)行了討論。引入Spring DAO或JDBC框架來(lái)克服傳統(tǒng)DAO的不足。然后,根據(jù)Spring框架提供的“開(kāi)箱即用”命名參數(shù)支持對(duì)脆弱的DAO代碼進(jìn)行了改進(jìn)。最后,回調(diào)功能展示了如何在指定點(diǎn)修改框架行為。
參考資料
- Core J2EE Patterns: Data Access Object(Sun開(kāi)發(fā)人員網(wǎng)絡(luò))- 提供了DAO設(shè)計(jì)模式的詳細(xì)描述
- Spring DAO Framework - 官方Spring DAO文檔
- Spring Integration with WebLogic Server (Dev2Dev) - Spring與WebLogic Server集成一覽
- WebLogic 8.1 Datasource Configuration(文檔) - 逐步指導(dǎo)您使用Administration控制臺(tái)配置數(shù)據(jù)源
- Refactoring - 重構(gòu)基礎(chǔ)知識(shí)講解和Martin Fowler撰寫(xiě)的Refactoring: Improving the Design of the Existing Code一書(shū)中所有重構(gòu)詳細(xì)資料的目錄;該站點(diǎn)還包含重構(gòu)使用的工具列表

作者簡(jiǎn)介 | |
Dhrubojyoti Kayal 是Capgemini Consulting的高級(jí)顧問(wèn)。在利用企業(yè)Java技術(shù)開(kāi)發(fā)和設(shè)計(jì)應(yīng)用程序和產(chǎn)品方面,擁有5年以上的經(jīng)驗(yàn)。 |
posted on 2007-04-14 09:11 Ecko 閱讀(385) 評(píng)論(0) 編輯 收藏