• MyBatis(6):MyBatis集成Spring事务管理(下)


    前一篇文章复习了MyBatis的基本使用以及使用Spring管理MyBatis的事务的做法,本文的目的是在这个的基础上稍微做一点点的进阶:多数据的事务处理。文章内容主要包含两方面:

    1、单表多数据的事务处理

    2、多库/多表多数据的事务处理

    这两种都是企业级开发中常见的需求,有一定的类似,在处理的方法与技巧上又各有不同,在进入文章前,先做一些准备工作,因为后面会用到多表的插入事务管理,前面的文章建立了一个Student相关表及类,这里再建立一个Teacher相关的表及类。第一步是建立一张Teacher表:

    1
    2
    3
    4
    5
    6
    create table teacher
    (
        teacher_id    int            auto_increment,
        teacher_name  varchar(20)    not null,
        primary key(teacher_id)
    )

    建立teacher_mapper.xml:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <?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="TeacherMapper">
        <resultMap type="Teacher" id="TeacherMap">
            <id column="teacher_id" property="teacherId" jdbcType="INTEGER" />
            <result column="teacher_name" property="teacherName" jdbcType="VARCHAR" />
        </resultMap>
     
        <select id="selectAllTeachers" resultMap="TeacherMap">
            select teacher_id, teacher_name from teacher;
        </select>
     
        <insert id="insertTeacher" useGeneratedKeys="true" keyProperty="teacher_id" parameterType="Teacher">
            insert into teacher(teacher_id, teacher_name) values(null, #{teacherName, jdbcType=VARCHAR});
        </insert>
    </mapper>

    建立Teacher.java:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    public class Teacher
    {
        private int        teacherId;
        private String    teacherName;
     
        public int getTeacherId()
        {
            return teacherId;
        }
     
        public void setTeacherId(int teacherId)
        {
            this.teacherId = teacherId;
        }
     
        public String getTeacherName()
        {
            return teacherName;
        }
     
        public void setTeacherName(String teacherName)
        {
            this.teacherName = teacherName;
        }
     
        public String toString()
        {
            return "Teacher{teacherId:" + teacherId + "], [teacherName:" + teacherName + "}";
        }
    }

    还是再次提醒一下,推荐重写toString()方法,打印关键属性。不要忘了在config.xml里面给Teacher.java声明一个别名:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <?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>
            <typeAlias alias="Student" type="org.xrq.domain.Student" />
            <typeAlias alias="Teacher" type="org.xrq.domain.Teacher" />
        </typeAliases>
    </configuration>

    接着是TeacherDao.java接口:

    1
    2
    3
    4
    5
    public interface TeacherDao
    {
        public List<Teacher> selectAllTeachers();
        public int insertTeacher(Teacher teacher);
    }

    其实现类TeacherDaoImpl.java:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    @Repository
    public class TeacherDaoImpl extends SqlSessionDaoSupport implements TeacherDao
    {
        private static final String NAMESPACE = "TeacherMapper.";
     
        @Resource
        public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory)
        {
            super.setSqlSessionFactory(sqlSessionFactory);
        }
     
        public List<Teacher> selectAllTeachers()
        {
            return getSqlSession().selectList(NAMESPACE + "selectAllTeachers");
        }
     
        public int insertTeacher(Teacher teacher)
        {
            return getSqlSession().insert(NAMESPACE + "insertTeacher", teacher);
        }
    }

    OK,这样准备工作就全部做完了,有需要的朋友可以实际去把TeacherDao中的方法正确性先验证一下,下面进入文章的内容。

    单表事务管理

    有一个很常见的需求,在同一张表里面,我想批量插入100条数据,但是由于这100条数据之间存在一定的相关性,只要其中任何一条事务的插入失败,之前插入成功的数据就全部回滚,这应当如何实现?这里有两种解决方案:

    1、使用MyBatis的批量插入功能

    2、使用Spring管理事务,任何一条数据插入失败

    由于我们限定的前提是单表,因此比较推荐的是第一种做法

    第二种做法尽管也可以实现我们的目标,但是每插入一条数据就要发起一次数据库连接,即使使用了数据库连接池,但在性能上依然有一定程度的损失。而使用MyBatis的批量插入功能,只需要发起一次数据库的连接,这100次的插入操作在MyBatis看来是一个整体,其中任何一个插入的失败都将导致整体插入操作的失败,即:要么全部成功,要么全部失败

    下面来看一下实现,首先在student_mapper.xml中新增一个批量新增的方法<insert>:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    <?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="StudentMapper">
        <resultMap type="Student" id="StudentMap">
            <id column="student_id" property="studentId" jdbcType="INTEGER" />
            <result column="student_name" property="studentName" jdbcType="VARCHAR" />
        </resultMap>
     
        ...
     
        <insert id="batchInsert" useGeneratedKeys="true" parameterType="java.util.List">
            <selectKey resultType="int" keyProperty="studentId" order="AFTER"
                SELECT 
                LAST_INSERT_ID() 
            </selectKey>
            insert into student(student_id, student_name) values
            <foreach collection="list" item="item" index="index" separator=",">
                (#{item.studentId, jdbcType=INTEGER}, #{item.studentName, jdbcType=VARCHAR})
            </foreach>
        </insert>
    </mapper>

    这里主要是利用MyBatis提供的foreach,对传入的List做了一次遍历,并取得其中的属性进行插入。

    然后在StudentDao.java中新增一个批量新增的方法batchInsert:

    1
    2
    3
    4
    5
    6
    public interface StudentDao
    {
        public List<Student> selectAllStudents();
        public int insertStudent(Student student);
        public int batchInsertStudents(List<Student> studentList);
    }

    StudentDaoImpl.java实现它:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @Repository
    public class StudentDaoImpl extends SqlSessionDaoSupport implements StudentDao
    {
        private static final String NAMESPACE = "StudentMapper.";
     
        @Resource
        public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory)
        {
            super.setSqlSessionFactory(sqlSessionFactory);
        }
     
        ...
     
        public int batchInsertStudents(List<Student> studentList)
        {
            return getSqlSession().insert(NAMESPACE + "batchInsert", studentList);
        }
    }

    接着验证一下,首先drop一下student这张表并重新建一下,然后写一段测试程序:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    public class StudentTest
    {
        @SuppressWarnings("resource")
        public static void main(String[] args)
        {
            ApplicationContext ac = new ClassPathXmlApplicationContext("spring.xml");
            StudentDao studentDao = (StudentDao)ac.getBean("studentDaoImpl");
            List<Student> studentList = null;
     
            Student student0 = new Student();
            student0.setStudentName("Smith");
            Student student1 = new Student();
            student1.setStudentName("ArmStrong");
            studentList = new ArrayList<>();
            studentList.add(student0);
            studentList.add(student1);
            studentDao.batchInsertStudents(studentList);
     
            System.out.println("-----Display students------");
            studentList = studentDao.selectAllStudents();
            for (int i = 0, length = studentList.size(); i < length; i++)
                System.out.println(studentList.get(i));
        }
    }

    运行结果为:

    1
    2
    3
    -----Display students------
    Student{[studentId:1], [studentName:Smith]}
    Student{[studentId:2], [studentName:ArmStrong]}

    看到批量插入成功。

    从另外一个角度来看,假如我们这么建立这个studentList:

    1
    2
    3
    4
    5
    6
    7
    8
    Student student0 = new Student();
    student0.setStudentName("Smith");
    Student student1 = new Student();
    student1.setStudentName(null);
    studentList = new ArrayList<>();
    studentList.add(student0);
    studentList.add(student1);
    studentDao.batchInsertStudents(studentList);

    故意制造第一条插入OK,第二条插入报错的场景,此时再运行一下程序,程序会抛出异常,即使第一条数据是OK的,依然不会插入。

    最后,这里是批量插入,批量修改、批量删除也是一样的做法,可以自己试验一下。

    多库/多表事务管理

    上面的场景是对于单表的事务管理做法的推荐:实际上这并没有用到事务管理,而是使用MyBatis批量操作数据的做法,目的是为了减少和数据库的交互次数。

    现在有另外一种场景,我要对单库/多库的两张表(Student表、Teacher表)同时插入一条数据,要么全部成功,要么全部失败,该如何处理?此时明显就不可以使用MyBatis批量操作的方法了,要实现这个功能,可以使用Spring的事务管理。

    前面文章有讲,Dao层中的方法更多的是一种对数据库的增删改查的原子性操作,而Service层中的方法相当于对这些原子性的操作做一个组合,这里要同时操作TeacherDao、StudentDao中的insert方法,因此建立一个SchoolService接口:

    1
    2
    3
    4
    public interface SchoolService
    {
        public void insertTeacherAndStudent(Teacher teacher, Student student);
    }

    写一下这个接口的实现类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @Service
    public class SchoolServiceImpl implements SchoolService
    {
        @Resource
        private StudentDao studentDao;
     
        @Resource
        private TeacherDao teacherDao;
     
        @Transactional
        public void insertTeacherAndStudent(Teacher teacher, Student student)
        {
            studentDao.insertStudent(student);
            teacherDao.insertTeacher(teacher);
        }
    }

    这里用到了两个注解,解释一下。

    (1)@Service注解

    严格地说这里使用@Service注解不是特别好,因为Service作为服务层,更多的是应该对同一个Dao中的多个方法进行组合,如果要用到多个Dao中的方法,建议应该是放到Controller层中,引入两个Service,这里为了简单,就简单在一个Service中注入了StudentDao和TeacherDao两个了。

    (2)@Transactional注解

    这个注解用于开启事务管理,注意@Transactional注解的使用前提是该方法所在的类是一个Spring Bean,因此(1)中的@Service注解是必须的。换句话说,假如你给方法加了@Transactional注解却没有给类加@Service、@Repository、@Controller、@Component四个注解其中之一将类声明为一个Spring的Bean,那么对方法的事务管理,是不会起作用的。关于@Transactional注解,会在下面进一步解读。

    接着写一个测试类测试一下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class SchoolTest
    {
        @SuppressWarnings("resource")
        public static void main(String[] args)
        {
            ApplicationContext ac = new ClassPathXmlApplicationContext("spring.xml");
            SchoolService schoolService =
                    (SchoolService)ac.getBean("schoolServiceImpl");
     
            Student student = new Student();
            student.setStudentName("Billy");
            Teacher teacher = new Teacher();
            teacher.setTeacherName("Luna");
     
            schoolService.insertTeacherAndStudent(teacher, student);
        }
    }

    可以看一下数据库,Student表和Teacher表会同时多一条记录。接着继续从另外一个角度讲,我这么建立Student和Teacher:

    1
    2
    3
    4
    Student student = new Student();
    student.setStudentName("White");
    Teacher teacher = new Teacher();
    teacher.setTeacherName(null);

    故意制造Teacher报错的场景,此时尽管Student没有问题,但是由于Teacher插入报错,因此Student的插入进行回滚,查看Student表,是不会有student_name为”White”这条记录的。

    @Transactional注解

    @Transactional这个注解绝对是Java程序员的一个福音,如果没有@Transactional注解,我们使用配置文件的做法进行声明式事务管理,我网上随便找一段配置文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <!-- 事物切面配置 -->
    <tx:advice id="advice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="update*" propagation="REQUIRED" read-only="false" rollback-for="java.lang.Exception"/>
            <tx:method name="insert" propagation="REQUIRED" read-only="false"/>
        </tx:attributes>
    </tx:advice>
     
    <aop:config>
        <aop:pointcut id="testService" expression="execution (* com.baobao.service.MyBatisService.*(..))"/>
        <aop:advisor advice-ref="advice" pointcut-ref="testService"/>
    </aop:config>

    这种声明式的做法不得不说非常不好控制以及进行调试,尤其在要进行事务管理的内容不断增多之后,尤其体现出它的不方便。

    使用@Transactional注解就不一样了,它可以精细到具体的类甚至具体的方法上(区别是同一个类,对方法的事务管理配置会覆盖对类的事务管理配置),另外,声明式事务中的一些属性,在@Transaction注解中都可以进行配置,下面总结一下常用的一些属性。

    (1) @Transactional(propagation = Propagation.REQUIRED)

    最重要的先说,propagation属性表示的是事务的传播特性,一共有以下几种:

    事务传播特性 作      用
    Propagation.REQUIRED 方法运行时如果已经处在一个事务中,那么就加入到这个事务中,否则自己新建一个事务,REQUIRED是默认的事务传播特性
    Propagation.NOT_SUPPORTED 如果方法没有关联到一个事务,容器不会为它开启一个事务,如果方法在一个事务中被调用,该事务会被挂起直到方法调用结束再继续执行
    Propagation.REQUIRES_NEW 不管是否存在事务,该方法总会为自己发起一个新的事务,如果方法已经运行在一个事务中,则原有事务挂起,新的事务被创建
    Propagation.MANDATORY 该方法只能在一个已经存在的事务中执行,业务方法不能发起自己的事务,如果在没有事务的环境下被调用,容器抛出异常
    Propagation.SUPPORTS 该方法在某个事务范围内被调用,则方法成为该事务的一部分,如果方法在该事务范围内被调用,该方法就在没有事务的环境下执行
    Propagation.NEVER 该方法绝对不能在事务范围内执行,如果在就抛出异常,只有该方法没有关联到任何事务,才正常执行
    Propagation.NESTED 如果一个活动的事务存在,则运行在一个嵌套的事务中。如果没有活动事务,则按REQUIRED属性执行,它只对DataSourceTransactionManager事务管理器有效

    因此我们可以来简单分析一下上面的insertTeacherAndStudent方法:

    1. 由于没有指定propagation属性,因此事务传播特性为默认的REQUIRED
    2. StudentDao的insertStudent方法先运行,此时没有事务,因此新建一个事务
    3. TeacherDao的insertTeacher方法接着运行,此时由于StudentDao的insertStudent方法已经开启了一个事务,insertTeacher方法加入到这个事务中
    4. StudentDao的insertStudent方法和TeacherDao的insertTeacher方法组成了一个事务,两个方法要么同时执行成功,要么同时执行失败

    (2)@Transactional(isolation = Isolation.DEFAULT)

    事务隔离级别,这个不细说了,可以参看事务及事务隔离级别一文。

    (3)@Transactional(readOnly = true)

    该事务是否为一个只读事务,配置这个属性可以提高方法执行效率。

    (4)@Transactional(rollbackFor = {ArrayIndexOutOfBoundsException.class, NullPointerException.class})

    遇到方法抛出ArrayIndexOutOfBoundsException、NullPointerException两种异常会回滚数据,仅支持RuntimeException的子类

    (5)@Transactional(noRollbackFor = {ArrayIndexOutOfBoundsException.class, NullPointerException.class})

    这个和上面的相反,遇到ArrayIndexOutOfBoundsException、NullPointerException两种异常不会回滚数据,同样也是仅支持RuntimeException的子类。对(4)、(5)不是很理解的朋友,我给一个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    @Transactional(rollbackForClassName = {"NullPointerException"})
    public void insertTeacherAndStudent(Teacher teacher, Student student)
    {
        studentDao.insertStudent(student);
        teacherDao.insertTeacher(teacher);
        String s = null;
        s.length();
    }

    构造Student、Teacher的数据运行一下,然后查看下库里面有没有对应的记录就好了,然后再把rollbackForClassName改为noRollbackForClassName,对比观察一下。

    (6)@Transactional(rollbackForClassName = {“NullPointerException”})、@Transactional(noRollbackForClassName = {“NullPointerException”})

    这两个放在一起说了,和上面的(4)、(5)差不多,无非是(4)、(5)是通过.class来指定要回滚和不要回滚的异常,这里是通过字符串形式的名字来制定要回滚和不要回滚的异常。

    (7)@Transactional(timeout = 30)

    事务超时时间,单位为秒。

    (8)@Transactional(value = “tran_1″)

    value这个属性主要就是给某个事务一个名字而已,这样在别的地方就可以使用这个事务的配置。

  • 相关阅读:
    【Leetcode】【Easy】Remove Duplicates from Sorted List
    【Leetcode】【Easy】Pascal's Triangle II
    【Leetcode】【Easy】Pascal's Triangle
    【Leetcode】【Easy】Binary Tree Level Order Traversal II
    【Leetcode】【Easy】Binary Tree Level Order Traversal
    【Leetcode】【Easy】Maximum Depth of Binary Tree
    【Leetcode】【Easy】Minimum Depth of Binary Tree
    【Leetcode】【Easy】Balanced Binary Tree
    【Leetcode】【Easy】Symmetric Tree
    如何使用Action.Invoke()触发一个Storyboard
  • 原文地址:https://www.cnblogs.com/tuojunjie/p/6210295.html
Copyright © 2020-2023  润新知