35. Spring7 JdbcTemplate 及事务控制

增删改

数据库新增 emp 表

新建 Emp 实体类,并实现 get/set,toString 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.atguigu.jdbctemplate;

public class Emp {
private Integer eid;
private String ename;
private Integer age;
private String sex;

public Integer getEid() {
return eid;
}
public void setEid(Integer eid) {
this.eid = eid;
}
public String getEname() {
return ename;
}
public void setEname(String ename) {
this.ename = ename;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
@Override
public String toString() {
return "Emp [eid=" + eid + ", ename=" + ename + ", age=" + age + ", sex=" + sex + "]";
}
}

新建 db.properties 文件

1
2
3
4
jdbc.driver = com.mysql.jdbc.Driver
jdbc.url = jdbc:mysql://localhost:3306/ssm
jdbc.username = root
jdbc.password = 123456

新建 xml 配置文件,并在其中加载 db.properties,创建数据源和创建 jdbcTemplate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?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-4.2.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">

<!-- 加载 db.properties 方式1 -->
<!-- <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="location" value="db.properties"></property>
</bean> -->

<!-- 加载 db.properties 方式2 -->
<context:property-placeholder location="db.properties"/>

<!-- 创建数据源-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driver}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>

<!-- 根据数据源创建 jdbcTemplate -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
</beans>

最后创建测试类,测试增删改方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.atguigu.jdbctemplate;

import static org.junit.Assert.*;

import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.jdbc.core.JdbcTemplate;

public class TestJdbcTemplate {

ApplicationContext applicationContext = new ClassPathXmlApplicationContext("jdbc.xml");
JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);

@Test
public void test() {
// String sqlString = "insert into emp values (null, 'zhangsan', 23, 'nan')";
// jdbcTemplate.update(sqlString);

// String sqlString2 = "insert into emp values (null, ?, ?, ?)";
// jdbcTemplate.update(sqlString2, "lisi", 23, "nv");

// String sqlString3 = "delete from emp where eid = ?";
// jdbcTemplate.update(sqlString3, 1);

String sqlString4 = "update emp set ename = ? where eid = ?";
jdbcTemplate.update(sqlString4, "wangwu", 2);
}
}

批量操作

1
2
3
4
5
6
7
8
9
@Test
public void testBatchUpdate() {
String sqlString = "insert into emp values (null, ?, ?, ?)";
List<Object[]> list = new ArrayList<>();
list.add(new Object[] {"a1", 1, "nan"});
list.add(new Object[] {"a2", 2, "nan"});
list.add(new Object[] {"a3", 3, "nan"});
jdbcTemplate.batchUpdate(sqlString, list);
}

注意⚠️:在 SQL 语句中有两类不能使用通配符 ?赋值。第一类是批量删除和批量修改。第二类是模糊查询。因为使用通配符赋值(通配符赋值其实相当于 PreparedStatement 的方式, 字符串拼接相当于 Statement 的方式)会默认在赋值字符串的两边加上单引号。导致 SQL 发生变化。

queryForObject

queryForObject 可以获取单条数据,并赋给对象。也可以用来获取单个的值,例如 count,sum…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
public void testQueryForObject() {
// jdbcTemplate.queryForObject(sql, rowMapper) 用来获取单条数据
// jdbcTemplate.queryForObject(sql, requiredType) 用来获取单个的值,例如 count,sum...

String sql = "select eid, ename, age, sex from emp where eid=?";
// 将查询出的结果列名与实体类中的属性进行映射
RowMapper<Emp> rowMapper = new BeanPropertyRowMapper<>(Emp.class);
Emp emp = jdbcTemplate.queryForObject(sql, new Object[] {5}, rowMapper);
System.out.println(emp);

String sql2 = "select count(1) from emp";
Integer count = jdbcTemplate.queryForObject(sql2, Integer.class);
System.out.println(count);
}

query

查询结果,结果是 List

1
2
3
4
5
6
7
8
9
@Test
public void testQuery() {
String sql = "select eid, ename, age, sex from emp";
RowMapper<Emp> rowMapper = new BeanPropertyRowMapper<>(Emp.class);
List<Emp> query = jdbcTemplate.query(sql, rowMapper);
for (Emp emp : query) {
System.out.println(emp);
}
}

事务问题演示

