DAO 基礎 事務界定 使用 JDBC 進行事務界定 JTA 概述 使用 JTA 的事務界定 用於事務控制的 JTA 方法 使用 JTA 和 JDBC 選擇最好的方式 日志記錄和 DAO DAO 中的異常處理 實現實例:MovieDAO 結束語 參考資料 關於作者 對本文的評價 相關內容: 使用 SDAO 進行 J2EE 測試的分步方法 Create persistent application data with Java Data Objects 理解 JTS -- 事務處理簡介 developerWorks Toolbox subscription Java 專區中還有: 教學 工具與產品 代碼與組件 所有文章 實用技巧 學習編譯更好的 DAO 的技巧
級別:高級
Sean C. Sullivan ([email protected])
軟件工程師
2003年10月
在過去 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 所執行的每一個操作 -- 如創建、更新或者刪除數據 -- 都與一個事務相關聯。因此,事務界定的概念就變得非凡重要了。
事務界定是定義事務邊界的方式。J2EE 規范描述了兩種事務界定的模型:編程式(programmatic)和聲明式(declarative)。表 1 分析了這兩種模型:
表 1. 兩種事務界定的模型 聲明式事務界定 編程式事務界定 程序員用 EJB 部署描述符聲明事務屬性。程序員負責編寫事務邏輯。 運行時環境(EJB 容器)用這些屬性自動治理事務。應用程序通過一個 API 控制事務。
我們將側重於編程式事務界定。
設計考慮
如前所述,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 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 提供了更多的靈活性。在下面一節中我將更深入地分析事務界定的機制。
用 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 事務:
PersistenceManager
對象
使用 JTA 的事務界定
要用 JTA 進行事務界定,應用程序要調用 javax.transaction.UserTransaction
接口中的方法。清單 4 顯示了對 UserTransaction
對象的典型 JNDI 查詢:
import javax.transaction.*;
import javax.naming.*;
// ...
InitialContext ctx = new InitialContext();
Object txObj = ctx.lookup("java:comp/UserTransaction");
UserTransaction utx = (UserTransaction) txObj;
當應用程序找到了 UserTransaction
對象後,就可以開始事務了,如清單 5 所示:
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 類可以總結如下:
JDBC 事務並不總是適合復雜的企業應用程序。假如您的事務要跨越多個 DAO 或者多個數據庫,那麼下列實現策略也許更合適:
JDBC 方式由於其簡單性而具有吸引力,JTA 方式提供了更大的靈活性。您所選擇的實現將取決於應用程序的特定需求。
日志記錄和 DAO
一個良好實現的 DAO 類將使用日志記錄來捕捉有關其運行時行為的細節。您可以選擇記錄異常、配置信息、連接狀態、JDBC 驅動程序元數據、或者查詢參數。日志對於開發的所有階段都很有用。我經常在開發時、測試時和生產中分析應用程序日志。
在本節,我將展示一個顯示如何將 Jakarta Commons Logging 加入到 DAO 中的代碼示例。在這之前,讓我們回顧一下一些基本知識。
選擇日志庫
許多開發人員使用一種原始格式進行日志記錄:System.out.println
和 System.err.println
。Println
語句速度快且使用方便,但是它們沒有提供全功能的日志記錄系統所具有的功能。表 2 列出了 Java 平台的日志庫:
表 2. Java 平台的日志庫 日志庫 開放源代碼? URL 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
java.util.logging
是 J2SE 1.4 平台上的標准 API。不過,大多數開發人員同意 Jakarta Log4j 提供了更多的功能和更大的靈活性。Log4j 優於 java.util.logging 的一點是它同時支持 J2SE 1.3 和 J2SE 1.4 平台。
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:
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:
java.lang.Exception
。java.lang.Exception
太一般化了。它不傳遞關於底層問題的任何信息。 java.sql.SQLException
。SQLException 是一個低級別的 JDBC 異常。一個 DAO 應該力爭封裝 JDBC 而不是將 JDBC 公開給應用程序的其余部分。有關異常和異常處理技術的更多信息參閱參考資料。
實現實例: 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 實現 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 所示。
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。
參考資料