SpringBoot整合MyBatis
本小记学习目标:
1.SpringBoot中整合MyBatis
2.MyBatis插件
3.数据库事务
一、SpringBoot整合MyBatis
有了Hibernate为什么还要使用MyBatis?
Hibernate的模型化有助于系统的分析和建模,重点在业务模型的分析和设计上,而当前一般是业务简单、变化快、高并发访问,在这些特点上Mybatis占有优势。
MyBatis官方定义
MyBatis是支持定制化SQL,存储过程以及高级映射的优秀持久层框架。它避免了几乎所有的JDBC代码和手动设置参数以及获取结果集。可以对配置和原生Map使用简单的XML或注解,把接口和Java的POJO映射为数据库中的记录。
从上面的定义可以看到使用MyBatis需要有如下几个东西
SQL
映射关系(xml或注解)
POJO
在项目开发过程:
1、引入MyBatis的starter
<!-- MyBatis的依赖引入 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
同时要使用到mysql的依赖和数据源的依赖
<!-- mysql依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- Druid的依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
2、MyBatis的配置
MyBatis是一个基于SqlSessionFactory构建的框架,SqlSessionFactory的作用是用来生成SqlSession接口对象,这个接口对象是MyBatis的核心
构建SqlSessionFactory是通过配置类Configuration来完成的