新建 BookDao 接口类,里面有三个方法,分别是 selectPrice 查询某本图书的价格,updateSt 更新某本图书的库存,updateBlance 更新账户余额。

1
2
3
4
5
6
7
8
9
package com.atguigu.book.dao;

public interface BookDao {
Integer selectPrice(String bid);

void updateSt(String bid);

void updateBlance(String uid, Integer price);
}

BookDaoImpl 实现 BookDao

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.atguigu.book.dao.Impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import com.atguigu.book.dao.BookDao;

@Repository
public class BookDaoImpl implements BookDao{
@Autowired
private JdbcTemplate jdbcTemplate;

@Override
public Integer selectPrice(String bid) {
Integer price= jdbcTemplate.queryForObject("select price from book where bid=?", new Object[] {bid}, Integer.class);
return price;
}

@Override
public void updateSt(String bid) {
// 先获取书籍的库存, 如果库存够则卖出一本
Integer st = jdbcTemplate.queryForObject("select st from stock where sid = ?", new Object[] {bid}, Integer.class);
if (st < 0) {
throw new RuntimeException("库存不足");
}else {
jdbcTemplate.update("update stock set st=st-1 where sid=?", bid);
}
}

@Override
public void updateBlance(String uid, Integer price) {
// 先获取用户余额
Integer blance = jdbcTemplate.queryForObject("select blance from money where uid = ?", new Object[] {uid}, Integer.class);
if (blance < price) {
throw new RuntimeException("余额不足");
}else {
jdbcTemplate.update("update money set blance = blance - ? where uid = ?", price, uid);
}
}
}

新建 BookService 接口类,里面创建 buyBook 方法

1
2
3
4
5
package com.atguigu.book.service;

public interface BookService {
void buyBook(String bid, String uid);
}

BookServiceImpl 实现接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.atguigu.book.service.Impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import com.atguigu.book.dao.BookDao;
import com.atguigu.book.service.BookService;

@Service
public class BookServiceImpl implements BookService{
@Autowired
private BookDao bookDao;

public void buyBook(String bid, String uid) {
// 查询图书价格
Integer bookPrice = bookDao.selectPrice(bid);
// 减库存
bookDao.updateSt(bid);
// 减用户余额
bookDao.updateBlance(uid, bookPrice);
}
}

新建 BookController 类购买图书

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.atguigu.book.controller;

import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

import com.atguigu.book.service.BookService;

@Controller
public class BookController {
@Autowired
private BookService bookService;

// 一次买一本
public void buyBook() {
bookService.buyBook("1", "1001");
}
}

新建 Test 类,进行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.atguigu.book;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.atguigu.book.controller.BookController;

public class Test {
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("book.xml");
BookController bookController = applicationContext.getBean("bookController", BookController.class);
bookController.buyBook();
}
}

现象。当用于多次购买图书的时候,数据库中用户余额不够程序报错 “余额不够” 但是图书的库存却任然在减少,原因就是我们没有对下面的三个数据库操作方法进行事务控制。导致减库存成功,减用户余额失败。

1
2
3
4
5
6
// 查询图书价格
Integer bookPrice = bookDao.selectPrice(bid);
// 减库存
bookDao.updateSt(bid);
// 减用户余额
bookDao.updateBlance(uid, bookPrice);

注解配置事务

需要在 xml 中配置事务管理器 并且需要开启注解驱动。配置事务管理器的时候需要依赖 datasource。开启注解驱动时候传入配置好的事务管理器即可。这里需要注意的是 事务管理器有很多种,有基于 Hibernate 的,有基于 JDBC 的 DataSource 的。这里我们使用的是 JdbcTemplate 来操作数据库,所以使用 DataSourceTransactionManager 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?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-4.2.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:component-scan base-package="com.atguigu.book"></context:component-scan>

<context:property-placeholder location="db.properties"/>

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driver}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>

<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>

<!-- 配置事务管理器 -->
<bean id ="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 开启注解驱动,即对事务相关的注解进行扫描,解析含义并执行功能 -->
<tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>

</beans>

在需要需要加上事务的地方,加上 @Transactional 即可。需要注意的是 Transactional 可以使用在方法上,也可能使用在类上,当使用在类上时,对类中的所有方法都有效。当类和方法都配置 Transactional 时,且都有相同的属性配置,那么这时生效的是近的那个,即采用就近原则。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.atguigu.book.service.Impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.atguigu.book.dao.BookDao;
import com.atguigu.book.service.BookService;

