Data Access Object (DAO) 模式是 J2EE 架構中分離資料存取邏輯與業務邏輯的標準模式。但真正生產級的 DAO 實作,遠不止 CRUD 方法這麼簡單。本文深入探討三大常被忽視卻至關重要的面向:事務界定、異常處理、日誌記錄。
適用於單一資料庫、單一 DAO 操作的簡單場景:
public class JdbcMovieDao implements MovieDao {
private DataSource dataSource;
public void createMovie(Movie movie) {
Connection conn = null;
try {
conn = dataSource.getConnection();
conn.setAutoCommit(false); // 開啟手動提交
PreparedStatement ps = conn.prepareStatement(
"INSERT INTO MOVIES (title, year, rating) VALUES (?, ?, ?)");
ps.setString(1, movie.getTitle());
ps.setString(2, movie.getYear());
ps.setString(3, movie.getRating());
ps.executeUpdate();
conn.commit();
} catch (SQLException e) {
rollbackQuietly(conn);
throw new DaoException("建立電影失敗", e);
} finally {
closeQuietly(conn);
}
}
}
缺點:無法跨 DAO 組合事務,事務範圍侷限於單一 Connection。
適用於跨多 DAO、多資料源、結合 JMS 等資源的企業級場景:
// 業務服務層 / Servlet / EJB
UserTransaction utx = MovieUtil.getUserTransaction();
utx.begin();
try {
Movie batman = dao.createMovie("Batman Reloaded", "2008", "R");
jmsPublisher.publishTextMessage("New movie: " + batman.getTitle());
dao.updateMovie(topgun.getId(), "Top Gun", "1986", "PG-13");
dao.deleteMovie(legallyblonde.getId());
utx.commit();
} catch (Exception e) {
utx.rollback();
throw new ServiceException("業務流程失敗", e);
}
javax.sql.XADataSource、XAConnection、XAResourceconn.commit()/rollback() 或使用 auto-commitUserTransaction.begin/commit/rollback| 方法 | 用途 |
|---|---|
begin() | 啟動新事務 |
commit() | 提交事務(兩階段提交) |
rollback() | 回滾事務 |
setRollbackOnly() | 標記僅能回滾 |
setTransactionTimeout(seconds) | 設定超時時間 |
getStatus() | 獲取事務狀態 |
| 框架 | 開源 | 特點 |
|---|---|---|
| java.util.logging | 否 | JDK 1.4+ 內建,功能較弱 |
| Log4j | 是 | 功能豐富、支援 1.3/1.4、業界標準 |
| Commons Logging | 是 | 抽象層,可切換底層實作,Struts/HttpClient 採用 |
| 特性 | MovieDaoImpl (JDBC) | MovieDaoImplJTA (JTA) |
|---|---|---|
| 事務邊界 | DAO 內部管理 | 調用者管理 (外部) |
| 事務類型 | JDBC 本地事務 | JTA 分布式事務 |
| DataSource | 普通 DataSource | XA DataSource |
| Connection 獲取 | dao 內部 getConnection() | 調用者傳入或 JNDI 查找 |
| 跨 DAO 事務 | 不支援 | 支援 |
| 結合 JMS | 不可靠 | 同一全域事務 |
public class DemoServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
MovieDao dao = MovieDaoFactory.getInstance("jta");
UserTransaction utx = MovieUtil.getUserTransaction();
try {
utx.begin();
Movie batman = dao.create("R", "2008", "Batman Reloaded");
new MessagePublisher().publishTextMessage("I'll be back");
dao.update(topgun.getId(), "PG-13", topgun.getYear(), topgun.getTitle());
dao.delete(legallyblonde.getId());
utx.commit();
resp.getWriter().println("分布式事務提交成功");
} catch (Exception e) {
try { utx.rollback(); } catch (Exception ignored) {}
throw new ServletException("事務失敗", e);
}
}
}
| 應用特徵 | 推薦策略 | 理由 |
|---|---|---|
| 單資料庫、簡單 CRUD、無跨服務調用 | JDBC 事務 (DAO 內部) | 簡單、效能佳、無分布式協調開銷 |
| 多 DAO 組合業務、單資料庫 | JDBC 事務 (Service 層管理) | 傳遞 Connection 或使用 ThreadLocal 綁定 |
| 跨多資料庫、結合 JMS/EJB、需 ACID | JTA (調用者管理) | 唯一支援分布式 ACID 的標準方案 |
| Spring 環境 | @Transactional (聲明式) | 最佳開發體驗,自動處理傳播行為 |
優秀的 DAO 不僅是 SQL 的封裝器,更是事務邊界的守門人、異常語義的翻譯官、系統可觀測性的資料源。根據應用複雜度選擇適當的事務策略、建立領域化的異常體系、貫穿結構化日誌記錄,才能構建經得起生產環境考驗的資料存取層。