spring(十二)之基于注解方式配置事务管理

1.事务简介

  • 事务管理是企业级应用程序开发中必不可少的技术,用来确保数据的完整性和一致性
  • 事务就是一系列动作,它们被当做一个单独的工作单元,这些动作要么全部完成,要么全部不起作用
  • 事务的四个关键属性(ACID)
    原子性:事务是一个原子操作,要么都执行成功,要么都执行失败
    一致性:一旦所有事务动作完成,事务就被提交。数据和资源就处于一种满足业务规则的一致性状态中
    隔离性:可能有许多事务会同时处理相同的数据。因此每个事务都应该和其它事务隔离开来,防止数据损坏
    持久性:一旦事务完成,无论发生什么系统错误,它的结果都不应该受到影响。通常情况下,事务的结果被写到持久化存储器中

2.jdbc对事务的管理

	public void purchase(String isba, String username) {
		Connection conn = null;
		try {
			conn = dataSource.getConnection();
			conn.setAutoCommit(false);
			
			// 目标方法
			
			conn.commit();
		} catch(SQLException e) {
			if(null != conn) {
				try {
					conn.rollback();
				} catch (SQLException e1) {
					e1.printStackTrace();
				}
			}
			throw new RuntimeException(e);
		} finally {
			if(null != conn) {
				try {
					conn.close();
				} catch (SQLException e) {
					e.printStackTrace();
				}
			}
		}
	}

先获取数据库连接、开启事务,如果执行目标方法成功,提交事务、关闭连接
如果执行目标方法出现异常,则回滚事务,关闭连接

以上的问题:

  • 必须为不同的业务方法重写类似的样板代码
  • 这段代码是特定于JDBC的,一旦选择其它数据库存取技术,代码需要做相应的修改

获取数据库连接、开启事务 ---- 前置通知
提交事务 ---- 返回通知
回滚事务 ---- 异常通知
关闭连接 ---- 后置通知

这就是spring使用AOP处理事务的原理出发点

3.spring对事务的管理

  • spring既支持编程式事务管理又支持声明式事务管理
  • 编程式事务管理:将事务管理代码嵌入到物业方法中来控制事务的提交和回滚
  • 声明式事务管理:大多数情况下比编程式事务管理更好用。它将事务管理代码从业务方法中抽离出来,以声明的方式实现事务管理。事务管理作为一种横切关注点,可以通过AOP方法模块化。spring通过AOP框架支持声明式事务管理

3.1需求:

一个用户买书,但他的余额不足够买一本书,但他执行了此操作后,结果如何?

3.2开发步骤

项目结构图:
在这里插入图片描述

【第一步】、创建一个java工程,如:spring-04-tx
【第二步】、创建数据表:
book表:

mysql> create table book
    -> (
    -> id INT(5) PRIMARY KEY AUTO_INCREMENT,
    -> name VARCHAR(50),
    -> price DOUBLE
    -> );

mysql> INSERT INTO book VALUES (NULL, 'Java', 37.8), (NULL, 'Oracle', 35.6);

字段:

  • id:书的编号
  • name:书的名称
  • price:书的单价

account表:

mysql> create table account
    -> (
    -> username VARCHAR(30),
    -> balance double
    -> );

mysql> INSERT INTO account VALUES ('zzc', 100);

字段:

  • username :用户名
  • balance :余额

stock表:

mysql> create table stock
    -> (
    -> id INT(5) PRIMARY KEY AUTO_INCREMENT,
    -> stock INT(3)
    -> );

mysql> INSERT INTO stock VALUES (NULL, 10), (NULL, 10);

字段:

  • id:库存id
  • stock:库存量

【第三步】、创建包和类
com.zzc.dao包下创建BookShopDao接口
BookShopDao.java

public interface BookShopDao {

	/**
	 * 根据书号获取单价
	 * 
	 * @param id
	 * @return
	 */
	double getPriceById(Integer id);
	/**
	 * 根据书号更新书的库存
	 * 
	 * @param id
	 */
	void updateBookStock(Integer id);
	/**
	 * 更新账户余额
	 * 
	 * @param username
	 * @param price
	 */
	void updateUserAccount(String username, double price);
}

com.zzc.dao.impl包下创建其实现类BookShopDaoImpl
BookShopDaoImpl.java

@Repository(value="bookShop")
public class BookShopDaoImpl implements BookShopDao{

