高级 DAO 编程
在過去 18 個月中,我參加了一個由有才華的軟件工程師組成的小組,構建定制的、基于 Web 的供應鏈管理應用程序。我們的應用程序訪問范圍廣泛的持久性數據,包括配送狀態、供應鏈衡量(metrics)、庫存、貨運發票、項目管理數據和用戶信息。我們用 JDBC API 連接到我們公司的不同數據庫平臺上,并在整個應用程序中使用 DAO 設計模式。
圖 1 顯示了應用程序和數據源之間的關系:
圖 1. 應用程序和數據源
在整個應用程序中使用數據訪問對象(DAO)使我們可以將底層數據訪問邏輯與業務邏輯分離開來。我們構建了為每一個數據源提供 GRUD (創建、讀取、更新、刪除)操作的 DAO 類。
在本文中,我將為您介紹構建更好的 DAO 類的 DAO 實現策略和技術。更確切地說,我將討論日志、異常處理和事務界定。您將學到如何將這三者結合到自己的 DAO 類中。本文假定您熟悉 JDBC API、SQL 和關系數據庫編程。
我們將以對 DAO 設計模式和數據訪問對象的概述開始。
DAO基礎
DAO 模式是標準 J2EE 設計模式之一。開發人員用這種模式將底層數據訪問操作與高層業務邏輯分離開。一個典型的 DAO 實現有以下組件:
- 一個 DAO 工廠類
- 一個 DAO 接口
- 一個實現了 DAO 接口的具體類
- 數據傳輸對象(有時稱為值對象)
具體的 DAO 類包含訪問特定數據源的數據的邏輯。在下面一節中您將學習設計和實現數據訪問對象的技術。有關 DAO 設計模式的更多內容請參閱?參考資料。
回頁首
事務界定
關于 DAO 要記住的重要一點是它們是事務性對象。由 DAO 所執行的每一個操作 -- 如創建、更新或者刪除數據 -- 都與一個事務相關聯。因此,事務界定的概念就變得特別重要了。
事務界定是定義事務邊界的方式。J2EE 規范描述了兩種事務界定的模型:編程式(programmatic)和聲明式(declarative)。表 1 分析了這兩種模型:
表 1. 兩種事務界定的模型
| 程序員用 EJB 部署描述符聲明事務屬性。 | 程序員負責編寫事務邏輯。 |
| 運行時環境(EJB 容器)用這些屬性自動管理事務。 | 應用程序通過一個 API 控制事務。 |
我們將側重于編程式事務界定。
設計考慮
如前所述,DAO 是事務性對象。一個典型的 DAO 執行像創建、更新和刪除這樣的事務性操作。在設計 DAO 時,首先要問自己以下問題:
- 事務要如何開始?
- 事務應如何結束?
- 哪一個對象將負責開始一個事務?
- 哪一個對象將負責結束一個事務?
- DAO 是否要負責事務的開始和結束?
- 應用程序是否需要通過多個 DAO 訪問數據?
- 事務涉及到一個 DAO 還是多個 DAO?
- 一個 DAO 是否調用另一個 DAO 的方法?
了解上述問題的答案將有助于您選擇最適合的 DAO 的事務界定策略。在 DAO 中有兩種主要的界定事務的策略。一種方式是讓 DAO 負責界定事務,另一種將事務界定交給調用這個 DAO 方法的對象處理。如果選擇了前一種方式,那么就將事務代碼嵌入到 DAO 中。如果選擇后一種方式,那么事務界定代碼就是在 DAO 類外面。我們將使用簡單的代碼示例幫助您更好理解每一種方式是如何工作的。
清單 1 顯示了一個有兩種數據操作的 DAO:創建和更新:
清單 1. DAO 方法
public void createWarehouseProfile(WHProfile profile);public void updateWarehouseStatus(WHIdentifier id, StatusInfo status);清單 2 顯示了一個簡單的事務。事務界定在 DAO 類外面。注意在這個例子中調用者是如何在一個事務中結合多個 DAO 操作的。
清單 2. 調用者管理的事務
tx.begin(); // start the transactiondao.createWarehouseProfile(profile);dao.updateWarehouseStatus(id1, status1);dao.updateWarehouseStatus(id2, status2);tx.commit(); // end the transaction這種事務界定策略對于需要在一個事務中訪問多個 DAO 的應用程序特別有用。
可以用 JDBC API 或者 Java 事務 API(Java Transaction API JTA)實現事務界定。 JDBC 事務界定比 JTA 事務界定要簡單,但是 JTA 提供了更多的靈活性。在下面一節中我將更深入地分析事務界定的機制。
回頁首
用 JDBC 進行事務界定
JDBC 事務是用?Connection?對象控制的。JDBC Connection 接口(?java.sql.Connection?)提供了兩種事務模式:自動提交和手工提交。java.sql.Connection?提供了以下控制事務的方法:
- public void setAutoCommit(boolean)
- public boolean getAutoCommit()
- public void commit()
- public void rollback()
清單 3 顯示了如何用 JDBC API 界定一個事務:
清單 3. 用 JDBC API 進行事務界定
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 事務界定時,您可以將多個 SQL 語句結合到一個事務中。JDBC 事務的一個缺點是事務的范圍局限于一個數據庫連接。一個 JDBC 事務不能跨越多個數據庫。在下面,我們將看一下如何用 JTA 進行事務界定。因為 JTA 不像 JDBC 那樣有名,所以我們首先做一個簡介。
回頁首
JTA簡介
Java 事務 API(JTA) 及其同門兄弟 Java 事務服務(Java Transaction Service JTS)為 J2EE 平臺提供了分布式事務服務。一個?分布式的事務涉及一個事務管理器和一個或者多個資源管理器。一個?資源管理器是任何類型的持久性的數據存儲。事務管理器負責協調所有事務參與者之間的通信。事務管理器與資源管理器之間的關系如圖 2 所示:
圖 2. 一個事務管理器和資源管理器
JTA 事務比 JDBC 事務功能更強。JDBC 事務局限為一個數據庫連接,而 JTA 事務可以有多個參與者。所有下列 Java 平臺組件都可以參與 JTA 事務:
- JDBC 連接
- JDO?PersistenceManager?對象
- JMS 隊列
- JMS 主題
- 企業 JavaBeans
- 符合 J2EE 連接體系結構(J2EE Connector Architecture)規范的資源適配器
回頁首
使用 JTA 的事務界定
要用 JTA 進行事務界定,應用程序要調用?javax.transaction.UserTransaction?接口中的方法。清單 4 顯示了對?UserTransaction對象的典型 JNDI 查詢:
清單 4. 一個對 UserTransaction 對象的 JDNI 查詢
import javax.transaction.*;import javax.naming.*;// ...InitialContext ctx = new InitialContext();Object txObj = ctx.lookup("java:comp/UserTransaction");UserTransaction utx = (UserTransaction) txObj;當應用程序找到了?UserTransaction?對象后,就可以開始事務了,如清單 5 所示:
清單 5. 用 JTA 開始一個事務
utx.begin();// ...DataSource ds = obtainXADataSource();Connection conn = ds.getConnection();pstmt = conn.prepareStatement("UPDATE MOVIES ...");pstmt.setString(1, "Spinal Tap");pstmt.executeUpdate();// ...utx.commit();// ...當應用程序調用?commit()?時,事務管理器用一個兩階段的提交協議結束事務。
回頁首
控制事務的 JTA 方法
javax.transaction.UserTransaction?接口提供了以下事務控制方法:
- public void begin()
- public void commit()
- public void rollback()
- public int getStatus()
- public void setRollbackOnly()
- public void setTransactionTimeout(int)
應用程序調用?begin()?開始事務。應用程序調用?commit()?或者?rollback()?結束事務。參閱?參考資料以了解更多關于用 JTA 進行事務管理的內容。
回頁首
使用 JTA 和 JDBC
開發人員通常在 DAO 類中用 JDBC 進行底層數據操作。如果計劃用 JTA 界定事務,那么就需要有一個實現?javax.sql.XADataSource?、javax.sql.XAConnection?和?javax.sql.XAResource?接口的 JDBC 驅動程序。一個實現了這些接口的驅動程序將可以參與 JTA 事務。一個?XADataSource?對象就是一個?XAConnection?對象的工廠。?XAConnection?s 是參與 JTA 事務的 JDBC 連接。
您將需要用應用服務器的管理工具設置?XADataSource?。從應用服務器和 JDBC 驅動程序的文檔中可以了解到相關的指導。
J2EE 應用程序用 JNDI 查詢數據源。一旦應用程序找到了數據源對象,它就調用?javax.sql.DataSource.getConnection()?以獲得到數據庫的連接。
XA 連接與非 XA 連接不同。一定要記住 XA 連接參與了 JTA 事務。這意味著 XA 連接不支持 JDBC 的自動提交功能。同時,應用程序一定不要對 XA 連接調用?java.sql.Connection.commit()?或者?java.sql.Connection.rollback()?。相反,應用程序應該使用UserTransaction.begin()、UserTransaction.commit()?和?serTransaction.rollback()?。
回頁首
選擇最好的方式
我們討論了如何用 JDBC 和 JTA 界定事務。每一種方式都有其優點,您需要決定哪一種最適合于您的應用程序。
在最近的許多項目中,我們小組是用 JDBC API 進事務界定來構建 DAO 類的。這些 DAO 類可以總結如下:
- 事務界定代碼嵌入在 DAO 類中。
- DAO 類使用 JDBC API 進行事務界定。
- 調用者不能界定事務。
- 事務范圍局限于單個 JDBC 連接。
JDBC 事務并不總是適合復雜的企業應用程序。如果您的事務要跨越多個 DAO 或者多個數據庫,那么下列實現策略也許更合適:
- 事務用 JTA 界定。
- 事務界定代碼從 DAO 中分離出來。
- 調用者負責界定事務。
- DAO 加入一個全局事務。
JDBC 方式由于其簡單性而具有吸引力,JTA 方式提供了更大的靈活性。您所選擇的實現將取決于應用程序的特定需求。
回頁首
日志記錄和 DAO
一個良好實現的 DAO 類將使用日志記錄來捕捉有關其運行時行為的細節。您可以選擇記錄異常、配置信息、連接狀態、JDBC 驅動程序元數據、或者查詢參數。日志對于開發的所有階段都很有用。我經常在開發時、測試時和生產中分析應用程序日志。
在本節,我將展示一個顯示如何將 Jakarta Commons Logging 加入到 DAO 中的代碼示例。在這之前,讓我們回顧一下一些基本知識。
選擇日志庫
許多開發人員使用一種原始格式進行日志記錄:?System.out.println?和?System.err.println?。?Println?語句速度快且使用方便,但是它們沒有提供全功能的日志記錄系統所具有的功能。表 2 列出了 Java 平臺的日志庫:
表 2. Java 平臺的日志庫
| java.util.logging | 不是 | http://java.sun.com/j2se/ |
| Jakarta Log4j | 是 | http://jakarta.apache.org/log4j/ |
| Jakarta Commons Logging | 是 | http://jakarta.apache.org/commons/logging.html |
Jakarta Commons Logging 可以與?java.util.logging?或者 Jakarta Log4j 一同使用。Commons Logging 是一個日志抽象層,它隔離了應用程序與底層日志實現。使用 Commons Logging,您可以通過改變配置文件更換底層日志實現。Commons Logging 在 Jakarta Struts 1.1 和 Jakarta HttpClient 2.0 中使用。
一個日志記錄示例
清單 7 顯示了如何在 DAO 類中使用 Jakarta Commons Logging:
清單 7. DAO 類中的 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 ...}} }日志記錄是所有任務關鍵型應用程序的重要部分。如果在 DAO 中遇到故障,那么日志通常可以提供判斷出錯位置的最好信息。將日志加入到 DAO 可以保證您有機會進行調試和故障排除。
回頁首
DAO 中的異常處理
我們討論過了事務界定和日志,現在對于如何在數據訪問對象上應用它們有了更深入的理解。我們的第三個和最后一個討論議題是異常處理。遵從幾個簡單的異常處理指導可以使您的 DAO 更容易使用、更健壯及更易于維護。
在實現 DAO 模式時,考慮以下問題:
- DAO 的公共接口中的方法是否拋出檢查過的異常?
- 如果是的話,拋出何種檢查過的異常?
- 在 DAO 實現類中如何處理異常?
在使用 DAO 模式的過程中,我們的小組開發了一些處理異常的原則。遵從這些原則可以極大地改進您的 DAO:
- DAO 方法應該拋出有意義的異常。
- DAO 方法不應該拋出?java.lang.Exception?。?java.lang.Exception?太一般化了。它不傳遞關于底層問題的任何信息。
- DAO 方法不應該拋出?java.sql.SQLException?。SQLException 是一個低級別的 JDBC 異常。一個 DAO 應該力爭封裝 JDBC 而不是將 JDBC 公開給應用程序的其余部分。
- 只有在可以合理地預期調用者可以處理異常時,DAO 接口中的方法才應該拋出檢查過的異常。如果調用者不能以有意義的方式處理這個異常,那么考慮拋出一個未檢查的(運行時)異常。
- 如果數據訪問代碼捕獲了一個異常,不要忽略它。忽略捕獲的異常的 DAO 是很難進行故障診斷的。
- 使用鏈接的異常將低級別的異常轉化為高級別的異常。
- 考慮定義標準 DAO 異常類。Spring Framework (參閱?參考資料)提供了很好的一套預定義的 DAO 異常類。
有關異常和異常處理技術的更多信息參閱?參考資料。
回頁首
實現實例: MovieDAO
MovieDAO?是一個展示本文中討論的所有技術的 DAO:事務界定、日志和異常處理。您可以在?參考資料一節中找到?MovieDAO?源代碼。代碼分為三個包:
- daoexamples.exception
- daoexamples.movie
- daoexamples.moviedemo
DAO 模式的這個實現包含下面列出的類和接口:
- daoexamples.movie.MovieDAOFactory
- daoexamples.movie.MovieDAO
- daoexamples.movie.MovieDAOImpl
- daoexamples.movie.MovieDAOImplJTA
- daoexamples.movie.Movie
- daoexamples.movie.MovieImpl
- daoexamples.movie.MovieNotFoundException
- daoexamples.movie.MovieUtil
MovieDAO?接口定義了 DAO 的數據操作。這個接口有五個方法,如下所示:
- 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?包包含?MovieDAO?接口的兩個實現。每一個實現使用一種不同的方式進行事務界定,如表 3 所示:
表 3. MovieDAO 實現
| 實現 MovieDAO 接口? | 是 | 是 |
| 通過 JNDI 獲得 DataSource? | 是 | 是 |
| 從 DataSource 獲得 java.sql.Connection 對象? | 是 | 是 |
| DAO 在內部界定事務? | 是 | 否 |
| 使用 JDBC 事務? | 是 | 否 |
| 使用一個 XA DataSource? | 否 | 是 |
| 參與 JTA 事務? | 否 | 是 |
MovieDAO 演示應用程序
這個演示應用程序是一個名為?daoexamples.moviedemo.DemoServlet?的 servlet 類。?DemoServlet?使用這兩個 Movie DAO 查詢和更新表中的電影數據。
這個 servlet 展示了如何將支持 JTA 的?MovieDAO?和 Java 消息服務(Java Message Service)結合到一個事務中,如清單 8 所示。
清單 8. 將 MovieDAO 和 JMS 代碼結合到一個事務中
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();要運行這個演示應用程序,需要在應用服務器上配置一個 XA 數據源和一個非 XA 數據源。然后,部署 daoexamples.ear 文件。這個應用程序可以在任何兼容 J2EE 1.3 的應用服務器上運行。參閱?參考資料以獲得 EAR 文件和源代碼。
回頁首
結束語
正如本文所展示的,實現 DAO 模式需要做比編寫低級別的數據訪問代碼更多的工作。現在,通過選擇一個適合您的應用程序的事務界定策略、通過在 DAO 類中加入日志記錄,以及通過遵從幾項簡單的異常處理原則,您可以構建更好的 DAO。
參考資料
- 您可以參閱本文在 developerWorks 全球站點上的?英文原文.
- 從?daoexamples.sourceforge.net上下載 MovieDAO 源代碼。
- 想要學習有關數據訪問對象模式的更多內容?可以從?核心 J2EE 模式主頁開始。
- Kyle Brown 的“?A stepped approach to J2EE testing with SDAO”(?developerWorks,2003 年 3 月)提供了對數據訪問對象和 DAO 設計模式的簡要介紹。
- Dragonslayer 的教程“?Create persistent application data with Java Data Objects”(developerWorks,2003 年 7 月)向您展示了如何結合 Struts 與 DAO 模式以用于影響小的企業數據持久性。
- Srikanth Shenoy 的“?EJB 異常處理的最佳做法”(?developerWorks,2002 年 5 月)介紹了異常處理基礎和使用 Log4J 進行日志記錄。
- Java 理論與實踐系列從“?理解 JTS -- 事務處理簡介”開始(?developerWorks,2002年3月)提供了對 Java 事務 API 的三部分介紹。
- Java Transaction API是 J2EE 平臺的關鍵部分。
- Jakarta Log4j是 Java 應用程序的世界級日志庫。
- Jakarta Commons Logging提供了容易使用的日志抽象層。
- Spring Framework為 JDBC 和事務管理提供了抽象層。此外,這個框架包含標準的 DAO 異常類和 JNDI 幫助器類。
- Rod Johnson 的?J2EE Design and Development(Wrox Press,2002 年)是每一位 J2EE 開發人員都應該收藏的。本書充滿了應用程序設計策略、實用編程技巧和實際的例子。
- Josh Bloch 的?Effective Java Programming Language Guide?(Addison Wesley,2001 年)展示了異常處理和類庫設計的最佳實踐。
- 參閱 Java 技術專區的教程頁面,從?developerWorks?獲得免費的?Java 技術教程的完整列表。
- 在?developerWorks?Java 技術專區?中可以找到數百篇關于 Java 編程的各個方面的文章。
總結
- 上一篇: 涨姿势!北京地铁原来是16条旅游专线
- 下一篇: Java 语言中 Enum 类型的使用介