java的事務處理,如果對數據庫進行多次操作,每一次的執行或步驟都是一個事務.
如果數據庫操作在某一步沒有執行或出現異常而導致事務失敗,這樣有的事務被執行有的就沒有被執行,從而就有了事務的回滾,取消先前的操作.....
注:在Java中使用事務處理,首先要求數據庫支持事務。如使用MySQL的事務功能,就要求MySQL的表類型為Innodb才支持事務。否則,在Java程序中做了commit或rollback,但在數據庫中根本不能生效。
JavaBean中使用JDBC方式進行事務處理
public int delete(int sID) { dbc = new DataBaseConnection(); Connection con = dbc.getConnection(); try { con.setAutoCommit(false); // 更改JDBC事務的默認提交方式 dbc.executeUpdate("delete from xiao where ID=" + sID); dbc.executeUpdate("delete from xiao_content where ID=" + sID); dbc.executeUpdate("delete from xiao_affix where bylawid=" + sID); con.commit(); //提交JDBC事務 con.setAutoCommit(true); // 恢復JDBC事務的默認提交方式 dbc.close(); return 1; } catch (Exception exc) { con.rollBack(); //回滾JDBC事務 exc.printStackTrace(); dbc.close(); return -1; } }
在數據庫操作中,一項事務是指由一條或多條對數據庫更新的sql語句所組成的一個不可分割的工作單元。只有當事務中的所有操作都正常完成了,整個事務才能被提交到數據庫,如果有一項操作沒有完成,就必須撤消整個事務。 例如在銀行的轉帳事務中,假定張三從自己的帳號上把1000元轉到李四的帳號上,相關的sql語句如下:
update account set monery=monery-1000 where name='zhangsan' update account set monery=monery+1000 where name='lisi' 這個兩條語句必須作為一個完成的事務來處理。只有當兩條都成功執行了,才能提交這個事務。如果有一句失敗,整個事務必須撤消。
在connection類中提供了3個控制事務的方法:
(1) setAutoCommit(Boolean autoCommit):設置是否自動提交事務;
(2) commit();提交事務;
(3) rollback();撤消事務;
在jdbc api中,默認的情況為自動提交事務,也就是說,每一條對數據庫的更新的sql語句代表一項事務,操作成功后,系統自動調用commit()來提交,否則將調用rollback()來撤消事務。
在jdbc api中,可以通過調用setAutoCommit(false) 來禁止自動提交事務。然后就可以把多條更新數據庫的sql語句做為一個事務,在所有操作完成之后,調用commit()來進行整體提交。倘若其中一項 sql操作失敗,就不會執行commit()方法,而是產生相應的sqlexception,此時就可以捕獲異常代碼塊中調用rollback()方法撤 消事務。
事務處理是企業應用需要解決的最主要的問題之一。J2EE通過JTA提供了完整的事務管理能力,包括多個事務性資源的管理能力。但是大部分應用都是運行在單一的事務性資源之上(一個數據庫),他們並不需要全局性的事務服務。本地事務服務已然足夠(比如JDBC事務管理)。
本文並不討論應該采用何種事務處理方式,主要目的是討論如何更為優雅地設計事務服務。僅以JDBC事務處理為例。涉及到的DAO,Factory,Proxy,Decorator等模式概念,請閱讀相關資料。 也許你聽說過,事務處理應該做在service層,也許你也正這樣做,但是否知道為什么這樣做?為什么不放在DAO層做事務處理。顯而易見的原因是業務層 接口的每一個方法有時候都是一個業務用例(User Case),它需要調用不同的DAO對象來完成一個業務方法。比如簡單地以網上書店購書最后的確定定單為例,業務方法首先是調用BookDAO對象(一般 是通過DAO工廠產生),BookDAO判斷是否還有庫存余量,取得該書的價格信息等,然后調用 CustomerDAO從帳戶扣除相應的費用以及記錄信息,然后是其他服務(通知管理員等)。簡化業務流程大概如此: 注意,我們的例子忽略了連接的處理,只要保證同一個線程內取的是相同的連接即可(可用ThreadLocal實現):
首先是業務接口,針對接口,而不是針對類編程:
public interface BookStoreManager{ public boolean buyBook(String bookId,int quantity)throws SystemException; ....其他業務方法 }
接下來就是業務接口的實現類??業務對象:
public class BookStoreManagerImpl implements BookStoreManager{ public boolean buyBook(String bookId)throws SystemException{ Connection conn=ConnectionManager.getConnection(); //獲取數據庫連接 boolean b=false; try{ conn.setAutoCommit(false); //取消自動提交 BookDAO bookDAO=DAOFactory.getBookDAO(); CustomerDAO customerDAO=DAOFactory.getCustomerDAO(); //嘗試從庫存中取書 if(BookDAO.reduceInventory(conn,bookId,quantity)){ BigDecimal price=BookDAO.getPrice(bookId); //取價格 //從客戶帳戶中扣除price*quantity的費用 b=CustomerDAO.reduceAccount(conn,price.multiply(new BigDecimal(quantity)); ....
其他業務方法,如通知管理員,生成定單等. ... conn.commit(); //提交事務 conn.setAutoCommit(true); } }catch(SQLException e){ conn.rollback(); //出現異常,回滾事務 con.setAutoCommit(true); e.printStackTrace(); throws new SystemException(e); } return b; } }
然后是業務代表工廠:
public final class ManagerFactory { public static BookStoreManager getBookStoreManager() { return new BookStoreManagerImpl(); } }
這樣的設計非常適合於DAO中的簡單活動,我們項目中的一個小系統也是采用這樣的設計方案,但是它不適合於更大規模的應用。
首先,你有沒有聞到代碼重復的 bad smell?每次都要設置AutoCommit為false,然后提交,出現異常回滾,包裝異常拋到上層,寫多了不煩才怪,那能不能消除呢?
其次,業務代 表對象現在知道它內部事務管理的所有的細節,這與我們設計業務代表對象的初衷不符。對於業務代表對象來說,了解一個與事務有關的業務約束是相當恰當的,但 是讓它負責來實現它們就不太恰當了。
再次,你是否想過嵌套業務對象的場景?業務代表對象之間的互相調用,層層嵌套,此時你又如何處理呢?你要知道按我們現 在的方式,每個業務方法都處於各自獨立的事務上下文當中(Transaction Context),互相調用形成了嵌套事務,此時你又該如何處理?也許辦法就是重新寫一遍,把不同的業務方法集中成一個巨無霸包裝在一個事務上下文中。
我們有更為優雅的設計來解決這類問題,如果我們把Transaction Context的控制交給一個被業務代表對象、DAO和其他Component所共知的外部對象。當業務代表對象的某個方法需要事務管理時,它提示此外部 對象它希望開始一個事務,外部對象獲取一個連接並且開始數據庫事務。也就是將事務控制從service層抽離,當 web層調用service層的某個業務代表對象時,返回的是一個經過Transaction Context外部對象包裝(或者說代理)的業務對象。此代理對象將請求發送給原始業務代表對象,但是對其中的業務方法進行事務控制。那么,我們如何實現 此效果呢?答案是JDK1.3引進的動態代理技術。動態代理技術只能代理接口,這也是為什么我們需要業務接口BookStoreManager的原因。
首先,我們引入這個Transaction Context外部對象,它的代碼其實很簡單,如果不了解動態代理技術的請先閱讀其他資料。
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.sql.Connection; import com.strutslet.demo.service.SystemException; public final class TransactionWrapper { /** * 裝飾原始的業務代表對象,返回一個與業務代表對象有相同接口的代理對象 */ public static Object decorate(Object delegate) { return Proxy.newProxyInstance(delegate.getClass().getClassLoader(), delegate.getClass().getInterfaces(), new XAWrapperHandler( delegate)); } //動態代理技術 static final class XAWrapperHandler implements InvocationHandler { private final Object delegate; XAWrapperHandler(Object delegate) { this.delegate = delegate; } //簡單起見,包裝業務代表對象所有的業務方法 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object result = null; Connection con = ConnectionManager.getConnection(); try { //開始一個事務 con.setAutoCommit(false); //調用原始業務對象的業務方法 result = method.invoke(delegate, args); con.commit(); //提交事務 con.setAutoCommit(true); } catch (Throwable t) { //回滾 con.rollback(); con.setAutoCommit(true); throw new SystemException(t); } return result; } } }
正如我們所見,此對象只不過把業務對象需要事務控制的業務方法中的事務控制部分抽取出來而已。
請注意,業務代表對象內部調用自身的方法將不會開始新的事務,因為這些調用不會傳給代理對象。
如此,我們去除了代表重復的味道。此時,我們的業務代表對象修改成:
public class BookStoreManagerImpl implements BookStoreManager { public boolean buyBook(String bookId)throws SystemException{ Connection conn=ConnectionManager.getConnection();// 獲取數據庫連接 boolean b=false; try{ BookDAO bookDAO=DAOFactory.getBookDAO(); CustomerDAO customerDAO=DAOFactory.getCustomerDAO(); // 嘗試從庫存中取書 if(BookDAO.reduceInventory(conn,bookId,quantity)){ BigDecimal price=BookDAO.getPrice(bookId); // 取價格 // 從客戶帳戶中扣除price*quantity的費用 b= CustomerDAO.reduceAccount(conn,price.multiply(new BigDecimal(quantity)); .... 其他業務方法,如通知管理員,生成定單等. ... } }catch(SQLException e){ throws new SystemException(e); } return b; } .... }
可以看到,此時的業務代表對象專注於實現業務邏輯,它不再關心事務控制細節,把它們全部委托給了外部對象。業務代表工廠也修改一下,讓它返回兩種類型的業務代表對象:
public final class ManagerFactory { //返回一個被包裝的對象,有事務控制能力 public static BookStoreManager getBookStoreManagerTrans() { return (BookStoreManager) TransactionWrapper .decorate(new BookStoreManagerImpl()); } //原始版本 public static BookStoreManager getBookStoreManager() { return new BookStoreManagerImpl(); } ...... }
我們在業務代表工廠上提供了兩種不同的對象生成方法:一個用於創建被包裝的對象,它會為每次方法調用創建一個新的事務;另外一個用於創建未被包裝的版本,它用於加入到已有的事務(比如其他業務代表對象的業務方法),解決了嵌套業務代表對象的問題。
我們的設計還不夠優雅,比如我們默認所有的業務代表對象的方法調用都將被包裝在一個Transaction Context。可事實是很多方法也許並不需要與數據庫打交道,如果我們能配置哪些方法需要事務聲明,哪些不需要事務管理就更完美了。解決辦法也很簡單, 一個XML配置文件來配置這些,調用時判斷即可。說到這里,了解spring的大概都會意識到這不正是聲明式事務控制嗎?正是如此,事務控制就是AOP的 一種服務,spring的聲明式事務管理是通過AOP實現的。AOP的實現方式包括:動態代理技術,字節碼生成技術(如CGLIB庫),java代碼生成 (早期EJB采用),修改類裝載器以及源代碼級別的代碼混合織入(aspectj)等。我們這里就是利用了動態代理技術,只能對接口代理;對類的動態代理 可以使用cglib庫簡單事務的概念
我不想從原理上說明什么是事務,應為那太枯燥了。我只想從一個簡單的例子來說明什么是事務。
例如我們有一個訂單庫存管理系統,每一次生成訂單的同時我們都要消減庫存。通常來說訂單和庫存在數據庫里是分兩張表來保存的:訂單表,庫存表。每一次我們追加一個訂單實際上需要兩步操作:在訂單表中插入一條數據,同時修改庫存的數據。
這樣問題來了,例如我們需要一個單位為10的訂單,庫存中有30件,理想的操作是我們在訂單表中插入了一條單位為10的訂單,之后將庫存表中的數據修 改為20。但是有些時候事情並不是總是按照你的想法發生,例如:在你修改庫存的時候,數據庫突然由於莫名其妙的原因無法連接上了。也就是說庫存更新失敗 了。但是訂單已經產生了,那么怎么辦呢?沒辦法,只有手動的修改。所以最好的方式是將訂單插入的操作和庫存修改的操作綁定在一起,必須同時成功或者什么都 不做。這就是事務。
Java如何處理事務呢? 我們從java.sql.Connection說起,Connection表示了一個和數據庫的鏈接,可以通過Connection來對數據庫操作。 在通常情況是Connection的屬性是自動提交的,也就是說每次的操作真的更新了數據庫,真的無法回退了。針對上述的例子,一旦庫存更新失敗了,訂單 無法回退,因為訂單真的插入到了數據庫中。這並不是我們希望的。
我們希望的是:看起來成功了,但是沒有真的操作數據庫,知道我想讓他真的發生。可以通過Connection的setAutoCommit (false)讓Connection不自動提交你的數據,除非你真的想提交。那么如何讓操作真的發生呢?可以使用Connection的commit方 法。如何讓操作回退呢?使用rollback方法。
例如:
try{ Connection conn = getConnection(); // 不管如何我們得到了鏈接 conn.setAutoCommit(false); // 插入訂單 // 修改庫存 conn.commit(); // 成功的情況下,提交更新。 } catch(SQLException ex) { conn.rollback(); // 失敗的情況下,回滾所有的操作 } finally { conn.close(); }
這里有一點非常重要,事務是基於數據庫鏈接的。所以在但數據庫的情況下,事務操作很簡單。
那么如果表分布在兩個不同的數據庫中呢?
例如訂單表在訂單庫中,庫存表在庫存庫中,那么我們如何處理這樣的事務呢?
需要注意,提交也可以遇到錯誤呀!
try{ Connection conn1 = getConnection1(); Connection conn2 = getConnection2(); // 基於conn1做插入操作 // 基於conn2做更新操作 try{ conn1.commit() } catch(SQLExcetion ) { conn1.rollback(); } try { conn2.commit(); } catch(SQLException ) { conn2.rollbakc(); // 保證肯定刪除剛才插入的訂單。 } } catch(SQLException ex) { // 如果插入失敗, conn1.rollback // 如果更新失敗, conn1.rollback && conn2.rollback } finally { conn1.close(); conn2.close(); }
看看上述的代碼就知道,其實操作非常的復雜,甚至:保證肯定刪除剛才插入的訂單根本無法保證。
在上述情況下的事務可以稱之為分布式事務,通過上述的代碼中事務同時提交處理的部分我們可以得出,要想處理分布式事務,必須有獨立於數據庫的第三方的事務處理組件。
幸運的是通常情況下,JavaEE兼容的應用服務器,例如:Weblogic,Websphere,JBoss,Glassfish等都有這種分布式事務處理的組件。
如何使用應用服務器的分布式事務管理器處理分布式事務?
以galssfish為例
1 建立對應兩個數據庫的XA(javax.sql.XADataSource)類型的數據源。
2 使用UserTransaction來保證分布式事務。
try{ Connection conn1 = datasource1.getConnection(); Connection conn2 = datasource2.getConnection(); UserTransaction ut = getUserTransaction(); ut.begin(); // 插入訂單 // 修改庫存 ut.commit(); // 成功的情況下,提交更新。 } catch(SQLException ex) { ut.rollback(); // 失敗的情況下,回滾所有的操作 } finally { conn.close(); } 如何獲取UserTransaction呢?可以使用如下方法 UserTransaction tx = (UserTransaction) ctx.lookup("jndi/UserTransaction");
J2EE開發人員使用數據訪問對象(DAO)設計模式把底層的數據訪問邏輯和高層的商務邏輯分開。實現DAO模式能夠更加專注於編寫數據訪問代碼。這篇文章中,Java開發人員Sean C. Sullivan從三個方面討論DAO編程的結構特征:事務划分,異常處理,日志記錄。
在最近的18個月,我和一個優秀的軟件開發團隊一起工作,開發定制基於WEB的供應鏈管理應用程序.我們的應用程序訪問廣泛的持久層數據,包括出貨狀態,供應鏈制度,庫存,貨物發運,項目管理數據,和用戶屬性等.我們使用JDBC API連接我們公司的各種數據庫平台,並且在整個應用程序中應用了DAO設計模式. 通過在整個應用程序中應用數據訪問對象(DAO)設計模式使我們能夠把底層的數據訪問邏輯和上層的商務邏輯分開.我們為每個數據源創建了提供CRUD(創建,讀取,更新,刪除)操作的DAO類. 在本文中,我將向你介紹DAO的實現策略以及創建更好的DAO類的技術.我會明確的介紹日志記錄,異常處理,和事務划分三項技術.你將學在你的DAO類中怎樣把這三種技術結合在一起.這篇文章假設你熟悉JDBC API,SQL和關系性數據庫編程.
我們先來回顧一下DAO設計模式和數據訪問對象.
DAO基礎
DAO模式是標准的J2EE設計模式之一.開發人員使用這個模式把底層的數據訪問操作和上層的商務邏輯分開.一個典型的DAO實現有下列幾個組件:
1. 一個DAO工廠類;
2. 一個DAO接口;
3. 一個實現DAO接口的具體類;
4. 數據傳遞對象(有些時候叫做值對象).
具體的DAO類包含了從特定的數據源訪問數據的邏輯。在下面的這段中你將學到設計和實現數據訪問對象的技術。
事務划分:
關於DAO要記住的一件重要事情是它們是事務性對象。每個被DAO執行的操作(象創建,更新、或刪除數據)都是和事務相關聯的。同樣的,事務划分(transaction demarcation)的概念是特別重要的。
事務划分是在事務界定定義中的方式。J2EE規范為事務划分描述了兩種模式:編程性事務(programmatic)和聲明性事務(declarative)。下表是對這兩種模式的拆分:
聲明性事務划分 |
編程性事務划分 |
程序員使用EJB的布署描述符聲明事務屬性 |
程序員擔負編寫事務邏輯代碼的責任。 |
運行時環境(EJB容器)使用這些屬性來自動的管理事務。 |
應用程序通過一個API接口來控制事務。 |
我將把注意力集中的編程性事務划分上。
象前面的介紹一樣,DAOs是一些事務對象。一個典型的DAO要執行象創建、更新、和刪除這的事務性操作。在設計一個DAO時,首先要問自己如下問題:
1、 事務將怎樣開始?
2、 事務將怎樣結束?
3、 那個對象將承擔起動一個事務的責任?
4、 那個對象將承擔結束一個事務的責任?
5、 DAO應該承擔起動和結束事務的責任?
6、 應用程序需要交叉訪問多個DAO嗎?
7、 一個事務包含一個DAO還是多個DAO?
8、 一個DAO包含其它的DAO中的方法嗎?
回答這些問題將有助於你為DAO對象選擇最好的事務划分策略。對ADO中的事務划分有兩個主要的策略。一種方法是使用DAO承擔事務划分的責任;另一 種是延期性事務,它把事務划分到調用DAO對象的方法中。如果你選擇前者,你將要在DAO類中嵌入事務代碼。如果你選擇后者,事務代碼將被寫在DAO類的 外部。我們將使用簡單的代碼實例來更好的理解這兩種方法是怎樣工作的。
實例1展示了一個帶有兩種數據操作的DAO:創建(create)和更新(update):
public void createWarehouseProfile(WHProfile profile); public void updateWarehouseStatus(WHIdentifier id, StatusInfo status);
|
實例2展示了一個簡單的事務,事務划分代碼是在DAO類的外部。注意:在這個例子中的調用者把多個DOA操作組合到這個事務中。
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(JTA)來實現事務的划分。JDBC事務划分比JTA事務划分簡單,但是JTA提供了更好的靈活性。在下面的這段中,我們會進一步的看事務划分機制。
使用JDBC的事務划分
JDBC事務是使用Connection對象來控制的。JDBC的連接接口(java.sql.Connection)提供了兩種事務模式:自動提交和手動提交。Java.sql.Connection為控制事務提供了下列方法:
.public void setAutoCommit(Boolean) .public Boolean getAutoCommit() .public void commit() .public void rollback()
|
實例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。
JTA概要介紹
Java事務API(JTA;Java Transaction API)和它的同胞Java事務服務(JTS;Java Transaction Service),為J2EE平台提供了分布式事務服務。一個分布式事務(distributed transaction)包括一個事務管理器(transaction manager)和一個或多個資源管理器(resource manager)。一個資源管理器(resource manager)是任意類型的持久化數據存儲。事務管理器(transaction manager)承擔着所有事務參與單元者的相互通訊的責任。下車站顯示了事務管理器和資源管理的間的關系。
JTA事務比JDBC事務更強大。一個JTA事務可以有多個參與者,而一個JDBC事務則被限定在一個單一的數據庫連接。下列任一個Java平台的組件都可以參與到一個JTA事務中:
.JDBC連接
.JDO PersistenceManager 對象
.JMS 隊列
.JMS 主題
.企業JavaBeans(EJB)
.一個用J2EE Connector Architecture 規范編譯的資源分配器。
使用JTA的事務划分
要用JTA來划分一個事務,應用程序調用javax.transaction.UserTransaction接口中的方法。示例4顯示了一個典型的JNDI搜索的UseTransaction對象。
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 void getStatus() .public void setRollbackOnly() .public void setTransactionTimeout(int)
|
應用程序調用begin()來起動事務,即可調用commit()也可以調用rollback()來結束事務。
使用JTA和JDBC
開發人員經常使用JDBC來作為DAO類中的底層數據操作。如果計划使用JTA來划分事務,你將需要一個實現了javax.sql.XADataSource,javax.sql.XAConnection和javax.sql.XAResource接口JDBC的驅動。實現了這些接口的驅動將有能力參與到JTA事務中。一個XADataSource對象是一個XAConnection對象的工廠。XAConnections是參與到JTA事務中的連接。
你需要使用應用程序服務器管理工具來建立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()和UserTransaction.rollback().
選擇最好的方法
我們已經討論了JDBC和JTA是怎樣划分事務的。每一種方法都有它的優點,回此你需要決定為你的應用程序選擇一個最適應的方法。 在我們團隊許多最近的對於事務划分的項目中使用JDBC API來創建DAO類。這DAO類總結如下:
.事務划分代碼被嵌入到DAO類內部
.DAO類使用JDBC API來進行事務划分
.調用者沒有划分事務的方法
.事務范圍被限定在一個單一的JDBC連接
JDBC事務對復雜的企業應用程序不總是有效的。如果你的事務將跨越多個DAO對象或多個數據庫,那么下面的實現策略可能會更恰當:
.用JTA對事務進行划分
.事務划分代碼被DAO分開
.調用者承擔划分事務的責任
.DAO參與一個全局的事務中
JDBC方法由於它的簡易性而具有吸引力,JTA方法提供了更多靈活性。你選擇什么樣的實現將依賴於你的應用程序的特定需求。
日志記錄和DAO
一個好的DAO實現類將使用日志記錄來捕獲有關它在運行時的行為細節。你可以選擇記錄異常、配置信息、連接狀態、JDBC驅動程序的元數據或查詢參數。日志對開發整個階段都是有益的。我經常檢查應用程序在開發期間、測試期間和產品中的日志記錄。
在這段中,我們將展現一段如何把Jakarta Commaons Logging結合中一個DAO中的例子。在我們開始之前,讓我們先回顧一些基礎知識。
選擇一個日志例庫
許多開發人員使用的基本日志形式是:System.out.println和System.err.println.Println語句。這種形式快捷方便,但它們不能提供一個完整的日志系統的的能力。下表列出了Java平台的日志類庫:
日志類庫 |
開源嗎? |
URL |
Java.util.logging |
否 |
http://java.sun.com/j2ee |
Jakarta Log4j |
是 |
http://hajarta.apache.org/log4j/ |
Jakarta Commons Logging |
是 |
http:/Jakarta.apache.org/commons/logging.html |
Java.util.logging是J2SE1.4平台上的標准的API。但是,大多數開發人員都認為Jakarta Log4j提供了更大的功能性和靈活性。Log4j超越java.util.logging的優點之一就是它支持J2SE1.3和J2SE1.4平台。
Jakarta Commons Logging能夠被用於和java.util.loggin或Jakarta Log4j一起工作。Commons Logging是一個把你的應用程序獨立於日志實現的提取層。使用Commons Logging你能夠通過改變一個配置文件來與下面的日志實現來交換數據。Commons Logging被用於JAKARTA Struts1.1和Jakarta HttpClient2.0中。
一個日志示例 示例7顯示了在一個DOA類中怎樣使用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的public接口中的方法將拋出被檢查的異常嗎?
.如果是,將拋出什么樣的檢查性異常?
.在DAO實現類中怎能樣處理異常。
在用DAO模式工作的過程中,我們的團隊為異常處理開發了一組方針。下面的這些方針會很大程度的改善你的DAO:
.DAO方法應該拋出有意義的異常。
.DAO方法不應該拋出java.lang.Exception異常。因為java.lang.Exception太一般化,它不能包含有關潛在問題的所有信息。
.DAO方法不應該拋出java.sql.SQLException異常。SQLException是一個底層的JDBC異常,DAO應用努力封裝JDBC異常而不應該把JDBC異常留給應用程序的其它部分。
.在DAO接口中的方法應該只拋出調用者期望處理的檢查性異常。如果調用者不能用適當的方法來處理異常,考濾拋出不檢查性(運行時run-time)異常。 .如果你的數據訪問代碼捕獲了一個異常,不可要忽略它。忽略捕獲異常的DAO是很處理的。
.使用異常鏈把底層的異常傳遞給高層的某個處理器。
.考濾定義一個標准的DAO異常類。Spring框架提供了一個優秀的預定義的DAO異常類的集合。
看Resources,查看有異常和異常處理技術的更詳細信息。
實現示例:MovieDAO
MoveDAO是一個示范了在這篇文章中所討論的所有技術,包括事務划分、日志記錄和異常處理。你會在Resources段找到MovieDAO的源代碼。它被分下面的三個包:
.daoexamples.exception
.daoexamples.move
.daoexamples.moviedemo
|
這個DAO模式的實現由下面的類和接口組成:
.daoexamples.movie.MovieDAOFactory
.daoexamples.movie.MovieDAO
.daoexamples.movie.MovieDAOImpl
.daoexamples.movie.MovieDAOImplJTA
.daoexamples.movie.Movie
.daoexamples.movie.MovieImple
.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接口的實現。每個實現使用了一個同的事務划分方法,如下表所示:
|
MovieDAOImpl |
MovieDAOImplJTA |
實現了MovieDAO接口嗎? |
Yes |
Yes |
通過JNDI獲得DataSource嗎? |
Yes |
Yes |
從一個DataSource獲得java.sql.Connection對象嗎? |
Yes |
Yes |
DAO界定內部的事務嗎? |
Yes |
No |
使用JDBC事務嗎? |
Yes |
No |
使用一個XA DataSource嗎? |
No |
Yes |
分擔JTA事務嗎? |
No |
Yes |
MovieDAO 示范應用程序
這個示范應用程序是一個叫做daoexamples.moviedemo.DemoServlet.DemoServlet的servlet類,它使用Movie DAO來查詢和更新一個表中的movie數據。
這個servlet示范了把JTA感知的MovieDAO和Java消息服務組合到一個單一的事務中,如示例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兼容的應用程序服務器。
事務處理
信息是任何企事業單位的重要資產,任何企業部門都包含着信息的流入、流出,任何企業部門都控制着某些信息。同時,信息必須在適當的時機傳播給需要的 人。而且,信息還需要安全約束,通常根據信息的類型和內容實施訪問控制。為了保證數據的安全有效和正確可靠,數據庫管理系統(DBMS)必須提供統一的數 據保護功能。
事務是現代數據庫理論中的核心概念之一。如果一組處理步驟或者全部發生或者一步也不執行,我們稱該組處理步驟為一個事務。當所有的步驟像一個操作一樣 被完整地執行,我們稱該事務被提交。由於其中的一部分或多步執行失敗,導致沒有步驟被提交,則事務必須回滾(回到最初的系統狀態)。事務必須服從 ISO/IEC所制定的ACID原則。ACID是原子性(atomicity)、一致性(consistency)、隔離性(isolation)和持久 性(durability)的縮寫。事務的原子性表示事務執行過程中的任何失敗都將導致事務所做的任何修改失效。一致性表示當事務執行失敗時,所有被該事 務影響的數據都應該恢復到事務執行前的狀態。隔離性表示在事務執行過程中對數據的修改,在事務提交之前對其他事務不可見。持久性表示已提交的數據在事務執 行失敗時,數據的狀態都應該正確。
在下面我們列舉一個使用SQL Server數據庫進行事務處理的例子。主表是一個規章制度信息表(bylaw),主要字段有記錄編號、標題、作者、書寫日期等。兩個子表分別是附件表 (bylaw_affix)和文本信息表(bylaw_content)。表結構見圖1所示。bylaw表的記錄編號與bylaw_affix表的記錄編 號、bylaw_content表的記錄編號是對應的,每次對規章制度信息的操作也就是對這三個表的聯合操作。例如要刪除規章制度中的一條記錄,如果不使 用事務,就可能會出現這樣的情況:第一個表中成功刪除后,數據庫突然出現意外狀況,而第二、三個表中的操作沒有完成,這樣,刪除操作並沒有完成,甚至已經 破壞數據庫中的數據。要避免這種情況,就應該使用事務,它的作用是:要么三個表都操作成功,要么都失敗。換句話說,就是保持數據的一致性。所以,為了確保 對數據操作的完整和一致,在程序設計時要充分考慮到事務處理方面的問題。 圖1 示例表結構 Java中的事務處理
一般情況下,J2EE應用服務器支持JDBC事務、JTA(Java Transaction API)事務、容器管理事務。一般情況下,最好不要在程序中同時使用上述三種事務類型,比如在JTA事務中嵌套JDBC事務。第二方面,事務要在盡可能短 的時間內完成,不要在不同方法中實現事務的使用。下面我們列舉兩種事務處理方式。 1、JavaBean中使用JDBC方式進行事務處理 在JDBC中怎樣將多個SQL語句組合成一個事務呢?在JDBC中,打開一個連接對象Connection時,缺省是auto-commit模式,每個 SQL語句都被當作一個事務,即每次執行一個語句,都會自動的得到事務確認。為了能將多個SQL語句組合成一個事務,要將auto-commit模式屏蔽 掉。在auto-commit模式屏蔽掉之后,如果不調用commit()方法,SQL語句不會得到事務確認。在最近一次commit()方法調用之后的 所有SQL會在方法commit()調用時得到確認。
public int delete(int sID) { dbc = new DataBaseConnection(); Connection con = dbc.getConnection(); try { con.setAutoCommit(false);// 更改JDBC事務的默認提交方式 dbc.executeUpdate("delete from bylaw where ID=" + sID); dbc.executeUpdate("delete from bylaw _content where ID=" + sID); dbc.executeUpdate("delete from bylaw _affix where bylawid=" + sID); con.commit();//提交JDBC事務 con.setAutoCommit(true);// 恢復JDBC事務的默認提交方式 dbc.close(); return 1; } catch (Exception exc) { con.rollBack();//回滾JDBC事務 exc.printStackTrace(); dbc.close(); return -1; } }
|
2、SessionBean中的JTA事務 JTA 是事務服務的 J2EE 解決方案。本質上,它是描述事務接口(比如 UserTransaction 接口,開發人員直接使用該接口或者通過 J2EE 容器使用該接口來確保業務邏輯能夠可靠地運行)的 J2EE 模型的一部分。JTA 具有的三個主要的接口分別是 UserTransaction 接口、TransactionManager 接口和 Transaction 接口。這些接口共享公共的事務操作,例如commit() 和 rollback(), 但是也包含特殊的事務操作,例如 suspend(),resume() 和enlist(),它們只出現在特定的接口上,以便在實現中允許一定程度的訪問控制。例如,UserTransaction 能夠執行事務划分和基本的事務操作,而 TransactionManager 能夠執行上下文管理。 應用程序可以調用UserTransaction.begin()方法開始一個事務,該事務與應用程序正在其中運行的當前線程相關聯。底層的事務管理器實 際處理線程與事務之間的關聯。UserTransaction.commit()方法終止與當前線程關聯的事務。UserTransaction.rollback()方法將放棄與當前線程關聯的當前事務。
public int delete(int sID) { DataBaseConnection dbc = null; dbc = new DataBaseConnection(); dbc.getConnection(); UserTransaction transaction = sessionContext.getUserTransaction();//獲得 JTA事務 try { transaction.begin(); //開始JTA事務 dbc.executeUpdate("delete from bylaw where ID=" + sID); dbc.executeUpdate("delete from bylaw _content where ID=" + sID); dbc.executeUpdate("delete from bylaw _affix where bylawid=" + sID); transaction.commit(); //提交JTA事務 dbc.close(); return 1; } catch (Exception exc) { try { transaction.rollback();//JTA事務回滾 } catch (Exception ex) { //JTA事務回滾出錯處理 ex.printStackTrace(); } exc.printStackTrace(); dbc.close(); return -1; } }
|
Can't start a cloned connection while in manual transaction mode錯誤2008-03-13 20:30出現Can't start a cloned connection while in manual transaction mode錯誤,從網上找到原因及解決辦法如下:
原因一般是當你在一個SQL SERVER的JDBC連接上執行多個STATEMENTS的操作,或者是手動事務狀態(AutoCommit=false) 並且使用默認的模式. direct (SelectMethod=direct) 模式.
解決辦法 當你使用手動事務模式時,必須把SelectMethod 屬性的值設置為 Cursor, 或者是確保在你的連接只有一個STATEMENT操作。
修改url
加入SelectMethod=cursor即可
如:
jdbc:microsoft:sqlserver://localhost:1433; DatabaseName=ys; SelectMethod=Cursor;Us er=ys;Password=ys"); package _class; import java.sql.*; import java.util.StringTokenizer; public class connDB{ String sDBDriver = "com.microsoft.jdbc.sqlserver.SQLServerDriver"; String sConnStr = "jdbc:microsoft:sqlserver://127.0.0.1:1433;SelectMethod=cursor;DatabaseName=myDB;u ser=sa;password=river"; Connection cn = null; Statement stmt; boolean autoCommit; private String DbType="MYSQL"; //private String DbType="Oracle"; private connDB(){ init(); } private void init(){ try{ Class.forName(sDBDriver).newInstance(); cn = DriverManager.getConnection(sConnStr); }catch(Exception e){ System.err.println("conndb():連接異常. " + e.getMessage()); } } public static connDB getNewInstance(){ return new connDB(); } //數據綁定的資料好像很少,有空給大家一個例子。在這里只能返回PreparedStatement。 public PreparedStatement getPreparedStmt(String sql) throws SQLException{ PreparedStatement preStmt=null; try{ preStmt = cn.prepareStatement(sql); }catch(SQLException ex){ ex.printStackTrace(); throw ex; } return preStmt; } public void beginTrans() throws SQLException{ try{ autoCommit=cn.getAutoCommit(); cn.setAutoCommit(false); }catch(SQLException ex){ ex.printStackTrace(); System.out.print("beginTrans Errors"); throw ex; } } public void commit()throws SQLException{ try{ cn.commit(); cn.setAutoCommit(autoCommit); }catch(SQLException ex){ ex.printStackTrace(); System.out.print("Commit Errors"); throw ex; } } public void rollback(){ try{ cn.rollback(); cn.setAutoCommit(autoCommit); }catch(SQLException ex){ ex.printStackTrace(); System.out.print("Rollback Errors"); //throw ex; } } public boolean getAutoCommit() throws SQLException{ boolean result=false; try{ result=cn.getAutoCommit(); }catch(SQLException ex){ ex.printStackTrace(); System.out.println("getAutoCommit fail"+ex.getMessage()); throw ex; } return result; } //默認的情況下一次executeQuery(String sql)是一次事務。 //但是可以調用beginTrans(),然后多次executeQuery(String sql), //最后commit()實現多sql的事務處理(注意在這種情況下如果發生違例,千萬不要忘了在catch(){調用rollBack()})。 // public ResultSet executeQuery(String sql) throws SQLException{ ResultSet rs = null; try{ stmt=cn.createStatement(); rs = stmt.executeQuery(sql); } catch(SQLException ex) { ex.printStackTrace(); System.out.println("conndb.executeQuery:"+ex.getMessage()); throw ex; } return rs; } public void executeUpdate(String sql) throws SQLException{ try{ stmt=cn.createStatement(); stmt.executeUpdate(sql); }catch(SQLException ex){ ex.printStackTrace(); System.out.println("conndb.executeUpdate:"+ex.getMessage()); throw ex; } } //Method doBatch 的參數sql,是由一些sql語句拼起來的,用;隔開。可以將許多的sql放在一個事務中,一次執行。 public int[] doBatch(String sql) throws SQLException{ int[] rowResult=null; String a; try{ //boolean autoCommit=cn.getAutoCommit(); //cn.setAutoCommit(false); stmt=cn.createStatement(); StringTokenizer st = new StringTokenizer(sql,";"); while (st.hasMoreTokens()){ a=st.nextToken(); stmt.addBatch(a); } rowResult=stmt.executeBatch(); }catch(SQLException ex){ ex.printStackTrace(); System.out.println("conndb.doBatch:"+ex.getMessage()); throw ex; } return rowResult; } public String getDbType(){ return DbType; } public void close() throws SQLException{ try{ stmt.close(); stmt=null; cn.close(); cn=null; }catch(SQLException ex){ ex.printStackTrace(); System.out.println("Closeing connection fail"+ex.getMessage()); throw ex; } } public static void main(String[] args)throws Exception{ connDB con=connDB.getNewInstance(); System.out.println(con.getDbType()); String sql2="insert into test values('0510321315','李白',80);"; String s1="select *from test"; con.beginTrans(); ResultSet rs=con.executeQuery(s1); con.executeUpdate(sql2);System.out.println("con.executeUpdate(sql2);"); /*try{ int up=s.executeUpdate(sql2); if(up!=0)System.out.println("語句:"+sql2+"插入成功!"); else System.out.println("語句:"+sql2+"插入失敗!"); }catch(SQLException e){System.out.println(e);}*/ //ResultSet rs=s.executeQuery("select *from titles"); con.executeUpdate("delete from test where sno='0510321315'");System.out.println("con.executeUpdate(\"delete from test where sno='0510321315'\");"); con.commit(); while(rs.next()){ System.out.print(rs.getString(1)+"\t"); System.out.print(rs.getString(2)+"\t"); System.out.print(rs.getInt(3)+"\t"); System.out.println(" "); } con.close(); } }