	@Autowired
	private JdbcTemplate jdbcTemplate;
	
	@Override
	public double getPriceById(Integer id) {
		String sql = "SELECT price FROM book WHERE id = ?";
		return jdbcTemplate.queryForObject(sql, Double.class, id);
	}

	@Override
	public void updateBookStock(Integer id) {
		String sql = "UPDATE book SET stock = stock - 1 WHERE id = ?";
		jdbcTemplate.update(sql, id);
	}

	@Override
	public void updateUserAccount(String username, double price) {
		String sql = "UPDATE account SET balance = balance - ? WHERE username = ?";
		jdbcTemplate.update(sql, price, username);
	}

}

上述方法使用jdbcTemplate操作数据库

  • getPriceById():根据书名获取书的单价
  • updateBookStock():根据书号更新书的库存
  • updateUserAccount():根据用户名更新账户余额

【第四步】、添加配置文件

添加数据库配置文件db.properties

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf8
jdbc.username=root
jdbc.password=root

jdbc.initPoolSize=5
jdbc.maxPoolSize=10

添加spring配置文件applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">
	
	<!-- 导入资源文件 -->
	<context:property-placeholder location="classpath:db.properties" />
	
	<context:component-scan base-package="com.zzc"></context:component-scan>
	
	<!-- 配置c3p0数据源 -->
	<bean id="dataSource" 
		class="com.mchange.v2.c3p0.ComboPooledDataSource">
		
		<property name="driverClass" value="${jdbc.driver}"></property>
		<property name="jdbcUrl" value="${jdbc.url}"></property>
		<property name="user" value="${jdbc.username}"></property>
		<property name="password" value="${jdbc.password}"></property>
		
		<property name="initialPoolSize" value="${jdbc.initPoolSize}"></property>	
		<property name="maxPoolSize" value="${jdbc.maxPoolSize}"></property>	
	</bean>
	
	<!-- 配置spring中的jdbcTemplate -->
	<bean id="jdbcTemplate" 
		class="org.springframework.jdbc.core.JdbcTemplate">
		<property name="dataSource" ref="dataSource"></property>
	</bean>
	
	<bean id="namedParameterJdbcTemplate" 
		class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate">
		<constructor-arg ref="dataSource"></constructor-arg>
	</bean>
	
</beans>
  • 导入数据库资源配置文件
  • 配置c3p0数据源
  • 配置spring中的jdbcTemplate

【第五步】、创建一个测试类
TxTest.java

public class TxTest {
	
	private ApplicationContext ctx = null;
	private BookShopDao bookShopDao = null;
	
	{
		ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
		bookShopDao = (BookShopDao) ctx.getBean("bookShop");
	}
	
	@Test
	public void test01() {
		double price = bookShopDao.getPriceById(1);
		System.out.println(price);
	}

}

测试:通过书号获取此书的单价

【第六步】、优化实现类中的方法

  1. updateBookStock():由于库存量不能小于0,故当库存量等于0时,是不能更新的,要进行手动抛出异常

  2. updateUserAccount():由于购买书时,余额是要大于书的价格的

在com.zzc.exception包下自定义两个异常类:BookStockException类、UserAccountException类,并继承RuntimeException

BookStockException.java

public class BookStockException extends RuntimeException{

	public BookStockException() {
		super();
	}

	public BookStockException(String message) {
		super(message);
	}
}

UserAccountException.java

public class UserAccountException extends RuntimeException {

	public UserAccountException() {
		super();
	}

	public UserAccountException(String message) {
		super(message);
	}
}

修改updateBookStock()方法:

	@Override
	public void updateBookStock(Integer id) {
		// 1.验证库存是否足够
		String sql2 = "SELECT stock FROM book WHERE id = ?";
		int stock = jdbcTemplate.queryForObject(sql2, Integer.class, id);
		if (0 == stock) {
			throw new BookStockException("库存不足!");
		}
		String sql = "UPDATE book SET stock = stock - 1 WHERE id = ?";
		jdbcTemplate.update(sql, id);
	}

修改updateUserAccount()方法:

	@Override
	public void updateUserAccount(String username, double price) {
		// 1.验证余额是否足够
		String sql2 = "SELECT balance FROM account WHERE username = ?";
		double balance = jdbcTemplate.queryForObject(sql2, Double.class, username);
		if(balance < price) {
			throw new UserAccountException("余额不足!");
		}
		String sql = "UPDATE account SET balance = balance - ? WHERE username = ?";
		jdbcTemplate.update(sql, price, username);
	}

