DBUtils处理事务
上一小节,用DBUtils完成了对数据库增删改查的操作,其中使用了QueryRunner类中有参数的构造方法,参数即数据源,这时,框架会自动创建数据库连接,并释放连接。但这是处理一般操作的时候,当要进行事务处理时,连接的创建和释放就要由程序员自己实现了。本小节将结合案例针对用DBUtils框架处理事务进行详细的讲解。
为了讲解DBUtils如何处理事务,接下来向大家模拟银行之间的转账业务。具体步骤如下:
(1) 建立所需的数据表account作为账目记录表,并添加数据,具体语句如下:
USE chapter03;
CREATE TABLE account(
  id int primary key auto_increment,
  name varchar(40),
  money float
); 
INSERT INTO account(name,money) VALUES('a',1000);
INSERT INTO account(name,money) VALUES('b',1000);
上述SQL语句执行成功后,使用SELECT语句查询account表中的数据,SQL语句的执行结果如下:
mysql> select * from account;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | a  | 1000 |
| 2 | b  | 1000 |
+----+------+-------+
2 rows in set (0.03 sec)
(2) 创建实体类Account,具体代码如下:
 1  package cn.itcast.jdbc.example.domain;
 2  public class Account {
 3    private int id;
 4    private String name;
 5    private float money;
 6    public int getId() {
 7       return id;
 8    }
 9    public void setId(int id) {
 10      this.id = id;
 11    }
 12    public String getName() {
 13      return name;
 14    }
 15    public void setName(String name) {
 16      this.name = name;
 17    }
 18    public float getMoney() {
 19      return money;
 20    }
 21    public void setMoney(float money) {
 22      this.money = money;
 23    }
 24 }
创建类JDBCUtils,该类封装了创建连接、开启事务、关闭事务等方法。需要注意的是,请求中的一个事务涉及多个数据库操作,如果这些操作中的Connection 是从连接池获得的话,两个 DAO操作就用到了两个Connection,这样的话是没有办法完成一个事务的。因此,需要借助ThreadLocal类。
ThreadLocal类的作用是在一个线程里记录变量。我们可以生成一个连接放在这个线程中,只要是这个线程中的任何对象都可以共享这个连接,当线程结束后就删除这个连接。这样就保证了一个事务,一个连接。具体实现如例1所示。
例1 JDBCUtils.java
 1  package cn.itcast.jdbc.utils;
 2  import java.sql.Connection;
 3  import java.sql.SQLException;
 4  import javax.sql.DataSource;
 5  import com.mchange.v2.c3p0.ComboPooledDataSource;
 6  public class JDBCUtils {
 7    // 创建一个ThreadLocal 对象,以当前线程作为key
 8    private static ThreadLocal<Connection> threadLocal = 
 9        new ThreadLocal<Connection>();
 10   // 从c3p0-config.xml配置文件中读取默认的数据库配置,生成c3p0数据源
 11   private static DataSource ds = new ComboPooledDataSource();
 12   // 返回数据源对象
 13   public static DataSource getDataSource() {
 14     return ds;
 15   }
 16   // 获取c3p0数据库连接池中的连接对象
 17   public static Connection getConnection() throws SQLException {
 18     Connection conn = threadLocal.get();
 19     if (conn == null) {
 20       conn = ds.getConnection();
 21       threadLocal.set(conn);
 22     }
 23     return conn;
 24   }
 25   // 开启事务
 26   public static void startTransaction() {
 27     try {
 28       // 获得链接
 29       Connection conn = getConnection();
 30       // 开启事务
 31       conn.setAutoCommit(false);
 32     } catch (SQLException e) {
 33       e.printStackTrace();
 34     }
 35   }
 36   // 提交事务
 37   public static void commit() {
 38     try {
 39       // 获得链接
 40       Connection conn = threadLocal.get();
 41       // 提交事务
 42       if (conn != null)
 43         conn.commit();
 44     } catch (SQLException e) {
 45       e.printStackTrace();
 46     }
 47   }
 48   // 回滚事务
 49   public static void rollback() {
 50     try {
 51       // 获得链接
 52       Connection conn = threadLocal.get();
 53       // 回滚事务
 54       if (conn != null)
 55         conn.rollback();
 56     } catch (SQLException e) {
 57       e.printStackTrace();
 58     }
 59   }
 60   // 关闭数据库连接,释放资源
 61   public static void close() {
 62     // 获得链接
 63     Connection conn = threadLocal.get();
 64     // 关闭事务
 65     if (conn != null) {
 66       try {
 67         conn.close();
 68       } catch (SQLException e) {
 69         e.printStackTrace();
 70       } finally {
 71         // 从集合中移除当前绑定的连接
 72         threadLocal.remove();
 73         conn = null;
 74       }
 75     }
 76   }
 77 }
