高級 DAO 編程
學(xué)習(xí)編譯更好的 DAO 的技巧
|
|
級別: 初級
Sean C. Sullivan
, 軟件工程師
2003 年 10 月 15 日
J2EE 開發(fā)人員使用數(shù)據(jù)訪問對象(Data Access Object DAO)設(shè)計模式,以便將低級別的數(shù)據(jù)訪問邏輯與高級別的業(yè)務(wù)邏輯分離。實現(xiàn) DAO 模式涉及比編寫數(shù)據(jù)訪問代碼更多的內(nèi)容。在本文中,Java 開發(fā)人員 Sean C. Sullivan 討論了 DAO 編程中三個常常被忽略的方面:事務(wù)界定、異常處理和日志記錄。
在過去 18 個月中,我參加了一個由有才華的軟件工程師組成的小組,構(gòu)建定制的、基于 Web 的供應(yīng)鏈管理應(yīng)用程序。我們的應(yīng)用程序訪問范圍廣泛的持久性數(shù)據(jù),包括配送狀態(tài)、供應(yīng)鏈衡量(metrics)、庫存、貨運發(fā)票、項目管理數(shù)據(jù)和用戶信息。我們用 JDBC API 連接到我們公司的不同數(shù)據(jù)庫平臺上,并在整個應(yīng)用程序中使用 DAO 設(shè)計模式。
圖 1 顯示了應(yīng)用程序和數(shù)據(jù)源之間的關(guān)系:
圖 1. 應(yīng)用程序和數(shù)據(jù)源
在整個應(yīng)用程序中使用數(shù)據(jù)訪問對象(DAO)使我們可以將底層數(shù)據(jù)訪問邏輯與業(yè)務(wù)邏輯分離開來。我們構(gòu)建了為每一個數(shù)據(jù)源提供 GRUD (創(chuàng)建、讀取、更新、刪除)操作的 DAO 類。
在本文中,我將為您介紹構(gòu)建更好的 DAO 類的 DAO 實現(xiàn)策略和技術(shù)。更確切地說,我將討論日志、異常處理和事務(wù)界定。您將學(xué)到如何將這三者結(jié)合到自己的 DAO 類中。本文假定您熟悉 JDBC API、SQL 和關(guān)系數(shù)據(jù)庫編程。
我們將以對 DAO 設(shè)計模式和數(shù)據(jù)訪問對象的概述開始。
DAO基礎(chǔ)
DAO 模式是標(biāo)準 J2EE 設(shè)計模式之一。開發(fā)人員用這種模式將底層數(shù)據(jù)訪問操作與高層業(yè)務(wù)邏輯分離開。一個典型的 DAO 實現(xiàn)有以下組件:
- 一個 DAO 工廠類
- 一個 DAO 接口
- 一個實現(xiàn)了 DAO 接口的具體類
- 數(shù)據(jù)傳輸對象(有時稱為值對象)
具體的 DAO 類包含訪問特定數(shù)據(jù)源的數(shù)據(jù)的邏輯。在下面一節(jié)中您將學(xué)習(xí)設(shè)計和實現(xiàn)數(shù)據(jù)訪問對象的技術(shù)。有關(guān) DAO 設(shè)計模式的更多內(nèi)容請參閱 參考資料。
事務(wù)界定
關(guān)于 DAO 要記住的重要一點是它們是事務(wù)性對象。由 DAO 所執(zhí)行的每一個操作 -- 如創(chuàng)建、更新或者刪除數(shù)據(jù) -- 都與一個事務(wù)相關(guān)聯(lián)。因此, 事務(wù)界定的概念就變得特別重要了。
事務(wù)界定是定義事務(wù)邊界的方式。J2EE 規(guī)范描述了兩種事務(wù)界定的模型:編程式(programmatic)和聲明式(declarative)。表 1 分析了這兩種模型:
表 1. 兩種事務(wù)界定的模型
聲明式事務(wù)界定
|
編程式事務(wù)界定
|
程序員用 EJB 部署描述符聲明事務(wù)屬性。 |
程序員負責(zé)編寫事務(wù)邏輯。 |
運行時環(huán)境(EJB 容器)用這些屬性自動管理事務(wù)。 |
應(yīng)用程序通過一個 API 控制事務(wù)。 |
我們將側(cè)重于編程式事務(wù)界定。
設(shè)計考慮
如前所述,DAO 是事務(wù)性對象。一個典型的 DAO 執(zhí)行像創(chuàng)建、更新和刪除這樣的事務(wù)性操作。在設(shè)計 DAO 時,首先要問自己以下問題:
- 事務(wù)要如何開始?
- 事務(wù)應(yīng)如何結(jié)束?
- 哪一個對象將負責(zé)開始一個事務(wù)?
- 哪一個對象將負責(zé)結(jié)束一個事務(wù)?
- DAO 是否要負責(zé)事務(wù)的開始和結(jié)束?
- 應(yīng)用程序是否需要通過多個 DAO 訪問數(shù)據(jù)?
- 事務(wù)涉及到一個 DAO 還是多個 DAO?
- 一個 DAO 是否調(diào)用另一個 DAO 的方法?
了解上述問題的答案將有助于您選擇最適合的 DAO 的事務(wù)界定策略。在 DAO 中有兩種主要的界定事務(wù)的策略。一種方式是讓 DAO 負責(zé)界定事務(wù),另一種將事務(wù)界定交給調(diào)用這個 DAO 方法的對象處理。如果選擇了前一種方式,那么就將事務(wù)代碼嵌入到 DAO 中。如果選擇后一種方式,那么事務(wù)界定代碼就是在 DAO 類外面。我們將使用簡單的代碼示例幫助您更好理解每一種方式是如何工作的。
清單 1 顯示了一個有兩種數(shù)據(jù)操作的 DAO:創(chuàng)建和更新:
清單 1. DAO 方法
public void createWarehouseProfile(WHProfile profile);
public void updateWarehouseStatus(WHIdentifier id, StatusInfo status);
|
清單 2 顯示了一個簡單的事務(wù)。事務(wù)界定在 DAO 類外面。注意在這個例子中調(diào)用者是如何在一個事務(wù)中結(jié)合多個 DAO 操作的。
清單 2. 調(diào)用者管理的事務(wù)
tx.begin(); // start the transaction
dao.createWarehouseProfile(profile);
dao.updateWarehouseStatus(id1, status1);
dao.updateWarehouseStatus(id2, status2);
tx.commit(); // end the transaction
|
這種事務(wù)界定策略對于需要在一個事務(wù)中訪問多個 DAO 的應(yīng)用程序特別有用。
可以用 JDBC API 或者 Java 事務(wù) API(Java Transaction API JTA)實現(xiàn)事務(wù)界定。 JDBC 事務(wù)界定比 JTA 事務(wù)界定要簡單,但是 JTA 提供了更多的靈活性。在下面一節(jié)中我將更深入地分析事務(wù)界定的機制。
用 JDBC 進行事務(wù)界定
JDBC 事務(wù)是用 Connection
對象控制的。JDBC Connection 接口( java.sql.Connection
)提供了兩種事務(wù)模式:自動提交和手工提交。 java.sql.Connection
提供了以下控制事務(wù)的方法:
-
public void setAutoCommit(boolean)
-
public boolean getAutoCommit()
-
public void commit()
-
public void rollback()
清單 3 顯示了如何用 JDBC API 界定一個事務(wù):
清單 3. 用 JDBC API 進行事務(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ù)界定時,您可以將多個 SQL 語句結(jié)合到一個事務(wù)中。JDBC 事務(wù)的一個缺點是事務(wù)的范圍局限于一個數(shù)據(jù)庫連接。一個 JDBC 事務(wù)不能跨越多個數(shù)據(jù)庫。在下面,我們將看一下如何用 JTA 進行事務(wù)界定。因為 JTA 不像 JDBC 那樣有名,所以我們首先做一個簡介。
JTA簡介
Java 事務(wù) API(JTA) 及其同門兄弟 Java 事務(wù)服務(wù)(Java Transaction Service JTS)為 J2EE 平臺提供了分布式事務(wù)服務(wù)。一個 分布式的事務(wù)涉及一個事務(wù)管理器和一個或者多個資源管理器。一個 資源管理器是任何類型的持久性的數(shù)據(jù)存儲。事務(wù)管理器負責(zé)協(xié)調(diào)所有事務(wù)參與者之間的通信。事務(wù)管理器與資源管理器之間的關(guān)系如圖 2 所示:
圖 2. 一個事務(wù)管理器和資源管理器
JTA 事務(wù)比 JDBC 事務(wù)功能更強。JDBC 事務(wù)局限為一個數(shù)據(jù)庫連接,而 JTA 事務(wù)可以有多個參與者。所有下列 Java 平臺組件都可以參與 JTA 事務(wù):
- JDBC 連接
- JDO
PersistenceManager
對象
- JMS 隊列
- JMS 主題
- 企業(yè) JavaBeans
- 符合 J2EE 連接體系結(jié)構(gòu)(J2EE Connector Architecture)規(guī)范的資源適配器
使用 JTA 的事務(wù)界定
要用 JTA 進行事務(wù)界定,應(yīng)用程序要調(diào)用 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;
|
當(dāng)應(yīng)用程序找到了 UserTransaction
對象后,就可以開始事務(wù)了,如清單 5 所示:
清單 5. 用 JTA 開始一個事務(wù)
utx.begin();
// ...
DataSource ds = obtainXADataSource();
Connection conn = ds.getConnection();
pstmt = conn.prepareStatement("UPDATE MOVIES ...");
pstmt.setString(1, "Spinal Tap");
pstmt.executeUpdate();
// ...
utx.commit();
// ...
|
當(dāng)應(yīng)用程序調(diào)用 commit()
時,事務(wù)管理器用一個兩階段的提交協(xié)議結(jié)束事務(wù)。
控制事務(wù)的 JTA 方法
javax.transaction.UserTransaction
接口提供了以下事務(wù)控制方法:
-
public void begin()
-
public void commit()
-
public void rollback()
-
public int getStatus()
-
public void setRollbackOnly()
-
public void setTransactionTimeout(int)
應(yīng)用程序調(diào)用 begin()
開始事務(wù)。應(yīng)用程序調(diào)用 commit()
或者 rollback()
結(jié)束事務(wù)。參閱 參考資料以了解更多關(guān)于用 JTA 進行事務(wù)管理的內(nèi)容。
使用 JTA 和 JDBC
開發(fā)人員通常在 DAO 類中用 JDBC 進行底層數(shù)據(jù)操作。如果計劃用 JTA 界定事務(wù),那么就需要有一個實現(xiàn) javax.sql.XADataSource
、 javax.sql.XAConnection
和 javax.sql.XAResource
接口的 JDBC 驅(qū)動程序。一個實現(xiàn)了這些接口的驅(qū)動程序?qū)⒖梢詤⑴c JTA 事務(wù)。一個 XADataSource
對象就是一個 XAConnection
對象的工廠。 XAConnection
s 是參與 JTA 事務(wù)的 JDBC 連接。
您將需要用應(yīng)用服務(wù)器的管理工具設(shè)置 XADataSource
。從應(yīng)用服務(wù)器和 JDBC 驅(qū)動程序的文檔中可以了解到相關(guān)的指導(dǎo)。
J2EE 應(yīng)用程序用 JNDI 查詢數(shù)據(jù)源。一旦應(yīng)用程序找到了數(shù)據(jù)源對象,它就調(diào)用 javax.sql.DataSource.getConnection()
以獲得到數(shù)據(jù)庫的連接。
XA 連接與非 XA 連接不同。一定要記住 XA 連接參與了 JTA 事務(wù)。這意味著 XA 連接不支持 JDBC 的自動提交功能。同時,應(yīng)用程序一定不要對 XA 連接調(diào)用 java.sql.Connection.commit()
或者 java.sql.Connection.rollback()
。相反,應(yīng)用程序應(yīng)該使用 UserTransaction.begin()、
UserTransaction.commit()
和 serTransaction.rollback()
。
選擇最好的方式
我們討論了如何用 JDBC 和 JTA 界定事務(wù)。每一種方式都有其優(yōu)點,您需要決定哪一種最適合于您的應(yīng)用程序。
在最近的許多項目中,我們小組是用 JDBC API 進事務(wù)界定來構(gòu)建 DAO 類的。這些 DAO 類可以總結(jié)如下:
- 事務(wù)界定代碼嵌入在 DAO 類中。
- DAO 類使用 JDBC API 進行事務(wù)界定。
- 調(diào)用者不能界定事務(wù)。
- 事務(wù)范圍局限于單個 JDBC 連接。
JDBC 事務(wù)并不總是適合復(fù)雜的企業(yè)應(yīng)用程序。如果您的事務(wù)要跨越多個 DAO 或者多個數(shù)據(jù)庫,那么下列實現(xiàn)策略也許更合適:
- 事務(wù)用 JTA 界定。
- 事務(wù)界定代碼從 DAO 中分離出來。
- 調(diào)用者負責(zé)界定事務(wù)。
- DAO 加入一個全局事務(wù)。
JDBC 方式由于其簡單性而具有吸引力,JTA 方式提供了更大的靈活性。您所選擇的實現(xiàn)將取決于應(yīng)用程序的特定需求。
日志記錄和 DAO
一個良好實現(xiàn)的 DAO 類將使用日志記錄來捕捉有關(guān)其運行時行為的細節(jié)。您可以選擇記錄異常、配置信息、連接狀態(tài)、JDBC 驅(qū)動程序元數(shù)據(jù)、或者查詢參數(shù)。日志對于開發(fā)的所有階段都很有用。我經(jīng)常在開發(fā)時、測試時和生產(chǎn)中分析應(yīng)用程序日志。
在本節(jié),我將展示一個顯示如何將 Jakarta Commons Logging 加入到 DAO 中的代碼示例。在這之前,讓我們回顧一下一些基本知識。
選擇日志庫
許多開發(fā)人員使用一種原始格式進行日志記錄: System.out.println
和 System.err.println
。 Println
語句速度快且使用方便,但是它們沒有提供全功能的日志記錄系統(tǒng)所具有的功能。表 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 |
Jakarta Commons Logging 可以與 java.util.logging
或者 Jakarta Log4j 一同使用。Commons Logging 是一個日志抽象層,它隔離了應(yīng)用程序與底層日志實現(xiàn)。使用 Commons Logging,您可以通過改變配置文件更換底層日志實現(xiàn)。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 ...
}
}
}
|
日志記錄是所有任務(wù)關(guān)鍵型應(yīng)用程序的重要部分。如果在 DAO 中遇到故障,那么日志通常可以提供判斷出錯位置的最好信息。將日志加入到 DAO 可以保證您有機會進行調(diào)試和故障排除。
DAO 中的異常處理
我們討論過了事務(wù)界定和日志,現(xiàn)在對于如何在數(shù)據(jù)訪問對象上應(yīng)用它們有了更深入的理解。我們的第三個和最后一個討論議題是異常處理。遵從幾個簡單的異常處理指導(dǎo)可以使您的 DAO 更容易使用、更健壯及更易于維護。
在實現(xiàn) DAO 模式時,考慮以下問題:
- DAO 的公共接口中的方法是否拋出檢查過的異常?
- 如果是的話,拋出何種檢查過的異常?
- 在 DAO 實現(xiàn)類中如何處理異常?
在使用 DAO 模式的過程中,我們的小組開發(fā)了一些處理異常的原則。遵從這些原則可以極大地改進您的 DAO:
- DAO 方法應(yīng)該拋出有意義的異常。
- DAO 方法不應(yīng)該拋出
java.lang.Exception
。 java.lang.Exception
太一般化了。它不傳遞關(guān)于底層問題的任何信息。
- DAO 方法不應(yīng)該拋出
java.sql.SQLException
。SQLException 是一個低級別的 JDBC 異常。一個 DAO 應(yīng)該力爭封裝 JDBC 而不是將 JDBC 公開給應(yīng)用程序的其余部分。
- 只有在可以合理地預(yù)期調(diào)用者可以處理異常時,DAO 接口中的方法才應(yīng)該拋出檢查過的異常。如果調(diào)用者不能以有意義的方式處理這個異常,那么考慮拋出一個未檢查的(運行時)異常。
- 如果數(shù)據(jù)訪問代碼捕獲了一個異常,不要忽略它。忽略捕獲的異常的 DAO 是很難進行故障診斷的。
- 使用鏈接的異常將低級別的異常轉(zhuǎn)化為高級別的異常。
- 考慮定義標(biāo)準 DAO 異常類。Spring Framework (參閱 參考資料)提供了很好的一套預(yù)定義的 DAO 異常類。
有關(guān)異常和異常處理技術(shù)的更多信息參閱 參考資料。
實現(xiàn)實例: MovieDAO
MovieDAO
是一個展示本文中討論的所有技術(shù)的 DAO:事務(wù)界定、日志和異常處理。您可以在 參考資料一節(jié)中找到 MovieDAO
源代碼。代碼分為三個包:
-
daoexamples.exception
-
daoexamples.movie
-
daoexamples.moviedemo
DAO 模式的這個實現(xiàn)包含下面列出的類和接口:
-
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 的數(shù)據(jù)操作。這個接口有五個方法,如下所示:
-
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
接口的兩個實現(xiàn)。每一個實現(xiàn)使用一種不同的方式進行事務(wù)界定,如表 3 所示:
表 3. MovieDAO 實現(xiàn)
|
MovieDAOImpl
|
MovieDAOImplJTA
|
實現(xiàn) MovieDAO 接口? |
是 |
是 |
通過 JNDI 獲得 DataSource? |
是 |
是 |
從 DataSource 獲得 java.sql.Connection 對象? |
是 |
是 |
DAO 在內(nèi)部界定事務(wù)? |
是 |
否 |
使用 JDBC 事務(wù)? |
是 |
否 |
使用一個 XA DataSource? |
否 |
是 |
參與 JTA 事務(wù)? |
否 |
是 |
MovieDAO 演示應(yīng)用程序
這個演示應(yīng)用程序是一個名為 daoexamples.moviedemo.DemoServlet
的 servlet 類。 DemoServlet
使用這兩個 Movie DAO 查詢和更新表中的電影數(shù)據(jù)。
這個 servlet 展示了如何將支持 JTA 的 MovieDAO
和 Java 消息服務(wù)(Java Message Service)結(jié)合到一個事務(wù)中,如清單 8 所示。
清單 8. 將 MovieDAO 和 JMS 代碼結(jié)合到一個事務(wù)中
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();
|