【第七步】、创建com.zzc.service包和类
BookShopService接口

public interface BookShopService {
	/**
	 * 买一本书
	 * 
	 * @param username 	用户名
	 * @param id		书的Id
	 */
	void purchase(String username, int id);
}

BookShopServiceImpl

@Service(value="bookShopService")
public class BookShopServiceImpl implements BookShopService {

	@Autowired
	private BookShopDao bookShopDao;
	
	@Override
	public void purchase(String username, int id) {
		// 1.获取书的单价
		double price = bookShopDao.getPriceById(id);
		// 2.更新书的库存
		bookShopDao.updateBookStock(id);
		// 3.更新用户账户余额
		bookShopDao.updateUserAccount(username, price);
	}

}

买一本书要执行两个操作:

  • 书的库存减一
  • 用户的余额也相应地减少

【第八步】、修改测试类
添加 买一本书方法

public class TxTest {
	
	private ApplicationContext ctx = null;
	private BookShopDao bookShopDao = null;
	private BookShopService bookShopService = null;
	
	{
		ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
		bookShopDao = (BookShopDao) ctx.getBean("bookShop");
		bookShopService = (BookShopService) ctx.getBean("bookShopService");
	}
	
	@Test
	public void test02() {
		bookShopService.purchase("zzc", 1);
	}
	...
}

当账户余额不足买一本书时,会出现如下问题:
书的库存减一,但账户余额没变。也就是说,你买书成功,但没付钱。这在现实生活中是不可能的

原因:
先执行的是bookShopDao.updateBookStock(id)方法,即先更新了库存量,再执行的是bookShopDao.updateUserAccount(username, price)方法,此方法(余额不足,报错了)会报错,无法更新账户余额。导致了一个方法执行成功,一个方法执行失败,这种情况下是不允许的(要么都执行成功,要么都执行失败)。所以必须使用事务

3.3基于注解方式配置事务

【第九步】、配置事务

一:在applicationContext.xml配置文件中配置事务并开启事务:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:tx="http://www.springframework.org/schema/tx"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
		http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd">
	
	<!-- 导入资源文件 -->
	<context:property-placeholder location="classpath:db.properties" />
	
	<context:component-scan base-package="com.zzc"></context:component-scan>
	
	<!-- 配置c3p0数据源 -->
	<bean id="dataSource" 
		class="com.mchange.v2.c3p0.ComboPooledDataSource">
		
		<property name="driverClass" value="${jdbc.driver}"></property>
		<property name="jdbcUrl" value="${jdbc.url}"></property>
		<property name="user" value="${jdbc.username}"></property>
		<property name="password" value="${jdbc.password}"></property>
		
		<property name="initialPoolSize" value="${jdbc.initPoolSize}"></property>	
		<property name="maxPoolSize" value="${jdbc.maxPoolSize}"></property>	
	</bean>
	
	<!-- 配置spring中的jdbcTemplate -->
	<bean id="jdbcTemplate" 
		class="org.springframework.jdbc.core.JdbcTemplate">
		<property name="dataSource" ref="dataSource"></property>
	</bean>
	
	<!-- 配置事务管理器 -->
	<bean id="transactionManager" 
		class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<property name="dataSource" ref="dataSource"></property>
	</bean>
	<!-- 开启事务注解 -->
	<tx:annotation-driven transaction-manager="transactionManager"/>
	
</beans>
  • 配置事务管理器
  • 开启事务注解:需要添加tx命名空间

二:在需要配置事务的方法上添加注解@Transactional
修改BookShopServiceImpl类中的purchase()方法

	// 1.添加事务注解
	@Transactional
	@Override
	public void purchase(String username, int id) {
		// 1.获取书的单价
		double price = bookShopDao.getPriceById(id);
		// 2.更新书的库存
		bookShopDao.updateBookStock(id);
		// 3.更新用户账户余额
		bookShopDao.updateUserAccount(username, price);
	}

【第十步】、再次测试
不会出现之前的问题了,两个方法要么都执行成功,要么都执行失败

发布了78 篇原创文章 · 获赞 2 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/Lucky_Boy_Luck/article/details/100033860