您好,欢迎来到欧得旅游网。
搜索
您的当前位置:首页Spring整合MyBatis的事务管理

Spring整合MyBatis的事务管理

来源:欧得旅游网

环境

  • Ubuntu 22.04
  • IntelliJ IDEA 2022.1.3
  • JDK 17.0.3
  • Spring 5.3.21
  • MyBatis 3.5.10

准备

创建Maven项目 test0703

修改 pom.xml ,添加依赖以及设置静态资源:

    ......
    <dependencies>
        <!-- https://mvnrepository.com/artifact/junit/junit -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.10</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.29</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.3.21</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.mybatis/mybatis-spring -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>2.0.7</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.3.21</version>
        </dependency>

    </dependencies>

    <build>
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
                <filtering>false</filtering>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
                <filtering>false</filtering>
            </resource>
        </resources>
    </build>
    ......

创建POJO MyObject.java

package pojo;

public class MyObject {
    private int c1;
    private int c2;

    public int getC1() {
        return c1;
    }

    public void setC1(int c1) {
        this.c1 = c1;
    }

    public int getC2() {
        return c2;
    }

    public void setC2(int c2) {
        this.c2 = c2;
    }

    @Override
    public String toString() {
        return "MyObject{" +
                "c1=" + c1 +
                ", c2=" + c2 +
                '}';
    }
}

创建Mapper MyMapper.javaMyMapper.xml

package dao;

import pojo.MyObject;

import java.util.List;

public interface MyMapper {
    public List<MyObject> read();

    public void add(int c1, int value);

    public void reduce(int c1, int value);
}
<?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="dao.MyMapper">
    <select id = "read" resultType="pojo.MyObject">
        select c1, c2 from t1
    </select>

    <update id="add">
        update t1 set c2 = c2 + #{param2} where c1 = #{param1}
    </update>

    <update id="reduce">
        update t1 set c2 = c2 - #{param2} where c1 = #{param1}
    </update>
</mapper>

创建Service MyService.java (接口)和 MyServiceImpl1.java (实现类):

package service;

public interface MyService {
    public void show();

    public void change();
}
package service;

import dao.MyMapper;
import pojo.MyObject;

import java.util.List;

public class MyServiceImpl1 implements MyService{
    private MyMapper mapper;

    public void setMyMapper(MyMapper mapper) {
        this.mapper = mapper;
    }

    @Override
    public void show() {
        List<MyObject> list = mapper.read();
        list.forEach(System.out::println);
    }

