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对象选择最好的事务划分策略。对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;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);");
        /*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();
           
    }
}

你可能感兴趣的:(java,DAO,sql,jdbc,企业应用)