1、JdbcTemplate
为了使 JDBC 更加易于使用,Spring 在 JDBC API 上定义了一个抽象层,以此建立一个 JDBC 存取框架。
作为 Spring JDBC 框架的核心,JDBC 模板的设计目的是为不同类型的 JDBC 操作提供模板方法。每个模板方法都能控制整个过程,并允许覆盖过程中的特定任务。 通过这种方式, 可以在尽可能保留灵活性的情况下,将数据库存取的工作量降到最低。
1.1 MySql数据库环境准备:
CREATE DATABASE spring4;
USE spring4;
CREATE TABLE t_employee (
id INT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(50),
email VARCHAR(50),
dept_id INT
);
CREATE TABLE t_dept(
id INT PRIMARY KEY AUTO_INCREMENT,
dept_name VARCHAR(100)
);
ALTER TABLE t_employee ADD CONSTRAINT fk_emp_dept FOREIGN KEY (dept_id) REFERENCES t_dept(id);
INSERT INTO t_dept VALUES (1,'研发部');
INSERT INTO t_dept VALUES (2,'技术部');
INSERT INTO t_employee VALUES (1,'tom','tom@aa.com',1);
INSERT INTO t_employee VALUES (2,'bob','bob@aa.com',2);
INSERT INTO t_employee VALUES (3,'jack','jack@aa.com',1);
INSERT INTO t_employee VALUES (4,'jerry','jerry@aa.com',2);
ALTER TABLE t_employee CHANGE COLUMN NAME last_name VARCHAR(50);
1.2 创建普通Java工程
1.2.1 创建工程,导入jar包
跟之前一样创建Java工程,需要导入的jar包:
1.2.2 配置文件
在类路径下创建conf资源文件夹,创建db.properties文件
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql:///spring4
jdbc.username=root
jdbc.password=123
jdbc.initPoolSize=5
jdbc.maxPoolSize=20
conf下面再创建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:aop="http://www.springframework.org/schema/aop"
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/aop http://www.springframework.org/schema/aop/spring-aop-4.0.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"/>
<!--配置数据源-->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${jdbc.driver}"/>
<property name="jdbcUrl" value="${jdbc.url}"/>
<property name="user" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<property name="initialPoolSize" value="${jdbc.initPoolSize}"/>
<property name="maxPoolSize" value="${jdbc.maxPoolSize}"/>
</bean>
<!--配置Spring的JdbcTemplate-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!--他必须要配置一个数据源-->
<property name="dataSource" ref="dataSource"/>
</bean>
</beans>
1.2.3 创建表对应的实体
package com.spring.jdbc;
public class Employee {
private Integer id;
private String lastName;
private String email;
private Department department;
//get/set以及toString方法
}
package com.spring.jdbc;
public class Department {
private Integer id;
private String departmentName;
//get/set以及toString方法
}
1.2.4 JdbcTemplate的测试
package com.spring.jdbc;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
public class JdbcTemplateTest {
private ApplicationContext applicationContext;
private JdbcTemplate jdbcTemplate;
@Before//在执行其他测试方法之前,会先执行该方法
public void init(){
applicationContext = new ClassPathXmlApplicationContext("spring/applicationContext.xml");
jdbcTemplate = applicationContext.getBean(JdbcTemplate.class);
}
/**
* 获取单个列的值, 或做统计查询
* 使用 queryForObject(String sql, Class<Long> requiredType)
*/
@Test
public void testQueryForObject2(){
String sql = "select count(*) from t_employee";
Long count = jdbcTemplate.queryForObject(sql, Long.class);
System.out.println(count);
}
/**
* 查到实体类的集合
* 注意调用的不是 queryForList 方法
*/
@Test
public void testQueryForList(){
String sql = "SELECT id, last_name lastName, email FROM t_employee WHERE id < ?";
RowMapper<Employee> rowMapper = new BeanPropertyRowMapper<>(Employee.class);
List<Employee> employees = jdbcTemplate.query(sql, rowMapper,5);
System.out.println(employees);
}
/**
* 从数据库中获取一条记录, 实际得到对应的一个对象
* 注意不是调用 queryForObject(String sql, Class<Employee> requiredType, Object... args) 方法!
* 而需要调用 queryForObject(String sql, RowMapper<Employee> rowMapper, Object... args)
* 1. 其中的 RowMapper 指定如何去映射结果集的行, 常用的实现类为 BeanPropertyRowMapper
* 2. 使用 SQL 中列的别名完成列名和类的属性名的映射. 例如 last_name lastName
* 3. 不支持级联属性. JdbcTemplate 到底是一个 JDBC 的小工具, 而不是 ORM 框架
*/
@Test
public void testQueryForObject(){
String sql = "select id,last_name lastName,email,dept_id deptId from t_employee where id = ?";
RowMapper<Employee> rowMapper = new BeanPropertyRowMapper<>(Employee.class);
Employee employee = jdbcTemplate.queryForObject(sql, rowMapper, 1);
System.out.println(employee);
}
/**
* 执行批量更新: 批量的 INSERT, UPDATE, DELETE
* 最后一个参数是 Object[] 的 List 类型: 因为修改一条记录需要一个 Object 的数组, 那么多条不就需要多个 Object 的数组吗
*/
@Test
public void testBatchUpdate(){
String sql = "insert into t_employee (last_name,email,dept_id) values (?,?,?)";
List<Object[]> list = new ArrayList<>();
Object[] objects1 = new Object[]{"A","aa@aa.com",1};
Object[] objects2 = new Object[]{"B","bb@aa.com",2};
Object[] objects3 = new Object[]{"C","cc@aa.com",2};
Object[] objects4 = new Object[]{"D","dd@aa.com",1};
list.add(objects1);
list.add(objects2);
list.add(objects3);
list.add(objects4);
//批量新增
int[] inserts = jdbcTemplate.batchUpdate(sql, list);
System.out.println(inserts.length);
//删除语句
sql = "delete from t_employee where name = ?";
List<Object[]> nameList = new ArrayList<>();
nameList.add(new Object[]{"A"});
nameList.add(new Object[]{"B"});
nameList.add(new Object[]{"C"});
nameList.add(new Object[]{"D"});
//批量删除
int[] deletes = jdbcTemplate.batchUpdate(sql, nameList);
System.out.println(deletes.length);
}
// update 方法可以进行 INSERT UPDATE DELETE操作
@Test
public void testUpdate(){
//单条新增
String sql = "insert into t_employee (last_name,email,dept_id) values (?,?,?)";
Object[] args = new Object[]{"pipi","pipi@aa.com",2};
int insert = jdbcTemplate.update(sql, args);
System.out.println(insert);
//单条修改
String sql2 = "update t_employee set last_name = ? where id = ?";
int update = jdbcTemplate.update(sql2, "bobo", 5);
System.out.println(update);
//单条删除
String sql3 = "delete from t_employee where id = ?";
int delete = jdbcTemplate.update(sql3, 5);
System.out.println(delete);
}
//测试是否可以获取到连接
@Test
public void testConnection() throws SQLException {
DataSource da = applicationContext.getBean(DataSource.class);
System.out.println(da.getConnection());
}
}
1.3 实际项目中JdbcTemplate的用法
Spring JDBC 框架还提供了一个 JdbcDaoSupport 类来简化 DAO 实现,该类声明了 jdbcTemplate 属性, 它可以从 IOC 容器中注入,或者自动从数据源中创建。(不推荐使用,使用起来没那么方便,需要注入数据源,但是不能通过注解注入)。
实际开发中,JdbcTemplate会在持久层(Dao)进行注入,然后配置文件配置扫描,扫描持久层类上的@Repository注解,在其他业务层,注入持久层就行了:
1.3.1 修改配置文件
配置文件中,添加包扫描:
<!--配置包扫描,扫描@Repository @Service @Controller-->
<context:component-scan base-package="com.spring.jdbc"/>
1.3.2 创建Dao持久层
package com.spring.jdbc;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
@Repository
public class EmployeeDao {
@Autowired
private JdbcTemplate jdbcTemplate;
public Employee getById(Integer id){
String sql = "select id , last_name lastName,email from t_employee where id = ?";
RowMapper<Employee> rowMapper = new BeanPropertyRowMapper<>(Employee.class);
Employee employee = jdbcTemplate.queryForObject(sql, rowMapper, 1);
return employee;
}
}
1.3.3 测试
public class JdbcTemplateTest {
private ApplicationContext applicationContext;
private JdbcTemplate jdbcTemplate;
private EmployeeDao employeeDao;
@Before//在执行其他测试方法之前,会先执行该方法
public void init(){
applicationContext = new ClassPathXmlApplicationContext("spring/applicationContext.xml");
jdbcTemplate = applicationContext.getBean(JdbcTemplate.class);
employeeDao = applicationContext.getBean(EmployeeDao.class);
}
/**
* 一般项目中,会有一个持久层,持久层中注入JdbcTemplate,然后业务层再调用持久层的方法
*/
@Test
public void getByDao(){
Employee en = employeeDao.getById(1);
System.out.println(en);
}
}
2、NamedParameterJdbcTemplate
2.1 配置NamedParameterJdbcTemplate
<!--配置NamedParameterJdbcTemplate 该对象可以使用具名参数, 其没有无参数的构造器, 所以必须为其构造器指定参数-->
<bean id="namedParameterJdbcTemplate" class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate">
<constructor-arg name="dataSource" ref="dataSource"/>
</bean>
2.2 测试
package com.spring.jdbc;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import java.util.HashMap;
import java.util.Map;
public class NamedParameterJdbcTemplateTest {
private ApplicationContext applicationContext;
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
@Before
public void init(){
applicationContext = new ClassPathXmlApplicationContext("spring/applicationContext.xml");
namedParameterJdbcTemplate = applicationContext.getBean(NamedParameterJdbcTemplate.class);
}
/**
* 使用具名参数时, 可以使用 update(String sql, SqlParameterSource paramSource) 方法进行更新操作
* 1. SQL 语句中的参数名和类的属性一致!
* 2. 使用 SqlParameterSource 的 BeanPropertySqlParameterSource 实现类作为参数.
*/
@Test
public void testBean(){
String sql = "insert into t_employee (id,last_name,email) values (:id,:lastName,:email)";
Employee employee = new Employee();
employee.setId(200);
employee.setLastName("erbai");
employee.setEmail("erbai@aa.com");
//创建参数
SqlParameterSource paramSource = new BeanPropertySqlParameterSource(employee);
int row = namedParameterJdbcTemplate.update(sql, paramSource);
System.out.println(row);
}
/**
* 可以为参数起名字.
* 1. 好处: 若有多个参数, 则不用再去对应位置, 直接对应参数名, 便于维护
* 2. 缺点: 较为麻烦.
*/
@Test
public void testMap(){
String sql = "insert into t_employee (id,last_name,email,dept_id) values (:id,:lastName,:email,:deptId)";
Map<String,Object> paramMap = new HashMap<>();
paramMap.put("id",100);
paramMap.put("lastName","baige");
paramMap.put("email","baige@aa.com");
paramMap.put("deptId",2);
int row = namedParameterJdbcTemplate.update(sql, paramMap);
System.out.println(row);
}
}
3、Spring事务管理
3.1 简介
事务管理是企业级应用程序开发中必不可少的技术,用来确保数据的完整性和一致性.。
事务就是一系列的动作,它们被当做一个单独的工作单元,这些动作要么全部完成, 要么全部不起作用。
事务的四个关键属性(ACID):
原子性(atomicity): 事务是一个原子操作, 由一系列动作组成;事务的原子性确保动作要么全部完成,要么完全不起作用;
一致性(consistency):一旦所有事务动作完成, 事务就被提交, 数据和资源就处于一种满足业务规则的一致性状态中;
隔离性(isolation):可能有许多事务会同时处理相同的数据,因此每个事物都应该与其他事务隔离开来,防止数据损坏;
持久性(durability):一旦事务完成,无论发生什么系统错误,它的结果都不应该受到影响,通常情况下,事务的结果被写到持久化存储器中。
3.2 Spring 中的事务管理器
作为企业级应用程序框架,Spring 在不同的事务管理 API 之上定义了一个抽象层。而应用程序开发人员不必了解底层的事务管理 API,就可以使用 Spring 的事务管理机制。
Spring 既支持编程式事务管理,也支持声明式的事务管理。
编程式事务管理:将事务管理代码嵌入到业务方法中来控制事务的提交和回滚,在编程式管理事务时, 必须在每个事务操作中包含额外的事务管理代码。
声明式事务管理:大多数情况下比编程式事务管理更好用, 它将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。 事务管理作为一种横切关注点,可以通过 AOP 方法模块化,Spring 通过 Spring AOP 框架支持声明式事务管理
Spring 从不同的事务管理 API 中抽象了一整套的事务机制,开发人员不必了解底层的事务 API,就可以利用这些事务机制; 有了这些事务机制,事务管理代码就能独立于特定的事务技术了;
Spring 的核心事务管理抽象是 TransactionManager ,它为事务管理封装了一组独立于技术的方法;无论使用 Spring 的哪种事务管理策略(编程式或声明式),事务管理器都是必须的;
3.2.1 Spring 中的事务管理器的不同实现
1、DataSourceTransactionManager:在应用程序中只需要处理一个数据源,而且通过 JDBC 存取;
2、JtaTransactionManager:在 JavaEE 应用服务器上用 JTA(Java Transaction API) 进行事务管理;
3、HibernateTransactionManager:用 Hibernate 框架存取数据库;
事务管理器以普通的 Bean 形式声明在 Spring IOC 容器中
3.3 需求
以网上购书为例:客户购一本书,客户的余额要减少,书的库存同时也要减少;这两个操作应该要在同一个事务中。
创建数据库表:
CREATE TABLE book (
isbn VARCHAR(20) PRIMARY KEY ,
book_name VARCHAR(50),
price INT
);
CREATE TABLE account (
username VARCHAR(20) ,
balance INT
);
CREATE TABLE book_stock (
isbn VARCHAR(20) PRIMARY KEY ,
stock INT
);
ALTER TABLE book_stock ADD CONSTRAINT fk_book_bs FOREIGN KEY (isbn) REFERENCES book(isbn);
INSERT INTO book VALUES ('0001','java',100);
INSERT INTO book VALUES ('0002','python',70);
INSERT INTO account VALUES ('tom',200);
INSERT INTO book_stock VALUES ('0001',10);
INSERT INTO book_stock VALUES ('0002',10);
3.4 基于注解的声明式事务管理
Spring 还允许简单地用 @Transactional 注解来标注事务方法;
为了将方法定义为支持事务处理的,可以为方法添加 @Transactional 注解, 根据 Spring AOP 基于代理机制, 只能标注公有方法。
可以在方法或者类级别上添加 @Transactional 注解,当把这个注解应用到类上时, 这个类中的所有公共方法都会被定义成支持事务处理的。
在 Bean 配置文件中只需要启用 <tx:annotation-driven> 元素,并为之指定事务管理器就可以了。
如果事务处理器的名称是 transactionManager, 就可以在<tx:annotation-driven> 元素中省略 transaction-manager 属性,这个元素会自动检测该名称的事务处理器。
3.4.1 配置事务管理器
<!--配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
3.4.2 开启注解事务
<!--开启事务注解,如果事务管理器是名称是transactionManager ,那么可以不用配置transaction-manager="transactionManager" ,因为是默认值-->
<tx:annotation-driven transaction-manager="transactionManager"/>
加上包扫描:
<!--配置包扫描,扫描@Repository @Service @Controller-->
<context:component-scan base-package="com.spring.jdbc,com.spring.tx"/>
3.4.3 代码编写
1、自定义两个异常
package com.spring.tx.annotation;
//自定义库存不足异常
public class BookStockException extends RuntimeException {
public BookStockException(String message) {
super(message);
}
}
package com.spring.tx.annotation;
//余额不足的异常
public class UserAccountException extends RuntimeException {
public UserAccountException(String message) {
super(message);
}
}
2、定义dao接口和实现类
package com.spring.tx.annotation;
public interface BookShopDao {
//根据书号获取书的单价
public int findBookPriceByIsbn(String isbn);
//更新数的库存. 使书号对应的库存 - 1
public void updateBookStock(String isbn);
//更新用户的账户余额: 使 username 的 balance - price
public void updateUserAccount(String username, int price);
}
package com.spring.tx.annotation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository("bookShopDao")
public class BookShopDaoImpl implements BookShopDao {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public int findBookPriceByIsbn(String isbn) {
String sql = "select price from book where isbn = ?";
Integer price = jdbcTemplate.queryForObject(sql, Integer.class, isbn);
return price;
}
@Override
public void updateBookStock(String isbn) {
//检查书的库存是否足够, 若不够, 则抛出异常
String sql2 = "SELECT stock FROM book_stock WHERE isbn = ?";
int stock = jdbcTemplate.queryForObject(sql2, Integer.class, isbn);
if(stock == 0){
throw new BookStockException("库存不足!");
}
String sql = "update book_stock set stock = stock - 1 where isbn = ?";
jdbcTemplate.update(sql,isbn);
}
@Override
public void updateUserAccount(String username, int price) {
//验证余额是否足够, 若不足, 则抛出异常
String sql2 = "SELECT balance FROM account WHERE username = ?";
int balance = jdbcTemplate.queryForObject(sql2, Integer.class, username);
if(balance < price){
throw new UserAccountException("余额不足!");
}
String sql = "UPDATE account SET balance = balance - ? WHERE username = ?";
jdbcTemplate.update(sql, price, username);
}
}
3、定义service接口和实现类
需要事务管理的方法purchase上面一定要添加@Transactional注解
package com.spring.tx.annotation;
public interface BookShopService {
//购买书本的方法
public void purchase(String username, String isbn);
}
package com.spring.tx.annotation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service("bookShopService")
public class BookShopServiceImpl implements BookShopService{
@Autowired
private BookShopDao bookShopDao;
//添加事务注解
//1.使用 propagation 指定事务的传播行为, 即当前的事务方法被另外一个事务方法调用时
//如何使用事务, 默认取值为 REQUIRED, 即使用调用方法的事务
//REQUIRES_NEW: 事务自己的事务, 调用的事务方法的事务被挂起.
//2.使用 isolation 指定事务的隔离级别, 最常用的取值为 READ_COMMITTED
//3.默认情况下 Spring 的声明式事务对所有的运行时异常进行回滚. 也可以通过对应的
//属性进行设置. 通常情况下去默认值即可.
//4.使用 readOnly 指定事务是否为只读. 表示这个事务只读取数据但不更新数据,
//这样可以帮助数据库引擎优化事务. 若真的事一个只读取数据库值的方法, 应设置 readOnly=true
//5.使用 timeout 指定强制回滚之前事务可以占用的时间
@Transactional
@Override
public void purchase(String username, String isbn) {
//查询书本单价
int price = bookShopDao.findBookPriceByIsbn(isbn);
//更新库存
bookShopDao.updateBookStock(isbn);
//更新余额
bookShopDao.updateUserAccount(username,price);
}
}
4、测试
package com.spring.tx.annotation;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class TxAnnotationTest {
private ApplicationContext applicationContext;
private BookShopService bookShopService;
@Before
public void init(){
applicationContext = new ClassPathXmlApplicationContext("spring/applicationContext.xml");
bookShopService = (BookShopService) applicationContext.getBean("bookShopService");
}
@Test
public void testAnnotationTx(){
bookShopService.purchase("tom","0001");
}
}
3.4.4 事务的传播性
可以参考地址:https://segmentfault.com/a/1190000013341344
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如: 方法可能继续在现有事务中运行, 也可能开启一个新事务,并在自己的事务中运行。
事务的传播行为可以由 propagation 传播属性指定, Spring 定义了 7 种类传播行为,他们在枚举 Propagation 定义
事务传播行为类型 | 说明 |
---|---|
REQUIRED | 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。(默认值) |
SUPPORTS | 支持当前事务,如果当前没有事务,就以非事务方式执行。 |
MANDATORY | 使用当前的事务,如果当前没有事务,就抛出异常。 |
REQUIRES_NEW | 新建事务,如果当前存在事务,把当前事务挂起。 |
NOT_SUPPORTED | 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 |
NEVER | 以非事务方式执行,如果当前存在事务,则抛出异常。 |
NESTED | 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与REQUIRED类似的操作。 |
下面演示一下REQUIRED和REQUIRED_NEW:
需求:买多本书,用户的结账操作,但是用户余额不足以支付所有的费用
1、REQUIRED
定义 新的 Cashier 接口和实现类:
package com.spring.tx.annotation;
import java.util.List;
public interface Cashier {
//支付费用
public void checkout(String username, List<String> isbns);
}
package com.spring.tx.annotation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service("cashier")
public class CashierImpl implements Cashier {
@Autowired
private BookShopService bookShopService;
@Transactional
@Override
public void checkout(String username, List<String> isbns) {
for(String isbn: isbns){
bookShopService.purchase(username, isbn);
}
}
}
CashierImpl中的checkout方法上面添加了@Transactional注解,表明这是一个事务方法,同时默认的传播属性 propagation=REQUIRED;
测试方法如下:
//测试事务的传播属性:propagation = REQUIRED和propagation = REQUIRED_NEW
@Test
public void testPropagation(){
cashier.checkout("tom", Arrays.asList("0001","0002"));
}
如上,因为我们的事务方法的传播属性都是REQUIRED,所以嵌套的事务方法会使用最外层的方法是事务,里面只要有一个地方出现了异常,就会回滚整个事务。
当 bookService 的 purchase() 方法被另一个事务方法 checkout() 调用时, 它默认会在现有的事务内运行; 这个默认的传播行为就是 REQUIRED。因此在 checkout() 方法的开始和终止边界内只有一个事务,这个事务只在 checkout() 方法结束的时候被提交, 结果用户一本书都买不了。
2、REQUIRES_NEW
一种常见的传播行为是 REQUIRES_NEW,它表示该方法必须启动一个新事务, 并在自己的事务内运行;如果有事务在运行, 就应该先挂起它。
修改之前的purchase方法,其他都不要改:
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Override
public void purchase(String username, String isbn) {
//查询书本单价
int price = bookShopDao.findBookPriceByIsbn(isbn);
//更新库存
bookShopDao.updateBookStock(isbn);
//更新余额
bookShopDao.updateUserAccount(username,price);
}
请注意:@Transactional(propagation = Propagation.REQUIRES_NEW),我们把传播属性设置了,这样的话,每次调用purchase方法,会挂起外部事务,只会在purchase方法的内部事务运行。如果我们的钱足够买第一本书,那么第一本书会购买成功,库存和余额都会正确的减少,但是买第二本的时候,会报余额不足异常,购买失败。
3.4.5 事务的隔离级别
脏读:对于两个事物T1, T2;T1读取了已经被 T2 更新但还没有被提交的字段,之后,若 T2回滚, T1读取的内容就是临时且无效的
不可重复读:对于两个事物 T1, T2; T1 读取了一个字段, 然后 T2 更新了该字段, 之后,, T1再次读取同一个字段, 值就不同了
虚读(幻读):对于两个事物 T1, T2;T1 从一个表中读取了一个字段, 然后 T2 在该表中插入了一些新的行, 之后, 如果 T1 再次读取同一个表,就会多出几行;
Spring支持五种事务隔离级别:
隔离级别 | 说明 |
DEFAULT | 这是一个PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别, |
read uncommited | 允许事务读取未被其他事务提交的变更;脏读,不可重复读,虚读都有可能发生 |
read commited | 只允许事务读取其他事务已经提交的变更;不可重复读,虚读有可能发生(Oracle默认事务隔离级别,开发时通常使用的隔离级别) |
repeatable read | 确保事务可以多次从一个字段中读取相同的值,在这个事务持续期间,禁止其他事务对这个字段更新,可以避免脏读,不可重复读,但是虚读还是有可能发生(MySQL默认事务隔离级别) |
serializable | 确保事务可以从一个表中读取相同的行,在这个事务持续期间,禁止其他事务对该表执行插入、更新和删除操作,可以避免脏读,不可重复读,虚读,但是性能低下。 |
在@Transactional注解上,可以设置隔离级别属性:isolation = Isolation.DEFAULT,他的可取值在枚举类Isolation中定义。
事务的隔离级别要得到底层数据库引擎的支持,而不是应用程序或者框架的支持。
Oracle 支持的 2 种事务隔离级别:READ_COMMITED(默认值) , SERIALIZABLE;
Mysql 支持 4 中事务隔离级别;
3.4.6 事务的回滚属性
默认情况下只有未检查异常(RuntimeException和Error类型的异常)会导致事务回滚,而受检查异常不会。
事务的回滚规则可以通过 @Transactional 注解的 rollbackFor 和 noRollbackFor 属性来定义,这两个属性被声明为 Class[] 类型的, 因此可以为这两个属性指定多个异常类;例如:rollbackFor = {UserAccountException.class,BookStockException.class}
3.4.7 超时和只读属性
由于事务可以在行和表上获得锁, 因此长事务会占用资源,并对整体性能产生影响; 如果一个事物只读取数据但不做修改,数据库引擎可以对这个事务进行优化。
超时事务属性:事务在强制回滚之前可以保持多久, 这样可以防止长期运行的事务占用资源;通过 @Transactional 注解的timeout=3(单位是秒)
只读事务属性:表示这个事务只读取数据但不更新数据, 这样可以帮助数据库引擎优化事务;通过 @Transactional 注解的readOnly = true
3.5 基于xml配置的声明式事务管理
事务管理是一种横切关注点
为了在 Spring 2.x 中启用声明式事务管理,可以通过 tx Schema 中定义的 <tx:advice> 元素声明事务通知,为此必须事先将这个 Schema 定义添加到 <beans> 根元素中去;
声明了事务通知后,就需要将它与切入点关联起来。由于事务通知是在 <aop:config> 元素外部声明的,所以它无法直接与切入点产生关联,所以必须在 <aop:config> 元素中声明一个增强器通知与切入点关联起来。
由于 Spring AOP 是基于代理的方法,所以只能增强公共方法,因此,只有公有方法才能通过 Spring AOP 进行事务管理。
3.5.1 修改上述service类
首先,要把之前测试注解的那些类和接口,测试类以及自定义异常代码,都复制一份到tx目录下面:
修改CashierImpl和BookShopServiceImpl,BookShopDaoImpl,把这些个类中的@Service注解、@Autowired注解和@Transactional注解都删掉,等会全部在xml配置文件中,通过<bean>进行配置。
凡是之前通过@Autowired进行自动注入的,都变成添加一个对应的set方法,然后在配置文件中进行注入,例如:
public class BookShopDaoImpl implements BookShopDao {
private JdbcTemplate jdbcTemplate;
//通过set方法注入JdbcTemplate
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
}
3.5.2 创建配置文件applicationContext-tx-xml.xml
创建一个新的配置文件,所有的IOC和DI,AOP等都在配置文件中,不使用注解了:
<?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:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
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/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">
<!--配置包扫描,扫描@Repository @Service @Controller-->
<context:component-scan base-package="com.spring.jdbc,com.spring.tx"/>
<!--引入外部数据源-->
<context:property-placeholder location="classpath:db.properties"/>
<!--配置数据源-->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${jdbc.driver}"/>
<property name="jdbcUrl" value="${jdbc.url}"/>
<property name="user" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<property name="initialPoolSize" value="${jdbc.initPoolSize}"/>
<property name="maxPoolSize" value="${jdbc.maxPoolSize}"/>
</bean>
<!--配置Spring的JdbcTemplate-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!--他必须要配置一个数据源-->
<property name="dataSource" ref="dataSource"/>
</bean>
<!--配置Dao-->
<bean id="bookShopDao" class="com.spring.tx.xml.BookShopDaoImpl">
<property name="jdbcTemplate" ref="jdbcTemplate"/>
</bean>
<!--配置bookShopService-->
<bean id="bookShopService" class="com.spring.tx.xml.service.impl.BookShopServiceImpl">
<property name="bookShopDao" ref="bookShopDao"/>
</bean>
<!--配置CashierImpl-->
<bean id="cashier" class="com.spring.tx.xml.service.impl.CashierImpl">
<property name="bookShopService" ref="bookShopService"/>
</bean>
<!--1 配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--2 配置事务属性-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!-- 根据方法名指定事务的属性 -->
<tx:method name="purchase" propagation="REQUIRED"/>
<tx:method name="get*" read-only="true"/>
<tx:method name="find*" read-only="true"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<!-- 3. 配置事务切入点, 以及把事务切入点和事务属性关联起来 -->
<aop:config>
<aop:pointcut id="txPointCut" expression="execution(* com.spring.tx.xml.service.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/>
</aop:config>
</beans>
其他基本上不用变,只要注意一点,我们的类中,导包要正确。