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工具处理事务的整个过程。