数据库的事务是一种SQL语句可执行的缓存机制,事务允许我们在失败的情况下,让数据回归到业务之前的状态!
事务使用场景
一个业务涉及多条修改数据库语句
经典的转账案例,转账业务(加钱和减钱)
事务的特性 (ACID)
- 原子性,事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
- 一致性,事务必须使数据库从一个一致性状态变换到另外一个一致性状态。
- 隔离性,并发事务访问的时候事务之间不受其他事务干扰。
- 持久性,事务一旦提交,真的就会改变数据库数据的一种方式。
事务的类型
- 自动提交:每条语句自动存储在一个事务中,执行成功自动提交,执行失败自动回滚(MySQL)
- 列表项目:手动开启事物,添加语句,手动提交或手动回滚即可
在Java中如何使用事务操作?使用try-catch。
通过转账案例演示事务。
代码结构设计
测试类
BankService业务层
储存t_bank表的业务方法!先设计一个转账业务
成员方法:tansfer(String addAccount,String subAccount,int money) 通过方法调用dao类的加钱和减钱的方法
BankDao
存储操作t_bank表的方法!通过Dao方法来调用数据库
add(String account,int money)加钱的jdbc动作
sub(String account,int money)减钱的jdbc动作
让业务层调用Dao层的这两个方法就可
数据库表t_bank
通过测试类调用service层然后调用dao层,dao层调用数据库。但是书写代码的时候先书写dao层
先根据这个结构创建出代码
BankDao类
/*
表的数据库方法存储类
*/
public class BankDao {
/*
加钱的数据库操作方法(具体的JDBC动作)
String account 加钱的符号
int money 加钱的金额
*/
public void add(String account,int money) throws Exception {
//加钱就是所谓的修改
//1.注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//2.获取连接
Connection connection = DriverManager.getConnection("jdbc:mysql:///atguigu?user=root&password=abc123");
//3.编写SQL语句结构
String sql = "update t_bank set money = money + ? where account = ?";
//4.创建statement
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//5.占位符赋值
preparedStatement.setObject(1,money);
preparedStatement.setObject(2,account);
//6.发送sql语句
preparedStatement.executeUpdate();
//7.关闭资源
preparedStatement.close();
connection.close();
System.out.println("加钱成功!");
}
/*
减钱的数据库操作方法(具体的JDBC动作)
String account 减钱的符号
int money 减钱的金额
*/
public void sub(String account,int money) throws Exception{
//加钱就是所谓的修改
//1.注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//2.获取连接
Connection connection = DriverManager.getConnection("jdbc:mysql:///atguigu?user=root&password=abc123");
//3.编写SQL语句结构
String sql = "update t_bank set money = money - ? where account = ?";
//4.创建statement
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//5.占位符赋值
preparedStatement.setObject(1,money);
preparedStatement.setObject(2,account);
//6.发送sql语句
preparedStatement.executeUpdate();
//7.关闭资源
preparedStatement.close();
connection.close();
System.out.println("减钱成功!");
}
}
BankService类
/*
银行卡业务方法,调用Dao方法
*/
public class BankService {
@Test
public void start() throws Exception {
//直接在测试方法中调用Service层
//二狗子 给驴蛋蛋转500
transefer("lvdandan","ergouzi",500);
//现在我们把减钱独立一个事务中,加钱独立在另一个事务中,它们两互不印象,会出现什么问题呢?
}
public void transefer(String addAccount,String subAccount,int money) throws Exception {
//创建Dao类的对象
BankDao bankDao = new BankDao();
//调用里面的两个方法
bankDao.add(addAccount,money);
System.out.println("----------------------");
bankDao.sub(subAccount,money);
}
}
并且我在Service类中书写了一个测试方法,这样等效于书写一个测试类。
这样书写会导致加钱,减钱的方法互相独立,处于两个事务中,我们运行看看会发生什么?
我们不妨多运行几次看看会发生什么。注意,设计表的时候account字段的数据类型是int unsigned不可以为负数.
我们发现在多次转账后,二狗子已经没钱了,程序报错,但是驴蛋蛋加钱成功。
查看数据库
我们会发现确实只有加钱成功,没有扣钱。
原因就是出现在两个事务中
我们之前出现了两个connect连接当然是有一定问题
我们需要调整我们的Service层,因为一个事务是转账,包括了加减钱。事务的开启是在业务层开启的
- 我们同时对add和sub方法提供一个连接,抽取 注册驱动 和 获取链接 到Service类中
- 然后再Service类补充try-catch方法
再try-catch方法中
- 开启事务,关闭MySQL的自动事务提交
- 执行数据库动作,把Dao层的加钱减钱方法放入
- 手动提交
- catch捕获异常,如果有异常,那么事务回滚
- 我们对于异常不能隐藏,选择抛出
- finally中关闭连接
- 修改Dao层,删除add和sub中的 注册驱动 和 获取连接 ,在参数列表中加入connection,并在try中补充形参。
Service类
public void transefer(String addAccount,String subAccount,int money) throws Exception {
//创建Dao类的对象
BankDao bankDao = new BankDao();
//一个事务的最基本要求,必须是同一个链接对象,connection
//一个转账方法属于一个事务,(加钱 减钱)
//1.注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//2.获取连接
Connection connection = DriverManager.getConnection("jdbc:mysql:///atguigu?user=root&password=abc123");
try{
//开启事务,关闭MySQL的自动事务提交
//关闭事务提交
connection.setAutoCommit(false);
//执行数据库动作
//执行加钱减钱
bankDao.add(addAccount,money,connection);
System.out.println("----------------------");
bankDao.sub(subAccount,money,connection);
//事务提交
connection.commit();
}catch (Exception e){
//事务回滚
connection.rollback();
//我们捕捉异常为了做事务回滚,但是不能隐藏异常信息,我们选择抛出
//抛出
throw e;
}finally {
connection.close();
}
}
Dao类
public class BankDao {
/*
加钱的数据库操作方法(具体的JDBC动作)
String account 加钱的符号
int money 加钱的金额
*/
public void add(String account,int money,Connection connection) throws Exception {
//加钱就是所谓的修改
//3.编写SQL语句结构
String sql = "update t_bank set money = money + ? where account = ?";
//4.创建statement
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//5.占位符赋值
preparedStatement.setObject(1,money);
preparedStatement.setObject(2,account);
//6.发送sql语句
preparedStatement.executeUpdate();
//7.关闭资源
preparedStatement.close();
System.out.println("加钱成功!");
}
/*
减钱的数据库操作方法(具体的JDBC动作)
String account 减钱的符号
int money 减钱的金额
*/
public void sub(String account,int money,Connection connection) throws Exception{
//3.编写SQL语句结构
String sql = "update t_bank set money = money - ? where account = ?";
//4.创建statement
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//5.占位符赋值
preparedStatement.setObject(1,money);
preparedStatement.setObject(2,account);
//6.发送sql语句
preparedStatement.executeUpdate();
//7.关闭资源
preparedStatement.close();
System.out.println("减钱成功!");
}
}
测试一下
这里报错后会被catch捕捉到,触发事务回滚
我们看见这里就并没有再进行加钱的操作了,这样事务就添加完毕了
源码
Service + 测试类
/*
银行卡业务方法,调用Dao方法
*/
public class BankService {
@Test
public void start() throws Exception {
//直接在测试方法中调用Service层
//二狗子 给驴蛋蛋转500
transefer("lvdandan","ergouzi",500);
//现在我们把减钱独立一个事务中,加钱独立在另一个事务中,它们两互不印象,会出现什么问题呢?
}
/*
事务添加是在业务方法中!
利用try—catch代码块,开启事务和提交事务,和事务回滚!
将connection传入dao层即可,而dao只负责使用,不要close();
*/
public void transefer(String addAccount,String subAccount,int money) throws Exception {
//创建Dao类的对象
BankDao bankDao = new BankDao();
//一个事务的最基本要求,必须是同一个链接对象,connection
//一个转账方法属于一个事务,(加钱 减钱)
//1.注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//2.获取连接
Connection connection = DriverManager.getConnection("jdbc:mysql:///atguigu?user=root&password=abc123");
try{
//开启事务,关闭MySQL的自动事务提交
//关闭事务提交
connection.setAutoCommit(false);
//执行数据库动作
//执行加钱减钱
bankDao.add(addAccount,money,connection);
System.out.println("----------------------");
bankDao.sub(subAccount,money,connection);
//事务提交
connection.commit();
}catch (Exception e){
//事务回滚
connection.rollback();
//我们捕捉异常为了做事务回滚,但是不能隐藏异常信息,我们选择抛出
//抛出
throw e;
}finally {
connection.close();
}
}
}
Dao 类
/*
表的数据库方法存储类
*/
public class BankDao {
/*
加钱的数据库操作方法(具体的JDBC动作)
String account 加钱的符号
int money 加钱的金额
*/
public void add(String account,int money,Connection connection) throws Exception {
//加钱就是所谓的修改
//3.编写SQL语句结构
String sql = "update t_bank set money = money + ? where account = ?";
//4.创建statement
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//5.占位符赋值
preparedStatement.setObject(1,money);
preparedStatement.setObject(2,account);
//6.发送sql语句
preparedStatement.executeUpdate();
//7.关闭资源
preparedStatement.close();
System.out.println("加钱成功!");
}
/*
减钱的数据库操作方法(具体的JDBC动作)
String account 减钱的符号
int money 减钱的金额
*/
public void sub(String account,int money,Connection connection) throws Exception{
//3.编写SQL语句结构
String sql = "update t_bank set money = money - ? where account = ?";
//4.创建statement
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//5.占位符赋值
preparedStatement.setObject(1,money);
preparedStatement.setObject(2,account);
//6.发送sql语句
preparedStatement.executeUpdate();
//7.关闭资源
preparedStatement.close();
System.out.println("减钱成功!");
}
}
我书写这篇文章的初衷就是总结学习的进度,遗忘之际可以拿出来翻看,如有不对的地方还望指正,多多海涵。
[...]这里我要做一定解释,之前学习JDBC的时候写过工具类封装,分为DQL语句和非DQL语句,在使用JdbcTemplate类完成数据库的增删改查的时候我非常激动,因为我看到了JdbcTemplate中query和update方法的形参列表和我当初写的工具类封装几乎一模一样,尤其是query在查询返回list集合的时候我深有体会,传递的是一个字节码对象,当时通过传递一个类的字节码对象,进行反射。对比当初[...]