JdbcTemplate基本用法
1.1 JdbcTemplate概念
Spring为简化特定领域代码,封装了很多 『Template』形式的模板类。例如:JdbcTemplate、RedisTemplate、RestTemplate 等等。
JdbcTemplate是 Spring JDBC 的核心类之一,它简化了与关系型数据库的交互。通过使用 JdbcTemplate,开发者不需要编写样板代码来管理资源(如打开或关闭连接),处理 SQL 语句,或者处理异常转换。以下是 JdbcTemplate 的一些关键特性和使用方式:
- 减少样板代码:自动处理数据库资源的获取和释放。
- SQL 执行:提供了多种方法来执行查询、更新等操作。
- 结果集映射:支持将结果集映射为 Java 对象,简化数据处理。
- 事务管理:可以轻松地在 Spring 的事务管理框架下工作。
- 异常处理:将 JDBC 异常转换为 Spring 的 DataAccessExceptions,便于错误处理。
1.2 JdbcTemplate基本使用
- 导入jar包(启动器)
-
编写配置文件:application.properties
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql://localhost:3306/spring_tx spring.datasource.username=root spring.datasource.password=root -
将JdbcTemplate对象装配到IOC容器中
- SpringBoot自动配置原理,默认将JdbcTemplate装配到IOC容器
1.3 JdbcTemplate常用API
jdbcTemplate.update(String sql ,Object... args):通用增删改功能
jdbcTemplate.queryForObject(String sql,RowMapper):查询单个对象
jdbcTemplate.queryForList(String sql):查询多个数据Map<String,Object>
jdbcTemplate.query(String sql,RowMapper):查询多个对象
1.4 案例代码
package com.mytest;
import com.mytest.pojo.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import java.util.List;
import java.util.Map;
@SpringBootTest
class Day15SpringTxApplicationTests {
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
void contextLoads() {
System.out.println("jdbcTemplate = " + jdbcTemplate);
}
//添加user信息
@Test
public void testAddStudent(){
String sql = "INSERT INTO t_user(ACCOUNT,PASSWORD,nickname)VALUES(?,?,?)";
jdbcTemplate.update(sql,"zhangsan","123456","普通员工");
}
//查询单个对象
@Test
public void testQueryStudent(){
String sql = "SELECT id,`account`,`password`,nickname FROM t_user WHERE id = ?";
// 创建一个映射器
RowMapper<User> rowMapper = new BeanPropertyRowMapper<>(User.class);
User user = jdbcTemplate.queryForObject(sql, rowMapper,20010);
System.out.println("user = " + user);
}
//查询所有对象
@Test
public void testQueryForList(){
String sql = "SELECT id,`account`,`password`,nickname FROM t_user";
// 创建一个映射器
RowMapper<User> rowMapper = new BeanPropertyRowMapper<>(User.class);
List<User> userList = jdbcTemplate.query(sql, rowMapper);
for (User user : userList) {
System.out.println("user = " + user);
}
System.out.println(" ====================== ");
List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql);
for (Map<String, Object> map : maps) {
System.out.println("map = " + map);
}
}
}
2 事务概念回顾
2.1 事务概念
在 MySQL 中,事务(Transaction)是一组 SQL 操作的集合,这组操作要么全部执行成功,要么全部不执行,以此来确保数据的一致性和完整性。事务是数据库管理系统(DBMS)执行过程中的一个逻辑工作单元,它具有四个关键属性,通常被简称为 ACID 属性
- 原子性(Atomicity)
- 事务是一个不可分割的工作单位,事务中包含的所有操作要么全部提交成功,要么全部回滚失败。任何一部分操作的失败都会导致整个事务的撤销。
- 一致性(Consistency)
- 事务必须使数据库从一个一致状态转变到另一个一致状态。这意味着即使在系统崩溃或断电的情况下,事务也必须保证数据的完整性和约束条件不会被破坏。
- 隔离性(Isolation)
- 多个并发事务之间的操作应该是隔离的,即一个事务的结果不应该影响其他正在运行的事务,除非它们已经提交。MySQL 提供了不同的隔离级别来实现不同程度的隔离效果。
- 持久性(Durability)
- 一旦事务提交,它对数据库所做的更改就会永久保存下来,即使系统发生故障也不会丢失。
2.2 事务控制语句
在 MySQL 中,你可以使用以下 SQL 语句来控制事务:
START TRANSACTION或BEGIN
- 开始一个新的事务。
COMMIT
- 提交当前事务,使得所有更改成为永久性的。提交后不能回滚。
ROLLBACK
- 回滚当前事务,撤销所有未提交的操作。这会将数据库恢复到事务开始之前的状态。
2.3 事务隔离级别
MySQL 支持四种标准的事务隔离级别,可以通过 SET TRANSACTION ISOLATION LEVEL 命令来设置:
- 读未提交(READ UNCOMMITTED)
- 最低的隔离级别,允许脏读、不可重复读和幻读。
- 读已提交(READ COMMITTED)
- 允许不可重复读和幻读,但不允许脏读。
- 可重复读(REPEATABLE READ)
- MySQL 的默认隔离级别,仅允许幻读,防止脏读和不可重复读。
- 串行化(SERIALIZABLE)
- 最高的隔离级别,完全串行化的读写操作,避免了脏读、不可重复读和幻读。
3 Spring声明式事务基本使用
3.1 事务管理概念
spring中支持两种事务管理,分别是编程式事务管理和声明式事务管理两种.
-
编程式事务管理概念
-
编程式事务是指手动编写程序来管理事务,即通过编写代码的方式直接控制事务的提交和回滚。在 Java 中,通常使用事务管理器(如 Spring 中的
PlatformTransactionManager)来实现编程式事务。 -
编程式事务的主要优点是灵活性高,可以按照自己的需求来控制事务的粒度、模式等等。但是,编写大量的事务控制代码容易出现问题,对代码的可读性和可维护性有一定影响。
Connection conn = ...; try { // 开启事务:关闭事务的自动提交 conn.setAutoCommit(false); // 核心操作 // 业务代码 // 提交事务 conn.commit(); }catch(Exception e){ // 回滚事务 conn.rollBack(); }finally{ // 释放数据库连接 conn.close(); }- 编程式的实现方式存在缺陷:
- 细节没有被屏蔽:具体操作过程中,所有细节都需要程序员自己来完成,比较繁琐。
- 代码复用性不高:如果没有有效抽取出来,每次实现功能都需要自己编写代码,代码就没有得到复用。
- 事务管理代码与核心业务代码相耦合
-
-
声明式事务管理概念
- 在 Spring 框架中,声明式事务管理是一种通过配置而非编程方式来管理事务的技术。它允许开发者以非侵入的方式定义方法的事务行为,而无需修改业务逻辑代码。Spring 的声明式事务管理主要依赖于 AOP(面向切面编程)来实现,并且可以通过 XML 配置或者基于注解的方式来进行配置。
- 声明式事务管理的优势
- 减少样板代码:减少了围绕事务控制所需的大量重复代码。
- 提高代码可读性:将事务逻辑与业务逻辑分离,使代码更易于维护和理解。
- 集中化管理:可以在一处集中管理事务规则,便于维护和调整。
-
小结
- 编程式事务需要手动编写代码来管理事务(不推荐使用)
- 声明式事务可以通过配置文件或注解来控制事务。(推荐使用)
3.2 声明式事务管理基本实现
-
SpringBoot环境中默认装配事务管理器,在service层使用@Transactional注解管理事务即可
-
当然也可以手动装配事务管理器,具体代码如下:
-
编写配置文件:jdbc.properties
#配置DruidDataSource jdbc.driverClassName=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/demo jdbc.username=root jdbc.password=root -
编写属性类:JdbcProperties
package com.mytest.properties; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Component("jdbcProperties") @ConfigurationProperties(prefix = "jdbc") @Data public class JdbcProperties { private String driverClassName; private String url; private String username; private String password; } -
编写配置类:DruidConfig
package com.mytest.config; import com.alibaba.druid.pool.DruidDataSource; import com.mytest.properties.JdbcProperties; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; @Configuration public class DruidConfig { @Autowired @Qualifier("jdbcProperties") private JdbcProperties jdbcProperties; @Bean public DataSource dataSource(/*JdbcProperties jdbcProperties*/){ DruidDataSource ds = new DruidDataSource(); ds.setDriverClassName(jdbcProperties.getDriverClassName()); ds.setUrl(jdbcProperties.getUrl()); ds.setUsername(jdbcProperties.getUsername()); ds.setPassword(jdbcProperties.getPassword()); return ds; } /** * 实例化JdbcTemplate对象,需要使用ioc中的DataSource * @param dataSource * @return */ @Bean public JdbcTemplate jdbcTemplate(DataSource dataSource){ JdbcTemplate jdbcTemplate = new JdbcTemplate(); jdbcTemplate.setDataSource(dataSource); return jdbcTemplate; } } -
编写事务管理器配置类:TxConfig
@Configuration @EnableTransactionManagement //开启声明式事务管理 public class TxConfig { /** * 装配事务管理实现对象 * @param dataSource * @return */ @Bean public TransactionManager transactionManager(DataSource dataSource){ return new DataSourceTransactionManager(dataSource); } } -
整合DruidConfig与TxConfig
@Import(value = {DruidConfig.class, TxConfig.class}) @ComponentScan(basePackages = "com.mytest") @Configuration public class SpringConfig { } -
使用@Transactional注解实现:声明式事务管理
-
service层代码
package com.mytest.service; import com.mytest.pojo.User; public interface UserService { //修改User信息 public void updateUser(User user); //通过用户名修改密码 public void updatePwdByUsername(String username, String password); //通过用户名修改昵称 public void updateNicknameByUsername(String nickname, String username); }package com.mytest.service.impl; import com.mytest.dao.UserDao; import com.mytest.pojo.User; import com.mytest.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @Service //@Transactional public class UserServiceImpl implements UserService { @Autowired private UserDao userDao; /** * 1. 通过姓名修改密码; 2. 通过姓名修改昵称 */ @Transactional( readOnly = false, timeout = 3, noRollbackFor = ArithmeticException.class, isolation = Isolation.REPEATABLE_READ ) @Override public void updateUser(User user) { //1. 通过姓名修改密码 userDao.updatePwdByUsername(user.getAccount(),user.getPassword()); //事务超时 // try { // Thread.sleep(4000); // } catch (InterruptedException e) { // throw new RuntimeException(e); // } // bug int i = 1/0; //2. 通过姓名修改昵称 userDao.updateNicknameByUsername(user.getNickname(),user.getAccount()); } @Transactional(propagation=Propagation.REQUIRES_NEW) @Override public void updatePwdByUsername(String username, String password) { userDao.updatePwdByUsername(username, password); } @Transactional(propagation=Propagation.REQUIRES_NEW) @Override public void updateNicknameByUsername(String nickname, String username) { userDao.updateNicknameByUsername(nickname, username); } }package com.mytest.service.impl; import com.mytest.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @Service("userServiceAll") public class UserServiceAll { @Autowired private UserService userService; //测试事务传播行为(Propagation默认值:REQUIRED) @Transactional public void updateUserAll(){ //1. 通过用户名修改密码 userService.updatePwdByUsername("zhangsan", "666666"); // int i = 1/0; //2. 通过用户名修改昵称(普通员工) userService.updateNicknameByUsername("普通员工", "zhangsan"); } //测试事务传播行为(Propagation默认值:REQUIRES_NEW) @Transactional(propagation = Propagation.REQUIRED) public void updateUserAllNew(){ //1. 通过用户名修改密码 userService.updatePwdByUsername("zhangsan", "666666"); // int i = 1/0; //2. 通过用户名修改昵称(普通员工) userService.updateNicknameByUsername("普通员工", "zhangsan"); } } -
测试类代码
package com.mytest; import com.mytest.pojo.User; import com.mytest.service.UserService; import com.mytest.service.impl.UserServiceAll; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest public class TestUserService { @Autowired private UserService userService; @Autowired @Qualifier("userServiceAll") private UserServiceAll userServiceAll; //测试事务四个属性 @Test public void testUpdateUser() { User user = new User(); user.setAccount("zhangsan"); user.setPassword("666666"); user.setNickname("普通员工666"); userService.updateUser(user); } //测试事务propagation属性(传播行为) @Test public void testPropagation() { //测试 REQUIRED // userServiceAll.updateUserAll(); System.out.println("userService.getClass().getName() = " + userService.getClass().getName()); System.out.println("userServiceAll.getClass().getName() = " + userServiceAll.getClass().getName()); //测试 REQUIRES_NEW userServiceAll.updateUserAllNew(); } }
-
4 @Transactional注解(事务属性)
4.1 事务只读
-
只读介绍
对一个查询操作来说,如果我们把它设置成只读,就能够明确告诉数据库,这个操作不涉及写操作。这样数据库就能够针对查询操作来进行优化。
-
设置方式
// readOnly = true把当前事务设置为只读 默认是false! @Transactional(readOnly = true) -
针对DML动作设置只读模式
会抛出下面异常:
Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed
-
@Transactional注解放在类上
-
生效原则
如果一个类中每一个方法上都使用了 @Transactional 注解,那么就可以将 @Transactional 注解提取到类上。反过来说:@Transactional 注解在类级别标记,会影响到类中的每一个方法。同时,类级别标记的 @Transactional 注解中设置的事务属性也会延续影响到方法执行时的事务属性。除非在方法上又设置了 @Transactional 注解。
对一个方法来说,离它最近的 @Transactional 注解中的事务属性设置生效。
-
用法举例
在类级别@Transactional注解中设置只读,这样类中所有的查询方法都不需要设置@Transactional注解了。因为对查询操作来说,其他属性通常不需要设置,所以使用公共设置即可。
然后在这个基础上,对增删改方法设置@Transactional注解 readOnly 属性为 false。
@Service @Transactional(readOnly = true) public class EmpService { // 为了便于核对数据库操作结果,不要修改同一条记录 @Transactional(readOnly = false) public void updateTwice(……) { …… } // readOnly = true把当前事务设置为只读 // @Transactional(readOnly = true) public String getEmpName(Integer empId) { …… } } -
4.2 事务超时
-
需求
事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,从而长时间占用数据库资源。而长时间占用资源,大概率是因为程序运行出现了问题(可能是Java程序或MySQL数据库或网络连接等等)。
此时这个很可能出问题的程序应该被回滚,撤销它已做的操作,事务结束,把资源让出来,让其他正常程序可以执行。
概括来说就是一句话:超时回滚,释放资源。
-
设置超时时间
@Service public class StudentService { @Autowired private StudentDao studentDao; /** * timeout设置事务超时时间,单位秒! 默认: -1 永不超时,不限制事务时间! */ @Transactional(readOnly = false,timeout = 3) public void changeInfo(){ studentDao.updateAgeById(100,1); //休眠4秒,等待方法超时! try { Thread.sleep(4000); } catch (InterruptedException e) { throw new RuntimeException(e); } studentDao.updateNameById("test1",1); } } -
测试超时效果
org.springframework.transaction.TransactionTimedOutException: Transaction timed out: deadline was Wed May 24 09:10:43 IRKT 2023
4.3 事务回滚|不回滚异常
-
默认情况
默认只针对运行时异常回滚,编译时异常不回滚。情景模拟代码如下:
-
设置回滚异常
@Service public class StudentService { @Autowired private StudentDao studentDao; /** * timeout设置事务超时时间,单位秒! 默认: -1 永不超时,不限制事务时间! * rollbackFor = 指定哪些异常才会回滚,默认是 RuntimeException and Error 异常方可回滚! * noRollbackFor = 指定哪些异常不会回滚, 默认没有指定,如果指定,应该在rollbackFor的范围内! */ @Transactional(readOnly = false,timeout = 3) public void changeInfo() throws FileNotFoundException { studentDao.updateAgeById(100,1); //主动抛出一个检查异常,测试! 发现不会回滚,因为不在rollbackFor的默认范围内! new FileInputStream("xxxx"); studentDao.updateNameById("test1",1); } } -
设置不回滚的异常
在默认设置和已有设置的基础上,再指定一个异常类型,碰到它不回滚。
noRollbackFor属性:指定哪些异常不会回滚, 默认没有指定,如果指定,应该在rollbackFor的范围内!
@Service public class StudentService { @Autowired private StudentDao studentDao; /** * timeout设置事务超时时间,单位秒! 默认: -1 永不超时,不限制事务时间! * rollbackFor = 指定哪些异常才会回滚,默认是 RuntimeException and Error 异常方可回滚! * noRollbackFor = 指定哪些异常不会回滚, 默认没有指定,如果指定,应该在rollbackFor的范围内! */ @Transactional(readOnly = false,timeout = 3,rollbackFor = Exception.class,noRollbackFor = FileNotFoundException.class) public void changeInfo() throws FileNotFoundException { studentDao.updateAgeById(100,1); //主动抛出一个检查异常,测试! 发现不会回滚,因为不在rollbackFor的默认范围内! new FileInputStream("xxxx"); studentDao.updateNameById("test1",1); } }
4.4 事务隔离级别
-
事务隔离级别回滚
数据库事务的隔离级别是指在多个事务并发执行时,数据库系统为了保证数据一致性所遵循的规定。常见的隔离级别包括:
- 读未提交(Read Uncommitted):事务可以读取未被提交的数据,容易产生脏读、不可重复读和幻读等问题。实现简单但不太安全,一般不用。
- 读已提交(Read Committed):事务只能读取已经提交的数据,可以避免脏读问题,但可能引发不可重复读和幻读。
- 可重复读(Repeatable Read):在一个事务中,相同的查询将返回相同的结果集,不管其他事务对数据做了什么修改。可以避免脏读和不可重复读,但仍有幻读的问题。
- 串行化(Serializable):最高的隔离级别,完全禁止了并发,只允许一个事务执行完毕之后才能执行另一个事务。可以避免以上所有问题,但效率较低,不适用于高并发场景。 不同的隔离级别适用于不同的场景,需要根据实际业务需求进行选择和调整。
-
事务隔离级别设置
package com.mytest.service; import com.mytest.dao.StudentDao; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; import java.io.FileInputStream; import java.io.FileNotFoundException; /** * projectName: com.mytest.service */ @Service public class StudentService { @Autowired private StudentDao studentDao; /** * timeout设置事务超时时间,单位秒! 默认: -1 永不超时,不限制事务时间! * rollbackFor = 指定哪些异常才会回滚,默认是 RuntimeException and Error 异常方可回滚! * noRollbackFor = 指定哪些异常不会回滚, 默认没有指定,如果指定,应该在rollbackFor的范围内! * isolation = 设置事务的隔离级别,mysql默认是repeatable read! */ @Transactional(readOnly = false, timeout = 3, rollbackFor = Exception.class, noRollbackFor = FileNotFoundException.class, isolation = Isolation.REPEATABLE_READ) public void changeInfo() throws FileNotFoundException { studentDao.updateAgeById(100,1); //主动抛出一个检查异常,测试! 发现不会回滚,因为不在rollbackFor的默认范围内! new FileInputStream("xxxx"); studentDao.updateNameById("test1",1); } }
4.5 事务传播行为
-
事务传播行为概念
事务传播行为(Propagation Behavior)定义了当一个方法被调用时,如何与已有的事务进行交互。在 Spring 框架中,
@Transactional注解的propagation属性用于指定方法的事务传播行为。不同的传播行为决定了当前方法是否应该运行在一个新事务中、加入现有事务、或者以非事务方式运行等。举例说明:
- 假设MethodA()方法的事务是tx1,MethodB()方法的事务tx2,MethodA()中调用MethodB()
- 事务传播行为决定了MethodB()按照tx1或tx2事务执行
- REQUIRED(默认值):tx1
- REQUIRES_NEW:tx2
-
案例代码
@Transactional public void MethodA(){ // ... MethodB(); // ... } //在被调用的子方法中设置传播行为,代表如何处理调用的事务! 是加入,还是新事务等! @Transactional(propagation = Propagation.REQUIRES_NEW) public void MethodB(){ // ... } -
propagation属性
-
propagation属性值
spring中支持七种事务传播行为,常用两种:REQUIRED和REQUIRES_NEW
REQUIRED(默认值)- 如果当前存在事务,则加入该事务;如果不存在,则创建一个新的事务。
- SUPPORTS
- 如果当前存在事务,则加入该事务;如果没有事务,则以非事务方式执行。这意味着此方法对事务的存在与否不敏感。
- MANDATORY
- 必须在一个已有事务中运行,否则抛出异常(
IllegalTransactionStateException)。这通常用于必须确保在事务上下文中执行的方法。
- 必须在一个已有事务中运行,否则抛出异常(
REQUIRES_NEW- 创建一个新的事务,如果当前存在事务,则挂起当前事务。这意味着即使有现有的事务,也会为这个方法启动一个全新的事务。
- NOT_SUPPORTED
- 不支持事务,总是以非事务方式执行。如果有活动的事务,它将被暂停。
- NEVER
- 方法不应该在事务中运行。如果尝试在事务中调用该方法,将会抛出异常。
- NESTED
- 如果当前存在事务,则在嵌套事务内执行;如果不存在,则创建新的事务。嵌套事务可以独立于外部事务回滚,但它的提交依赖于外部事务的提交。
-
测试事务传播行为
package com.mytest.service.impl; import com.mytest.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @Service("userServiceAll") public class UserServiceAll { @Autowired private UserService userService; //测试事务传播行为(Propagation默认值:REQUIRED) @Transactional public void updateUserAll(){ //1. 通过用户名修改密码 userService.updatePwdByUsername("zhangsan", "666666"); // int i = 1/0; //2. 通过用户名修改昵称(普通员工) userService.updateNicknameByUsername("普通员工", "zhangsan"); } //测试事务传播行为(Propagation默认值:REQUIRES_NEW) @Transactional(propagation = Propagation.REQUIRED) public void updateUserAllNew(){ //1. 通过用户名修改密码 userService.updatePwdByUsername("zhangsan", "666666"); // int i = 1/0; //2. 通过用户名修改昵称(普通员工) userService.updateNicknameByUsername("普通员工", "zhangsan"); } }
spring 中配置数据源
- 导入jar包
-
配置
-
属性类(可选)
- 装配
- 装配事物管理器