Spring5学习笔记(四、持久层整合与事务处理)

文章目录

  • Spring与持久层
  • Spring与MyBatis整合
    • MyBatis开发步骤的回顾
      • MyBatis在开发过程中存在的问题
    • Spring与MyBatis整合思路分析
    • Spring与MyBatis整合的开发步骤
    • Spring与Mybatis整合细节
  • Spring的事务处理
      • 什么是事务?
    • 如何控制事务?
    • Spring控制事务的开发
      • 1. 原始对象
      • 2. 额外功能
      • 3. 切入点
      • 4. 组装切面
    • Spring控制事务的编码
  • Spring中的事务属性(Transaction Attribute)
    • 事务属性详解
      • 1. 隔离属性(isolation)
        • 事务并发产生的问题
        • 数据库对应隔离属性的支持
        • 默认的隔离属性
      • 2. 传播属性(propagation)
        • 传播属性的值(7个)及其用法
      • 3. 只读属性(readOnly)
      • 4. 超时属性(timeout)
      • 5. 异常属性(rollbackFor、noRollbackFor)
      • 事务属性常见配置总结
    • 基于标签的事务配置方式(事务开发的第二种形式)
      • 基于标签的事务配置在实战中的引用方式

Spring与持久层

Spring框架为什么要与持久层技术进行整合?

  1. JavaEE开发需要持久层进行数据库的访问操作。
  2. JDBC、Hibernate、MyBatis进行持久开发过程存在大量的代码冗余。
  3. Spring基于模板设计模式对于上述的持久层技术进行了封装。

Spring可以与哪些持久层技术进行整合?

1. JDBC
	|-	JDBCTemplate

2. Hibernate(JPA)
	|-	HibernateTemplate

3. MyBatis
	|-	SqlSessionFactoryBean	MapperScannerConfigure

因为MyBatis是目前用得最多的持久层框架,所以这里只讲与MyBatis的整合。

Spring与MyBatis整合

MyBatis开发步骤的回顾

-------七步骤--------

  1. 实体
  2. 实体别名
  3. 创建DAO接口
  4. 实现Mapper文件
  5. 注册Mapper文件
  6. MyBatisAPI调用

引入mysql和mybatis的jar包,搭建开发环境

<dependency>
    <groupId>mysqlgroupId>
    <artifactId>mysql-connector-javaartifactId>
    <version>8.0.18version>
dependency>
<dependency>
    <groupId>org.mybatisgroupId>
    <artifactId>mybatisartifactId>
    <version>3.5.3version>
dependency>

创建mybatis配置文件



<configuration>
    
    <typeAliases>
        
    typeAliases>

    
    <environments default="mysql">
        <environment id="mysql">
            <transactionManager type="JDBC">transactionManager>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url"
                          value="jdbc:mysql://localhost:3306/angenin?useSSL=false&serverTimezone=Asia/Shanghai"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            dataSource>
        environment>
    environments>

    
    <mappers>
        
    mappers>
configuration>