@Service
public class BookServiceImpl implements BookService{
@Autowired
private BookDao bookDao;

/**
* @Transactional
* 在方法上使用时,只对方法有效果
* 在类上使用时候,对类中的所有方法都有效果
*/
@Transactional
public void buyBook(String bid, String uid) {
// 查询图书价格
Integer bookPrice = bookDao.selectPrice(bid);
// 减库存
bookDao.updateSt(bid);
// 减用于余额
bookDao.updateBlance(uid, bookPrice);
}
}

Transactional 属性

propagation

事务的传播行为。A方法 和 B方法 都有事务的情况下,A 方法在调用 B 方法时,会将 A 中的事务传播给 B 方法。B 方法对于事务的处理方式就是事务的传播行为。

这时 B 对事务的处理有两种选择,一种是使用 A 传过来的事务,也就是和 A 相同 propagation = Propagation.REQUIRED。另外一种是不使用 A 传过来的事务,将 A 传过来的事务挂起,自己新建一个事务。propagation = Propagation.REQUIRES_NEW 默认是使用调用者传递过来的传播行为。即 REQUIRED

例子说明:这里新建 CashierServiceImpl 类,并实现 Cashier 接口,重写 checkOut 结账方法。checkOut 这里是支持购买的书籍多本一起结账,也就是多次调用 bookService 中的 buyBook 方法。这里已经为 CashierServiceImpl 类加上了 Transactional。这里就是一个事务的传播行为,因为 checkOut 有事务(CashierServiceImpl 类上加了,那么里面的方法就都会有)buyBook 里面也有事务。因为我们没有对 buyBook 中事务的属性进行修改,所以 buyBook 中 Transactional 的 propagation 默认为 REQUIRED 。即如果在循环中出现一次失败,那么整个循环都会回滚。反之,如果 buyBook 中 Transactional 的 propagation 值为 REQUIRES_NEW 那么即每次循环 buyBook 中的事务都是新建的,如果出现失败,则不会导致全部回滚。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.atguigu.book.service.Impl;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.atguigu.book.service.BookService;
import com.atguigu.book.service.Cashier;

@Service
@Transactional
public class CashierServiceImpl implements Cashier{

@Autowired
private BookService bookService;

@Override
public void checkOut(String uid, List<String> bids) {
for (String bid : bids) {
bookService.buyBook(bid, uid);
}
}
}

Spring定义了7种类传播行为。最常用的是 REQUIRED 和 REQUIRES_NEW。默认是 REQUIRED。

isolation

isolation 即事务的隔离级别。假设现在有两个事务:Transaction01和Transaction02并发执行。

1) 脏读

​ ①Transaction01 将某条记录的 AGE 值从 20 修改为 30。

​ ②Transaction02 读取了 Transaction01 更新后的值:30。

​ ③Transaction01 回滚,AGE 值恢复到了 20。

​ ④Transaction02 读取到的 30 就是一个无效的值。

2) 不可重复读

​ ①Transaction01 读取了 AGE 值为20。

​ ②Transaction02 将 AGE 值修改为30。

​ ③Transaction01 再次读取 AGE 值为30,和第一次读取不一致。

3) 幻读

​ ①Transaction01 读取了 STUDENT 表中的一部分数据。

​ ②Transaction02 向 STUDENT 表中插入了新的行。

​ ③Transaction01 读取了 STUDENT 表时,多出了一些行。

数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。一个事务与其他事务隔离的程度称为隔离级别。SQL 标准中规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。

1) 读未提交:READ UNCOMMITTED

允许 Transaction01 读取 Transaction02 未提交的修改。

2) 读已提交:READ COMMITTED

​ 要求 Transaction01 只能读取 Transaction02 已提交的修改。

3) 可重复读:REPEATABLE READ

​ 确保 Transaction01 可以多次从一个字段中读取到相同的值,即 Transaction01 执行期间禁止其它事务对这个字段进行更新。

4) 串行化:SERIALIZABLE

​ 确保 Transaction01 可以多次从一个表中读取到相同的行,在 Transaction01 执行期间,禁止其它事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但性能十分低下。

