- 一、Spring对持久层技术支持
- 1、Spring支持的持久层技术
- 2、Spring JDBC
- 2.1、 JDBCTemplate类
- 2.2、Spring JDBC CRUD操作
- 2.3、Spring提供的JdbcDaoSupport
- 二、Spring中的事务控制
- 1、引出事务
- 2、事务回顾
- 3、Spring对事务的管理API
- 4、事务传播规则
Spring系列
- Spring — Spring简介、入门、配置 , IoC和DI思想
- Spring — IoC核心(基于XML)、DI核心(基于XML)
- Spring — 使用IoC和DI模拟注册案例、注解配置IoC和DI
- Spring — 静态代理、动态代理、拦截器思想
- Spring — AOP思想、AOP开发、Pointcut语法、注解配置AOP
- Spring — DAO层、Spring JDBC、Spring事务控制
- Spring — XML配置事务、注解+XML、纯注解的配置方式
- Spring整合MyBatis
- Spring Java Config — 组件注册相关注解
- Spring Java Config — 常用注解
跳转到目录 很多持久层技术,单独使用的话,操作API会很麻烦, 有了Spring的支持,操作起来就会很简单; 更强大的是: Spring提供了对事务的支持
跳转到目录 Spring自身并没有提供持久层框架,但是提供了和持久层技术无缝整合的API;
跳转到目录
2.1、 JDBCTemplate类跳转到目录
- 使用方法和QueryRunner基本一致
- 构造方法传递数据源DataSource对象
- API方法
- update(String sql, Object…obj)执行insert,update,delete语句
- queryForObject(String sql,RowMapper mapper,Object…obj)查询返回单个对象
- queryForObject(String sql,Class cla,Object…obj)查询返回单个对象,基本类型及其包装类和字符串
- query(String sql,RowMapper mapper,Object…obj)查询返回集合对象
- RowMapper接口实现类BeanPropertyRowMapper,查询的结果集封装,适用单个对象或者集合
跳转到目录 数据库
CREATE TABLE `employee` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(40) NOT NULL,
`age` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8;
Java代码
// Dao包
public interface EmployeeDao1 {
void save(Employee1 emp);
void update(Employee1 emp);
void delete(Long id);
Employee1 get(Long id);
List listAll();
}
public class EmployeeDaoImpl1 implements EmployeeDao1 {
// =================核心===================
private JdbcTemplate jdbcTemplate;
// 属性: dataSource (通过setter注入ds的值)
public void setDataSourse(DataSource ds) {
this.jdbcTemplate = new JdbcTemplate(ds);
}
// =================核心===================
@SuppressWarnings("unchecked")
public void save(Employee1 emp) {
String sql = "INSERT INTO employee (name, age) VALUES (?, ?)";
jdbcTemplate.update(sql, emp.getName(), emp.getAge());
/*
// 包含了通过名称占位符的模板方法,简化开发;适合?比较多的情况
NamedParameterJdbcTemplate namedParameterJdbcTemplate = null;
namedParameterJdbcTemplate.update("INSERT INTO employee (name,age) VALUES (:ename,:eage)", new HashMap(){{
this.put("ename", emp.getName());
this.put("eage", emp.getAge());
}});*/
}
public void update(Employee1 emp) {
String sql = "UPDATE employee SET name = ?, age = ? WHERE id = ?";
jdbcTemplate.update(sql, emp.getName(), emp.getAge(), emp.getId());
}
public void delete(Long id) {
String sql = "DELETE from employee WHERE id = ?";
jdbcTemplate.update(sql, id);
}
public Employee1 get(Long id) {
List list = jdbcTemplate.query("SELECT id, name, age FROM employee WHERE id = ?", new Object[]{id},
new RowMapper() {
public Employee1 mapRow(ResultSet resultSet, int i) throws SQLException {
Employee1 emp = new Employee1();
emp.setId(resultSet.getLong("id"));
emp.setName(resultSet.getString("name"));
emp.setAge(resultSet.getInt("age"));
return emp;
}
});
return list.size() == 1 ? list.get(0) : null;
/*Employee1 employee1 = jdbcTemplate.queryForObject("SELECT id, name, age FROM employee WHERE id = ?",
new Object[]{id},new RowMapper() {
public Employee1 mapRow(ResultSet rs, int i) throws SQLException {
Employee1 emp = new Employee1();
emp.setId(rs.getLong("id"));
emp.setName(rs.getString("name"));
emp.setAge(rs.getInt("age"));
return emp;
}
});
return employee1;*/
}
public List listAll() {
return jdbcTemplate.query("SELECT id, name, age FROM employee", new RowMapper() {
// 把每一行的结果集映射成一个Employee对象
public Employee1 mapRow(ResultSet resultSet, int i) throws SQLException {
Employee1 emp = new Employee1();
emp.setId(resultSet.getLong("id"));
emp.setName(resultSet.getString("name"));
emp.setAge(resultSet.getInt("age"));
return emp;
}
});
}
}
xml
测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class SpringJdbcTest {
@Autowired //表示将xml中创建的dao对象,注入到下面的变量中
private EmployeeDao1 dao;
@Test
public void testSave(){
Employee1 emp = new Employee1();
emp.setName("文");
emp.setAge(21);
dao.save(emp);
}
@Test
public void testUpdate(){
Employee1 emp = new Employee1();
emp.setName("西门吹风");
emp.setAge(43);
emp.setId(10L);
dao.update(emp);
}
@Test
public void testDelete(){
dao.delete(11L);
}
@Test
public void testGet(){
Employee1 employee1 = dao.get(10L);
System.out.println(employee1);
}
@Test
public void testListAll(){
List employee1s = dao.listAll();
for (Employee1 employee1 : employee1s) {
System.out.println(employee1);
}
}
}
通过查询操作,在代码中没有迭代结果集的操作;那么是谁在迭代结果集并且设置属性后,又是谁把创建出来的每一个Employee对象放到List集合中去的呢?
跳转到目录
JdbcDaoSupport类中定义了JdbcTemplate类并提供了get方法,我们dao层类直接继承即可,自己无需在声明JdbcTemplate对象;
public class EmployeeDaoImpl1ByJdbcTemplate extends JdbcDaoSupport implements EmployeeDao1 {
public void save(Employee1 emp) {
String sql = "INSERT INTO employee (name, age) VALUES (?, ?)";
super.getJdbcTemplate().update(sql, emp.getName(), emp.getAge());
}
xml
Dao的bean元素依然要配置dataSource属性
二、Spring中的事务控制跳转到目录
看这篇: https://www.yuque.com/baiyunhecanggou/qnvllg/gw00gg#6a6a86f6
1、引出事务跳转到目录 银行转账案例: 需求: 从id为10086账户给id为10010账户转1000元
- 数据库表设计略;
- Java代码
// domain包
@Data
public class Account {
private Long id;
private int balance;
}
// Dao包
public interface AccountDao {
/**
* 从指定账户转出多少钱
* @param outId
* @param money
*/
void transOut(Long outId, int money);
/**
* 从指定账户转入多少钱
* @param inId
* @param money
*/
void transIn(Long inId, int money);
}
public class AccountDaoImpl implements AccountDao {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource ds){
this.jdbcTemplate = new JdbcTemplate(ds);
}
public void transOut(Long outId, int money) {
String sql = "UPDATE account SET balance = balance - ? WHERE id = ?";
jdbcTemplate.update(sql, money, outId);
}
public void transIn(Long inId, int money) {
String sql = "UPDATE account SET balance = balance + ? WHERE id = ?";
jdbcTemplate.update(sql, money, inId);
}
}
// Service包
public interface AccountService {
/**
* 从指定账户转出指定金额给另一个账户
* @param outId
* @param inId
* @param money
*/
void trans(Long outId, Long inId, int money);
}
public class AccountServiceImpl implements AccountService {
private AccountDao dao;
public void setDao(AccountDao dao) {
this.dao = dao;
}
public void trans(Long outId, Long inId, int money) {
dao.transOut(outId, money);
int a = 1 / 0; // 抛出异常
dao.transIn(inId, money);
}
}
xml文件
测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class SpringTxTest {
@Autowired
private AccountService service;
@Test
public void test(){
service.trans(10086L, 10010L, 1000);
}
}
结果
分析原因:
出现问题的原因: 转入和转出的操作不在同一个事务之中;
跳转到目录 Spring的事务机制是Spring给我们提供的一套事务管理的方式,项目中我们就不需要手动去控制事务了
编程式事务:我们的事务控制逻辑(增强逻辑)和业务逻辑混合在一起,比如我们之前的tcf模式控制转账事务,这种方式就叫做编程式事务
声明式事务:通过配置,在不侵犯原有业务逻辑代码的基础上就添加了事务控制功能,这种方式叫做声明式事务(我们这里的Spring声明式事务控制就是通过AOP达到这个目的的)
关于事务事务(Transaction,简写为tx):在数据库中,事务是指一组逻辑操作,不论成功与失败都作为一个整体进行工作,要么全部执行成功,要么全部不执行(执行失败)。
-
事务基本特性(ACID,是针对单个事务的一个完美状态)
- 原子性:一个事务内的操作,要么都成功,要么都失败。很经典的例子:转账,汇款和收款要成功都成功,要失败都失败。
- 一致性:指的是数据的一致性,和原子性其实是一件事情,只不过描述的角度不一样,原子性是从事务的操作的角度,一致性是从数据的角度来描述的,比如转账之前(1000,1000),如果转账100,那么数据状态应该是(900、1100),不应该出现中间状态(900,1000)或者(1000,1100)
- 隔离性:事务并发的时候,比如事务1做的动作给员工涨工资2000块,但是此时事务还没有提交,事务2去查询工资发现工资多了2000块,这就是脏读。解决方法就是建立事务之间的隔离机制。
- 持久性:事务一旦提交,事务提交,变化即生效。即使数据库服务器宕机,那么恢复之后,数据也应该是事务提交之后的状态,不应该回滚到以前了。
-
事务并发问题
-
脏读
财务人员今天心情不好,状态不好,误操作发起事务1给员工张三本月涨了1w块钱工资,但是还没有提交事务
张三发起事务2,查询当月工资,发现多了1W块钱,涨工资了,财务人员发现不对劲,把操作撤回,把涨工资的事务1给回滚了
-
幻读(幻读出现在增加insert和删除delete的时候)
- 比如事务1查询工资表中工资为1w的员工的个数(10个员工),此时事务1还没有结束
- 正在这个时候,事务2,人力部门有两个新员工入职,他们的工资也是1w,人力部门通过事务2向工资表插入了两条记录,并且提交事务了
- 这个时候,事务1又去查询工资为1w的员工个数,发现多了两个员工(12个人),见鬼了,这种情况就叫做幻读
-
不可重复读(出现在修改update的时候)
- 员工发起事务1查询工资,工资为1w,事务1尚未关闭
- 人力部门发起事务2给你涨了工资,涨工资到1.2W(update你的工资表的字段信息),并且提交了事务了。
- 此时,事务1又再次查询自己的工资,发现工资为1.2W,原有的1w这个数据已经读不到了,这就叫做不可重复读
-
-
事务隔离级别(解决是事务并发问题的) 由低到高分别为 Read uncommitted > Read committed > Repeatable read > Serializable 。
-
Read_uncommited (读未提交),就好比十字路口没有红绿灯一样,效率高,但是风险也高,此时什么事务控制都没有。不要使用这种模式
-
Read_commited (读已提交),顾名思义,其他事务提交之后,才能读取到这个事务提交的数据,这种模式能解决脏读(因为脏读事务没提交造成的)问题,解决不了幻读和不可重复读(因为这两个问题的产生就是insert delete update的时候提交了事务造成)
-
Repeatable_Read (可重复读),可重复读解决脏读和不可重复读
-
Serializable (可序化):所有的事务一个个来,不争不抢,一个事务处理完了,另外一个事务继续进行,这样不会出现并发问题。比如ATM机
MySQL数据库默认隔离级别可重复读
Repeatable_Read
Oracle数据库默认级别读已提交Read_commited
-
-
设置事务隔离级别
- 1 read uncommitted 未提交读,脏读,不可重复读,虚读都可能发生.
- 2 read committed 已提交读,避免脏读,但是不可重复读和虚读有可能发生(Oracle默认)
- 4 repeatable read 可重复读,避免脏读,不可重复读,但是虚读有可能发生(MySql默认)
- 8 serializable 串行化的,避免脏读,不可重复读,虚读的发生
- 查看当前的事务隔离级别:SELECT @@TX_ISOLATION;
- 更改当前的事务隔离级别:SET TRANSACTION ISOLATION LEVEL 四个级别之一
隔离级别越高,数据库事务并发执行性能会越差,在项目中为了考虑并发性能一般使用 Read committed(读已提交),它能避免丢失更新和脏读,但是不可重复读和幻读不可避免。更多情况下使用悲观锁或乐观锁来解决。
跳转到目录 Spring 支持编程式事务和声明式事务管理两种方式,这里学习声明式事务管理。
Spring 声明式事务管理建立在AOP
之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。
1、Spring 事务管理主要有3个接口:
-
TransactionDefinition接口:
在Spring中,事务是通过TransactionDefinition接口来定义的,该接口包含与事务属性相关的方法,TransactionDefinition定义了五个表示隔离级别的常量,代表传播行为的常量,在TransactionDefinition中以int值表示超时时间。
-
PlatformTransactionManager接口:
Platform TransactionManager.getInstance()方法返回一个Transaction Status对象,返回的Transaction Status对象可能代表一个新的或已经存在的事务(如果当前调用堆栈中有一个符合条件的事务)。
-
Transaction Status接口:
封装了一些控制事务查询和执行的方法。
2、使用Spring管理事务时,首先需要告诉Spring使用哪一个事务管理器 常用的事务管理器:
- DataSourceTransactionManager:支持JDBC和MyBatis
- HiberbateTransactionManager:支持Hibernate
3、Spring 事务回滚规则 指示Spring 事务管理器回滚一个事务的推荐方法是在当前事务的上下文内抛出异常。spring事务管理器会捕捉任何未处理的异常,然后依据规则决定是否回滚抛出异常的事务。
默认配置下,spring只有在抛出的异常为运行时异常时才回滚该事务,也就是抛出的异常为RuntimeException的子类(Errors也会导致事务回滚),而抛出checked异常则不会导致事务回滚。可以明确的配置在抛出那些异常时回滚事务,包括checked异常。也可以明确定义那些异常抛出时不回滚事务。还可以编程性的通过setRollbackOnly()方法来指示一个事务必须回滚,在调用完setRollbackOnly()后你所能执行的唯一操作就是回滚。
4、事务传播规则跳转到目录 所谓事务的传播行为是指,如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为。在TransactionDefinition定义中包括了如下几个表示传播规则的常量:
情况一: 需要/遵从当前事务
TransactionDefinition.PROPAGATION_REQUIRED
:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是默认值。- TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
- TransactionDefinition.PROPAGATION_MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
情况二: 不遵从当前事务
TransactionDefinition.PROPAGATION_REQUIRES_NEW
:不管当前是否存在事务,都会新开启一个事务,必须是一个新的事务;- TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
- TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
情况三: 寄生事务
- TransactionDefinition.PROPAGATION_NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于- TransactionDefinition.PROPAGATION_REQUIRED。
所谓事务超时,就是指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。在 TransactionDefinition 中以 int 的值来表示超时时间,其单位是秒。
事务类型1)本地事务和分布式事务 本地事务:就是普通事务,能保证单台数据库上操作的ACID,被限定在一台数据库上。 分布式事务:涉及多个数据库源的事务,即跨越多台同类或异类数据库的事务(由每台数据库的本地事务组成的),分布式事务旨在保证这些本地事务的所有操作的ACID,是事务可以跨越多台数据库。
2)JDBC事务和JTA事务 JDBC事务:就是数据库事务类型中的本地事务,通过 Connection对象的控制来管理事务。 JTA事务:JTA(Java Transaction API)是Java EE数据库事务规范,JTA只提供了事务管理接口,由应用程序服务器厂商提供实现,JTA事务比JDBC更强大,支持分布式事务。
3)通过编程实现事务可分为编程式事务和声明式事务 编程式事务:通过编写代码来管理事务 声明式事务:通过注解或XML配置来管理事务