一、Spring JdbcTemplate
在Spring中为了更加方便的操作JDBC,在JDBC的基础之上定义了一个抽象层,此设计的目的是为了给不同类型的JDBC操作提供模板方法,每个模板方法都能控制整个过程,并允许覆盖过程中的特定任务,通过这种方式尽可能的保留了灵活性,将数据库存取的工作量降到最低。
1、应用实践
1.1 导入相关pom依赖
<!--导入SpringIOC的核心依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.28</version>
</dependency>
<!--连接数据库的核心以来-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.19</version>
</dependency>
<!--druid数据源连接池的核心依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>
<!--SpringJDBC和事务的核心依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>5.3.28</version>
</dependency>
<!--测试单元的核心依赖-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
<scope>test</scope>
</dependency>
1.2 编写数据库信息的资源文件
db.driver=com.mysql.cj.jdbc.Driver
db.url=jdbc:mysql://localhost:3306/trs-db?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT%2B8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull
db.username=root
db.password=rootxq
1.3 编写Spring的核心配置文件
<?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 https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<!--加载外部属性资源文件-->
<context:property-placeholder location="classpath:db.properties"></context:property-placeholder>
<!--装配数据源-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${db.driver}"></property>
<property name="url" value="${db.url}"></property>
<property name="username" value="${db.username}"></property>
<property name="password" value="${db.password}"></property>
</bean>
<!--装配JdbcTemplate-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
</beans>
1.4 常用API的实践
- 通用的增删改方法:jdbcTemplate.update(Spring sql,Object…args)
- 通用的批处理增删改方法:jdbcTemplate.batchUpdate(String sql,List args)
- 查询单个值:jdbcTemplate.queryForObject(String sql,Class clazz,Object…args)
- 查询单个对象:jdbcTemplate.queryForObject(String sql,RowMapper rm,Object … args)
- 查询多个对象:jdbcTemplate.query(String sql,RowMapper rm,Object … args)
ClassPathXmlApplicationContext applicationContext = null;
@Before
public void before(){
applicationContext = new ClassPathXmlApplicationContext("spring-ioc-jdbc.xml");
}
@Test
public void queryForObject(){
//获取jdbcTemplate对象
JdbcTemplate jdbcTemplate = applicationContext.getBean(JdbcTemplate.class);
// 查询单个值
Long count = jdbcTemplate.queryForObject("select count(1) from users", Long.class);
System.out.println("查询结果:" + count);
//查询单个实体(实体属性与数据库字段名称一致)
User user1 = jdbcTemplate.queryForObject("select id ,u_name as name,salary from users where id = 1", new BeanPropertyRowMapper<>(User.class));
System.out.println(user1);
//查询单个实体(实体属性与数据库字段名称不一致)
User user2 = jdbcTemplate.queryForObject("select * from users where id = 2", (RowMapper<User>) (rs, rowNum) -> {
User u = new User();
u.setId(rs.getInt("id"));
u.setName(rs.getString("u_name"));
u.setSalary(rs.getDouble("salary"));
return u;
});
System.out.println(user2);
//查询列表(实体属性与数据库字段名称一致)
List<User> users = jdbcTemplate.query("select id ,u_name as name,salary from users", new BeanPropertyRowMapper<>(User.class));
System.out.println(users);
//查询列表(实体属性与数据库字段名称不一致)
List<User> users2 = jdbcTemplate.query("select * from users", (RowMapper<User>) (rs, rowNum) -> {
User u = new User();
u.setId(rs.getInt("id"));
u.setName(rs.getString("u_name"));
u.setSalary(rs.getDouble("salary"));
return u;
});
System.out.println(users2);
}
二、声明式事务
-
事务:是把一组业务当成一个业务来做,要么全都成功,要么全都失败,是保证业务操作完整性的一种数据库机制。
-
事务的ACID四大特性:
原子性
:在一组业务所有的操作步骤,要么全都成功,要么全都失败;一致性
:事务执行前后,要保证数据一致性,例如,两个人转账,转账前后他们的账户总额应该是不变的。隔离性
:在多个事务并发执行时,每个事务之间都是独立的,互不影响。持久性
:事务一旦执行成功,对数据的影响是不可逆的、永久性的。
-
事务分为
编程式事务
和声明式事务
两种:- 编程式事务:
- 指由用户自己通过代码来控制事务的处理过程,需要在代码中显式调用开启、提交、回滚事务等方法,可以使用TransactionTemplate来实现。
- 声明式事务:
- 在方法的外部添加注解或者直接在配置文件中定义,将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。
- 其本质就是通过AOP的功能,对方法前后进行拦截,将事务处理的逻辑编织在拦截的方法中。也就是在目标方法开始之前加入一个事务,然后根据目标方法的执行情况来提交或者回滚事务。
- 编程式事务:
1、声明式事务的配置
- Spring从不同的事务管理API中抽象出了一整套事务管理机制,让事务管理代码从特定的事务技术中独立出来。开发人员可以通过配置的方式进行事务管理。
- Spring的事务管理器是
PlatformTransactionManager
,它为事务管理封装了一组独立于技术的方法。无论使用Spring的哪种事务管理策略(编程式或声明式),事务管理器都是不可缺少的。
1.1 在配置文件中添加事务管理器
<?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 https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<!--加载外部属性文件-->
<context:property-placeholder location="classpath:db.properties"></context:property-placeholder>
<!--装配数据源-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${db.driver}"></property>
<property name="url" value="${db.url}"></property>
<property name="username" value="${db.username}"></property>
<property name="password" value="${db.password}"></property>
</bean>
<!--配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--开启事务注解的支持-->
<tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
</beans>
1.2 添加事务注解
- 在需要进行事务管理的方法或者类上添加事务注解:
@Transactional
- 用法:
- 可以标记在类上面,表示当前类中的所有方法都将被事务管理;
- 可以标记在某个方法上,表示当前方法被事务管理;
- 可以在类和方法上面同时标记:
- 如果类和方法上都存在@Transactional,则会以方法上面的为准。
- 如果方法上面没有@Transactional,则会以类上面的为准。
- 建议:
- @Transactional写在方法上面,控制粒度更细。
- @Transactional写在业务逻辑层上,因为只有业务逻辑层才会有嵌套调用的情况。
1.3 设置事务属性
1.3.1 设置事务隔离级别
-
设置事务隔离级别是为了解决并发事务过程中产生的一些问题,例如:脏读、幻读、不可重复读等等。
-
常见的事务问题:
- 脏读:一个事务读取到了另一个事务没有提交的数据,称为脏读;
- 例如:张三的工资到位了,总共1000元。他和他的媳妇儿同时收到了短信。事务A:表示他的媳妇儿从中银行卡取了200元,此时钱还没到手,并发事务B:张三立马去查询银行卡的余额信息,发现只有800元。而事务A在操作过程中出现了一些问题取钱失败,事务A回滚。实际上,银行卡的余额还是1000元,事务B读取到的就是一个脏数据。
- 不可重复读:一个事务前后多次读取同一行记录,但读取到的内容却不一样,称为不可重复读。因为并发事务对这行记录做了变更,所以前后读到的内容不同。
- 例如:张三的工资到位了,总共1000元。他和他媳妇儿同时收到了短信。事务A表示张三查询账户余额,事务B表示他媳妇儿查看账户并取钱。他们两同时查看账户信息,但是张三略微快一点点先看到余额是 1000元,他媳妇儿紧接着就取了200元,此时事务A还想再确认一下,就又看看了账户余额,发现是 800元。事务A前后两次看到的余额不一致。
- 幻读:一个事务前后多次读取整张表的数据,发现读取前后的内容多了几行或者少了几行,就好像发生幻觉了一样,称为幻读。
- 例如:张三公司要统计上个月给每个人发的工资情况,A事务表示人事部门统计薪资发放表,B事务表示财务部统计薪资发放表。两个部门同时统计,但是人事部门略微先统计完,统计结果为5000元,而财务部此时发现有个人漏发了,就补发了1000元,财务部拿到的结果是6000元。当事务A再次统计时,发现变成了6000元,前后统计到的结果不同。
- 脏读:一个事务读取到了另一个事务没有提交的数据,称为脏读;
-
事务隔离级别:
- DEFAULT:它是 PlatfromTransactionManager 默认的隔离级别。通常是数据库默认的隔离级别(REPEATABLE_READ)
@Transactional(isolation = Isolation.DEFAULT)
- READ_UNCOMMITTED:【读未提交】,最低的隔离级别。它允许一个事务读取另一个事务未提交的数据。会导致脏读、不可重复读和幻读的问题。
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
- READ_COMMITTED:【读已提交】,保证一个事务只能读取其他事务已经提交的数据,不能读取到未提交的数据。避免了脏读,但是仍然会有不可重复读、幻读的问题。
@Transactional(isolation = Isolation.READ_COMMITTED)
- REPEATABLE_READ:【可重复读】,这种隔离级别保证一个事务在执行期间,其他事务不能对这个事务所操作的行记录做修改,从而保证这个事务多次读取到的内容是一致的。实际上:就是用了一个行锁将这行记录给锁起来了。
@Transactional(isolation = Isolation.REPEATABLE_READ)
- SERIALIZABLE:【串行化】,最高的隔离级别。它通过强制事务串行执行,避免了所有可能的并发问题,但也最大限度地降低了系统的并发能力。
- 确保事务A可以多次从一个表中读取到相同的行,在事务A执行期间禁止其它事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但性能十分低下。实际上:就是用表锁把整个表都给锁了。
@Transactional(isolation = Isolation.SERIALIZABLE)
- DEFAULT:它是 PlatfromTransactionManager 默认的隔离级别。通常是数据库默认的隔离级别(REPEATABLE_READ)
1.3.2 设置事务传播行为
事务的传播特性指的是当一个事务方法被另一个事务方法调用(事务方法嵌套)时,需要指定事务应该如何传播。
/**
* 转账
*/
@Transactional
public void transferMoney(){
//扣款
subMoney();
//记录转账流水日志
addLog();
//加款
addMoney();
}
@Transactional
public void subMoney(){
System.out.println("扣除转账人的银行账户1000元");
}
public void addLog(){
System.out.println("记录转账的银行交易流水日志");
}
@Transactional
public void addMoney(){
System.out.println("收款人银行账户加款1000元");
}
此时,所有的方法都有事务,每个事务方法执行时事务应该怎么来控制?
- 事务的7种事务传播行为:
REQUIRED(默认)
:- 如果外部不存在事务,就开启一个自己的新事务。如果外部存在事务,就融合到外部的事务中(以外部事务为准)。
@Transactional(propagation = Propagation.REQUIRED)
- 适用于增删改查操作。
SUPPORTS
:- 如果外部不存在事务,就不开启事务。如果外部存在事务,就融合到外部的事务中。
@Transactional(propagation = Propagation.SUPPORTS)
- 适用于查询操作。
REQUIRES_NEW
:- 如果外部不存在事务,就开启一个自己的新事务。如果外部存在事务,就将外部事务挂起(外部事务暂不执行),创建一个自己的新事务去执行。
@Transactional(propagation = Propagation.REQUIRES_NEW)
- 适用于内部事务,例如:记录日志时,无论其他方法执行成功失败,都需要记录日志,而不是回滚,这时候,记录日志的方法就需要玩自己的事务,而不跟着外部事务走。
- 注意:当涉及到事务挂起时,要求外部方法和内部方法必须存在不同的类中;
NOT_SUPPORTED
:- 如果外部不存在事务,也不会开启自己的新事务。如果外部存在事务,则挂起外部事务。
@Transactional(propagation = Propagation.NOT_SUPPORTED)
NEVER
:- 如果外部不存在事务,也不会开启自己的新事务。如果外部存在事务,则抛出异常。
@Transactional(propagation = Propagation.NEVER)
MANDATORY
:- 如果外部不存在事务,则抛出异常。如果外部存在事务,就融合到外部的事务中。
@Transactional(propagation = Propagation.MANDATORY)
NESTED
:- 如果外部不存在事务,则执行与REQUIRED类似的操作。如果外部存在事务,就融合到外部事务中,以嵌套事务的方式执行。
@Transactional(propagation = Propagation.NESTED)
1.3.3 设置超时属性
- 指定事务等待的最长时间(秒)
- 当前事务访问数据时,有可能访问的数据被别的事务进行了加锁的处理,那么此时事务就
必须等待,如果等待时间过长给用户造成的体验感差。 @Transactional(timeout = 5)
1.3.4 设置事务只读
- 一般事务方法中只有查询操作时才将事务设置为只读;
- 当将事务设置只读 就必须要要求你的业务方法中没有增删改的操作。由于只
读事务不存在数据的修改,数据库将会为只读事务提供一些优化手段。 - 默认值:readonly = false
@Transactional(readOnly = true)
1.3.4 设置事务异常回滚
- 设置当前事务出现的那些异常就进行回滚或者提交。
- 默认对于RuntimeException 及其子类 采用的是回滚的策略。
- 默认对于Exception 及其子类 采用的是提交的策略。
- rollbackFor:设置发生异常时需要回滚的异常类型
@Transactional(rollbackFor = NullPointerException.class)
- noRollbackkFor:设置发生异常时不需要回滚的异常类型
@Transactional(noRollbackkFor = NullPointerException.class)
2、基于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"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--如果基于注解和XML的事务配置都存在,则会以注解的为主-->
<!--基于XML的事务配置-->
<!--用于声明事务切入的所有方法-->
<aop:config>
<aop:pointcut id="transactionCutPoint" expression="execution(* org.example.mvc.impl.*.*(..))"/>
</aop:config>
<!--用来明确切点匹配到的哪些方法需要使用事务-->
<tx:advice>
<tx:attributes>
<!--可以使用通配符-->
<tx:method name="add*"/>
<tx:method name="update*" timeout="5" isolation="REPEATABLE_READ"/>
<tx:method name="query*" propagation="SUPPORTS"/>
<tx:method name="delete*" read-only="true"/>
</tx:attributes>
</tx:advice>
</beans>