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 規範編譯的資源分配器。
使用J他的事務劃分
要用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(); } }