天天看點

Java中事務的處理

原文連結:http://zhenchengchagangzi.iteye.com/blog/1159493

2013.8.29号在家時阿裡巴巴的電話面試裡面問了一個關于java 事務的問題,當時隻知道跟資料庫有關,然後依稀記得commit,rollback什麼的 ,具體就不知道了,這篇文章關于java事務講的很詳細,是以轉載

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對象選擇最好的事務劃分政策。對DO中的事務劃分有兩個主要的政策。一種方法是使用DAO承擔事務劃分的責任;另一種是延期性事務,它把事務劃分到調用DAO對象的方法中。如果你選擇前者,你将要在DAO類中嵌入事務代碼。如果你選擇後者,事務代碼将被寫在DAO類的 外部。我們将使用簡單的代碼執行個體來更好的了解這兩種方法是怎樣工作的。 

  執行個體1展示了一個帶有兩種資料操作的DAO:建立(create)和更新(update): 

public void createWarehouseProfile(WHProfile profile); 

public void updateWarehouseStatus(WHIdentifier id, StatusInfo status); 

  執行個體2展示了一個簡單的事務,事務劃分代碼是在DAO類的外部。注意:在這個例子中的調用者把多個DAO操作組合到這個事務中。 

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顯示了在一個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的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;User=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;user=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);"); 

         //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(); 

    } 

}