七步骤

  1. 创建User实体类
    	public class User implements Serializable {
           
        private Integer id;
        private String name;
        private String password;
    
        //为了简洁,这里略写
    
        //有参无参
        //get/set
    }
    
  2. 在配置文件中设置User实体类的别名
    
    <typeAliases>
        <typeAlias alias="user" type="com.angenin1.mybatis.User"/>
    typeAliases>
    
  3. 创建表
    use angenin;
    
    create table t_users(
    	`id` int primary key auto_increment,
    	`name` varchar(12),
    	`password` varchar(12)
    );
    
    desc t_users;
    
  4. 创建UserDAO接口
    public interface UserDAO {
           
        void save(User user);
    }
    
  5. 实现UserMapper文件
    
    
    <mapper namespace="com.angenin1.mybatis.UserDAO">
        <insert id="save" parameterType="user">
            insert into t_users(`name`,`password`)values(#{name},#{password});
        insert>
    mapper>
    
  6. 在配置文件中注册Mapper文件
    
    <mappers>
        <mapper resource="UserMapper.xml"/>
    mappers>
    
  7. MyBatisAPI调用(测试)
    public class TestMyBatis {
           
        public static void main(String[] args) throws IOException {
           
            // 读取主配置文件
            InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
            // 获取sqlSession工厂对象
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
            // 获取sqlSession对象
            SqlSession session = sqlSessionFactory.openSession();
            // 获取UserDAO对象
            UserDAO userDAO = session.getMapper(UserDAO.class);
    
            // 创建用户
            User user = new User();
            user.setName("angenin");
            user.setPassword("123456");
            // 保存用户
            userDAO.save(user);
    
            // 提交事务
            session.commit();
        }
    }
    
    运行结果:
    在这里插入图片描述
    Spring5学习笔记(四、持久层整合与事务处理)_第1张图片

MyBatis在开发过程中存在的问题

  1. 配置繁琐:第2步起别名和第6步注册mapper文件。(其实MyBatis也可以通过指定包来解决这个问题)
  2. 代码冗余:第7步API的调用。

Spring与MyBatis整合思路分析

Spring5学习笔记(四、持久层整合与事务处理)_第2张图片
Spring5学习笔记(四、持久层整合与事务处理)_第3张图片

Spring与MyBatis整合的开发步骤

搭建环境


<dependency>
    <groupId>mysqlgroupId>
    <artifactId>mysql-connector-javaartifactId>
    <version>8.0.18version>
dependency>
<dependency>
    <groupId>org.mybatisgroupId>
    <artifactId>mybatisartifactId>
    <version>3.5.3version>
dependency>


<dependency>
    <groupId>org.springframeworkgroupId>
    <artifactId>spring-jdbcartifactId>
    <version>5.1.14.RELEASEversion>
dependency>
<dependency>
    <groupId>org.mybatisgroupId>
    <artifactId>mybatis-springartifactId>
    <version>2.0.2version>
dependency>
<dependency>
    <groupId>com.alibabagroupId>
    <artifactId>druidartifactId>
    <version>1.1.18version>
dependency>
  • Spring配置文件的配置

    
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/angenin?useSSL=false&serverTimezone=Asia/Shanghai"/>
        <property name="username" value="root"/>
        <property name="password" value="123456"/>
    bean>
    
    
    <bean id="sqlSessionFactoryBean" class="org.mybatis.spring.SqlSessionFactoryBean">
        
        <property name="dataSource" ref="dataSource"/>
        
        <property name="typeAliasesPackage" value="com.angenin1.entity"/>
        
        <property name="mapperLocations">
            <list>
                <value>classpath:com.angenin1.mapper/*Mapper.xmlvalue>
            list>
        property>
    bean>
    
    
    <bean id="scanner" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactoryBean"/>
        
        <property name="basePackage" value="com.angenin1.dao"/>
    bean>
    
  • 编码

    -------七步骤--------

    1. 实体
    2. 实体别名
    3. 创建DAO接口
    4. 实现Mapper文件
    5. 注册Mapper文件
    6. MyBatisAPI调用

    七步变四步

    1. 实体
    2. 创建DAO接口
    3. 实现Mapper文件
  1. 创建entity包,在其包下创建User实体类
    public class User implements Serializable {
           
        private Integer id;
        private String name;
        private String password;
    	//get set
    }
    
  2. 创建t_users表
    # 上面已经创建了,继续用t_users表
    create table t_users(
    	`id` int primary key auto_increment,
    	`name` varchar(12),
    	`password` varchar(12)
    );
    
  3. 创建dao包,在其包下创建UserDAO接口
    public interface UserDAO {
           
        void save(User user);
    }
    
  4. 在resources目录下创建com.angenin1.mapper目录(注意:resources目录下多级目录用/分隔,这里用.,所以不是多级目录,只是一个目录),用于存放mapper文件,在这个目录下创建UserMapper.xml文件
    
    
    <mapper namespace="com.angenin1.dao.UserDAO">
    
        <insert id="save" parameterType="User">
            insert into t_users(`name`,`password`)values(#{name},#{password});
        insert>
    
    mapper>
    
  5. 测试
    public class TestMyBatisSpring {
           
        @Test
        public void test() {
           
            ApplicationContext ctx = new ClassPathXmlApplicationContext("/applicationContext5.xml");
            UserDAO userDAO = (UserDAO) ctx.getBean("userDAO");
    
            User user = new User();
            user.setName("angenin");
            user.setPassword("11");
    
            userDAO.save(user);
        }
    }
    
    运行结果:
    Spring5学习笔记(四、持久层整合与事务处理)_第4张图片
    Spring5学习笔记(四、持久层整合与事务处理)_第5张图片

Spring与Mybatis整合细节

问题:Spring与Mybatis整合后,为什么DAO不提交事务,但是数据能够插入数据库中?

谁创建的Connection,谁就控制着事务(tx)。

本质上控制连接对象(Connection)--> 连接池(DataSource)

之前由MyBatis提供的连接池对象 --> 创建Connection
	Connection.setAutoCommit(false)	需要我们手动控制事务,操作完成后手工调用commit进行提交。

而现在由Druid(C3P0、DBCP)作为连接池	--> 创建Connection
	Connection.setAutoCommit(true)	默认true,保持着自动控制事务,每条sql提交一次。

答案:因为Spring与MyBatis整合时,引入了外部连接池对象,保存自动的事务提交这个机制(Connection.setAutoCommit(true)),不需要手工进行事务的操作,也能进行事务的提交。

注意:未来实战中,还会手工控制事务(多条sql一起成功,一起失败),后续Spring通过事务控制解决这个问题。

Spring的事务处理

什么是事务?

保证业务操作完整性的一种数据库机制。

事务的4个特点:A C I D
A:原子性
C:一致性
I:隔离性
D:持久性
口诀:原子一致才能隔离持久。(个人口诀,不喜勿喷( ̄▽ ̄)")

如何控制事务?

JDBC:
	Connection.setAutoCommit(false);	//开启事务,关闭自动提交
	Connection.commit();	//提交
	Connection.rollback();	//回滚

MyBatis:
	MyBatis自动开启事务
	sqlSession.commit();	//提交
	sqlSession.rollback();	//回滚
	注意:sqlSession底层封装了Connection,所以还是调用了Connection。

结论:控制事务的底层都是Connection对象完成的

Spring控制事务的开发

Spring是通过AOP的方式进行事务开发的。

1. 原始对象

public class XxxServiceImpl() {
     
	private XxxDao xxxDao;
	//set get
	
	public 原始方法() {
     
		核心功能;
	}
}		

要注意的细节:

  1. 原始对象 --> 原始方法 --> 核心功能(业务处理+DAO调用)
  2. 因为Service依赖DAO对象,所以需要把DAO作为Service的成员变量,并提供get/set方法,用依赖注入(set注入)的方式进行赋值。

2. 额外功能

  1. 实现MethodInterceptor接口
    public class MyAround implements MethodInterceptor {
           
    	public Object invoke(MethodInvocation methodInvocation) {
           
    		Object ret = null;
    		try {
           
    			// 开启事务
    			Connection.setAutoCommit(false);
    			// 执行原始方法
    			ret = methodInvocation.proceed();
    			// 提交
    			Connection.commit();
    		} catch(Exception e) {
           
    			// 事务回滚
    			Connection.rollback();
    		}
    		return ret;
    	}
    }
    
    
    <bean id="around" class="com.angenin.dynamic.MyAround"/>
    
    
    <aop:config>
        
        <aop:pointcut id="pc" expression="execution(* *(..)"/>
        
        <aop:advisor advice-ref="around" pointcut-ref="pc"/>
    aop:config>
    
  2. 切面类,使用@Aspect@Around注解
    @Aspect	//切面类
    public class MyAspect {
           
    	
    	@Around("execution(* *(..))")
    	public Object myAround(ProceedingJoinPoint joinPoint) {
           
    		Object ret = null;
    		try {
           
    			// 开启事务
    			Connection.setAutoCommit(false);
    			// 执行原始方法
    			ret = joinPoint.proceed();
    			// 提交
    			Connection.commit();
    		} catch(Exception e) {
           
    			// 事务回滚
    			Connection.rollback();
    		}
    		return ret;
    	}
    }
    
    
    <bean id="around" class="com.angenin.aspect.MyAspect"/>
    
    <aop:aspectj-autoproxy />
    

而Spring在也帮我们封装了额外功能的这些内容,我们只要调用org.springframework.jdbc.datasource.DataSourceTransactionManager,而由于需要Connection对象,需要等于依赖,依赖就要注入,所以需要注入Connection对象,而又由于Connection对象来着连接池,所以注入连接池即可。

所以以后我们只需要:

  1. 调用org.springframework.jdbc.datasource.DataSourceTransactionManager
  2. 注入连接池
    
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        ...
    bean>
    
    <bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    bean>

3. 切入点

使用DataSourceTransactionManager后,切入点由@Transactional注解决定。

@Transactional:事务的额外功能加入给哪些业务方法。

  1. 加在类上:这个类中的所有方法都会有额外功能,即加入事务。
  2. 加在方法上:这个方法会有额外功能,即加入事务。

4. 组装切面

切入点 + 额外功能


<tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>

组装切入点:会自动扫描@Transactional注解。

Spring控制事务的编码

  • 搭建开发环境

    
    <dependency>
        <groupId>org.springframeworkgroupId>
        <artifactId>spring-txartifactId>
        <version>5.1.14.RELEASEversion>
    dependency>
    
  • 原始对象
    创建service包,然后新建UserService和UserServiceImpl类

    public interface UserService {
           
        void register(User user);
    }
    
    public class UserServiceImpl implements UserService {
           
        private UserDAO userDAO;
    
        // 原始方法
        @Override
        public void register(User user) {
           
            // 核心功能
            userDAO.save(user);
        }
    
        public UserDAO getUserDAO() {
           
            return userDAO;
        }
    
        public void setUserDAO(UserDAO userDAO) {
           
            this.userDAO = userDAO;
        }
    }
    
        <bean id="userService" class="com.angenin.service.UserServiceImpl">
        
        <property name="userDAO" ref="userDAO"/>
    bean>
    
  • 额外功能

    <bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    bean>
    
  • 切入点
    在UserServiceImpl类上加上@Transactional注解,把整个类作为切入点。

  • 组装切面
    Spring5学习笔记(四、持久层整合与事务处理)_第6张图片

    
    <tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>
    
  • 测试

    @Test
    public void test02() {
           
        ApplicationContext ctx = new ClassPathXmlApplicationContext("/applicationContext5.xml");
        UserService userService = (UserService) ctx.getBean("userService");
        User user = new User();
        user.setName("angenin11");
        user.setPassword("123456");
        userService.register(user);
    }
    

    Spring5学习笔记(四、持久层整合与事务处理)_第7张图片

  • 细节

<tx:annotation-driven transaction-manager="dataSourceTransactionManager" proxy-target-class="true"/>

proxy-target-class属性:进行动态代理底层实现的切换
false:JDK代理(默认)
true:Cglib代理

Spring中的事务属性(Transaction Attribute)

属性:描述物体特征的一系列值。如一个人的属性有身高、体重、性别等。
事务属性:描述事务特征的一系列值,共5个。

  1. 隔离属性
  2. 传播属性
  3. 只读属性
  4. 超时属性
  5. 异常属性

添加事务属性:
@Transactional(isolation = xxx, propagation = xxx, readOnly = xxx, timeout = xxx, rollbackFor = xxx, noRollbackFor = xxx)

事务属性详解

1. 隔离属性(isolation)

隔离属性:描述了事务解决并发问题的特征。

  1. 什么是并发?
    多个事务(用户)在同一时间访问操作了相同的数据。
    同一时间:0.000几秒的差异,有 微小的前 和 微小的后 之分。

  2. 并发会产生哪些问题?

    1. 脏读
    2. 不可重复读
    3. 幻读
  3. 并发问题如何解决?
    通过隔离属性解决,隔离属性中设置不同的值,解决并发处理过程中的问题。

事务并发产生的问题

  1. 脏读
    一个事务,读取了另一个事务中还没提交的数据,会在本事务中产生数据不一致的问题。

    解决方案:@Transactional(isolation = Isolation.READ_COMMITTED)
    Isolation.READ_COMMITTED:只能读取已提交的数据。

  2. 不可重复读
    一个事务,多次读取相同的数据,但读取的结果不一样(可能是别的事务修改了这条数据),会在本事务中产生数据不一致的问题。
    注意:1. 不是脏读(读取的是已提交的数据);2. 一个事务中。

    解决方案:@Transactional(isolation = Isolation.REPEATABLE_READ
    Isolation.REPEATABLE_READ:数据库底层会对操作的数据加上行锁

  3. 幻读
    一个事务,多次整表进行查询统计(不是单条数据,是一个结果集),但结果不一样(可能是别的事务增加或删除了数据),会在本事务中产生数据不一致的问题。

    解决方案:@Transactional(isolation = Isolation.SERIALIZABLE
    Isolation.SERIALIZABLE:数据库底层会对操作的数据加上表锁

总结:

  • 并发安全:SERIALIZABLE > REPEATABLE_READ > READ_COMMITTED
  • 运行效率:READ_COMMITTED > REPEATABLE_READ > SERIALIZABLE

Isolation共有五个值:

  • DEFAULT (默认)
  • READ_UNCOMMITTED (读未提交)
  • READ_COMMITTED (读已提交)
  • REPEATABLE_READ(可重复读)
  • SERIALIZABLE(串行化)
    Spring5学习笔记(四、持久层整合与事务处理)_第8张图片

数据库对应隔离属性的支持

隔离属性 MySQL Oracle
READ_COMMITTED(解决脏读)
REPEATABLE_READ(解决不可重复读) ×
SERIALIZABLE(解决幻读)

Oracle采用多版本比对的方式解决不可重复读的问题。

默认的隔离属性

默认的隔离属性:Isolation.ISOLATION_DEFAULT(会调用不同数据库所设置的默认隔离属性)

MySQL:REPEATABLE_READ(解决不可重复读,加行锁)
Oracle:READ_COMMITTED(解决脏读,不加锁)

查看数据库隔离级别:
MySQL5:select @@tx_isolation;
MySQL8:select @@transaction_isolation;
Oracle:
Spring5学习笔记(四、持久层整合与事务处理)_第9张图片
电脑没装Oracle,所以这里直接截图。

建议:在实战中,使用默认值即可。

未来的实战中,并发访问情况很少,因为需要海量的数据,如果真遇到并发问题,使用乐观锁来解决,不会太影响效率。
乐观锁:
Hibernate(JPA):支持乐观锁,使用Version
MyBatis:不支持乐观锁,所以需要通过拦截器自定义开发。
MyBatis-plus:支持乐观锁,使用version

2. 传播属性(propagation)

传播属性:描述了事务解决嵌套问题的特征。
事务嵌套:一个大事务中,包含多个小事务。(service调用service时会发生事务嵌套)
问题:大事务中融入了很多小的事务,他们彼此影响,最终会导致外部大的事务丧失原子性。
Spring5学习笔记(四、持久层整合与事务处理)_第10张图片

传播属性的值(7个)及其用法

Spring5学习笔记(四、持久层整合与事务处理)_第11张图片

传播属性的值 外部不存在事务 外部存在事务 备注
REQUIRED(默认) 开启新的事务 融合到外部事务中 增删改方法
SUPPORTS 不开启新的事务 融合到外部事务中 查询方法
REQUIRES_NEW 开启新的事务 挂起(暂停)外部事务,创建新的事务 日志记录方法
MANDATORY 抛出异常 融合到外部事务中 极其不常用,知道即可
NOT_SUPPORTED 不开启新的事务 挂起(暂停)外部事务 极其不常用,知道即可
NEVER 不开启新的事务 抛出异常 极其不常用,知道即可
NESTED 开启新的事务 融合到外部事务中 极其不常用,知道即可

3. 只读属性(readOnly)

只读属性:针对于只进行查询操作的业务方法,可以加入只读属性,提高运行效率(不会加锁)。

默认为false,查询操作改为true,只读。

4. 超时属性(timeout)

超时属性:指定了事务等待的最长时间(以秒为单位)。
访问的数据被其他事务加上锁,此时需要等待解锁。

默认为-1,最终由对应的数据库底层设置的超时时间来决定等待多久,一般不用去设置超时属性,使用默认值即可。

5. 异常属性(rollbackFor、noRollbackFor)

Spring事务处理过程中

  • 对应RuntimeException及其子类,默认采用回滚策略。
  • 对应Exception及其子类,默认采用提交策略。

设置成回滚:rollbackFor = {java.lang.Exception.class, xxx, xxx}
设置成不回滚(即提交):{java.lang.RuntimeException.class, xxx, xxx}

建议:实战中使用默认值即可,异常尽量使用RuntimeException及其子类。

事务属性常见配置总结

  1. 隔离属性,默认即可。
  2. 传播属性:
    1. REQUIRED(默认):增删改方法
    2. SUPPORTS:查询方法
    3. REQUIRES_NEW:日志记录方法
  3. 只读属性:查询改为true。
  4. 超时属性:默认(-1)即可。
  5. 异常属性:默认即可。

使用建议:

  • 增删改操作:@Transactional
  • 查询操作:@Transactional(propagation = Propagation.SUPPORTS, readOnly = true)

基于标签的事务配置方式(事务开发的第二种形式)

基于注解 @Transactional 的事务配置回顾

1. 原始类
<bean id="userService" class="com.angenin.service.UserServiceImpl">
    <property name="userDAO" ref="userDAO"/>
</bean>

2. 额外功能
<bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>

3. 切入点
@Transactional(...)	// 类切入点
public class UserServiceImpl implements UserService {
     
	@Transactional(...)	// 方法切入点
    public void register(User user) {
     
    	...
    }
    ...
}

4. 组装切面
<tx:annotation-driven transaction-manager="dataSourceTransactionManager" proxy-target-class="true"/>

两种方法的差异在于第3步和第4步,第2步需要多配置一个事务属性。

基于标签的事务配置

1. 原始类
...

2. 额外功能与事务属性
...

<tx:advice id="txAdvice" transaction-manager="dataSourceTransactionManager">
    <tx:attributes>
        <tx:method name="register"/>
        <tx:method name="login" propagation="SUPPORTS" read-only="true"/>
    tx:attributes>
tx:advice>


<aop:config>
	3. 切入点
    <aop:pointcut id="pc" expression="execution(* com.angenin1.service.UserServiceImpl.*(..))"/>
    4. 组装切面
    <aop:advisor advice-ref="txAdvice" pointcut-ref="pc"/>
aop:config>

基于标签的事务配置在实战中的引用方式

事务属性配置

<tx:advice id="txAdvice" transaction-manager="dataSourceTransactionManager">
    <tx:attributes>
        
        
        
        
        
        <tx:method name="modify*"/>
        
        <tx:method name="*" propagation="SUPPORTS" read-only="true"/>
    tx:attributes>
tx:advice>

切入点

<aop:config>
    
    
	<aop:pointcut id="pc" expression="execution(* com.angenin1.service..*.*(..))"/>
	
    <aop:advisor advice-ref="txAdvice" pointcut-ref="pc"/>
aop:config>

下一篇:Spring5学习笔记(五、MVC框架整合)

学习视频(p108-p140):https://www.bilibili.com/video/BV185411477k?p=108

你可能感兴趣的:(SSM,spring,mybatis,mysql,java)