参考自B站视频《孙哥说Spring5》
文章目录
持久层整合总述
1、Spring 框架为什么要与持久层技术进行整合?
- JavaEE开发需要持久层进行数据库的访问操作
JDBC
、Hibernate
、MyBatis
进行持久开发过程存在大量的代码冗余- Spring 基于模板设计模式对于上述的持久层技术进行了封装
2、Spring 可以与哪些持久层技术进行整合?
JDBC
——JDBCTemplate
Hibernate(JPA)
——HibernateTemplate
MyBatis
——SqlSessionFactoryBean
、MapperScannerConfigure
MyBatis 开发步骤
- 实体类
public class User implements Serializable {
private Integer id;
private String name;
private String password;
public User() {
}
public User(Integer id, String name, String password) {
this.id = id;
this.name = name;
this.password = password;
}
// get set 方法 ...
}
- 配置文件
mybatis-config.xml
配置实体别名和注册mapper
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Confi 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 别名 -->
<typeAliases>
<typeAlias alias="user" type="com.yusael.mybatis.User"/>
</typeAliases>
<!-- JDBC连接环境 -->
<environments default="mysql">
<environment id="mysql">
<transactionManager type="JDBC"></transactionManager>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/yus?useSSL=false"/>
<property name="username" value="root"/>
<property name="password" value="1234"/>
</dataSource>
</environment>
</environments>
</configuration>
- 数据库表
create table t_users values (
id int(11) primary key auto_increment,
name varchar(12),
password varchar(12)
);
- 创建DAO接口
public interface UserDAO {
public void save(User user);
}
- 实现Mapper文件
UserDAOMapper.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.yusael.mybatis.UserDAO">
<insert id="save" parameterType="user">
insert into t_users(name, password) values (#{name}, #{password})
</insert>
</mapper>
- 注册Mapper文件
mybatis-config.xml
<mappers>
<mapper resource="UserDAOMapper.xml"/>
</mappers>
- MyBatis API调用
public class TestMybatis {
public static void main(String[] args) throws IOException {
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
// 会话工厂
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 打开会话
SqlSession session = sqlSessionFactory.openSession();
// 获得 DAO 层
UserDAO userDAO = session.getMapper(UserDAO.class);
// 创建实体类
User user = new User();
user.setName("yusael");
user.setPassword("123456");
// mybatis 保存操作
userDAO.save(user);
// 提交事务
session.commit();
}
}
以上开发步骤中存在的问题
配置繁琐、代码冗余
1. 实体
2. 实体别名 配置繁琐
3. 表
4. 创建 DAO 接口
5. 实现 Mapper 文件
6. 注册 Mapper 文件 配置繁琐
7. Mybatis API 调用 代码冗余
Spring 整合 MyBatis
思路
Spring 主要就是在Spring配置文件中简化 MyBatis 的步骤和代码
1. 实体
2. 实体别名 不需要
3. 表
4. 创建 DAO 接口
5. 实现 Mapper 文件
6. 注册 Mapper 文件 不需要
7. Mybatis API 调用 直接调用DAO接口即可
开发步骤
导入依赖 pom.xml
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.4</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.12</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.43</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.4</version>
</dependency>
Spring配置文件
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--连接池-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/yus?useSSL=false"/>
<property name="username" value="root"/>
<property name="password" value="1234"/>
</bean>
<!--创建SqlSessionFactory SqlSessionFactoryBean-->
<bean id="sqlSessionFactoryBean" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<!-- 指定实体类所在的包 -->
<property name="typeAliasesPackage" value="com.yusael.entity"/>
<!--指定配置文件(映射文件)的路径,还有通用配置-->
<property name="mapperLocations">
<list>
<!-- 通配符匹配 -->
<value>classpath:com.yusael.dao/*Mapper.xml</value>
</list>
</property>
</bean>
<!--创建DAO对象 MapperScannerConfigure-->
<bean id="scanner" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactoryBean"/>
<!--指定DAO接口放置的包-->
<property name="basePackage" value="com.yusael.dao"/>
</bean>
</beans>
Java类编码
实体类
public class User implements Serializable {
private Integer id;
private String name;
private String password;
// get set ...
}
DAO接口
public interface UserDAO {
public void save(User user);
}
数据库表
create table t_users values (
id int(11) primary key auto_increment,
name varchar(12),
password varchar(12)
);
Mapper 映射文件
<mapper namespace="com.baizhiedu.dao.UserDAO">
<insert id="save" parameterType="User">
insert into t_users (name, password) values (#{name}, #{password})
</insert>
</mapper>
测试类
/**
* 用于测试: Spring 与 Mybatis 的整合
*/
@Test
public void test() {
ApplicationContext ctx = new ClassPathXmlApplicationContext("/applicationContext.xml");
UserDAO userDAO = (UserDAO) ctx.getBean("userDAO");
User user = new User();
user.setName("xiaojr");
user.setPassword("999999");
userDAO.save(user);
}
Spring 整合 MyBatis 细节
Spring 与 MyBatis 整合之后,为什么DAO不提交事务,但是数据能够插入数据库中?
-
我们可以知道,事务是由连接控制的
-
同时我们查看Spring的日志输出可以知道Spring并没有管理连接
-
其实是 MyBatis 提供的连接池管理的连接对象
-
MyBatis 在创建连接对象的时候会执行
Connection.setAutoCommit(false)
手动控制事务:操作完成后,需要手工提交。 -
Spring整合MyBatis后是由 Druid(C3P0、DBCP) 作为连接池,创建连接对象
-
而 Druid 默认是设为 true 的,保持自动控制事务,完成一条 sql 操作后就会自动提交
答:因为 Spring 与 Mybatis 整合时,引入了外部连接池对象,保持自动的事务提交这个机制Connection.setAutoCommit(true)
,不需要手工进行事务的操作,也能进行事务的提交。
注意:实战中,还是会手工控制事务(可能需要多条SQL一起成功,一起失败),后续 Spring 通过 事务控制 解决这个问题。
事务概念
什么是事务?
- 事务是保证业务操作完整性的一种数据库机制。
事务的四大特点:ACID(原子性、一致性、隔离性、持久性)
如何控制事务?
- JDBC
Connection.setAutoCommit(false); // 开启事务
Connection.commit(); // 提交
Connection.rollback(); // 回滚
- MyBatis
Mybatis 自动开启事务 --> 手动提交
sqlSession.commit(); 底层还是调用的 Connection
sqlSession.rollback(); 底层还是调用的 Connection
- 控制事务的底层,都是通过 Connection 对象完成的。
Spring 事务开发
Spring 是通过AOP的方式进行事务的开发
对应AOP开发步骤:原始对象(DAO)、额外功能(AOP方式开启、提交、回滚)、切入点、组装切面
如果采用传统AOP的开发方式,需要实现 MethodInterceptor
接口,通过注入连接池获取连接对象
然后在invoke
方法中前后加上事务的操作,这样就完成了额外功能这一步骤
但是事务的操作都是基本一致的,因此Spring为我们封装好了传统AOP的方式以完成事务功能,我们可以直接使用Spring提供的注解@Transactional
,简化开发
开发步骤
导入依赖环境
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.2.6.RELEASE</version>
</dependency>
Service 切入点
// 标记切入点 类上就会给所有方法都加入事务 方法上就会给该方法加入事务
@Transactional
public class UserServiceImpl implements UserService {
private UserDAO userDAO;
@Override
public void register(User user){
userDAO.save(user);
}
}
配置类中配置事务
<!-- 原始对象 -->
<bean id="userService" class="com.yusael.service.UserServiceImpl">
<!-- userDAO 注入就是通过前面的 MapperScannerConfigure 扫描的 mapper 包中获得 id为类名首字母小写 -->
<property name="userDAO" ref="userDAO"/>
</bean>
<!--DataSourceTransactionManager 额外功能 注入连接池以获得连接对象-->
<bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 打开注解开发 transaction-manager获取额外功能 将额外功能与注解标记的切入点组合 -->
<tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>
因为是通过AOP实现的,所以可以切换动态代理层实现的两个方式(JDK、CGlib)
<tx:annotation-driven transaction-manager="dataSourceTransactionManager" proxy-target-class="true"/>
Spring中的事务属性(Transaction Attribute)
什么是事务属性?
- 属性:描述物体特征的一系列值(性别、身高、体重)
- 事务属性:描述事务特征的一系列值
- 隔离属性
- 传播属性
- 只读属性
- 超时属性
- 异常属性
如何添加事务属性?
@Transactional(isolation=, propagation=, readOnly=,timeout=,rollbackFor,noRollbackFor=,)
隔离属性
概念:描述了事务解决并发问题的特征。
什么是并发?
- 多个事务(用户)在同一时间,访问操作了相同的数据。
同一时间:0.000几秒左右 微乎其微
并发会产生那些问题?
- 脏读
- 不可重复读
- 幻影读
并发问题如何解决?
- 通过隔离属性解决,隔离属性中设置不同的值,解决并发处理的过程中的问题。
事务并发产生的问题:
- 脏读
一个事务,读取了另一个事务中没有提交的数据,会在本事务中产生数据不一样的现象
解决方案:@Transaction(isolation=Isolation.READ_COMMITTED)
- 要求只能读取提交的数据
- 不可重复读
一个事务中,多次读取相同的数据,但是读取结果不一样,会在本事务中产生数据不一样的现象
注意:1.不是脏读 2.在一个事务中
解决方案:@Transaction(isolation=Isolation.REPEATABLE_READ)
- 就是加上一把行锁(对数据库表的某一行加锁)
- 幻影读
一个事务中,多次对整表进行查询统计,但是结果不一样,会在本事务中产生数据不一致的问题
解决方案:@Transaction(isolation=Isolation.SERIALIZABLE)
- 就是一把表锁(对数据库某个表加锁)
安全与效率对比:
- 并发安全:
SERIALIZABLE
>READ_ONLY
>READ_COMMITTED
- 运行效率:
READ_COMMITTED
>READ_ONLY
>SERIALIZABLE
数据库对隔离属性的支持:
隔离属性的值 | MySQL | Oracle |
---|---|---|
ISOLATION_READ_COMMITTED | 支持 | 支持 |
ISOLATION_REPEATABLE_READ | 支持 | 不支持 |
ISOLATION_SERIALIZABLE | 支持 | 支持 |
Oracle 不支持 REPEATABLE_READ
,那该如何解决不可重复读?
- 采用 多版本比对 的方式解决不可重复读问题。
默认的隔离属性:
Spring 会指定为 ISOLATION_DEFAULT
,调用不同数据库所设置的默认隔离属性
- MySQL:
REPEATABLE_READ
- Oracle:
READ_COMMITTED
查看数据库的默认隔离属性:
- MySQL:
SELECT @@tx_isolation;
- Oracle
select s.sid, s.serial#,
case bitand(t.flag, power(2, 28))
when 0 then 'READ COMMITED'
else 'SERIALIZABLE'
end as isolation_level
from v$transaction t
join v$session s on t.addr = s.taddr
and s.sid = sys_context('USERENV', 'SID');
隔离属性在实战中的建议:
- 推荐使用 Spring 默认指定的
ISOLATION_DEFAULT
- 未来的实战中,遇到并发访问的情况,很少见
- 如果真的遇到并发问题,解决方案:乐观锁
Hibernate(JPA):Version
MyBatis:通过拦截器自定义开发
传播属性(PROPAGATION)
概念:描述了事务解决 嵌套 问题 的特征。
事务的嵌套:指的是一个大的事务中,包含了若干个小的事务。
事务嵌套产生的问题: 大事务中融入了很多小的事务,他们彼此影响,最终就导致外部大的事务丧失了事务的原子性。
传播属性的值及其用法:
传播属性的值 | 外部不存在事务 | 外部存在事务 | 用法 | 适用于 |
---|---|---|---|---|
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) | 极其不常用 |
- 融合到外部事务即自己的事务删除,将步骤放到外部事务中,一起成功或者一起失败
- 挂起外部事务就是暂停外部的事务
Spring 中传播属性的默认值是:REQUIRED
推荐传播属性的使用方式:
- 增删改 方法:使用默认值 REQUIRED
- 查询 方法:显式指定传播属性的值为 SUPPORTS
只读属性(readOnly)
针对于 只进行查询操作的业务方法,可以加入只读属性,提高运行效率。
- 默认值:
false
@Transactional(readOnly = true)
超时属性(timeout)
指定了事务等待的最长时间。
为什么事务会进行等待?
- 当前事务访问数据时,有可能访问的数据被别的事务进行加锁的处理,那么此时本事务就必须进行等待。
等待时间,单位是 秒
如何使用:@Transactional(timeout = 2)
超时属性的默认值:-1
- 表示超时属性由对应的数据库来指定(一般不会主动指定,-1 即可)
异常属性
Spring 事务处理过程中:
- 默认对于
RuntimeException
及其子类,采用 回滚 的策略。 - 默认对于
Exception
及其子类,采用 提交 的策略。
使用方法:
// 当然也可以自己指定一些异常
// 采用回滚策略
@Transactional(rollbackFor = {java.lang.Exception.class, xxx, xxx})
// 不回滚即采用提交的策略
@Transactional(noRollbackFor = {java.lang.RuntimeException, xxx, xxx})
建议:实战中使用 RuntimeException
及其子类,使用事务异常属性的默认值。
事务属性常见配置总结
- 隔离属性 默认值
- 传播属性
Required
(默认值)增删改、Supports
查询操作 - 只读属性
readOnly=false
增删改,true
查询操作 - 超时属性 默认值
-1
- 异常属性 默认值
增删改操作:@Transactional
查询操作:@Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
基于标签的事务配置方式
前面我们讲的事务开发就是基于注解 @Transaction
的事务配置;
那么对应的,就肯定存在基于标签的事务配置:
<!--DataSourceTransactionManager-->
<bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
切入点 --> 作为事务操作
<tx:advice id="txAdvice" transaction-manager="dataSourceTransactionManager">
事务属性
<tx:attributes>
事务操作的方法 方法名 事务属性(隔离属性、传播属性)
也可以使用 * 号作为通配符
<tx:method name="register" isolation="DEFAULT" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
组装切面
<aop:config>
<aop:pointcut id="pc" expression="execution(* com.yusael.service.UserServiceImpl.register(..))"/>
<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="modify*"/>
* 号作为通配符
编程时候, service中负责进行增删改操作的方法 都以 modify 开头
查询操作 命名无所谓
<tx:method name="*" propagation="SUPPORTS" read-only="true"/>
</tx:attributes>
</tx:advice>
<aop:config>
应用的过程中, 将 service 都放到 service 包下 方便指定切入点
<aop:pointcut id="pc" expression="execution(* com.yusael.service..*.*(..))"/>
组装切面
<aop:advisor advice-ref="txAdvice" pointcut-ref="pc"/>
</aop:config>