MyBatis可配置内容
1、properties(属性
):一般来用Spring进行配置
2、settings(设置
):这个配置可以更改MyBatis的底层行为,可以配置映射规则、执行器类型、缓存等内容
3、typeAliases(类型别名):使用类全限定名会比较长,因而对于常用的类MyBatis会提代默认的别名,此外还可以使用typeAliases来配置自定义的别名
4、typeHandler(类型处理器):在MyBatis写入和读取数据库的过程中对不同类型的数据(JavaType<--转换-->JdbcType)进行自定义的转换,通常情况下MyBatis会自动识别JavaType到JdbcType的转换,typeHandler常常用在枚举类型的转换上
5、objectFactory(对象工厂):在MyBatis生成返回的POJO时会调用的工厂类,通常使用MyBatis的默认工厂类(DefaultObjectFactory)则可以,一般是不用做特殊配置
6、plugins(插件):这个也称为拦截器
7、environments(数据库环境):可以配置数据库连接内容和事务,一般来说它交由Spring托管
8、databaseIdProvider(数据库厂商标识):不常用
9、mappers(映射器):它是MyBatis最重要的组件,提供SQL和POJO的映射关系(开发的核心)
3、数据库中新增表:t_user
CREATE TABLE `t_user` (
`id` int(12) NOT NULL AUTO_INCREMENT,
`user_name` varchar(60) NOT NULL,
`sex` int(3) NOT NULL DEFAULT '1' COMMENT '性别,1-男,2-女',
`note` varchar(512) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
4、POJO类
新增一个POJO类User,com.xiaoxie.pojo.User
package com.xiaoxie.pojo;
import com.xiaoxie.enums.SexEnum;
import org.apache.ibatis.type.Alias;
@Alias(value = "user") //指定别名
public class User {
private Long id = null;
private String userName = null;
private String note = null;
//SexEnum是一个性别枚举,需要用typeHandler进行转换
private SexEnum sex = null;
//默认的构造函数
public User(){}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getNote() {
return note;
}
public void setNote(String note) {
this.note = note;
}
public SexEnum getSex() {
return sex;
}
public void setSex(SexEnum sex) {
this.sex = sex;
}
}
SexEnum枚举:com.xiaoxie.enums.SexEnum
package com.xiaoxie.enums;
public enum SexEnum {
MALE(1,"男"),
FEMALE(2,"女");
private int id;
private String name;
SexEnum(int id,String name){
this.id = id;
this.name = name;
}
public static SexEnum getEnumById(int id){
for (SexEnum sex:SexEnum.values()) {
if(sex.getId() == id){
return sex;
}
}
return null;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
对于上面的User类,使用了@Alias来指定它的别名为user
5、POJO中使用了枚举SexEnum,使用typeHandler来进行类型的转换
com.xiaoxie.handler.SexTypeHandler
package com.xiaoxie.handler;
import com.xiaoxie.enums.SexEnum;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
@MappedJdbcTypes(JdbcType.BIGINT) //声明JdbcType为整型
@MappedTypes(value = SexEnum.class) //声明JavaType为SexEnum
public class SexTypeHandler extends BaseTypeHandler<SexEnum> {
/**
* 设置非空性别参数
* @param ps
* @param i
* @param parameter
* @param jdbcType
* @throws SQLException
*/
@Override
public void setNonNullParameter(PreparedStatement ps, int i, SexEnum parameter, JdbcType jdbcType) throws SQLException {
ps.setInt(i,parameter.getId());
}
/**
* 通过列名读取性别
* @param rs jdbc返回的结果集
* @param columnName 数据库中列名称
* @return
* @throws SQLException
*/
@Override
public SexEnum getNullableResult(ResultSet rs, String columnName) throws SQLException {
int sex = rs.getInt(columnName); //得到jdbc返回的数值
if(sex !=1 && sex !=2){
return null;
}
return SexEnum.getEnumById(sex);
}
/**
* 通过下标读取性别
* @param rs jdbc返回的结果集
* @param columnIndex 数据库中的列索引
* @return
* @throws SQLException
*/
@Override
public SexEnum getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
int sex = rs.getInt(columnIndex);
if(sex != 1 && sex != 2){
return null;
}
return SexEnum.getEnumById(sex);
}
/**
* 通过程序过程读取性别
* @param cs
* @param columnIndex
* @return
* @throws SQLException
*/
@Override
public SexEnum getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
int sex = cs.getInt(columnIndex);
if(sex != 1 && sex != 2){
return null;
}
return SexEnum.getEnumById(sex);
}
}
在MyBatis中typeHandler要实现TypeHandler<T>,为了方便可以直接继承BaseTypeHandler<T>抽像类即可,这个抽象类本身又实现了TypeHandler<T>接口。
在类上要使用两个注解
@MappedJdbcTypes(JdbcType.BIGINT):这里表示JDBC的类型为整型
@MappedTypes(vale = SexEnum.class):这里表示Java的类型为SexEnum
有上面的这个typeHandle后,MyBatis就可以做对应的类型转换了
6、映射文件
一般我们建立映射文件(xml文件)时会根据不同的模块建立在不同的文件夹中以便未来维护
在resources文件下新增\mybatis\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.xiaoxie.dao.MyBatisUserDao">
<select id="getUser" parameterType="Long" resultType="user">
select id,user_name as userName,sex,note from t_user where id = #{id}
</select>
</mapper>
关于这个映射配置的xml文件
namespace:指定了一个接口
<select>元素:指定了一个查询语句
id指代了下面配置的这个sql,同时在接口中会存在这样一个方法与之对应
parameterType:指定为Long,表示参数类型为长整型long
resultType:指定了返回的类型,这里使用了user别名,实际上这个别名则表示了pojo类:com.xiaoxie.pojo.User
关于查询sql语句,我们看到user_name中使用了as指定别名为userName,这里是为了让数据库返回的字段名与属性的名称对应上以便于MyBatis可以自动映射上
7、Dao接口
package com.xiaoxie.dao;
import com.xiaoxie.pojo.User;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
@Repository
public interface MyBatisUserDao {
public User getUser(Long id);
}
这里要注意一下,在类上添加了注解@Repository
8、spirng配置文件的整合
在resources文件下新增Spring配置文件application.properties
#durid的配置
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=org.gjt.mm.mysql.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/test
spring.datasource.username=root
spring.datasource.password=root
#连接池的属性
spring.datasource.druid.initial-size=15
spring.datasource.druid.max-active=100
spring.datasource.druid.min-idle=15
spring.datasource.druid.max-wait=60000
spring.datasource.druid.keep-alive=true
#mybatis映射文件配置
mybatis.mapper-locations=classpath:mybatis/mapper/*.xml
#mybatis扫描别名包与注解@Alias联用
mybatis.type-aliases-package=com.xiaoxie.pojo
#配置typeHandler的扫描包
mybatis.type-handlers-package=com.xiaoxie.handler
#日志配置
logging.level.root=DEBUG
logging.level.org.springframework=DEBUG
logging.level.org.mybatis=DEBUG
durid配置,是为了配置数据源
中间的那一部分则把前面做的映射文件、pojo别名、typeHandler进行配置
下面那一部分是针对日志的输出,这里指定为DEBUG,目的是为了在运行过程中可以看到更多的日志信息
9、Spring Boot整合MyBatis
MyBatis社区在与Spring整合的包中提供了两个类MapperFacotoryBean、MapperScannerConfigurer
MapperFactoryBean:它是针对一个接口配置
MapperScannerConfigurer:这是指的做扫描装配,也就是提供扫描装配MyBatis的接口到Spring IoC容器中
除了上面的两种方法MyBatis还提供了注解@MapperScan,也可以把MyBatis所需要对应接口扫描装配到Spring IoC容器中,使用这种方法则不需要做代码的开发,使用起来相对简单便捷,也是实际开发中常用的手段。
MapperFacotoryBean装配MyBatis接口
新增一个AppConfig类:com.xiaoxie.config.AppConfig
package com.xiaoxie.config;
import com.xiaoxie.dao.MyBatisUserDao;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.annotation.MapperScan;
import org.mybatis.spring.mapper.MapperFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Repository;
@Configuration
public class AppConfig {
@Autowired
SqlSessionFactory sqlSessionFactory = null;
//使用MapperFactoryBean
@Bean
public MapperFactoryBean<MyBatisUserDao> initMyBatisUserDao(){
MapperFactoryBean<MyBatisUserDao> bean = new MapperFactoryBean<>();
bean.setMapperInterface(MyBatisUserDao.class);
bean.setSqlSessionFactory(sqlSessionFactory);
return bean;
}
}
这里自动注入的sqlSessionFactory是SpringBoot自动生成的,直接拿来使用即可。
使用MapperFactoryBean来定义Mapper接口
使用MapperScannerConfigurer扫描装配MyBatis接口
将com.xiaoxie.config.AppConfig类调整为如下:
package com.xiaoxie.config;
import com.xiaoxie.dao.MyBatisUserDao;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.annotation.MapperScan;
import org.mybatis.spring.mapper.MapperFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Repository;
@Configuration
public class AppConfig {
//使用MapperScannerConfigurer
@Bean
public MapperScannerConfigurer mapperScannerConfigurer(){
//定义扫描器实例
MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
//加载SqlSessionFactory,SpringBoot会自动产生
mapperScannerConfigurer.setSqlSessionFactoryBeanName("sqlSessionFactory");
//定义扫描的包
mapperScannerConfigurer.setBasePackage("com.xiaoxie.*");
//限定被标注@Repository的接口才被扫描
mapperScannerConfigurer.setAnnotationClass(Repository.class);
return mapperScannerConfigurer;
}
}
上述代码中,定义了扫描的包,这样程序就会去扫描对应的包了。
这里面琮使用到了注解的限制,限制了被标注为@Repository注解的接口才会被扫描
使用@MapperScan注解
package com.xiaoxie.config;
import com.xiaoxie.dao.MyBatisUserDao;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.annotation.MapperScan;
import org.mybatis.spring.mapper.MapperFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Repository;
@Configuration
//定义JPA接口扫描包路径
@MapperScan(
//指定扫描包
basePackages = "com.xiaoxie.*",
//指定SqlSessionFactory
sqlSessionFactoryRef = "sqlSessionFactory",
//指定sqlSessionTemplate,如果这里指定则sqlSessionFactory配置则忽略
//sqlSessionTemplateRef = "sqlSessionTemplate",
annotationClass = Repository.class
)
public class AppConfig {
}
@MapperScan允许通过扫描加载MyBatis的Mapper
sqlSessionFactoryRef、sqlSessionTemplateRef,如果不存在多个的话不用指定可用可无,但是如果指定了后注意sqlSessionTemplateRef的优先级高于sqlSessionFactoryRef,同时使用annotationClass来限定扫描的接口
注意:在上面对接口使用了@Repository,这个注解也可以换为@Mapper完全是一样的效果
10、新增service
新增Service的接口:com.xiaoxie.service.MyBatisUserService
package com.xiaoxie.service;
import com.xiaoxie.pojo.User;
public interface MyBatisUserService {
public User getUser(Long id);
}
新增Service接口的实现类:com.xiaoxie.service.impl.MyBatisUserServiceImpl
package com.xiaoxie.service.impl;
import com.xiaoxie.dao.MyBatisUserDao;
import com.xiaoxie.pojo.User;
import com.xiaoxie.service.MyBatisUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class MyBatisUserServiceImpl implements MyBatisUserService {
@Autowired
private MyBatisUserDao myBatisUserDao = null;
@Override
public User getUser(Long id) {
return myBatisUserDao.getUser(id);
}
}
11、新增SpringBoot的启动类
新增SpringBoot的启动类:com.xiaoxie.SpringBootStartApplication
package com.xiaoxie;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringBootStartApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootStartApplication.class,args);
}
}
12、测试SpringBoot与MyBatis的集成
新增一个Controller类,com.xiaoxie.controller.MyBatisController
package com.xiaoxie.controller;
import com.xiaoxie.pojo.User;
import com.xiaoxie.service.MyBatisUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@RequestMapping("/mybatis")
public class MyBatisController {
@Autowired
private MyBatisUserService myBatisUserService = null;
@RequestMapping("/getUser")
@ResponseBody
public User getUser(Long id){
return myBatisUserService.getUser(id);
}
}
运行启动类,访问http://localhost:8080/mybatis/getUser?id=2,这个访问地址会把id为2这个参数带入去请求getUser,得到数据库中id为2的用户记录,并把用户信息是页面中使用JSON的格式展示出来
二、MyBatis插件
这里的插件也叫做拦截器
1、定义一个MyBatis插件com.xiaoxie.plugin.MyPlugin
package com.xiaoxie.plugin;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;
import java.sql.Connection;
import java.util.Properties;
//定义拦截器签名
@Intercepts({
@Signature(type = StatementHandler.class,
method="prepare",
args={Connection.class,Integer.class})
})
public class MyPlugin implements Interceptor {
Properties properties = null;
//拦截器的逻辑方法
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("插件拦截器逻辑方法...");
return invocation.proceed();
}
//生成拦截器代理对象
@Override
public Object plugin(Object target) {
return Plugin.wrap(target,this);
}
//设置插件属性
@Override
public void setProperties(Properties properties) {
this.properties = properties;
}
}
自定义的拦截器实现了Intercept接口,这个类主要操作做了如下几个操作
1、拦截器的逻辑方法,其中invocation.proceed()表示调用原方法,其它方法则是对原方法的增强
2、生成拦截器对象,Plugin.wrap(target,this)
3、设置插件属性
2、MyBatis配置文件
在resources/mybatis/mybatis-config.xml
<?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>
<plugins>
<plugin interceptor="com.xiaoxie.plugin.MyPlugin">
<property name="key1" value="value1"/>
<property name="key2" value="value2"/>
<property name="key3" value="value3"/>
</plugin>
</plugins>
</configuration>
3、application.properties文件中添加配置
#添加mybatis的配置文件
mybatis.config-location=classpath:mybatis/mybatis-config.xml
三、数据库事务处理
在Spring中,数据库事务是通过AOP技术提供服务的。
Spring的事务中可以有两种使用方式:编程式事务、声明式事务
编程式事务,它是一种比较底层的事务现在使用比较少,SpringBoot中也不推荐使用这种事务方式。
JDBC的数据库事务处理
1、添加一个DataSource的配置类,使用druid,返回一个DataSrouce的bean
新增类:com.xiaoxie.config.DataSourceConfig
package com.xiaoxie.config;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Configuration
public class DataSourceConfig {
@Bean(name = "dataSrouce_durid")
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druid(){
return new DruidDataSource();
}
}
2、在Service接口中新增一个insert方法
public int insert2(String userName,String note);
3、在实现类中新增insert的实现
//在jdbc中使用事务
@Autowired
private DataSource dataSrouce_durid = null;
@Override
public int insert2(String userName, String note) {
Connection conn = null;
int result = 0;
try{
//获取连接
conn = dataSrouce_durid.getConnection();
//开启事务
conn.setAutoCommit(false);
//设置隔离级别
conn.setTransactionIsolation(TransactionIsolationLevel.READ_COMMITTED.getLevel());
//sql
PreparedStatement ps = conn.prepareStatement("insert into t_user(user_name,note) values(?,?)");
ps.setString(1,userName);
ps.setString(2,note);
result = ps.executeUpdate();
//提交事务
conn.commit();
} catch (Exception e){
//回滚事务
if(conn!=null){
try {
conn.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
}
e.printStackTrace();
} finally {
//关闭数据库连接
try {
if(conn != null && !conn.isClosed())
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
return result;
}
在JDBC的处理方法中手动添加了事务处理的方法
4、在controller处理类中新增一个处理方法
@RequestMapping("/insertUser2")
@ResponseBody
public String insertUser2(String userName,String note){
int num = 0;
num = myBatisUserService.insert2(userName,note);
if(num>0)
return "成功插入user记录共" + num + "条!";
return "插入user记录失败";
}
启动springBoot,访问:http://localhost:8080/mybaits/insertUser2?userName=王五&noet=来啊,可以向数据库中新增一条记录。
Spring声明式事务的使用
在Spring中对于声明式事务,使用@Transactional进行标注的,这个注解可以在类或者方法上,当这个注解放在类上的时候表示这个类所有公共(public)非静态的方法都会启用事务功能,这个注解还可以配置许多属性,如事务的隔离级别、传播行为,异常的类型(明确在什么异常下回滚什么异常下不回滚),上面说的这些配置内容,在Spring IoC容器在加载时方就会解析出来,然后把这些信息存到事务定义器(TransactionDefinition接口的实现类)中,记录哪些类或者方法需要启动事务功能,用什么样的策略去执行事务。
当Spring的上下文开始调用被@Transactional标注的类或者方法时,Spring会产生AOP的功能。跟着Spring会开始调用开发者编写的业务代码,在业务代码中可能产生异常也可能不产生异常,Spring数据库事务的流程中,它会根据是否发生异常来采取不同的策略。
当没有异常时,Spring数据库拦截器会提交事务,如果产生异常则判断事务定义器内的配置,如果事务定义器约定这个异常不回滚事务则提交,如果没有配置或者不是配置不回滚事务的异常,则会回滚事务,并且会把异常抛出,这个是由事务拦截器这完成的。
注意,无论是否业务代码发生异常,Spring都会释放事务资源,以保证数据库连接池正常可用,这个也是由Spring事务拦截器来完成的。
1、Dao的接口中新增inser方法
public int insertUser(@Param("userName") String userName, @Param("note") String note);
这里参数前加上了@Param是指的我们在浏览器访问时提交的参数名称就是userName和note
2、在映射文件中新增inserUser信息,修改过后的映射文件如下
<?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.xiaoxie.dao.MyBatisUserDao">
<select id="getUser" parameterType="Long" resultType="user">
select id,user_name as userName,sex,note from t_user where id = #{id}
</select>
<insert id="insertUser" useGeneratedKeys="true" keyProperty="id">
insert into t_user(user_name,note) values(#{userName},#{note})
</insert>
</mapper>
3、在service接口中新增insert方法
public int insert(String userName,String note);
4、在service的接口实现类中新增insert的实现
@Override
@Transactional(isolation = Isolation.READ_COMMITTED,timeout = 1)
public int insert(String userName, String note) {
return myBatisUserDao.insertUser(userName,note);
}
在这个方法上添加了注解@Transactional表示需要启动数据库事务
5、在controller类中新增相应的方法
@RequestMapping("/insertUser")
@ResponseBody
public String insertUser(String userName, String note){
int num = 0;
num = myBatisUserService.insert(userName,note);
if(num >0) {
return "成功插入user记录共" + num + "条!";
} else {
return "插入user记录失败!";
}
}
启动SpringBoot,访问:http://localhost:8080/mybatis/inserUser?userName=赵六¬e=来啊 可以向数据库中新增一条记录,并且我们在后台日志中可以明确看到事务的相关内容
@Transactional源码如下:
package org.springframework.transaction.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
import org.springframework.transaction.TransactionDefinition;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
//通过bean name指定事务管理器
@AliasFor("transactionManager")
String value() default "";
//同value的属性
@AliasFor("value")
String transactionManager() default "";
//指定传播行为
Propagation propagation() default Propagation.REQUIRED;
//指定隔离级别
Isolation isolation() default Isolation.DEFAULT;
//指定超时时间(单位:秒)
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
//是否只读事务
boolean readOnly() default false;
//方法在发生指定异常时回滚,默认是所有异常都回滚
Class<? extends Throwable>[] rollbackFor() default {};
//方法在发生指定异常名称时回滚,默认是所有异常都回滚
String[] rollbackForClassName() default {};
//方法在发生指定异常时不回滚,默认是所有异常都回滚
Class<? extends Throwable>[] noRollbackFor() default {};
//方法在发生指定异常名称时不回滚,默认是所有异常都回滚
String[] noRollbackForClassName() default {};
}
一般来说@Transactional注解是放在实现类上的,这是Spring所推荐的方式。
Spring的事务管理器
在事务流程中,事务的打开、回滚和提交都是由事务管理器来完成的。
事务管理器的顶层接口为:PlatformTransactionManager
PlatformTransactionManager接口源码:
package org.springframework.transaction;
import org.springframework.lang.Nullable;
public interface PlatformTransactionManager {
//获取事务,它还会设置数据属性
TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;
//提交事务
void commit(TransactionStatus status) throws TransactionException;
//回滚事务
void rollback(TransactionStatus status) throws TransactionException;
}
getTransaction()方法中的参数是TransactionDefinition,它是依赖于配置@Transactional的配置项生成的,通过它就可以设置事务的属性了。
当项目依赖 mybatis-spring-boot-starter它会自动创建一个DataSourceTransactionManager对象作为事务管理器,如果依赖的是spring-boot-starter-data-jpa则会自动创建JpaTransactionManager对象作为事务管理器。所以对于事务管理器一般是不需要自行创建的。
数据库事务相关知识点
数据库事务的四个基本特征(ACID)
原子性:事务中包含的操作被看作一个整体的业务单元,这个业务单元中的操作要么全部成功,要么全部失败。不会存在部分失败部分成功的场景。
一致性:事务完成时,必须所有的数据都保持一致状态,在数据库中所有的修改都基于事务,保证数据的完整性
隔离性:当多个应用程序的线程同时访问同一数据时,同样的数据会在不同的事务中被访问,有产生丢失更新的可能,为了避免这个情况的发生,数据库中定义了隔离级别。
持久性:事务结束后,所有的数据要持久化到磁盘中。
关于隔离性,如下说明两类丢失更新的情况
情况一:(大部分的数据库已经克服了这种情况的产生)
商品初始库存1000
两个事务(T1,T2)在做扣减库存的操作,每次扣减1个
按下面的流程顺序操作数据库:
T1:初始库存1000 --> T1:扣减一个库存,余量999 --> T2:初始库存1000 --> T2:扣减一个库存,余量999 --> T2:提交事务,当前商品库存999 ---> T1:回滚事务,当前商品库存被置为1000
T2事务的结果丢失了!
情况二:
商品初始库存1000
两个事务(T1,T2)在做扣减库存的操作,每次扣减1个
按下面的流程顺序操作数据库:
T1:初始库存1000 --> T1:扣减一个库存,余量999 --> T2:初始库存1000 --> T2:扣减一个库存,余量999 --> T2:提交事务,当前商品库存999 ---> T1:提交事务,当前商品库存被置为999
多个事务提交导致结果丢失!
为了解决丢失更新的问题,数据库提出了四类隔离级别
1、未提交读
2、读写提交
3、可重复读
4、串行化
我们在选择隔离级别的时候要综合考虑数据的一致性避免脏读,又要考虑到系统的性能问题,如果数据一次只能一个线程读写其它线程挂起,当大量的线程处于等待时会导致系统响应慢体验差,并且大量的挂起恢复操作可能会导致服务器宕机。因而数据库规范中提出了上述四种隔离级别
未提交读
最低的隔离级别,允许一个事务读取另外一个事务没有提交的数据。它适用于对数据一致性要求不高但对并发能力要求高的场景,它的坏处是极有可能出现脏读。
读写提交
一个事务只能读取另一个事务已经提交的数据,不能读取未提交的数据
读写提交会存在不可重复读的问题
可重复读
可重复读的目标是克服读写提校中出现的不可重复读的现象
可重复读是在读取数据时必须要等当前的事务提交后才可以读取,这里也会产生一种新的问题就是幻读。
注意:幻读是针对多条数据库记录统计出来的,而不是一条数据库记录而言。
串行化
串行化是数据库最高的隔离级别,它会要求所有Sql按顺序执行,这样可以克服掉隔离级别出现的各种问题,以保证数据的一致性。
在现实中一般而言,选择隔离级别会以读写提交为主,它可以防止脏读,而不能避免不可重复读和幻读。为了克服数据不一致和性能问题,程序开发者还设计了乐观锁,甚至不再使用数据库而使用其他的手段。
注意:对于隔离级别,Oracle只能支持读写提交(默认)和串行化,而MySQL则可以支持四种,Mysql的默认级别是可重复读
在@Transactional注解中isolation可以定义四种隔离级别如下:
Isolation.ISOLATION_READ_UNCOMMITTED 未提交读 值:1
Isolation.ISOLATION_READ_COMMITTED 读写提交 值:2
Isolation.ISOLATION_REPEATABLE_READ 可重复读 值:4
Isolation.ISOLATION_SERIALIZABLE 串行化 值:8
在每一个方法中单独设置隔离级别比较麻烦,可以在Spring的配置文件中定义默认的隔离级别,如下所示
#数据源默认的隔离级别
spring.datasource.druid.default-transaction-isolation=2
传播行为
传播行为是方法之间调用事务采取的策略问题。在大部分情况下,我们认为数据库事务要么全部成功,要么全部失败。但是现实中也会有特殊的情况。
在Spring中,当一个方法调用另一个方法时,可以让事务采取不同的策略工作,如新建事务或者挂起当前事务,这便是事务的传播行为。
传播行为的定义
在Spring事务机制中对数据库存在7种传播行为,它是通过枚举类Propagation定义的,这个枚举类的源码如下:
package org.springframework.transaction.annotation;
import org.springframework.transaction.TransactionDefinition;
public enum Propagation {
//需要事务,这个是默认传播行为,如果当前存在事务则沿用当前事务,否则新建一个事务运行子方法
REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED),
//支持事务,如果当前存在事务则沿用当前事务,如果不存在,则继续采用无事务的方式运行子方法
SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),
//必须使用事务,如果当前没有事务,则会抛出异常,如果存在当前事务则沿用当前事务
MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY),
//无论当前事务是否存在,都会创建新事务运行方法,这样新事务则可以有新的锁和隔离级别等特性,与当前事务相互独立
REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),
//不支持事务,当前存在事务时,将挂起事务,运行方法
NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED),
//不支持事务,如果当前方法存在事务则抛出异常,否则继续使用无事务机制运行
NEVER(TransactionDefinition.PROPAGATION_NEVER),
//当前方法调用子方法时,如果子方法发生异常只回滚子方法执行过的sql,而不回滚当前方法的事务
NESTED(TransactionDefinition.PROPAGATION_NESTED);
private final int value;
Propagation(int value) { this.value = value; }
public int value() { return this.value; }
}
注意:上面源码中加粗的为常用的三种传播行为
新增一个service接口UserBatchService
package com.xiaoxie.service;
import com.xiaoxie.pojo.User;
import java.util.List;
public interface UserBatchService {
public int insertUsers(List<User> userList);
}
新增一个service的实现类UserBatchServiceImpl
package com.xiaoxie.service.impl;
import com.xiaoxie.pojo.User;
import com.xiaoxie.service.MyBatisUserService;
import com.xiaoxie.service.UserBatchService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class UserBatchServiceImpl implements UserBatchService {
@Autowired
MyBatisUserService myBatisUserService = null;
@Override
@Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRED)
public int insertUsers(List<User> userList) {
int count = 0;
for(User user:userList){
//调用子方法
count += myBatisUserService.insert(user.getUserName(),user.getNote());
}
return count;
}
}
从上面的代码中我们可以看到,传播行为我们使用了propagation=Propagation.REQUIRED
在controller类中新增方法
@Autowired
private UserBatchService userBatchService = null;
@RequestMapping("/insertUsers")
@ResponseBody
public Map<String,Object> insertUsers(String userName1,String note1,String userName2,String note2){
User user1 = new User();
user1.setUserName(userName1);
user1.setNote(note1);
User user2 = new User();
user2.setUserName(userName2);
user2.setNote(note2);
List<User> users = new ArrayList<>();
users.add(user1);
users.add(user2);
int inserts = userBatchService.insertUsers(users);
Map<String,Object> result = new HashMap<>();
result.put("success",inserts>0);
result.put("user",users);
return result;
}
启动SpringBoot,访问:http://localhost:8080/mybatis/insertUsers?userName1=小明¬e1=小明&userName2=小红¬e2=小红
这个时候控制台打印的结果中我们可以看到
Participating in existing transaction
这可以看出来在调用子方法时沿用了父方法的事务
当我们把被调用子方法service实现类中事务传播行为设置为:propagation=Propagation.REQUIRES_NEW
则在子方法执行的候会启用新的数据库事务去执行子方法,并且每一次调用都是独立提交的。
@Transactional自调和失效问题说明
当在同一个类中,一个方法调用这个类的别一个方法,也就是说子方法与当前方法在同一个类当中时,@Transactional会失效,在调用子方法时不会去创建任何新的事务,这个是由于Spring数据库事务的约定,其实现的原理是AOP,而AOP的原理是动态代理,在自调用过程中,是类自身的调用,而不是代理对象的调用,这样就不会产产生AOP。
为了解决这个问题有两种处理方法:
第一、用一个Service去调用另一个Service(前面的例子中就是使用这种方式)
第二、从Spring IoC容器中获取代理对象去启用AOP
如何从Sping IoC中获取代理对象
类要实现 ApplicaitonContextAware
类中新增属性 ApplicationContext
实现生命周期方法,设置IoC容器
private ApplicationContext applicationContext = null;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throw BeansException{
this.applicationContext = applicationContext;
}
在需要取出代理对象的使用的地方进行如下编码,通过这样就可以得行代理对象
UserService userService = applicationContext.getBean(UserService.class);