    @Override
    public void change() {
        int value = 100;

        try {
            mapper.reduce(1, value);
            mapper.add(2, value);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
<?xml version="1.0" encoding="utf-8"?>
<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">

    <!-- datasource -->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/repo"/>
        <property name="username" value="root"/>
        <property name="password" value="123456"/>
    </bean>

    <!-- sqlSessionFactory -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <!--        <property name="configLocation" value="classpath:mybatis-config.xml"/>-->
        <property name="mapperLocations" value="classpath:dao/*.xml"/>
    </bean>

    <bean id="myMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
        <property name="mapperInterface" value="dao.MyMapper" />
        <property name="sqlSessionFactory" ref="sqlSessionFactory" />
    </bean>

    <bean id="myService1" class="service.MyServiceImpl1">
        <property name="myMapper" ref="myMapper"/>
    </bean>
</beans>
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import service.MyService;

import java.io.IOException;

public class MyTest {
    @Test
    public void test1() throws IOException {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");

        MyService service = context.getBean("myService1", MyService.class);

        System.out.println("==================== Before: ====================");
        service.show();

        service.change();

        System.out.println("==================== After: ====================");
        service.show();
    }
}

运行测试,如下:

==================== Before: ====================
MyObject{c1=1, c2=10000}
MyObject{c1=2, c2=10000}
==================== After: ====================
MyObject{c1=1, c2=9900}
MyObject{c1=2, c2=10100}

本例模拟了一个简单的转账操作, c1 账户转出100元,同时 c2 账户转入100元。

没有事务管理的问题

假定转入操作失败了。本例中,为了模拟操作失败,我们故意把SQL写错:

    ......
    <update id="add">
        updateaaa t1 set c2 = c2 + #{param2} where c1 = #{param1}
    </update>
    ......

再次运行测试:

==================== Before: ====================
MyObject{c1=1, c2=9900}
MyObject{c1=2, c2=10100}

org.springframework.jdbc.BadSqlGrammarException: 
### Error updating database.  Cause: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'updateaaa t1 set c2 = c2 + 100 where c1 = 2' at line 1
### The error may exist in file [/home/ding/IdeaProjects/test0703/target/classes/dao/MyMapper.xml]
### The error may involve defaultParameterMap
### The error occurred while setting parameters
### SQL: updateaaa t1 set c2 = c2 + ? where c1 = ?
### Cause: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'updateaaa t1 set c2 = c2 + 100 where c1 = 2'
......

==================== After: ====================
MyObject{c1=1, c2=9800}
MyObject{c1=2, c2=10100}

可见,这样就会出现问题, c1 账户已经转出100元,而 c2账户转入100元的操作失败了。

转账是一个典型的事务处理操作,应该使用事务管理,如果某一个操作失败,应该把整个事务回滚。

Spring的事务管理

声明式事务

Spring的声明式事务基于AOP实现,它的优点是只需修改Spring配置文件,无需修改Service组件的代码逻辑。

spring-webmvc 已经包含了对Spring AOP的依赖,但是我们还需要添加对 aspectjweaver 的依赖:

        ......
        <!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.9.1</version>
            <scope>runtime</scope>
        </dependency>
        ......

修改 applicationContext.xml 配置文件:

    ......
    <beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        https://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">
    ......
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <constructor-arg ref="dataSource" />
    </bean>

    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="*" propagation="REQUIRED"/>
        </tx:attributes>
    </tx:advice>

    <aop:config>
        <aop:pointcut id="myPointcut" expression="execution(* service.*.*(..))"/>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="myPointcut"/>
    </aop:config>
    ......
  • 无论是声明式事务,还是编程式事务,都要添加 transactionManager ,格式是固定的,照抄就行(对于所需的 dataSource ,本例中使用的是构造器注入,如果换成属性注入也行);
  • txAdvice 的格式基本也是固定的:
    • 对于 method 属性,本例中使用了 name="*" ,表示所有方法都适用,可以改成比如 name="get*" ,则表示适用于 get 开头的方法;
    • propagation="REQUIRED" 是默认属性,不写也可以,表示如果当前没有事务,就新建一个事务,这是最常见的选择;
    • 此外,还有 read-onlytmeout 等属性,基本上顾名思义;
  • myPointcutexpression="execution(* service.*.*(..))" 表示 service 包下的所有类的所有方法,参数不限(本例中其实只有 change() 方法需要做事务管理);

现在就可以运行测试了,但有一点需要注意,虽然说声明式事务是非侵入式的,不用修改任何代码就能直接运行,但是在本例中,service里使用了 try 块:

        ......
        try {
            mapper.reduce(1, value);
            mapper.add(2, value);
        } catch (Exception e) {
            e.printStackTrace();
        }
        ......

这会对事务失败的判断产生影响。具体说来,因为代码中捕获并处理了异常,事务管理器就不会认为事务失败并回滚事务了,就跟没有事务管理的效果一样,所以这里一定要把 try 去掉:

//        try {
            mapper.reduce(1, value);
            mapper.add(2, value);
//        } catch (Exception e) {
//            e.printStackTrace();
//        }

为了使得异常之后的代码能继续运行,可以把 try 块放到测试里:

        ......
        try {
            service.change();
        } catch (Exception e) {
            e.printStackTrace();
        }
        ......

把两条记录的 c2 值都还原为 10000 ,重新运行测试,如下:

==================== Before: ====================
MyObject{c1=1, c2=10000}
MyObject{c1=2, c2=10000}

org.springframework.jdbc.BadSqlGrammarException: 
### Error updating database.  Cause: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'updateaaa t1 set c2 = c2 + 100 where c1 = 2' at line 1
### The error may exist in file [/home/ding/IdeaProjects/test0703/target/classes/dao/MyMapper.xml]
### The error may involve defaultParameterMap
### The error occurred while setting parameters
### SQL: updateaaa t1 set c2 = c2 + ? where c1 = ?
### Cause: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'updateaaa t1 set c2 = c2 + 100 where c1 = 2'
......

==================== After: ====================
MyObject{c1=1, c2=10000}
MyObject{c1=2, c2=10000}

可见,出现异常后,事务回滚了。具体来说,因为转入操作失败,则之前转出的操作回滚了。

编程式事务

修改 applicationContext.xml 文件:

    ......
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <constructor-arg ref="dataSource" />
    </bean>
    ......
  • transactionManager :和声明式事务一样,需要配置 transactionManager

创建 MyServiceImpl2.java

package service;

import dao.MyMapper;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import pojo.MyObject;

import java.util.List;

public class MyServiceImpl2 implements MyService{
    private final PlatformTransactionManager transactionManager;

    private MyMapper mapper;

    public MyServiceImpl2(PlatformTransactionManager transactionManager) {
        this.transactionManager = transactionManager;
    }

    public void setMyMapper(MyMapper mapper) {
        this.mapper = mapper;
    }

    @Override
    public void show() {
        List<MyObject> list = mapper.read();
        list.forEach(System.out::println);
    }

    @Override
    public void change() {
        TransactionStatus txStatus =
                transactionManager.getTransaction(new DefaultTransactionDefinition());
        try {
            int value = 100;
            mapper.reduce(1, value);
            System.out.println("===================================");
            mapper.add(2, value);
        } catch (Exception e) {
            transactionManager.rollback(txStatus);
            throw e;
        }
        transactionManager.commit(txStatus);
    }
}

注:为了防止事务一开始就报错引起混淆,代码里加了一行输出,如果看到这行输出,说明转出操作已经做了。

注意:该方法需要显式 try ,捕获异常,并回滚事务。而前面介绍的声明式事务则不能捕获异常,要留给事务管理器来判断异常并回滚事务。

applicationContext.xml 中注册 MyServiceImpl2

    ......
    <bean id="myService2" class="service.MyServiceImpl2">
        <constructor-arg ref="transactionManager"/>
        <property name="myMapper" ref="myMapper"/>
    </bean>
    ......

创建测试如下:

    @Test
    public void test2() throws IOException {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");

        MyService service = context.getBean("myService2", MyService.class);
        service.show();

        try {
            service.change();
        } catch (Exception e) {
            e.printStackTrace();
        }

        service.show();
    }

把SQL改成正确写法,把两条记录的 c2 值都还原为 10000 ,运行test2,如下:

MyObject{c1=1, c2=10000}
MyObject{c1=2, c2=10000}
===================================
MyObject{c1=1, c2=9900}
MyObject{c1=2, c2=10100}

把SQL改成错误写法,把两条记录的 c2 值都还原为 10000 ,运行test2,如下:

MyObject{c1=1, c2=10000}
MyObject{c1=2, c2=10000}
===================================

org.springframework.jdbc.BadSqlGrammarException: 
### Error updating database.  Cause: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'updateaaa t1 set c2 = c2 + 100 where c1 = 2' at line 1
### The error may exist in file [/home/ding/IdeaProjects/test0703/target/classes/dao/MyMapper.xml]
### The error may involve defaultParameterMap
### The error occurred while setting parameters
### SQL: updateaaa t1 set c2 = c2 + ? where c1 = ?
### Cause: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'updateaaa t1 set c2 = c2 + 100 where c1 = 2'
......

MyObject{c1=1, c2=10000}
MyObject{c1=2, c2=10000}

可见,转入操作失败,事务回滚了。

编程式事务有一个变种的用法:

    ......
    @Override
    public void change() {
        TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);

        transactionTemplate.execute(txStatus -> {
            int value = 100;
            mapper.reduce(1, value);
            System.out.println("===================================");
            mapper.add(2, value);
            return null;
        });
    }
    ......

效果和前面一样,不再赘述。

注意:这里跟声明式事务类似,千万不要 try 并且捕获异常,否则事务管理器就不会认为事务失败了。

还有一点需要注意的是,在配置 SqlSessionFactory 时,不要添加如下属性:

  <property name="transactionFactory">
    <bean class="org.apache.ibatis.transaction.managed.ManagedTransactionFactory" />
  </property>

否则会报错: org.springframework.dao.TransientDataAccessResourceException: SqlSessionFactory must be using a SpringManagedTransactionFactory in order to use Spring transaction synchronization

该属性值表示由容器管理事务,而不使用Spring的事务管理。

总结

  • 声明式事务使用AOP来管理事务,用法相对比较简单,而且是非侵入式的,推荐使用;
  • 编程式事务需要在代码逻辑里显式管理事务,相对比较麻烦;
  • 对于声明式事务以及编程式事务的变种写法,千万记得不要 try 并且捕获异常,否则事务管理器就不会认为事务失败了。当然,可以在外层捕获异常;

参考

  • http://mybatis.org/spring
  • http://mybatis.org/spring/zh/index.html (中文版)
  • https://www.bilibili.com/video/BV1WE411d7Dv?p=27&vd_source=2ec1bfab0d3afd52d760b0dd55

因篇幅问题不能全部显示,请点此查看更多更全内容

Copyright © 2019- ovod.cn 版权所有 湘ICP备2023023988号-4

违法及侵权请联系:TEL:199 1889 7713 E-MAIL:2724546146@qq.com

本站由北京市万商天勤律师事务所王兴未律师提供法律服务