事务功能的相关操作全部通过自己编写代码来实现:
Connection conn = ...;
try {
// 开启事务:关闭事务的自动提交
conn.setAutoCommit(false);
// 核心操作
// 提交事务
conn.commit();
}catch(Exception e){
// 回滚事务
conn.rollBack();
}finally{
// 释放数据库连接
conn.close();
}
编程式的实现方式存在缺陷:
既然事务控制的代码有规律可循,代码的结构基本是确定的,所以框架就可以将固定模式的代码抽取出来,进行相关的封装。
封装起来后,我们只需要在配置文件中进行简单的配置即可完成操作。
所以,我们可以总结下面两个概念:
①加入依赖
org.springframework
spring-context
5.3.23
org.springframework
spring-orm
5.3.23
org.springframework
spring-test
5.3.23
junit
junit
4.12
test
mysql
mysql-connector-java
8.0.16
com.alibaba
druid
1.0.31
②创建jdbc.properties
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/ssm?serverTimezone=UTC
jdbc.username=root
jdbc.password=Ht1971350878
③配置Spring的配置文件
④创建数据表
CREATE TABLE `t_book` (
`book_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`book_name` varchar(20) DEFAULT NULL COMMENT '图书名称',
`price` int(11) DEFAULT NULL COMMENT '价格',
`stock` int(10) unsigned DEFAULT NULL COMMENT '库存(无符号)',
PRIMARY KEY (`book_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
insert into `t_book`(`book_id`,`book_name`,`price`,`stock`) values (1,'斗破苍
穹',80,100),(2,'斗罗大陆',50,100);
CREATE TABLE `t_user` (
`user_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`username` varchar(20) DEFAULT NULL COMMENT '用户名',
`balance` int(10) unsigned DEFAULT NULL COMMENT '余额(无符号)',
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
insert into `t_user`(`user_id`,`username`,`balance`) values (1,'admin',50);
⑤创建组件
1.创建控制层
BookController类:
@Controller
public class BookController {
}
2.创建业务层
接口BookService:
public interface BookService {
}
创建实现类BookServiceImpl:
@Service
public class BookServiceImpl implements BookService {
}
3.创建持久层
接口BookDao:
public interface BookDao {
}
创建实现类BookDaoImpl:
@Repository
public class BookDaoImpl implements BookDao {
}
但是我们要通过注解来管理组件的话,我们除了注解,还得扫描。所以我们得去spring配置文件里面加上扫描组件加上
Controller需要用到BookService对象来进行自动装配
@Controller
public class BookController {
@Autowired
private BookService bookService;
public void buyBook(Integer userId,Integer bookId) {
bookService.buyBook(userId,bookId);
}
}
但这里BookService类中并没有这个方法,所以我们就需要来创建这个方法
public interface BookService {
/**
* 买书
* @param userId
* @param bookId
*/
void buyBook(Integer userId, Integer bookId);
}
在我们的Service层就需要来访问数据库了,所以我们就创建了持久层的对象
@Service
public class BookServiceImpl implements BookService {
@Autowired
private BookDao bookDao;
@Override
public void buyBook(Integer userId, Integer bookId) {
//查询图书的价格
Integer price = bookDao.getPriceByBookId(bookId);
//更新图书的库存
bookDao.updateStock(bookId);
//更新用户的余额
bookDao.updateBalance(userId,price);
}
}
把操作数据库的方法封装起来,以便于代码的复用
public interface BookDao {
/**
* 根据图书的id查询图书的价格
* @param bookId
* @return
*/
Integer getPriceByBookId(Integer bookId);
/**
* 更新图书的库存
* @param bookId
*/
void updateStock(Integer bookId);
/**
* 更新用户的余额
* @param userId
* @param price
*/
void updateBalance(Integer userId, Integer price);
}
@Repository
public class BookDaoImpl implements BookDao {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public Integer getPriceByBookId(Integer bookId) {
String sql = "select price from t_book where book_id = ?";
return jdbcTemplate.queryForObject(sql,Integer.class,bookId);
}
@Override
public void updateStock(Integer bookId) {
String sql = "update t_book set stock = stock - 1 where book_id = ?";
jdbcTemplate.update(sql,bookId);
}
@Override
public void updateBalance(Integer userId, Integer price) {
String sql = "update t_user set balance = balance - ? where user_id = ?";
jdbcTemplate.update(sql,price,userId);
}
}
测试类:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:ex-annotation.xml")
public class TxByAnnotationTest {
@Autowired
private BookController bookController;
@Test
public void testBuyBook() {
bookController.buyBook(1,1);
}
}
但我们测试后会发现报错了,这是因为我们的余额不够钱买书。但前两个方法执行成功了,在数据库中我们的库存也会减少
声明式事务中我们不需要手动去创建事务的切面,也不需要去写通知,因为在我们当前的Spring中它为我们提供了有事务管理的切面和通知
声明式事务的配置步骤:
1.在Spring的配置文件中配置事务管理器
2.开启事务的注解驱动
在需要被事务管理的方法上,添加@Transactional注解,该方法就会被事务管理
@Transactional注解标识的位置:
1.标识在方法上
2.标识在类上,则类中所有的方法都会被事务管理
①在配置文件中配置事务管理器
②开启事务的注解驱动
我们开启之后,在idea中这个标签的左边会出现一个环绕通知的图标,我们写了这样一个标签它就是环绕通知了吗,其实并非如此,我们配置的事务管理器才是切面,这个里面才是通知。开启事务的注解驱动,只是将我们的事务管理器,把这个切面里面的通知作用到我们当前的连接点上
1.介绍
对一个查询操作来说,如果我们把它设置成只读,就能够明确告诉数据库,这个操作不涉及写操作。这 样数据库就能够针对查询操作来进行优化。
2.使用方式
@Transactional(
readOnly = true
)
public void buyBook(Integer userId, Integer bookId) {
//查询图书的价格
Integer price = bookDao.getPriceByBookId(bookId);
//更新图书的库存
bookDao.updateStock(bookId);
//更新用户的余额
bookDao.updateBalance(userId,price);
}
3.注意
对增删改操作设置只读会抛出下面异常:
Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed
②事务属性:超时
1.介绍:
事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,从而长时间占用数据库资源。而长时间 占用资源,大概率是因为程序运行出现了问题(可能是Java程序或MySQL数据库或网络连接等等)。 此时这个很可能出问题的程序应该被回滚,撤销它已做的操作,事务结束,把资源让出来,让其他正常 程序可以执行。
概括来说就是一句话:超时回滚,释放资源。
2.使用方式
@Service
public class BookServiceImpl implements BookService {
@Autowired
private BookDao bookDao;
@Override
@Transactional(
// readOnly = true
timeout = 3
)
public void buyBook(Integer userId, Integer bookId) {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//查询图书的价格
Integer price = bookDao.getPriceByBookId(bookId);
//更新图书的库存
bookDao.updateStock(bookId);
//更新用户的余额
bookDao.updateBalance(userId,price);
}
}
3.观察结果
执行过程中抛出异常:
org.springframework.transaction.TransactionTimedOutException: Transaction timed out: deadline was Fri Jun 04 16:25:39 CST 2022
1.介绍
声明式事务默认只针对运行时异常回滚,编译时异常不回滚。
可以通过@Transactional中相关属性设置回滚策略
2.使用方式
@Service
public class BookServiceImpl implements BookService {
@Autowired
private BookDao bookDao;
@Override
@Transactional(
// readOnly = true
// timeout = 3
// noRollbackFor = ArithmeticException.class
noRollbackForClassName = "java.lang.ArithmeticException"
)
public void buyBook(Integer userId, Integer bookId) {
// try {
// TimeUnit.SECONDS.sleep(5);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
//查询图书的价格
Integer price = bookDao.getPriceByBookId(bookId);
//更新图书的库存
bookDao.updateStock(bookId);
//更新用户的余额
bookDao.updateBalance(userId,price);
System.out.println(1/0);
}
}
3.观察结果
虽然购买图书功能中出现了数学运算异常(ArithmeticException),但是我们设置的回滚策略是,当出现ArithmeticException不发生回滚,因此购买图书的操作正常执行