1) 各个隔离级别解决并发问题的能力见下表

脏读 不可重复读 幻读
READ UNCOMMITTED (读未提交)
READ COMMITTED (读已提交)
REPEATABLE READ (可重复读)
SERIALIZABLE (串行化)

2) 各种数据库产品对事务隔离级别的支持程度

Oracle MySQL
READ UNCOMMITTED ×
READ COMMITTED √(默认)
REPEATABLE READ × √(默认)
SERIALIZABLE

用@Transactional注解声明式地管理事务时可以在@Transactional的isolation属性中设置隔离级别。

timeout

在事务强制回滚前最多可执行(等待)的时间。例如下面的例子中,如果 3 秒没反应则会强制回滚。

1
2
3
4
5
6
7
8
9
@Transactional(propagation = Propagation.REQUIRED, timeout = 3)
public void buyBook(String bid, String uid) {
// 查询图书价格
Integer bookPrice = bookDao.selectPrice(bid);
// 减库存
bookDao.updateSt(bid);
// 减用于余额
bookDao.updateBlance(uid, bookPrice);
}
readOnly

指定当前事务中的一系列操作是否为只读。若设置为只读(readOnly = true),不管事务中有没有写的操作,那么 MySQL 在请求访问数据的时候不加锁。

但是如果有写操作的情况,建议一定不能设置 readOnly = true。因为这时没了锁,但是还对数据进行写的操作,那么就容易出现很多问题。

rollbackFor | rollbackForClassName | noRollbackFor | noRollbackForClassName

rollbackFor | rollbackForClassName 用于指定事务发生什么错误时进行回滚。rollbackFor 传入的是错误类型的 class 对象数组。rollbackForClassName 传入的是全现定名。

noRollbackFor | noRollbackForClassName 用于指定事务发生什么错误时不进行回滚。

1
2
3
4
5
6
7
8
9
@Transactional(noRollbackFor = {NullPointerException.class} ,rollbackFor = {RuntimeException.class})
public void buyBook(String bid, String uid) {
// 查询图书价格
Integer bookPrice = bookDao.selectPrice(bid);
// 减库存
bookDao.updateSt(bid);
// 减用于余额
bookDao.updateBlance(uid, bookPrice);
}

XML 配置事务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<?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"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.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">
<!-- 开启组件扫描 -->
<context:component-scan base-package="com.atguigu.book_xml"></context:component-scan>
<!-- 加载外部资源属性文件 -->
<context:property-placeholder location="db.properties"/>
<!-- 创建数据库连接池 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driver}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
<!-- 创建 jdbcTemplate -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>

<!-- 配置事务管理器(不管使用注解方式,还是 xml 方式配置事务,一定要有 dataSourceTransactionManager 事务管理器的支持) -->
<bean id ="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>

<!-- 配置切入点表达式 -->
<aop:config>
<aop:pointcut expression="execution(* com.atguigu.book_xml.service.Impl.*.*(..))" id="pointCut"/>
<!-- 将切入点和事务通知联系起来 -->
<aop:advisor advice-ref="tx" pointcut-ref="pointCut"/>
</aop:config>

<!-- 配置事务通知, 并指定事务管理器 -->
<tx:advice transaction-manager="dataSourceTransactionManager" id="tx">
<tx:attributes>
<!-- 在设置好的切入点表达式下面进行事务设置,即在上面切入点表达式下,哪些方式需要被事务管理 -->
<!-- 这里表示在 buyBook 方法上加上事务 -->
<tx:method name="buyBook" />
<!-- 这里表示在 checkOut 方法上加上事务 -->
<tx:method name="checkOut" />
<!-- 模糊匹配方法,匹配方法名以 add 开头的方法 -->
<tx:method name="add*" />
<!-- 模糊匹配方法,匹配方法名以 select 开头的方法 -->
<tx:method name="select*" read-only="true"/> <!-- 查询相关的方法可以将 read-only 设置为 true -->
</tx:attributes>
</tx:advice>
</beans>

测试事务是否生效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.atguigu.book_xml;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.atguigu.book_xml.controller.BookController;

public class Test {
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("book_xml.xml");
BookController bookController = applicationContext.getBean("bookController", BookController.class);
bookController.buyBook();
// bookController.checkout();
}
}

代码地址