J2EE開(kāi)發(fā)人員使用數據訪(fǎng)問(wèn)對象(DAO)設計模式把底層的數據訪(fǎng)問(wèn)邏輯和高層的商務(wù)邏輯分開(kāi)。實(shí)現DAO模式能夠更加專(zhuān)注于編寫(xiě)數據訪(fǎng)問(wèn)代碼。這篇文章中,Java開(kāi)發(fā)人員Sean C. Sullivan從三個(gè)方面討論DAO編程的結構特征:事務(wù)劃分,異常處理,日志記錄。
在最近的18個(gè)月,我和一個(gè)優(yōu)秀的軟件開(kāi)發(fā)團隊一起工作,開(kāi)發(fā)定制基于WEB的供應鏈管理應用程序.我們的應用程序訪(fǎng)問(wèn)廣泛的持久層數據,包括出貨狀態(tài),供應鏈制度,庫存,貨物發(fā)運,項目管理數據,和用戶(hù)屬性等.我們使用JDBC API連接我們公司的各種數據庫平臺,并且在整個(gè)應用程序中應用了DAO設計模式.
通過(guò)在整個(gè)應用程序中應用數據訪(fǎng)問(wèn)對象(DAO)設計模式使我們能夠把底層的數據訪(fǎng)問(wèn)邏輯和上層的商務(wù)邏輯分開(kāi).我們?yōu)槊總€(gè)數據源創(chuàng )建了提供CRUD(創(chuàng )建,讀取,更新,刪除)操作的DAO類(lèi).
在本文中,我將向你介紹DAO的實(shí)現策略以及創(chuàng )建更好的DAO類(lèi)的技術(shù).我會(huì )明確的介紹日志記錄,異常處理,和事務(wù)劃分三項技術(shù).你將學(xué)在你的DAO類(lèi)中怎樣把這三種技術(shù)結合在一起.這篇文章假設你熟悉JDBC API,SQL和關(guān)系性數據庫編程.
我們先來(lái)回顧一下DAO設計模式和數據訪(fǎng)問(wèn)對象.
DAO基礎 DAO模式是標準的J2EE設計模式之一.開(kāi)發(fā)人員使用這個(gè)模式把底層的數據訪(fǎng)問(wèn)操作和上層的商務(wù)邏輯分開(kāi).一個(gè)典型的DAO實(shí)現有下列幾個(gè)組件:
1. 一個(gè)DAO工廠(chǎng)類(lèi);
2. 一個(gè)DAO接口;
3. 一個(gè)實(shí)現DAO接口的具體類(lèi);
4. 數據傳遞對象(有些時(shí)候叫做值對象).
具體的DAO類(lèi)包含了從特定的數據源訪(fǎng)問(wèn)數據的邏輯。在下面的這段中你將學(xué)到設計和實(shí)現數據訪(fǎng)問(wèn)對象的技術(shù)。
事務(wù)劃分: 關(guān)于DAO要記住的一件重要事情是它們是事務(wù)性對象。每個(gè)被DAO執行的操作(象創(chuàng )建,更新、或刪除數據)都是和事務(wù)相關(guān)聯(lián)的。同樣的,事務(wù)劃分(transaction demarcation)的概念是特別重要的。
事務(wù)劃分是在事務(wù)界定定義中的方式。J2EE規范為事務(wù)劃分描述了兩種模式:編程性事務(wù)(programmatic)和聲明性事務(wù)(declarative)。下表是對這兩種模式的拆分:
我將把注意力集中的編程性事務(wù)劃分上。
象前面的介紹一樣,DAOs是一些事務(wù)對象。一個(gè)典型的DAO要執行象創(chuàng )建、更新、和刪除這的事務(wù)性操作。在設計一個(gè)DAO時(shí),首先要問(wèn)自己如下問(wèn)題:
1、 事務(wù)將怎樣開(kāi)始?
2、 事務(wù)將怎樣結束?
3、 那個(gè)對象將承擔起動(dòng)一個(gè)事務(wù)的責任?
4、 那個(gè)對象將承擔結束一個(gè)事務(wù)的責任?
5、 DAO應該承擔起動(dòng)和結束事務(wù)的責任?
6、 應用程序需要交叉訪(fǎng)問(wèn)多個(gè)DAO嗎?
7、 一個(gè)事務(wù)包含一個(gè)DAO還是多個(gè)DAO?
8、 一個(gè)DAO包含其它的DAO中的方法嗎?
回答這些問(wèn)題將有助于你為DAO對象選擇最好的事務(wù)劃分策略。對ADO中的事務(wù)劃分有兩個(gè)主要的策略。一種方法是使用DAO承擔事務(wù)劃分的責任;另一種是延期性事務(wù),它把事務(wù)劃分到調用DAO對象的方法中。如果你選擇前者,你將要在DAO類(lèi)中嵌入事務(wù)代碼。如果你選擇后者,事務(wù)代碼將被寫(xiě)在DAO類(lèi)的外部。我們將使用簡(jiǎn)單的代碼實(shí)例來(lái)更好的理解這兩種方法是怎樣工作的。
實(shí)例1展示了一個(gè)帶有兩種數據操作的DAO:創(chuàng )建(create)和更新(update):
public void createWarehouseProfile(WHProfile profile);
public void updateWarehouseStatus(WHIdentifier id, StatusInfo status);
實(shí)例2展示了一個(gè)簡(jiǎn)單的事務(wù),事務(wù)劃分代碼是在DAO類(lèi)的外部。注意:在這個(gè)例子中的調用者把多個(gè)DOA操作組合到這個(gè)事務(wù)中。
tx.begin(); // start the transaction
dao.createWarehouseProfile(profile);
dao.updateWarehouseStatus(id1, status1);
dao.updateWarehouseStatus(id2, status2);
tx.commit(); // end the transaction
這種事務(wù)事務(wù)劃分策略對在一個(gè)單一事務(wù)中訪(fǎng)問(wèn)多個(gè)DAO的應用程序來(lái)說(shuō)尤為重要。
你即可使用JDBC API也可以使用Java 事務(wù)API(JTA)來(lái)實(shí)現事務(wù)的劃分。JDBC事務(wù)劃分比JTA事務(wù)劃分簡(jiǎn)單,但是JTA提供了更好的靈活性。在下面的這段中,我們會(huì )進(jìn)一步的看事務(wù)劃分機制。
使用JDBC的事務(wù)劃分 JDBC事務(wù)是使用Connection對象來(lái)控制的。JDBC的連接接口(java.sql.Connection)提供了兩種事務(wù)模式:自動(dòng)提交和手動(dòng)提交。Java.sql.Connection為控制事務(wù)提供了下列方法:
.public void setAutoCommit(Boolean)
.public Boolean getAutoCommit()
.public void commit()
.public void rollback()
實(shí)例3展示怎樣使用JDBC API來(lái)劃分事務(wù):
import java.sql.*;
import javax.sql.*;
// ...
DataSource ds = obtainDataSource();
Connection conn = ds.getConnection();
conn.setAutoCommit(false);
// ...
pstmt = conn.prepareStatement(";UPDATE MOVIES ...";);
pstmt.setString(1, ";The Great Escape";);
pstmt.executeUpdate();
// ...
conn.commit();
// ...
使用JDBC事務(wù)劃分,你能夠把多個(gè)SQL語(yǔ)句組合到一個(gè)單一事務(wù)中。JDBC事務(wù)的缺點(diǎn)之一就是事務(wù)范圍被限定在一個(gè)單一的數據庫連接中。一個(gè)JDBC事務(wù)不能夠跨越多個(gè)數據庫。接下來(lái),我們會(huì )看到怎樣使用JTA來(lái)做事務(wù)劃分的。因為JTA不象JDBC那樣被廣泛的了解,所以我首先概要的介紹一下JTA。
JTA概要介紹 Java事務(wù)API(JTA;Java Transaction API)和它的同胞Java事務(wù)服務(wù)(JTS;Java Transaction Service),為J2EE平臺提供了分布式事務(wù)服務(wù)。一個(gè)分布式事務(wù)(distributed transaction)包括一個(gè)事務(wù)管理器(transaction manager)和一個(gè)或多個(gè)資源管理器(resource manager)。一個(gè)資源管理器(resource manager)是任意類(lèi)型的持久化數據存儲。事務(wù)管理器(transaction manager)承擔著(zhù)所有事務(wù)參與單元者的相互通訊的責任。下車(chē)站顯示了事務(wù)管理器和資源管理的間的關(guān)系。
JTA事務(wù)比JDBC事務(wù)更強大。一個(gè)JTA事務(wù)可以有多個(gè)參與者,而一個(gè)JDBC事務(wù)則被限定在一個(gè)單一的數據庫連接。下列任一個(gè)Java平臺的組件都可以參與到一個(gè)JTA事務(wù)中:
.JDBC連接
.JDO PersistenceManager 對象
.JMS 隊列
.JMS 主題
.企業(yè)JavaBeans(EJB)
.一個(gè)用J2EE Connector Architecture 規范編譯的資源分配器。
使用JTA的事務(wù)劃分
要用JTA來(lái)劃分一個(gè)事務(wù),應用程序調用javax.transaction.UserTransaction接口中的方法。示例4顯示了一個(gè)典型的JNDI搜索的UseTransaction對象。
import javax.transaction.*;
import javax.naming.*;
// ...
InitialContext ctx = new InitialContext();
Object txObj = ctx.lookup(";java:comp/UserTransaction";);
UserTransaction utx = (UserTransaction) txObj;
應用程序有了UserTransaction對象的引用之后,就可以象示例5那樣來(lái)起動(dòng)事務(wù)。
utx.begin();
// ...
DataSource ds = obtainXADataSource();
Connection conn = ds.getConnection();
pstmt = conn.prepareStatement(";UPDATE MOVIES ...";);
pstmt.setString(1, ";Spinal Tap";);
pstmt.executeUpdate();
// ...
utx.commit();
// ...
當應用程序調用commit()時(shí),事務(wù)管理器使用兩段提交協(xié)議來(lái)結束事務(wù)。JTA事務(wù)控制的方法:
.javax.transaction.UserTransaction接口提供了下列事務(wù)控制方法:
.public void begin()
.public void commit()
.public void rollback()
.public void getStatus()
.public void setRollbackOnly()
.public void setTransactionTimeout(int)
應用程序調用begin()來(lái)起動(dòng)事務(wù),即可調用commit()也可以調用rollback()來(lái)結束事務(wù)。
使用JTA和JDBC 開(kāi)發(fā)人員經(jīng)常使用JDBC來(lái)作為DAO類(lèi)中的底層數據操作。如果計劃使用JTA來(lái)劃分事務(wù),你將需要一個(gè)實(shí)現了javax.sql.XADataSource,javax.sql.XAConnection和javax.sql.XAResource接口JDBC的驅動(dòng)。實(shí)現了這些接口的驅動(dòng)將有能力參與到JTA事務(wù)中。一個(gè)XADataSource對象是一個(gè)XAConnection對象的工廠(chǎng)。XAConnections是參與到JTA事務(wù)中的連接。
你需要使用應用程序服務(wù)器管理工具來(lái)建立XADataSource對象。對于特殊的指令請參考應用程序服務(wù)器文檔和JDBC驅動(dòng)文檔。
J2EE應用程序使用JNDI來(lái)查找數據源。一旦應用程序有了一個(gè)數據源對象的引用,這會(huì )調用javax.sql.DataSource.getConnection()來(lái)獲得數據庫的連接。
XA連接區別于非XA連接。要記住的是XA連接是一個(gè)JTA事務(wù)中的參與者。這就意味著(zhù)XA連接不支持JDBC的自動(dòng)提交特性。也就是說(shuō)應用程序不必在XA連接上調用java.sql.Connection.commit()或java.sql.Connection.rollback()。相反,應用程序應該使用UserTransaction.begin()、UserTransaction.commit()和UserTransaction.rollback().
選擇最好的方法 我們已經(jīng)討論了JDBC和JTA是怎樣劃分事務(wù)的。每一種方法都有它的優(yōu)點(diǎn),回此你需要決定為你的應用程序選擇一個(gè)最適應的方法。 在我們團隊許多最近的對于事務(wù)劃分的項目中使用JDBC API來(lái)創(chuàng )建DAO類(lèi)。這DAO類(lèi)總結如下:
.事務(wù)劃分代碼被嵌入到DAO類(lèi)內部
.DAO類(lèi)使用JDBC API來(lái)進(jìn)行事務(wù)劃分
.調用者沒(méi)有劃分事務(wù)的方法
.事務(wù)范圍被限定在一個(gè)單一的JDBC連接
JDBC事務(wù)對復雜的企業(yè)應用程序不總是有效的。如果你的事務(wù)將跨越多個(gè)DAO對象或多個(gè)數據庫,那么下面的實(shí)現策略可能會(huì )更恰當:
.用JTA對事務(wù)進(jìn)行劃分
.事務(wù)劃分代碼被DAO分開(kāi)
.調用者承擔劃分事務(wù)的責任
.DAO參與一個(gè)全局的事務(wù)中
JDBC方法由于它的簡(jiǎn)易性而具有吸引力,JTA方法提供了更多靈活性。你選擇什么樣的實(shí)現將依賴(lài)于你的應用程序的特定需求。
日志記錄和DAO 一個(gè)好的DAO實(shí)現類(lèi)將使用日志記錄來(lái)捕獲有關(guān)它在運行時(shí)的行為細節。你可以選擇記錄異常、配置信息、連接狀態(tài)、JDBC驅動(dòng)程序的元數據或查詢(xún)參數。日志對開(kāi)發(fā)整個(gè)階段都是有益的。我經(jīng)常檢查應用程序在開(kāi)發(fā)期間、測試期間和產(chǎn)品中的日志記錄。
在這段中,我們將展現一段如何把Jakarta Commaons Logging結合中一個(gè)DAO中的例子。在我們開(kāi)始之前,讓我們先回顧一些基礎知識。
選擇一個(gè)日志例庫 許多開(kāi)發(fā)人員使用的基本日志形式是:System.out.println和System.err.println.Println語(yǔ)句。這種形式快捷方便,但它們不能提供一個(gè)完整的日志系統的的能力。下表列出了Java平臺的日志類(lèi)庫:
Java.util.logging是J2SE1.4平臺上的標準的API。但是,大多數開(kāi)發(fā)人員都認為Jakarta Log4j提供了更大的功能性和靈活性。Log4j超越j(luò )ava.util.logging的優(yōu)點(diǎn)之一就是它支持J2SE1.3和J2SE1.4平臺。
Jakarta Commons Logging能夠被用于和java.util.loggin或Jakarta Log4j一起工作。Commons Logging是一個(gè)把你的應用程序獨立于日志實(shí)現的提取層。使用Commons Logging你能夠通過(guò)改變一個(gè)配置文件來(lái)與下面的日志實(shí)現來(lái)交換數據。Commons Logging被用于JAKARTA Struts1.1和Jakarta HttpClient2.0中。
一個(gè)日志示例 示例7顯示了在一個(gè)DOA類(lèi)中怎樣使用Jakarta Commons Logging
import org.apache.commons.logging.*;
class DocumentDAOImpl implements DocumentDAO
{
static private final Log log = LogFactory.getLog(DocumentDAOImpl.class);
public void deleteDocument(String id)
{
// ...
log.debug(";deleting document: "; + id);
// ...
try
{
// ... data operations ...
}
catch (SomeException ex)
{
log.error(";Unable to delete document"; ex);
// ... handle the exception ...
}
}
}
日志是評估應用程序的基本部分。如果你在一個(gè)DAO中遇到了失敗,日志經(jīng)常會(huì )為理解發(fā)生的什么錯誤提供最好的信息。把日志結合到你的DAO中,確保得到調試和解決問(wèn)題的有效手段。
DAO中的異常處理 我們已經(jīng)看了事務(wù)劃分和日志記錄,并且現在對于它們是怎樣應用于數據訪(fǎng)問(wèn)對象的有一個(gè)深入的理解。我們第三部分也是最后要討論的是異常處理。下面的一些簡(jiǎn)單的異常處理方針使用你的DAO更容易使用,更加健壯和更具有可維護性。
在實(shí)現DAO模式的時(shí)候,要考濾下面的問(wèn)題:
.在DAO的public接口中的方法將拋出被檢查的異常嗎?
.如果是,將拋出什么樣的檢查性異常?
.在DAO實(shí)現類(lèi)中怎能樣處理異常。
在用DAO模式工作的過(guò)程中,我們的團隊為異常處理開(kāi)發(fā)了一組方針。下面的這些方針會(huì )很大程度的改善你的DAO:
.DAO方法應該拋出有意義的異常。
.DAO方法不應該拋出java.lang.Exception異常。因為java.lang.Exception太一般化,它不能包含有關(guān)潛在問(wèn)題的所有信息。
.DAO方法不應該拋出java.sql.SQLException異常。SQLException是一個(gè)底層的JDBC異常,DAO應用努力封裝JDBC異常而不應該把JDBC異常留給應用程序的其它部分。
.在DAO接口中的方法應該只拋出調用者期望處理的檢查性異常。如果調用者不能用適當的方法來(lái)處理異常,考濾拋出不檢查性(運行時(shí)run-time)異常。
.如果你的數據訪(fǎng)問(wèn)代碼捕獲了一個(gè)異常,不可要忽略它。忽略捕獲異常的DAO是很處理的。
.使用異常鏈把底層的異常傳遞給高層的某個(gè)處理器。
.考濾定義一個(gè)標準的DAO異常類(lèi)。Spring框架提供了一個(gè)優(yōu)秀的預定義的DAO異常類(lèi)的集合。
看Resources,查看有異常和異常處理技術(shù)的更詳細信息。
實(shí)現示例:MovieDAO MoveDAO是一個(gè)示范了在這篇文章中所討論的所有技術(shù),包括事務(wù)劃分、日志記錄和異常處理。你會(huì )在Resources段找到MovieDAO的源代碼。它被分下面的三個(gè)包:
.daoexamples.exception
.daoexamples.move
.daoexamples.moviedemo
這個(gè)DAO模式的實(shí)現由下面的類(lèi)和接口組成:
.daoexamples.movie.MovieDAOFactory
.daoexamples.movie.MovieDAO
.daoexamples.movie.MovieDAOImpl
.daoexamples.movie.MovieDAOImplJTA
.daoexamples.movie.Movie
.daoexamples.movie.MovieImple
.daoexamples.movie.MovieNotFoundException
.daoexamples.movie.MovieUtil
MovieDAO接口定義了DAO的數據操作。這個(gè)接口有如下五個(gè)方法:
.public Movie findMovieById(String id)
.public java.util.Collection findMoviesByYear(String year)
.public void deleteMovie(String id)
.public Movie createMovie(String rating,String year,String title)
.public void updateMovie(String id,String rating,String year,String title)
daoexamples.movie包包含了兩個(gè)MovieDAO接口的實(shí)現。每個(gè)實(shí)現使用了一個(gè)同的事務(wù)劃分方法,如下表所示:
MovieDAO 示范應用程序 這個(gè)示范應用程序是一個(gè)叫做daoexamples.moviedemo.DemoServlet.DemoServlet的servlet類(lèi),它使用Movie DAO來(lái)查詢(xún)和更新一個(gè)表中的movie數據。
這個(gè)servlet示范了把JTA感知的MovieDAO和Java消息服務(wù)組合到一個(gè)單一的事務(wù)中,如示例8所示:
UserTransaction utx = MovieUtil.getUserTransaction();
utx.begin();
batman = dao.createMovie(";R";
";2008";
";Batman Reloaded";);
publisher = new MessagePublisher();
publisher.publishTextMessage(";I’ll be back";);
dao.updateMovie(topgun.getId(),
";PG-13";
topgun.getReleaseYear(),
topgun.getTitle());
dao.deleteMovie(legallyblonde.getId());
utx.commit();
要運行這個(gè)范例應用程序,在你的應用程序服務(wù)器中配置一個(gè)XA 數據源和一個(gè)非XA數據源。然后布署daoexamples.ear文件。這個(gè)應用程序將運行在任何與J2EE兼容的應用程序服務(wù)器。