Spring框架为什么要与持久层技术进行整合?
- JavaEE开发需要持久层进行数据库的访问操作。
- JDBC、Hibernate、MyBatis进行持久开发过程存在大量的代码冗余。
- Spring基于模板设计模式对于上述的持久层技术进行了封装。
Spring可以与哪些持久层技术进行整合?
1. JDBC
|- JDBCTemplate
2. Hibernate(JPA)
|- HibernateTemplate
3. MyBatis
|- SqlSessionFactoryBean MapperScannerConfigure
因为MyBatis是目前用得最多的持久层框架,所以这里只讲与MyBatis的整合。
-------七步骤--------
- 实体
- 实体别名
- 表
- 创建DAO接口
- 实现Mapper文件
- 注册Mapper文件
- 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>
七步骤
public class User implements Serializable {
private Integer id;
private String name;
private String password;
//为了简洁,这里略写
//有参无参
//get/set
}
<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;
public interface UserDAO {
void save(User user);
}
<mapper namespace="com.angenin1.mybatis.UserDAO">
<insert id="save" parameterType="user">
insert into t_users(`name`,`password`)values(#{name},#{password});
insert>
mapper>
<mappers>
<mapper resource="UserMapper.xml"/>
mappers>
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();
}
}
运行结果:搭建环境
<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>
编码
-------七步骤--------
- 实体
实体别名- 表
- 创建DAO接口
- 实现Mapper文件
注册Mapper文件MyBatisAPI调用七步变四步
- 实体
- 表
- 创建DAO接口
- 实现Mapper文件
public class User implements Serializable {
private Integer id;
private String name;
private String password;
//get set
}
# 上面已经创建了,继续用t_users表
create table t_users(
`id` int primary key auto_increment,
`name` varchar(12),
`password` varchar(12)
);
public interface UserDAO {
void save(User user);
}
/
分隔,这里用.
,所以不是多级目录,只是一个目录),用于存放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>
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整合后,为什么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通过事务控制解决这个问题。
保证业务操作完整性的一种数据库机制。
事务的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是通过AOP的方式进行事务开发的。
public class XxxServiceImpl() {
private XxxDao xxxDao;
//set get
public 原始方法() {
核心功能;
}
}
要注意的细节:
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>
@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对象来着连接池,所以注入连接池即可。
所以以后我们只需要:
<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>
使用DataSourceTransactionManager后,切入点由
@Transactional
注解决定。
@Transactional
:事务的额外功能加入给哪些业务方法。
切入点 + 额外功能
<tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>
组装切入点:会自动扫描@Transactional
注解。
搭建开发环境
<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
注解,把整个类作为切入点。
<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代理
属性:描述物体特征的一系列值。如一个人的属性有身高、体重、性别等。
事务属性:描述事务特征的一系列值,共5个。
- 隔离属性
- 传播属性
- 只读属性
- 超时属性
- 异常属性
添加事务属性:
@Transactional(isolation = xxx, propagation = xxx, readOnly = xxx, timeout = xxx, rollbackFor = xxx, noRollbackFor = xxx)
隔离属性:描述了事务解决并发问题的特征。
什么是并发?
多个事务(用户)在同一时间访问操作了相同的数据。
同一时间: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:数据库底层会对操作的数据加上表锁。
总结:
Isolation共有五个值:
隔离属性 | 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
传播属性:描述了事务解决嵌套问题的特征。
事务嵌套:一个大事务中,包含多个小事务。(service调用service时会发生事务嵌套)
问题:大事务中融入了很多小的事务,他们彼此影响,最终会导致外部大的事务丧失原子性。
传播属性的值 | 外部不存在事务 | 外部存在事务 | 备注 |
---|---|---|---|
REQUIRED(默认) | 开启新的事务 | 融合到外部事务中 | 增删改方法 |
SUPPORTS | 不开启新的事务 | 融合到外部事务中 | 查询方法 |
REQUIRES_NEW | 开启新的事务 | 挂起(暂停)外部事务,创建新的事务 | 日志记录方法 |
MANDATORY | 抛出异常 | 融合到外部事务中 | 极其不常用,知道即可 |
NOT_SUPPORTED | 不开启新的事务 | 挂起(暂停)外部事务 | 极其不常用,知道即可 |
NEVER | 不开启新的事务 | 抛出异常 | 极其不常用,知道即可 |
NESTED | 开启新的事务 | 融合到外部事务中 | 极其不常用,知道即可 |
只读属性:针对于只进行查询操作的业务方法,可以加入只读属性,提高运行效率(不会加锁)。
默认为false,查询操作改为true,只读。
超时属性:指定了事务等待的最长时间(以秒为单位)。
访问的数据被其他事务加上锁,此时需要等待解锁。
默认为-1,最终由对应的数据库底层设置的超时时间来决定等待多久,一般不用去设置超时属性,使用默认值即可。
Spring事务处理过程中
- 对应RuntimeException及其子类,默认采用回滚策略。
- 对应Exception及其子类,默认采用提交策略。
设置成回滚:rollbackFor = {java.lang.Exception.class, xxx, xxx}
设置成不回滚(即提交):{java.lang.RuntimeException.class, xxx, xxx}
建议:实战中使用默认值即可,异常尽量使用RuntimeException及其子类。
使用建议:
@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