目录
事务
1 service层控制事务
2 service层控制事务失败的原因
3 解决方案一:传递Connection
4 解决方案二:ThreadLocal
事务的封装
1 问题描述
2 完善工具类
3 AccountServiceImpl类代码修改
事务
在JDBC中,获得Connection对象来处理事务--提交或回滚事务--关闭连接。其事务策略是:
- connection.setAutoCommit(false):true等价于1,false等于0
- connection.commit():手动提交事务
- connection.rollback():手动回滚事务
1 service层控制事务
package com.cxyzxc.examples07;
import java.sql.Connection;
import java.sql.SQLException;public class AccountServiceImpl {
/**
* 转账业务
*
* @param fromNo
* 转账人账号
* @param password
* 转账人账号密码
* @param toNo
* 收款人账号
* @param money
* 转账金额
*/
public String transfer(String fromNo, String password, String toNo,
double money) {
String result = "转账失败";
AccountDaoImpl accountDaoImpl = new AccountDaoImpl(); // 创建一个连接对象
Connection connection = null; try {
// 获取连接对象
connection = DBUtils.getConnection();
// 开启事务,关闭事务的自动提交,改为手动提交
connection.setAutoCommit(false);
// 1.验证fromNo账号是否存在
Account fromAccount = accountDaoImpl.select(fromNo);
if (fromAccount == null) {
throw new RuntimeException("卡号不存在");
} // 2.验证fromNo的密码是否正确
if (!fromAccount.getPassword().equals(password)) {
throw new RuntimeException("密码错误");
} // 3.验证余额是否充足
if (fromAccount.getBalance() < money) {
throw new RuntimeException("余额不足");
} // 4.验证toNo账号是否存在
Account toAccount = accountDaoImpl.select(toNo);
if (toAccount == null) {
throw new RuntimeException("对方卡号不存在");
}
// 5.减少fromNo账号的余额
fromAccount.setBalance(fromAccount.getBalance() - money);
accountDaoImpl.update(fromAccount); // 程序出现异常
int num = 10 / 0; // 6.增加toNo账号的余额
toAccount.setBalance(toAccount.getBalance() + money);
accountDaoImpl.update(toAccount);
// 代码执行到这里,说明转账成功,提交事务
connection.commit();
result = "转账成功";
return result;
} catch (Exception e) {
e.printStackTrace();
try {
// 出现异常,回滚整个事务
System.out.println("出现异常,回滚整个事务,转账失败");
connection.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
}finally{
DBUtils.closeAll(connection, null, null);
}
return result;
}
}
```
2 service层控制事务失败的原因
执行这个代码,观察account表中的数据发现,当程序出现异常,转账账号余额减少了,但是收款账户余额没有增加,事务控制失败了。失败的原因是:
- AccountServiceImpl类中的connection连接对象与AccountDaoImpl类中给各个方法里的connection连接对象是不同的connection连接对象。
- AccountServiceImpl类中的connection连接对象控制事务,只能控制AccountServiceImpl类中的事务,不能控制该类之外的类里的事务
3 解决方案一:传递Connection
为了解决AccountServiceImpl类中的connection连接对象与AccountDaoImpl类中给各个方法里的connection连接对象是不同步的问题,可以将Connection对象通过service传递给AccountDaoImpl类中的各个方法
3.1 AccountDaoImpl类代码
AccountDaoImpl类中的每个方法参数列表里都要添加一个Connection类型的参数
package com.cxyzxc.examples08;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;public class AccountDaoImpl {
// 新增:插入一个Account对象到数据库中
public int insert(Account account,Connection connection) {
PreparedStatement preparedStatement = null; String sql = "insert into account values(?,?,?,?)";
try {
preparedStatement = connection.prepareStatement(sql); // 绑定参数
preparedStatement.setString(1, account.getCardNo());
preparedStatement.setString(2, account.getPassword());
preparedStatement.setString(3, account.getName());
preparedStatement.setDouble(4, account.getBalance()); // 执行SQL
int result = preparedStatement.executeUpdate();
return result;
} catch (SQLException e) {
e.printStackTrace();
}
return 0;
} // 删除:根据卡号,删除账号
public int delete(String cardNo,Connection connection) {
PreparedStatement preparedStatement = null; String sql = "delete from account where cardNo = ?;";
try {
preparedStatement = connection.prepareStatement(sql);
// 绑定参数
preparedStatement.setString(1, cardNo);
// 执行SQL
int result = preparedStatement.executeUpdate();
return result;
} catch (SQLException e) {
e.printStackTrace();
}
return 0;
} // 修改
public int update(Account account,Connection connection) {
PreparedStatement preparedStatement = null; String sql = "update account set password = ?,name = ?,balance = ? where cardNo=?;";
try {
preparedStatement = connection.prepareStatement(sql); // 绑定参数
preparedStatement.setString(1, account.getPassword());
preparedStatement.setString(2, account.getName());
preparedStatement.setDouble(3, account.getBalance());
preparedStatement.setString(4, account.getCardNo()); // 执行SQL
int result = preparedStatement.executeUpdate();
return result;
} catch (SQLException e) {
e.printStackTrace();
}
return 0;
} // 查询单个
public Account select(String cardNo,Connection connection) {
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
Account account = null; String sql = "select * from account where cardNo = ?";
try {
preparedStatement = connection.prepareStatement(sql); // 绑定参数
preparedStatement.setString(1, cardNo); // 执行SQL
resultSet = preparedStatement.executeQuery(); if (resultSet.next()) {
String cardNumber = resultSet.getString("cardNo");
String password = resultSet.getString("password");
String name = resultSet.getString("name");
double balance = resultSet.getDouble("balance"); account = new Account(cardNumber, password, name, balance);
} return account;
} catch (SQLException e) {
e.printStackTrace();
}
return null;
} // 查询所有
public List<Account> selectAll(Connection connection) {
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
Account account = null;
List<Account> accountList = new ArrayList<Account>(); String sql = "select * from account;";
try {
preparedStatement = connection.prepareStatement(sql); // 执行SQL
resultSet = preparedStatement.executeQuery(); while (resultSet.next()) {
String cardNumber = resultSet.getString("cardNo");
String password = resultSet.getString("password");
String name = resultSet.getString("name");
double balance = resultSet.getDouble("balance"); account = new Account(cardNumber, password, name, balance);
accountList.add(account);
}
return accountList;
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
}
3.2 AccountServiceImpl类代码
package com.cxyzxc.examples08;
import java.sql.Connection;
import java.sql.SQLException;public class AccountServiceImpl {
/**
* 转账业务
*
* @param fromNo
* 转账人账号
* @param password
* 转账人账号密码
* @param toNo
* 收款人账号
* @param money
* 转账金额
*/
public String transfer(String fromNo, String password, String toNo,
double money) {
String result = "转账失败";
AccountDaoImpl accountDaoImpl = new AccountDaoImpl(); // 创建一个连接对象
Connection connection = null; try {
// 获取连接对象
connection = DBUtils.getConnection();
// 开启事务,关闭事务的自动提交,改为手动提交
connection.setAutoCommit(false);
// 1.验证fromNo账号是否存在
Account fromAccount = accountDaoImpl.select(fromNo,connection);
if (fromAccount == null) {
throw new RuntimeException("卡号不存在");
} // 2.验证fromNo的密码是否正确
if (!fromAccount.getPassword().equals(password)) {
throw new RuntimeException("密码错误");
} // 3.验证余额是否充足
if (fromAccount.getBalance() < money) {
throw new RuntimeException("余额不足");
} // 4.验证toNo账号是否存在
Account toAccount = accountDaoImpl.select(toNo,connection);
if (toAccount == null) {
throw new RuntimeException("对方卡号不存在");
}
// 5.减少fromNo账号的余额
fromAccount.setBalance(fromAccount.getBalance() - money);
accountDaoImpl.update(fromAccount,connection); // 程序出现异常
int num = 10 / 0; // 6.增加toNo账号的余额
toAccount.setBalance(toAccount.getBalance() + money);
accountDaoImpl.update(toAccount,connection);
// 代码执行到这里,说明转账成功,提交事务
connection.commit();
result = "转账成功";
return result;
} catch (Exception e) {
e.printStackTrace();
try {
// 出现异常,回滚整个事务
System.out.println("出现异常,回滚整个事务,转账失败");
connection.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
}finally{
DBUtils.closeAll(connection, null, null);
}
return result;
}
}
3.3 测试
测试发现,这种方法可以解决service层控制事务失败的问题。
3.4 解决方案的弊端
(1)如果使用传递Connection,更容易造成接口污染(BadSmell)。
(2)定义接口是为了更容易更换实现,而将Connection定义在接口中(XxxDao接口,XXXDaoImpl实现XxxDao接口)中,会造成污染当前接口。因为在当前代码中连接对象叫Connection,而在其它数据库连接框架中,连接对象不叫Connection(Mybatis框架中数据库连接对象叫SqlSession,Hibernate框架中数据库连接对象叫session),这时候,你需要重新定义接口,重新传递连接对象。
4 解决方案二:ThreadLocal
(1)在整个线程中(单线程),存储一个共享值(Connection对象)。
(2)线程类中拥有一个类似Map的属性(),以键值对的结构<ThreadLocal对象,值>存储数据。
4.1 ThreadLocal应用
一个线程中所有的操作共享一个ThreadLocal,ThreadLocal里存储的是Connection连接对象,在整个操作流程中任何一个操作环节都可以设置或者获取值。
![](img/13ThreadLocal核心流程.png)
4.2 ThreadLocal代码实现
在DBUtils类中,将当前Connection对象添加到ThreadLocal中。其它类代码保持不变。
```
package com.cxyzxc.examples09;import java.sql.*;
public class DBUtils {
// 声明一个ThreadLocal<Connection>对象用来存储数据库连接对象
private static ThreadLocal<Connection> threadLocal = new ThreadLocal<Connection>(); static {// 类加载,执行一次!
try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
} // 1.获取连接
public static Connection getConnection() {
// 将当前线程中绑定的Connection对象赋值给connection变量
Connection connection = threadLocal.get();
try {
// 如果连接对象为null,则创建一个连接对象
if (connection == null) {
connection = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/java22182", "root",
"123456");
// 将创建的连接对象存储到当前线程中共享
threadLocal.set(connection);
} } catch (SQLException e) {
e.printStackTrace();
}
return connection;
} // 2.释放资源
public static void closeAll(Connection connection, Statement statement,
ResultSet resultSet) {
try {
if (resultSet != null) {
resultSet.close();
}
if (statement != null) {
statement.close();
}
if (connection != null) {
connection.close();
// 将connection从threadLocal中移除
threadLocal.remove();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
```
事务的封装
1 问题描述
(1)XXXDaoImpl类是专门用来操作数据表的,在这个类中只存在对数据表增删改查的方法,没有其它的内容。这是我们需要的。但是在XXXServiceImpl类中,既有业务需求,还有获取数据库连接对象以及释放资源的代码,在XXXServiceImpl类中,应该只有业务逻辑需求,除此,没有其它操作代码,这才是我们需要的。
(2)因此我们需要将对事务的开启、提交、回滚都封装到DBUtils工具类中。业务层调用DBUtils类中的与事务相关的代码即可。
2 完善工具类
package com.cxyzxc.examples10;
import java.sql.*;
public class DBUtils {
// 声明一个ThreadLocal<Connection>对象用来存储数据库连接对象
private static ThreadLocal<Connection> threadLocal = new ThreadLocal<Connection>(); static {// 类加载,执行一次!
try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
} // 1.获取连接
public static Connection getConnection() {
// 将当前线程中绑定的Connection对象赋值给connection变量
Connection connection = threadLocal.get();
try {
// 如果连接对象为null,则创建一个连接对象
if (connection == null) {
connection = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/java22182", "root",
"123456");
// 将创建的连接对象存储到当前线程中共享
threadLocal.set(connection);
} } catch (SQLException e) {
e.printStackTrace();
}
return connection;
} // 2.释放资源
public static void closeAll(Connection connection, Statement statement,
ResultSet resultSet) {
try {
if (resultSet != null) {
resultSet.close();
}
if (statement != null) {
statement.close();
}
if (connection != null) {
connection.close();
// 将connection从threadLocal中移除
threadLocal.remove();
}
} catch (SQLException e) {
e.printStackTrace();
}
} // 3、开启事务
public static void startTransaction() {
Connection connection = null;
try {
connection = getConnection();
connection.setAutoCommit(false);
} catch (SQLException e) {
e.printStackTrace();
}
} // 4、提交事务
public static void commitTransaction() {
Connection connection = getConnection();
try {
connection.commit();
} catch (SQLException e) {
e.printStackTrace();
} finally {
DBUtils.closeAll(connection, null, null);
}
} // 5、回滚事务
public static void rollbackTransaction() {
Connection connection = getConnection();
try {
connection.rollback();
} catch (SQLException e) {
e.printStackTrace();
} finally {
DBUtils.closeAll(connection, null, null);
}
}
}
```
3 AccountServiceImpl类代码修改
package com.cxyzxc.examples10;
public class AccountServiceImpl {
/**
* 转账业务
*
* @param fromNo
* 转账人账号
* @param password
* 转账人账号密码
* @param toNo
* 收款人账号
* @param money
* 转账金额
*/
public String transfer(String fromNo, String password, String toNo,
double money) {
String result = "转账失败";
AccountDaoImpl accountDaoImpl = new AccountDaoImpl(); try {
// 开启事务,关闭事务的自动提交,改为手动提交
DBUtils.startTransaction();
// 1.验证fromNo账号是否存在
Account fromAccount = accountDaoImpl.select(fromNo);
if (fromAccount == null) {
throw new RuntimeException("卡号不存在");
} // 2.验证fromNo的密码是否正确
if (!fromAccount.getPassword().equals(password)) {
throw new RuntimeException("密码错误");
} // 3.验证余额是否充足
if (fromAccount.getBalance() < money) {
throw new RuntimeException("余额不足");
} // 4.验证toNo账号是否存在
Account toAccount = accountDaoImpl.select(toNo);
if (toAccount == null) {
throw new RuntimeException("对方卡号不存在");
}
// 5.减少fromNo账号的余额
fromAccount.setBalance(fromAccount.getBalance() - money);
accountDaoImpl.update(fromAccount); // 程序出现异常
@SuppressWarnings("unused")
int num = 10 / 0; // 6.增加toNo账号的余额
toAccount.setBalance(toAccount.getBalance() + money);
accountDaoImpl.update(toAccount);
// 代码执行到这里,说明转账成功,提交事务
DBUtils.commitTransaction();
result = "转账成功";
return result;
} catch (Exception e) {
e.printStackTrace();
// 出现异常,回滚整个事务
System.out.println("出现异常,回滚整个事务,转账失败");
DBUtils.rollbackTransaction();
}
return result;
}
}