| 級別: 初級 Sean C. Sullivan, 軟件工程師
 2003 年 10 月 15 日 J2EE 開發(fā)人員使用數據訪問對象(Data Access Object DAO)設計模式,以便將低級別的數據訪問邏輯與高級別的業(yè)務邏輯分離。實現 DAO 模式涉及比編寫數據訪問代碼更多的內容。在本文中,Java 開發(fā)人員 Sean C. Sullivan 討論了 DAO 編程中三個常常被忽略的方面:事務界定、異常處理和日志記錄。 在過去 18 個月中,我參加了一個由有才華的軟件工程師組成的小組,構建定制的、基于 Web 的供應鏈管理應用程序。我們的應用程序訪問范圍廣泛的持久性數據,包括配送狀態(tài)、供應鏈衡量(metrics)、庫存、貨運發(fā)票、項目管理數據和用戶信息。我們用 JDBC API 連接到我們公司的不同數據庫平臺上,并在整個應用程序中使用 DAO 設計模式。 圖 1 顯示了應用程序和數據源之間的關系: 圖 1. 應用程序和數據源
 
   
 在整個應用程序中使用數據訪問對象(DAO)使我們可以將底層數據訪問邏輯與業(yè)務邏輯分離開來。我們構建了為每一個數據源提供 GRUD (創(chuàng)建、讀取、更新、刪除)操作的 DAO 類。 在本文中,我將為您介紹構建更好的 DAO 類的 DAO 實現策略和技術。更確切地說,我將討論日志、異常處理和事務界定。您將學到如何將這三者結合到自己的 DAO 類中。本文假定您熟悉 JDBC API、SQL 和關系數據庫編程。 我們將以對 DAO 設計模式和數據訪問對象的概述開始。 DAO基礎 DAO 模式是標準 J2EE 設計模式之一。開發(fā)人員用這種模式將底層數據訪問操作與高層業(yè)務邏輯分離開。一個典型的 DAO 實現有以下組件: 
                一個 DAO 工廠類
                一個 DAO 接口
                一個實現了 DAO 接口的具體類
                數據傳輸對象(有時稱為值對象)  具體的 DAO 類包含訪問特定數據源的數據的邏輯。在下面一節(jié)中您將學習設計和實現數據訪問對象的技術。有關 DAO 設計模式的更多內容請參閱 參考資料。  
 
 
 事務界定 關于 DAO 要記住的重要一點是它們是事務性對象。由 DAO 所執(zhí)行的每一個操作 -- 如創(chuàng)建、更新或者刪除數據 -- 都與一個事務相關聯。因此, 事務界定的概念就變得特別重要了。  事務界定是定義事務邊界的方式。J2EE 規(guī)范描述了兩種事務界定的模型:編程式(programmatic)和聲明式(declarative)。表 1 分析了這兩種模型: 表 1. 兩種事務界定的模型 
             
                
                    
                        | 聲明式事務界定 | 編程式事務界定 |  
                        | 程序員用 EJB 部署描述符聲明事務屬性。 | 程序員負責編寫事務邏輯。 |  
                        | 運行時環(huán)境(EJB 容器)用這些屬性自動管理事務。 | 應用程序通過一個 API 控制事務。 |  我們將側重于編程式事務界定。 設計考慮 如前所述,DAO 是事務性對象。一個典型的 DAO 執(zhí)行像創(chuàng)建、更新和刪除這樣的事務性操作。在設計 DAO 時,首先要問自己以下問題: 
                事務要如何開始?
                事務應如何結束?
                哪一個對象將負責開始一個事務?
                哪一個對象將負責結束一個事務?
                DAO 是否要負責事務的開始和結束?
                應用程序是否需要通過多個 DAO 訪問數據?
                事務涉及到一個 DAO 還是多個 DAO?
                一個 DAO 是否調用另一個 DAO 的方法?  了解上述問題的答案將有助于您選擇最適合的 DAO 的事務界定策略。在 DAO 中有兩種主要的界定事務的策略。一種方式是讓 DAO 負責界定事務,另一種將事務界定交給調用這個 DAO 方法的對象處理。如果選擇了前一種方式,那么就將事務代碼嵌入到 DAO 中。如果選擇后一種方式,那么事務界定代碼就是在 DAO 類外面。我們將使用簡單的代碼示例幫助您更好理解每一種方式是如何工作的。 清單 1 顯示了一個有兩種數據操作的 DAO:創(chuàng)建和更新: 清單 1. DAO 方法
 
 
                
                    
                        | 
                               public void createWarehouseProfile(WHProfile profile);
                        public void updateWarehouseStatus(WHIdentifier id, StatusInfo status);
                        
 |  
 清單 2 顯示了一個簡單的事務。事務界定在 DAO 類外面。注意在這個例子中調用者是如何在一個事務中結合多個 DAO 操作的。 清單 2. 調用者管理的事務
 
 
                
                    
                        | 
                              tx.begin();    // start the transaction
                        dao.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 提供了更多的靈活性。在下面一節(jié)中我將更深入地分析事務界定的機制。 
 
 
 用 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 主題
                企業(yè) JavaBeans
                符合 J2EE 連接體系結構(J2EE Connector Architecture)規(guī)范的資源適配器  
 
 
 使用 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 開發(fā)人員通常在 DAO 類中用 JDBC 進行底層數據操作。如果計劃用 JTA 界定事務,那么就需要有一個實現 javax.sql.XADataSource、javax.sql.XAConnection 和javax.sql.XAResource接口的 JDBC 驅動程序。一個實現了這些接口的驅動程序將可以參與 JTA 事務。一個XADataSource對象就是一個XAConnection對象的工廠。XAConnections 是參與 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 界定事務。每一種方式都有其優(yōu)點,您需要決定哪一種最適合于您的應用程序。 在最近的許多項目中,我們小組是用 JDBC API 進事務界定來構建 DAO 類的。這些 DAO 類可以總結如下: 
                事務界定代碼嵌入在 DAO 類中。
                DAO 類使用 JDBC API 進行事務界定。
                調用者不能界定事務。
                事務范圍局限于單個 JDBC 連接。  JDBC 事務并不總是適合復雜的企業(yè)應用程序。如果您的事務要跨越多個 DAO 或者多個數據庫,那么下列實現策略也許更合適: 
                事務用 JTA 界定。
                事務界定代碼從 DAO 中分離出來。
                調用者負責界定事務。
                DAO 加入一個全局事務。  JDBC 方式由于其簡單性而具有吸引力,JTA 方式提供了更大的靈活性。您所選擇的實現將取決于應用程序的特定需求。 
 
 
 日志記錄和 DAO 一個良好實現的 DAO 類將使用日志記錄來捕捉有關其運行時行為的細節(jié)。您可以選擇記錄異常、配置信息、連接狀態(tài)、JDBC 驅動程序元數據、或者查詢參數。日志對于開發(fā)的所有階段都很有用。我經常在開發(fā)時、測試時和生產中分析應用程序日志。 在本節(jié),我將展示一個顯示如何將 Jakarta Commons Logging 加入到 DAO 中的代碼示例。在這之前,讓我們回顧一下一些基本知識。 選擇日志庫 許多開發(fā)人員使用一種原始格式進行日志記錄: System.out.println和System.err.println。Println語句速度快且使用方便,但是它們沒有提供全功能的日志記錄系統所具有的功能。表 2 列出了 Java 平臺的日志庫: 表 2. Java 平臺的日志庫 
             
                
                    
                        | 日志庫 | 開放源代碼? | URL |  
                        | java.util.logging | 不是 | http://java./j2se/ |  
                        | Jakarta Log4j | 是 | http://jakarta./log4j/ |  
                        | Jakarta Commons Logging | 是 | http://jakarta./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 模式的過程中,我們的小組開發(fā)了一些處理異常的原則。遵從這些原則可以極大地改進您的 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:事務界定、日志和異常處理。您可以在 參考資料一節(jié)中找到MovieDAO源代碼。代碼分為三個包:
 
                daoexamples.exceptiondaoexamples.moviedaoexamples.moviedemo DAO 模式的這個實現包含下面列出的類和接口: 
                daoexamples.movie.MovieDAOFactorydaoexamples.movie.MovieDAOdaoexamples.movie.MovieDAOImpldaoexamples.movie.MovieDAOImplJTAdaoexamples.movie.Moviedaoexamples.movie.MovieImpldaoexamples.movie.MovieNotFoundExceptiondaoexamples.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 實現 
             
                
                    
                        |  | MovieDAOImpl | MovieDAOImplJTA |  
                        | 實現 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 模式需要做比編寫低級別的數據訪問代碼更多的工作?,F在,通過選擇一個適合您的應用程序的事務界定策略、通過在 DAO 類中加入日志記錄,以及通過遵從幾項簡單的異常處理原則,您可以構建更好的 DAO。 
 
 
 參考資料  
 
                
                    
                        |  |  
 
 關于作者 
                
                    
                        |  |  
                        |  | Sean C. Sullivan 是一個在俄勒岡州波特蘭市工作的軟件工程師。他最近的項目包括構建供應鏈管理應用程序和一個 Internet 電子商務支付系統。他還參與 IBM 和 Image Systems Technology 的操作系統和 CAD 軟件項目。Sean 是一位 Apache Jakarta 開發(fā)人員,為 Jakarta HttpClient 項目編寫了很多代碼。他從 1996 年開始就一直用 Java 開發(fā)應用程序,并且是由 John Wiley & Sons 出版的“ Programming with the Java Media Framework”一書的作者。Sean 擁有倫斯勒工業(yè)學院(Rensselaer)計算機科學的理科學士學位??梢酝ㄟ^ dao-article@seansullivan.com 與他聯系。  |  |