在例1中,可以注意到,在关闭连接时为什么不能直接将conn对象置空,而是先要从集合中移除当前绑定的连接?首先,获得连接是从threadLocal集合中拿出元素的地址复制给conn对象,那么集合中还有指向该连接的变量记住这个对象的地址。ThreadLocal集合为静态集合,所以只要虚拟机不关闭,静态变量就永远不释放。这样就会造成内存泄露。所以要先从集合中移除当前绑定的连接,再将conn对象置空,变为垃圾对象。
(3) 创建类AccountDao,该类封装了转账所需的数据库操作,包括查询用户,转入,转出操作,具体实现代码如例2所示。
例2 AccountDao.java
 1  package cn.itcast.jdbc.example.dao;
 2  import java.sql.Connection;
 3  import java.sql.SQLException;
 4  import org.apache.commons.dbutils.QueryRunner;
 5  import org.apache.commons.dbutils.handlers.BeanHandler;
 6  import cn.itcast.jdbc.example.domain.Account;
 7  import cn.itcast.jdbc.utils.C3p0Utils;
 8  import cn.itcast.jdbc.utils.JDBCUtils;
 9  public class AccountDao {
 10   public Account find(String name) throws SQLException {
 11     QueryRunner runner = new QueryRunner();
 12     Connection conn = JDBCUtils.getConnection();
 13     String sql = "select * from account where name=?";
 14     Account account = (Account) runner.query(conn, sql, new BeanHandler(
 15         Account.class), new Object[] { name });
 16     return account;
 17   }
 18   public void update(Account account) throws SQLException {
 19     QueryRunner runner = new QueryRunner(C3p0Utils.getDataSource());
 20     Connection conn = JDBCUtils.getConnection();
 21     String sql = "update account set money=? where name=?";
 22     runner.update(conn, sql,
 23         new Object[] { account.getMoney(), account.getName() });
 24   }
 25 }
(5)创建类Business,该类包括转账过程的逻辑方法,导入了封装事务操作的JDBCUtils类和封装数据库操作的AccountDao类,完成转账操作。具体代码如下:
 1  package cn.itcast.example;
 1  import java.sql.SQLException;
 2  import cn.itcast.jdbc.example.dao.AccountDao;
 3  import cn.itcast.jdbc.example.domain.Account;
 4  import cn.itcast.jdbc.utils.JDBCUtils;
 5  public class Business {
 6    public static void transfer(String sourceAccountName,
 7          String toAccountName, float money) {
 8      try {
 9        // 开启事务
 10       JDBCUtils.startTransaction();
 11       // 根据用户名查询数据并存入实体类对象中
 12       AccountDao dao = new AccountDao();
 13       Account accountfrom = dao.find(sourceAccountName);
 14       Account accountto = dao.find(toAccountName);
 15       // 完成转账操作
 16       if(money<accountfrom.getMoney()){
 17         accountfrom.setMoney(accountfrom.getMoney()-money);
 18       }else{
 19         System.out.println("转出账户余额不足");
 20       }    
 21       accountto.setMoney(accountto.getMoney()+money);
 22       dao.update(accountfrom);
 23       dao.update(accountto);
 24       // 提交事务
 25       JDBCUtils.commit();
 26       System.out.println("提交成功");
 27     } catch (SQLException e) {
 28       System.out.println("提交失败");
 29       JDBCUtils.rollback();
 30       e.printStackTrace();
 31     } finally {
 32       // 关闭事务
 33       JDBCUtils.close();
 34     }
 35   }
 36   public static void main(String[] args) throws SQLException {
 37     // 调用方法,实现a向b转账200元操作
 38     transfer("a", "b", 200);
 39   }
 40 }
运行类Business,执行结果如图1所示。

图1 运行结果
查询数据库account 表,查询结果如下:
mysql> select * from account;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | a  |  800 |
| 2 | b  | 1200 |
+----+------+-------+
2 rows in set (0.00 sec)
根据查询结果可以看出,转账功能已经成功实现了。也就是说,已经成功演示了DBUtils工具处理事务的整个过程。
