Spring与持久层
Spring框架为什么要与持久层技术进行整合?
- JavaEE开发需要持久层进行数据库的访问操作。
- JDBC、Hibernate、MyBatis进行持久开发过程存在大量的代码冗余。
- Spring基于模板设计模式对于上述的持久层技术进行了封装。
Spring可以与哪些持久层技术进行整合?
1. JDBC
|- JDBCTemplate
2. Hibernate(JPA)
|- HibernateTemplate
3. MyBatis
|- SqlSessionFactoryBean MapperScannerConfigure
因为MyBatis是目前用得最多的持久层框架,所以这里只讲与MyBatis的整合。
Spring与MyBatis整合
MyBatis开发步骤的回顾
-------七步骤--------
- 实体
- 实体别名
- 表
- 创建DAO接口
- 实现Mapper文件
- 注册Mapper文件
- MyBatisAPI调用
引入mysql和mybatis的jar包,搭建开发环境
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.18</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.3</version>
</dependency>
创建mybatis配置文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<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>
<!-- 注册mapper文件 -->
<mappers>
</mappers>
</configuration>
七步骤
- 创建User实体类
public class User implements Serializable { private Integer id; private String name; private String password; //为了简洁,这里略写 //有参无参 //get/set }
- 在配置文件中设置User实体类的别名
<!-- 设置实体类的别名 --> <typeAliases> <typeAlias alias="user" type="com.angenin1.mybatis.User"/> </typeAliases>
- 创建表
use angenin; create table t_users( `id` int primary key auto_increment, `name` varchar(12), `password` varchar(12) ); desc t_users;
- 创建UserDAO接口
public interface UserDAO { void save(User user); }
- 实现UserMapper文件
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.angenin1.mybatis.UserDAO"> <insert id="save" parameterType="user"> insert into t_users(`name`,`password`)values(#{name},#{password}); </insert> </mapper>
- 在配置文件中注册Mapper文件
<!-- 注册mapper文件 --> <mappers> <mapper resource="UserMapper.xml"/> </mappers>
- 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(); } }
MyBatis在开发过程中存在的问题
- 配置繁琐:第2步起别名和第6步注册mapper文件。(其实MyBatis也可以通过指定包来解决这个问题)
- 代码冗余:第7步API的调用。
Spring与MyBatis整合思路分析
Spring与MyBatis整合的开发步骤
搭建环境
<!-- mybatis -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.18</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.3</version>
</dependency>
<!--除了上面导入的mybatis和mysql的jar包外,还需要导入三个jar包-->
<!-- spring整合mybatis -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.1.14.RELEASE</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.18</version>
</dependency>
-
Spring配置文件的配置
<!-- druid 连接池 --> <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> <!-- 通过 SqlSessionFactoryBean 创建SqlSessionFactory对象 --> <bean id="sqlSessionFactoryBean" class="org.mybatis.spring.SqlSessionFactoryBean"> <!--设置连接池对象--> <property name="dataSource" ref="dataSource"/> <!--指定实体类所在的包--> <property name="typeAliasesPackage" value="com.angenin1.entity"/> <!--指定mapper文件所在的包--> <property name="mapperLocations"> <list> <value>classpath:com.angenin1.mapper/*Mapper.xml</value> </list> </property> </bean> <!-- 通过 MapperScannerConfigurer 创建DAO对象 --> <bean id="scanner" class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <!--设置sqlsession工厂对象--> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactoryBean"/> <!--指定DAO接口所在的包--> <property name="basePackage" value="com.angenin1.dao"/> </bean>
-
编码
-------七步骤--------
- 实体
实体别名- 表
- 创建DAO接口
- 实现Mapper文件
注册Mapper文件MyBatisAPI调用
七步变四步
- 实体
- 表
- 创建DAO接口
- 实现Mapper文件
- 创建entity包,在其包下创建User实体类
public class User implements Serializable { private Integer id; private String name; private String password; //get set }
- 创建t_users表
# 上面已经创建了,继续用t_users表 create table t_users( `id` int primary key auto_increment, `name` varchar(12), `password` varchar(12) );
- 创建dao包,在其包下创建UserDAO接口
public interface UserDAO { void save(User user); }
- 在resources目录下创建com.angenin1.mapper目录(注意:resources目录下多级目录用
/
分隔,这里用.
,所以不是多级目录,只是一个目录),用于存放mapper文件,在这个目录下创建UserMapper.xml文件<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.angenin1.dao.UserDAO"> <insert id="save" parameterType="User"> insert into t_users(`name`,`password`)values(#{name},#{password}); </insert> </mapper>
- 测试
运行结果: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); } }
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 原始方法() {
核心功能;
}
}
要注意的细节:
- 原始对象 --> 原始方法 --> 核心功能(业务处理+DAO调用)
- 因为Service依赖DAO对象,所以需要把DAO作为Service的成员变量,并提供get/set方法,用依赖注入(set注入)的方式进行赋值。
2. 额外功能
- 实现
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 --> <bean id="around" class="com.angenin.dynamic.MyAround"/> <!--配置aop--> <aop:config> <!--配置切入点,id可任意起,expression:切入点表达式,这里代表所有的方法--> <aop:pointcut id="pc" expression="execution(* *(..)"/> <!--组装:把额外功能和切入点进行整合,advice-ref:额外功能,pointcut-ref:切入点--> <aop:advisor advice-ref="around" pointcut-ref="pc"/> </aop:config>
- 切面类,使用
@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 --> <bean id="around" class="com.angenin.aspect.MyAspect"/> <!-- 告知Spring基于注解进行AOP开发 --> <aop:aspectj-autoproxy />
而Spring在也帮我们封装了额外功能的这些内容,我们只要调用org.springframework.jdbc.datasource.DataSourceTransactionManager
,而由于需要Connection对象,需要等于依赖,依赖就要注入,所以需要注入Connection对象,而又由于Connection对象来着连接池,所以注入连接池即可。
所以以后我们只需要:
- 调用org.springframework.jdbc.datasource.DataSourceTransactionManager
- 注入连接池
<!-- druid 连接池 -->
<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
:事务的额外功能加入给哪些业务方法。
- 加在类上:这个类中的所有方法都会有额外功能,即加入事务。
- 加在方法上:这个方法会有额外功能,即加入事务。
4. 组装切面
切入点 + 额外功能
<!-- transaction-manager:组装额外功能bean -->
<tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>
组装切入点:会自动扫描@Transactional
注解。
Spring控制事务的编码
-
搭建开发环境
<!--引入Spring关于事务的jar包--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>5.1.14.RELEASE</version> </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"> <!-- 这里的userDAO是前面MapperScannerConfigurer创建的,创建的bean的id为首单词首字母小写 --> <property name="userDAO" ref="userDAO"/> </bean>
-
额外功能
<bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean>
-
切入点
在UserServiceImpl类上加上@Transactional
注解,把整个类作为切入点。 -
组装切面
<!-- transaction-manager:组装额外功能bean --> <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); }
-
细节
<tx:annotation-driven transaction-manager="dataSourceTransactionManager" proxy-target-class="true"/>
proxy-target-class属性:进行动态代理底层实现的切换
false:JDK代理(默认)
true:Cglib代理
Spring中的事务属性(Transaction Attribute)
属性:描述物体特征的一系列值。如一个人的属性有身高、体重、性别等。
事务属性:描述事务特征的一系列值,共5个。
- 隔离属性
- 传播属性
- 只读属性
- 超时属性
- 异常属性
添加事务属性:
@Transactional(isolation = xxx, propagation = xxx, readOnly = xxx, timeout = xxx, rollbackFor = xxx, noRollbackFor = xxx)
事务属性详解
1. 隔离属性(isolation)
隔离属性:描述了事务解决并发问题的特征。
-
什么是并发?
多个事务(用户)在同一时间访问操作了相同的数据。
同一时间:0.000几秒的差异,有 微小的前 和 微小的后 之分。 -
并发会产生哪些问题?
- 脏读
- 不可重复读
- 幻读
-
并发问题如何解决?
通过隔离属性解决,隔离属性中设置不同的值,解决并发处理过程中的问题。
事务并发产生的问题
-
脏读
一个事务,读取了另一个事务中还没提交的数据,会在本事务中产生数据不一致的问题。解决方案:
@Transactional(isolation = Isolation.READ_COMMITTED)
Isolation.READ_COMMITTED:只能读取已提交的数据。 -
不可重复读
一个事务,多次读取相同的数据,但读取的结果不一样(可能是别的事务修改了这条数据),会在本事务中产生数据不一致的问题。
注意:1. 不是脏读(读取的是已提交的数据);2. 一个事务中。解决方案:
@Transactional(isolation = Isolation.REPEATABLE_READ
Isolation.REPEATABLE_READ:数据库底层会对操作的数据加上行锁。 -
幻读
一个事务,多次整表进行查询统计(不是单条数据,是一个结果集),但结果不一样(可能是别的事务增加或删除了数据),会在本事务中产生数据不一致的问题。解决方案:
@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(串行化)
数据库对应隔离属性的支持
隔离属性 | 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:
电脑没装Oracle,所以这里直接截图。
建议:在实战中,使用默认值即可。
未来的实战中,并发访问情况很少,因为需要海量的数据,如果真遇到并发问题,使用乐观锁来解决,不会太影响效率。
乐观锁:
Hibernate(JPA):支持乐观锁,使用Version
MyBatis:不支持乐观锁,所以需要通过拦截器自定义开发。
MyBatis-plus:支持乐观锁,使用version
2. 传播属性(propagation)
传播属性:描述了事务解决嵌套问题的特征。
事务嵌套:一个大事务中,包含多个小事务。(service调用service时会发生事务嵌套)
问题:大事务中融入了很多小的事务,他们彼此影响,最终会导致外部大的事务丧失原子性。
传播属性的值(7个)及其用法
传播属性的值 | 外部不存在事务 | 外部存在事务 | 备注 |
---|---|---|---|
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及其子类。
事务属性常见配置总结
- 隔离属性,默认即可。
- 传播属性:
- REQUIRED(默认):增删改方法
- SUPPORTS:查询方法
- REQUIRES_NEW:日志记录方法
- 只读属性:查询改为true。
- 超时属性:默认(-1)即可。
- 异常属性:默认即可。
使用建议:
- 增删改操作:
@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>
<!-- 等效于
@Transactional
public void register(User user) {...}
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
public void login(String username, String password) {...}
-->
<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="register"/>-->
<!--<tx:method name="login" propagation="SUPPORTS" read-only="true"/>-->
<!--使用通配符解决tx:method标签过多的问题-->
<!--编程时,service中负责增删改操作的方法都以modify开头即可-->
<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.UserServiceImpl.*(..))"/>-->
<!--使用包切入点-->
<aop:pointcut id="pc" expression="execution(* com.angenin1.service..*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="pc"/>
</aop:config>
学习视频(p108-p140):https://www.bilibili.com/video/BV185411477k?p=108