Spring(四)Spring与数据库编程

  Spring最重要的功能毫无疑问就是操作数据。数据库的百年城是互联网编程的基础,Spring为开发者提供了JDBC模板模式,那就是它自身的JdbcTemplate。Spring还提供了TransactionTemplate支持事务的模板。Spring并没有支持MyBatis,好在MyBatis社区开发了接入Spring的开发包,该包也提供了SqlSessionTemplate给开发者使用,该包还可以屏蔽SqlSessionTemplate这样的功能性代码,可以在编程中擦除SqlSessionTemplate让开发者直接使用接口编程,大大提高了编码的可读性。

  一、传统JDBC代码的弊端

  例如,下面的代码的作用是,通过JDBC读取数据库,然后将结果集以POJO的形式返回。

package com.ssm.chapter12.jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import com.ssm.chapter12.pojo.Role;

public class JdbcExample {
    
    public Role getRole(Long id) {
        Role role = null;
        // 声明JDBC变量
        Connection con = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // 注册驱动程序
            Class.forName("com.mysql.jdbc.Driver");
            // 获取连接
            con = DriverManager.getConnection("jdbc:mysql://localhost:3306/chapter12", "root", "123456");
            // 预编译SQL
            ps = con.prepareStatement("select id, role_name, note from t_role where id = ?");
            // 设置参数
            ps.setLong(1, id);
            // 执行SQL
            rs = ps.executeQuery();
            // 组装结果集返回到POJO
            while (rs.next()) {
                role = new Role();
                role.setId(rs.getLong(1));
                role.setRoleName(rs.getString(2));
                role.setNote(rs.getString(3));
            }
        } catch (ClassNotFoundException | SQLException e) {
            // 异常处理
            e.printStackTrace();
        } finally {
            // 关闭数据库连接资源
            try {
                if (rs != null && !rs.isClosed()) {
                    rs.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
            try {
                if (ps != null && !ps.isClosed()) {
                    ps.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
            try {
                if (con != null && !con.isClosed()) {
                    con.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        return role;
    }
}
传统的JDBC

  从代码可以看出,即使是执行一条简单的SQL,其过程也不简单,太多的try...catch...finally...语句,造成了代码泛滥。

  在JDBC中,大量的JDBC代码都是用于Chau给你姐爱你连接和语句以及异常处理的样版代码。

  实际上,这些样版代码是非常重要的。清理资源和处理错误确保了数据访问的健壮性。如果没有它们的话,就不会发现错误而且资源也会处于打开的状态,这将会导致意外的代码和资源泄露。我们不仅需要这些代码,而且还要保证它是正确的。基于这样的原因,才需要框架来保证这些代码只写一次而且是正确的。

 

  二、使用Spring配置数据库资源

  在Spring中配置数据库资源很简单,在实际工作中,大部分会配置成数据库连接池,既可以通过使用Spring内部提供的类,也可以使用第三方数据库连接池或者从Web服务器中通过JNDI获取数据源。由于使用了第三方的类,一般而言在工程中会偏向于采用XML的方式进行配置。

  1.使用简单数据库配置

  Spring提供了一个类org.springframework.jdbc.datasource.SimpleDriverDataSource可以支持简单数据库配置,但是不支持数据库连接池。

  这种配置一般用于测试,因为它不是一个数据库连接池。

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

  2.使用第三方数据库连接池

  当使用第三方数据库连接池时,比如DBCP数据库连接池,需要下载第三方包common-dbcp.jar和common-pool包,然后在Spring中简单配置后,就能够使用它了。

    <!-- 数据库连接池 -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver" />
        <property name="url" value="jdbc:mysql://localhost:3306/chapter12" />
        <property name="username" value="root" />
        <property name="password" value="123456" />
        <!--连接池的最大数据库连接数 -->
        <property name="maxActive" value="255" />
        <!--最大等待连接中的数量 -->
        <property name="maxIdle" value="5" />
        <!--最大等待毫秒数 -->
        <property name="maxWait" value="10000" />
    </bean>

  3.使用JNDI数据库连接池

  在Tomcat、WebLogic等Java EE服务器上配置数据源,这是他存在一个JNDI的名称。也可以通过Spring所提供的JNDI机制获取对应的数据源,这也是常用的方式。

  假设在Tomcat上配置了JNDI为jdbc/chapter12的数据源,这样就可以在Web工程中获取这个JNDI数据源。

    <bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
        <property name="jndiName" vaule="java:comp/env/jdbc/chapter12" />
    </bean>

 

  三、JDBC代码失控的解决方案--JdbcTemplate

  JdbcTemplate是Spring针对JDBC代码失控提供的解决方案,虽然不算成功,但是用技术提供模板化的编程,减少了开发者的工作量。

  Spring的JDBC框架承担了资源管理和异常处理的工作,从而简化了JDBC代码,让我们只需编写从数据库读写数据的必须代码。

  1.配置JdbcTemplate,其中dataSource在之前的三种方法中选一种即可

    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource" />
    </bean>

  2.配置好了JdbcTemplate和dataSource就可以操作JdbcTemplate了,假设Spring配置文件为spring-cfg.xml,则要想完成第一个例子中JDBC完成的工作,只需要:

    public static void tesSpring() {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-cfg.xml");
        JdbcTemplate jdbcTemplate = ctx.getBean(JdbcTemplate.class);
        Long id = 1L;
        String sql = "select id, role_name, note from t_role where id = " + id;
        Role role = jdbcTemplate.queryForObject(sql, new RowMapper<Role>() {
            @Override
            public Role mapRow(ResultSet rs, int rownum) throws SQLException {
                Role result = new Role();
                result.setId(rs.getLong("id"));
                result.setRoleName(rs.getString("role_name"));
                result.setNote(rs.getString("note"));
                return result;
            }
        });
        System.out.println(role.getRoleName());
    }

  其中,使用了jdbcTemplate的queryForObject方法,它包含了两个参数,一个是SQL,另一个是RowMapper接口。在mapRow()方法中,从ResultSet对象中取出查询得到的数据,组装成一个Role对象,而无需再写任何关闭数据库资源的代码。因为JdbcTemplate内部实现了它们,这便是Spring所提供的模板规则。

  3.JdbcTemplate的增、删、改、查

package com.ssm.chapter12.jdbc;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.List;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.jdbc.core.JdbcTemplate;

import com.ssm.chapter12.pojo.Role;

public class JdbcTemplateTest {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-cfg.xml");
        JdbcTemplate jdbcTemplate = ctx.getBean(JdbcTemplate.class);
        
        JdbcTemplateTest test = new JdbcTemplateTest();
        test.getRoleByConnectionCallback(jdbcTemplate, 1L);
        test.getRoleByStatementCallback(jdbcTemplate, 1L);
        test.insertRole(jdbcTemplate);
        List roleList = test.findRole(jdbcTemplate, "role");
        System.out.println(roleList.size());
        Role role = new Role();
        role.setId(1L);
        role.setRoleName("update_role_name_1");
        role.setNote("update_note_1");
        test.updateRole(jdbcTemplate, role);
        test.deleteRole(jdbcTemplate, 1L);
    }


    /***
     * 插入角色
     * @param jdbcTemplate --模板
     * @return 影响条数
     */
    public int insertRole(JdbcTemplate jdbcTemplate) {
        String roleName = "role_name_1";
        String note = "note_1";
        String sql = "insert into t_role(role_name, note) values(?, ?)";
        return jdbcTemplate.update(sql, roleName, note);
    }

    /**
     * 删除角色
     * @param jdbcTemplate -- 模板
     * @param id -- 角色编号,主键
     * @return 影响条数
     */
    public int deleteRole(JdbcTemplate jdbcTemplate, Long id) {
        String sql = "delete from t_role where id=?";
        return jdbcTemplate.update(sql, id);
    }

    public int updateRole(JdbcTemplate jdbcTemplate, Role role) {
        String sql = "update t_role set role_name=?, note = ? where id = ?";
        return jdbcTemplate.update(sql, role.getRoleName(), role.getNote(), role.getId());
    }

    /**
     * 查询角色列表
     * @param jdbcTemplate--模板
     * @param roleName --角色名称
     * @return 角色列表
     */
    public List<Role> findRole(JdbcTemplate jdbcTemplate, String roleName) {
        String sql = "select id, role_name, note from t_role where role_name like concat('%',?, '%')";
        Object[] params = {roleName};//组织参数
        //使用RowMapper接口组织返回(使用lambda表达式)
        List<Role> list = jdbcTemplate.query(sql, params, (ResultSet rs, int rowNum) -> {
            Role result = new Role();
            result.setId(rs.getLong("id"));
            result.setRoleName(rs.getString("role_name"));
            result.setNote(rs.getString("note"));
            return result;
        });
        return list;
    }
    
    /**
     * 使用ConnectionCallback接口进行回调
     * @param jdbcTemplate 模板
     * @param id 角色编号
     * @return 返回角色
     */
    public Role getRoleByConnectionCallback(JdbcTemplate jdbcTemplate, Long id) {
        Role role = null;
        //这里写成Java 8的Lambda表达式,如果你使用低版本的Java,需要使用ConnectionCallback匿名类
        role = jdbcTemplate.execute((Connection con) -> {
            Role result = null;
            String sql = "select id, role_name, note from t_role where id = ?";
            PreparedStatement ps = con.prepareStatement(sql);
            ps.setLong(1, id);
            ResultSet rs = ps.executeQuery();
            while (rs.next()) {
                result = new Role();
                result.setId(rs.getLong("id"));
                result.setNote(rs.getString("note"));
                result.setRoleName(rs.getString("role_name"));
            }
            return result;
        });
        return role;
    }

    /**
     * 使用StatementCallback接口进行回调
     * @param jdbcTemplate模板
     * @param id角色编号
     * @return返回角色
     */
    public Role getRoleByStatementCallback(JdbcTemplate jdbcTemplate, Long id) {
        Role role = null;
         //这里写成Java 8的lambda表达式,如果你使用低版本的Java,需要使用StatementCallback的匿名类
        role = jdbcTemplate.execute((Statement stmt) -> {
            Role result = null;
            String sql = "select id, role_name, note from t_role where id = " + id;
            ResultSet rs = stmt.executeQuery(sql);
            while (rs.next()) {
                result = new Role();
                result.setId(rs.getLong("id"));
                result.setNote(rs.getString("note"));
                result.setRoleName(rs.getString("role_name"));
            }
            return result;
        });
        return role;
    }
}
JdbcTemplate的增、删、改、查

  4.执行多条SQL

  一个JdbcTemplate只执行了一条SQL,当需要多次执行SQL时,可以使用execute方法。它将允许传递ConnectionCallback或者StatementCallback等接口进行回调。

    /**
     * 使用ConnectionCallback接口进行回调
     * @param jdbcTemplate 模板
     * @param id 角色编号
     * @return 返回角色
     */
    public Role getRoleByConnectionCallback(JdbcTemplate jdbcTemplate, Long id) {
        Role role = null;
        //这里写成Java 8的Lambda表达式,如果你使用低版本的Java,需要使用ConnectionCallback匿名类
        role = jdbcTemplate.execute((Connection con) -> {
            Role result = null;
            String sql = "select id, role_name, note from t_role where id = ?";
            PreparedStatement ps = con.prepareStatement(sql);
            ps.setLong(1, id);
            ResultSet rs = ps.executeQuery();
            while (rs.next()) {
                result = new Role();
                result.setId(rs.getLong("id"));
                result.setNote(rs.getString("note"));
                result.setRoleName(rs.getString("role_name"));
            }
            return result;
        });
        return role;
    }

    /**
     * 使用StatementCallback接口进行回调
     * @param jdbcTemplate模板
     * @param id角色编号
     * @return返回角色
     */
    public Role getRoleByStatementCallback(JdbcTemplate jdbcTemplate, Long id) {
        Role role = null;
         //这里写成Java 8的lambda表达式,如果你使用低版本的Java,需要使用StatementCallback的匿名类
        role = jdbcTemplate.execute((Statement stmt) -> {
            Role result = null;
            String sql = "select id, role_name, note from t_role where id = " + id;
            ResultSet rs = stmt.executeQuery(sql);
            while (rs.next()) {
                result = new Role();
                result.setId(rs.getLong("id"));
                result.setNote(rs.getString("note"));
                result.setRoleName(rs.getString("role_name"));
            }
            return result;
        });
        return role;
    }
执行多条SQL

 

  四、MyBatis-Spring项目

  目前大部分的互联网项目中都使用SSM搭建平台的。使用Spring IoC可以有效管理各类Java资源,达到即插即拔的功能;通过AOP框架,数据库事务可以委托给Spring处理,消除很大一部分的事务代码,配合MyBatis的高灵活、可配置、可优化SQL等特性,完全可以构建高性能的大型网站。  

  在Spring环境中使用MyBatis也更加简单,节省了不少代码,甚至可以不用SqlSessionFactory、SqlSession等对象。因为MyBatis-Spring为我们封装了它们。

  配置MyBatis-Spring项目需要下面几步:

  1. 配置数据源
  2. 配置SqlSessionFactory
  3. 可以选择的配置由SqlSessionTemplate,在同时配置SqlSessionTemplate和SqlSessionFactory的情况下,优先采用SqlSessionTemplate
  4. 配置Mapper,可以配置单个Mapper,也可以通过扫描的方法生成Mapper,比较灵活。此时Spring IoC会生成对应接口的实例,这样就可以通过注入的方式来获取资源。
  5. 事务管理。

 

  1.配置SqlSessionFactory Bean

  MyBatis中SqlSessionFactory是产生SqlSession的基础,因此配置SqlSessionFactory十分关键。在MyBatis-Spring项目中提供了SqlSessionFactoryBean支持SqlSessionFactory的配置。

  (1)在Spring的配置文件spring-cfg.xml中配置SqlSessionFactoryBean

    这里虽然只是配置了数据源,然后引入了一个MyBatis配置文件,这样的好处在于不至于使得SqlSessionFactoryBean的配置全部依赖于Spring提供的规则,导致配置的复杂性。

    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="configLocation" value="classpath:sqlMapConfig.xml" />
    </bean>

  (2)引入的MyBatis配置文件sqlMapConfig.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>
    <settings>
        <!-- 这个配置使全局的映射器启用或禁用缓存 -->
        <setting name="cacheEnabled" value="true" />
        <!-- 允许 JDBC 支持生成的键。需要适合[修改为:适当]的驱动。如果设置为true,则这个设置强制生成的键被使用,尽管一些驱动拒绝兼容但仍然有效(比如 Derby) -->
        <setting name="useGeneratedKeys" value="true" />
        <!-- 配置默认的执行器。SIMPLE 执行器没有什么特别之处。REUSE 执行器重用预处理语句。BATCH 执行器重用语句和批量更新  -->
        <setting name="defaultExecutorType" value="REUSE" />
        <!-- 全局启用或禁用延迟加载。当禁用时,所有关联对象都会即时加载 -->
        <setting name="lazyLoadingEnabled" value="true"/>
        <!-- 设置超时时间,它决定驱动等待一个数据库响应的时间  -->
        <setting name="defaultStatementTimeout" value="25000"/> 
    </settings>

<!-- 别名配置 --> <typeAliases> <typeAlias alias="role" type="com.ssm.chapter12.pojo.Role" /> </typeAliases> <!-- 指定映射器路径 --> <mappers> <mapper resource="com/ssm/chapter12/sql/mapper/RoleMapper.xml" /> </mappers> </configuration>

  (3)然后引入映射器RoleMapper.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.ssm.chapter12.mapper.RoleMapper">

    <insert id="insertRole" useGeneratedKeys="true" keyProperty="id">
        insert into t_role(role_name, note) values (#{roleName}, #{note})
    </insert>

    <delete id="deleteRole" parameterType="long">
        delete from t_role where id=#{id}
    </delete>

    <select id="getRole" parameterType="long" resultType="role">
        select id, role_name as roleName, note from t_role where id = #{id}
    </select>

    <update id="updateRole" parameterType="role">
        update t_role
        set role_name = #{roleName},
        note = #{roleName}
        where id = #{id}
    </update>
</mapper>

  (4)与映射器配置文件对应的接口类java文件RoleMapper.java

package com.ssm.chapter12.mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

import com.ssm.chapter12.pojo.Role;

public interface RoleMapper {
    public int insertRole(Role role);
    public Role getRole(@Param("id") Long id);
    public int updateRole(Role role);
    public int deleteRole(@Param("id") Long id);
}

  至此,MyBatis框架的主要代码就已经配置完成了,但是,由于RoleMapper是一个接口,而不是一个类,它没有办法产生示例,因此应该如何配置呢?

  

  2.SqlSessionTemplate组件

  SqlSessionTemplate并不是一个必需配置的组件,但是它也存在一定的价值。首先,它是线程安全的类,也就是确保每个线程使用的SqlSession唯一且不互相冲突。其次,它提供了一系列的功能,比如增、删、改、查等常用功能。

  配置方法如下:SqlSessionTemplate类要通过带有参数的构造方法去创建对象,常用的参数是sqlSessionFactory和MyBatis执行器(Executor)类型,取值范围是SIMPLE、REUSE、BATCH。

    <bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
        <constructor-arg ref="sqlSessionFactory" />
        <!-- <constructor-arg value="BATCH"/> -->
    </bean>

  SqlSessionTemplate配置完成就可以使用它了,例如:

    public static void testSqlSessionTemplate() {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-cfg.xml");
        // ctx为Spring IoC容器
        SqlSessionTemplate sqlSessionTemplate = ctx.getBean(SqlSessionTemplate.class);
        Role role = new Role();
        role.setRoleName("role_name_sqlSessionTemplate");
        role.setNote("note_sqlSessionTemplate");
        sqlSessionTemplate.insert("com.ssm.chapter12.mapper.RoleMapper.insertRole", role);
        Long id = role.getId();
        sqlSessionTemplate.selectOne("com.ssm.chapter12.mapper.RoleMapper.getRole", id);
        role.setNote("update_sqlSessionTemplate");
        sqlSessionTemplate.update("com.ssm.chapter12.mapper.RoleMapper.updateRole", role);
        sqlSessionTemplate.delete("com.ssm.chapter12.mapper.RoleMapper.deleteRole", id);
    }

  运行结果:从结果中可以看到,每运行一个SqlSessionTemplate时,它就会重新获取一个新的SqlSession,也就是说每一个SqlSessionTemplate运行的时候会产生新的SqlSession,所以每一个方法都是独立的SqlSession,这意味着它是安全的线程。

  SqlSessionTemplate目前运用已经不多,它需要使用字符串表明运行哪个SQL,字符串包含业务含义,只是功能性代码,并不符合面向对象的规范。与此同时,使用字符串时,IDE无法检查代码逻辑的正确性,所以这样的用法渐渐被人们抛弃了。但是,SqlSessionTemplate允许配置执行器的类型,当同时配置SqlSessionTemplate和SqlSessionFactory时,优先采用SqlSessionTemplate。

DEBUG 2018-10-09 17:32:51,048 org.mybatis.spring.SqlSessionUtils: Creating a new SqlSession
DEBUG 2018-10-09 17:32:51,052 org.mybatis.spring.SqlSessionUtils: SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@38102d01] was not registered for synchronization because synchronization is not active
DEBUG 2018-10-09 17:32:51,065 org.springframework.jdbc.datasource.DataSourceUtils: Fetching JDBC Connection from DataSource
DEBUG 2018-10-09 17:32:51,329 org.mybatis.spring.transaction.SpringManagedTransaction: JDBC Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL Connector Java] will not be managed by Spring
DEBUG 2018-10-09 17:32:51,333 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ==>  Preparing: insert into t_role(role_name, note) values (?, ?) 
DEBUG 2018-10-09 17:32:51,367 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ==> Parameters: role_name_sqlSessionTemplate(String), note_sqlSessionTemplate(String)
DEBUG 2018-10-09 17:32:51,372 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: <==    Updates: 1
DEBUG 2018-10-09 17:32:51,375 org.mybatis.spring.SqlSessionUtils: Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@38102d01]
DEBUG 2018-10-09 17:32:51,375 org.springframework.jdbc.datasource.DataSourceUtils: Returning JDBC Connection to DataSource
DEBUG
2018-10-09 17:32:51,375 org.mybatis.spring.SqlSessionUtils: Creating a new SqlSession DEBUG 2018-10-09 17:32:51,375 org.mybatis.spring.SqlSessionUtils: SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@16610890] was not registered for synchronization because synchronization is not active DEBUG 2018-10-09 17:32:51,377 org.springframework.jdbc.datasource.DataSourceUtils: Fetching JDBC Connection from DataSource DEBUG 2018-10-09 17:32:51,378 org.mybatis.spring.transaction.SpringManagedTransaction: JDBC Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL Connector Java] will not be managed by Spring DEBUG 2018-10-09 17:32:51,378 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ==> Preparing: select id, role_name as roleName, note from t_role where id = ? DEBUG 2018-10-09 17:32:51,378 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ==> Parameters: 7(Long) DEBUG 2018-10-09 17:32:51,390 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: <== Total: 1 DEBUG 2018-10-09 17:32:51,393 org.mybatis.spring.SqlSessionUtils: Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@16610890] DEBUG 2018-10-09 17:32:51,393 org.springframework.jdbc.datasource.DataSourceUtils: Returning JDBC Connection to DataSource
DEBUG
2018-10-09 17:32:51,393 org.mybatis.spring.SqlSessionUtils: Creating a new SqlSession DEBUG 2018-10-09 17:32:51,393 org.mybatis.spring.SqlSessionUtils: SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6283d8b8] was not registered for synchronization because synchronization is not active DEBUG 2018-10-09 17:32:51,393 org.springframework.jdbc.datasource.DataSourceUtils: Fetching JDBC Connection from DataSource DEBUG 2018-10-09 17:32:51,394 org.mybatis.spring.transaction.SpringManagedTransaction: JDBC Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL Connector Java] will not be managed by Spring DEBUG 2018-10-09 17:32:51,394 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ==> Preparing: update t_role set role_name = ?, note = ? where id = ? DEBUG 2018-10-09 17:32:51,394 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ==> Parameters: role_name_sqlSessionTemplate(String), role_name_sqlSessionTemplate(String), 7(Long) DEBUG 2018-10-09 17:32:51,397 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: <== Updates: 1 DEBUG 2018-10-09 17:32:51,397 org.mybatis.spring.SqlSessionUtils: Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6283d8b8] DEBUG 2018-10-09 17:32:51,397 org.springframework.jdbc.datasource.DataSourceUtils: Returning JDBC Connection to DataSource
DEBUG
2018-10-09 17:32:51,397 org.mybatis.spring.SqlSessionUtils: Creating a new SqlSession DEBUG 2018-10-09 17:32:51,397 org.mybatis.spring.SqlSessionUtils: SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1da2cb77] was not registered for synchronization because synchronization is not active DEBUG 2018-10-09 17:32:51,397 org.springframework.jdbc.datasource.DataSourceUtils: Fetching JDBC Connection from DataSource DEBUG 2018-10-09 17:32:51,398 org.mybatis.spring.transaction.SpringManagedTransaction: JDBC Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL Connector Java] will not be managed by Spring DEBUG 2018-10-09 17:32:51,398 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ==> Preparing: delete from t_role where id=? DEBUG 2018-10-09 17:32:51,398 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ==> Parameters: 7(Long) DEBUG 2018-10-09 17:32:51,400 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: <== Updates: 1 DEBUG 2018-10-09 17:32:51,400 org.mybatis.spring.SqlSessionUtils: Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1da2cb77] DEBUG 2018-10-09 17:32:51,400 org.springframework.jdbc.datasource.DataSourceUtils: Returning JDBC Connection to DataSource

 

  3.配置MapperFactory Bean

  MyBatis的运行只需要提供类似于RoleMapper.java的接口,而无需提供一个实现类。而根据MyBatis的运行原理,它是由MyBatis体系创建的动态代理对象运行的,所以Spring也没有办法为其生成一个实现类。为了解决这个问题,MyBatis-Spring项目提供了一个MapperFactoryBean类作为中介,可以通过配置这个类来实现想要的Mapper。使用了Mapper接口编程方式可以有效地在逻辑代码中擦除SqlSessionTemplate,这样代码就按照面向对象的规范进行编写了。

  配置RoleMapper对象:

    <bean id="roleMapper" class="org.mybatis.spring.mapper.MapperFactoryBean"> 
        <property name="mapperInterface" value="com.ssm.chapter12.mapper.RoleMapper" />
        <property name="sqlSessionFactory" ref="sqlSessionFactory" />
        <property name="sqlSessionTemplate" ref="sqlSessionTemplate"/>
    </bean>

  有三个属性:

  • mapperInterface
  • sqlSessionFactory
  • SqlSessionTemplate

  其中,如果同时配置sqlSessionFactory和SqlSessionTemplate,那么就会启用sqlSessionFactory,而SqlSessionTemplate作废。

  可以通过RoleMapper roleMapper = ctx.getBean(RoleMapper.class);来获取映射器

    public static void testRoleMapper() {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-cfg.xml");
        RoleMapper roleMapper = ctx.getBean(RoleMapper.class);
        roleMapper.getRole(2L);
    }

  

  4.配置MapperScannerConfigurer

  在项目比较大的情况下,如果一个个配置Mapper会造成配置量大的问题,这显然不利于开发,因此可以使用MapperScannerConfigurer类来用扫描的形式去生产对应的Mapper。

  在Spring配置前需要给Mapper一个注解,在Spring中往往是使用@Repository表示DAO层的,

package com.ssm.chapter12.mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import com.ssm.chapter12.pojo.Role;
@Repository
public interface RoleMapper { public int insertRole(Role role); public Role getRole(@Param("id") Long id); public int updateRole(Role role); public int deleteRole(@Param("id") Long id); }

  然后在Spring配置文件中进行配置:在配置中:

    第一行:basePackage指定让Spring自动扫描的包,它会逐层深入扫描,如果遇到多个包可以使用半角逗号分隔。

    第二行:指定在Spring中定义的sqlSessionFactory的Bean名称。

    第三行:如果类被annotationClass声明的注解标识的时候,才进行扫描。这里是只将被@Repository注解的接口类注册成对应的Mapper。

    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.ssm.chapter12.mapper" />
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
        <!-- 使用sqlSessionTemplateBeanName将覆盖sqlSessionFactoryBeanName的配置 -->
        <!-- <property name="sqlSessionTemplateBeanName" value="sqlSessionFactory"/> -->
        <!-- 指定标注才扫描成为Mapper -->
        <property name="annotationClass" value="org.springframework.stereotype.Repository" />
    </bean>

  

  5.测试Spirng+Mybatis

  经过上面的归纳认识,整理出一份标准的XML配置文件:包括dataSourcesqlSessionFactory和MapperScannerConfigurer

<?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-4.0.xsd">

    <!-- 数据库连接池 -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver" />
        <property name="url" value="jdbc:mysql://localhost:3306/chapter6?useSSL=false" />
        <property name="username" value="root" />
        <property name="password" value="bjtungirc" />
        <!--连接池的最大数据库连接数 -->
        <property name="maxActive" value="255" />
        <!--最大等待连接中的数量 -->
        <property name="maxIdle" value="5" />
        <!--最大等待毫秒数 -->
        <property name="maxWait" value="10000" />
    </bean>

    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="configLocation" value="classpath:sqlMapConfig.xml" />
    </bean>

    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.ssm.chapter12.mapper" />
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
        <!-- 使用sqlSessionTemplateBeanName将覆盖sqlSessionFactoryBeanName的配置 -->
        <!-- <property name="sqlSessionTemplateBeanName" value="sqlSessionFactory"/> -->
        <!-- 指定标注才扫描成为Mapper -->
        <property name="annotationClass" value="org.springframework.stereotype.Repository" />
    </bean>
</beans>

  验证方法:

    public static void testMybatisSpring() {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-cfg.xml");
        // ctx为Spring IoC容器
        RoleMapper roleMapper = ctx.getBean(RoleMapper.class);
        Role role = new Role();
        role.setRoleName("role_name_mapper");
        role.setNote("note_mapper");
        roleMapper.insertRole(role);
        Long id = role.getId();
        roleMapper.getRole(id);
        role.setNote("note_mapper_update");
        roleMapper.updateRole(role);
        roleMapper.deleteRole(id);
    }

  输出结果:从日志中可以看出每当使用一个RoleMapper接口的方法吗,它就会产生一个新的SqlSession,运行完成后就会自动关闭。

  从关闭的日志Closing non transactional SqlSession中可以看出是在一个非事务的场景下运行,所以这里并不完整,只是简单地使用了数据库,并没有启动数据库事务。

DEBUG 2018-10-09 18:17:36,687 org.mybatis.spring.SqlSessionUtils: Creating a new SqlSession
DEBUG 2018-10-09 18:17:36,692 org.mybatis.spring.SqlSessionUtils: SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@10d68fcd] was not registered for synchronization because synchronization is not active
DEBUG 2018-10-09 18:17:36,697 org.springframework.jdbc.datasource.DataSourceUtils: Fetching JDBC Connection from DataSource
DEBUG 2018-10-09 18:17:36,937 org.mybatis.spring.transaction.SpringManagedTransaction: JDBC Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL Connector Java] will not be managed by Spring
DEBUG 2018-10-09 18:17:36,942 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ==>  Preparing: insert into t_role(role_name, note) values (?, ?) 
DEBUG 2018-10-09 18:17:36,964 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ==> Parameters: role_name_mapper(String), note_mapper(String)
DEBUG 2018-10-09 18:17:36,968 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: <==    Updates: 1
DEBUG 2018-10-09 18:17:36,971 org.mybatis.spring.SqlSessionUtils: Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@10d68fcd]
DEBUG 2018-10-09 18:17:36,971 org.springframework.jdbc.datasource.DataSourceUtils: Returning JDBC Connection to DataSource
DEBUG
2018-10-09 18:17:36,973 org.mybatis.spring.SqlSessionUtils: Creating a new SqlSession DEBUG 2018-10-09 18:17:36,973 org.mybatis.spring.SqlSessionUtils: SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@169e6180] was not registered for synchronization because synchronization is not active DEBUG 2018-10-09 18:17:36,974 org.springframework.jdbc.datasource.DataSourceUtils: Fetching JDBC Connection from DataSource DEBUG 2018-10-09 18:17:36,975 org.mybatis.spring.transaction.SpringManagedTransaction: JDBC Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL Connector Java] will not be managed by Spring DEBUG 2018-10-09 18:17:36,975 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ==> Preparing: select id, role_name as roleName, note from t_role where id = ? DEBUG 2018-10-09 18:17:36,975 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ==> Parameters: 8(Long) DEBUG 2018-10-09 18:17:36,985 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: <== Total: 1 DEBUG 2018-10-09 18:17:36,987 org.mybatis.spring.SqlSessionUtils: Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@169e6180] DEBUG 2018-10-09 18:17:36,987 org.springframework.jdbc.datasource.DataSourceUtils: Returning JDBC Connection to DataSource
DEBUG
2018-10-09 18:17:36,988 org.mybatis.spring.SqlSessionUtils: Creating a new SqlSession DEBUG 2018-10-09 18:17:36,988 org.mybatis.spring.SqlSessionUtils: SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4fb3ee4e] was not registered for synchronization because synchronization is not active DEBUG 2018-10-09 18:17:36,988 org.springframework.jdbc.datasource.DataSourceUtils: Fetching JDBC Connection from DataSource DEBUG 2018-10-09 18:17:36,988 org.mybatis.spring.transaction.SpringManagedTransaction: JDBC Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL Connector Java] will not be managed by Spring DEBUG 2018-10-09 18:17:36,988 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ==> Preparing: update t_role set role_name = ?, note = ? where id = ? DEBUG 2018-10-09 18:17:36,989 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ==> Parameters: role_name_mapper(String), role_name_mapper(String), 8(Long) DEBUG 2018-10-09 18:17:36,990 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: <== Updates: 1 DEBUG 2018-10-09 18:17:36,991 org.mybatis.spring.SqlSessionUtils: Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4fb3ee4e] DEBUG 2018-10-09 18:17:36,991 org.springframework.jdbc.datasource.DataSourceUtils: Returning JDBC Connection to DataSource
DEBUG
2018-10-09 18:17:36,991 org.mybatis.spring.SqlSessionUtils: Creating a new SqlSession DEBUG 2018-10-09 18:17:36,991 org.mybatis.spring.SqlSessionUtils: SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2c35e847] was not registered for synchronization because synchronization is not active DEBUG 2018-10-09 18:17:36,991 org.springframework.jdbc.datasource.DataSourceUtils: Fetching JDBC Connection from DataSource DEBUG 2018-10-09 18:17:36,992 org.mybatis.spring.transaction.SpringManagedTransaction: JDBC Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL Connector Java] will not be managed by Spring DEBUG 2018-10-09 18:17:36,992 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ==> Preparing: delete from t_role where id=? DEBUG 2018-10-09 18:17:36,992 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ==> Parameters: 8(Long) DEBUG 2018-10-09 18:17:36,994 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: <== Updates: 1 DEBUG 2018-10-09 18:17:36,994 org.mybatis.spring.SqlSessionUtils: Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2c35e847] DEBUG 2018-10-09 18:17:36,994 org.springframework.jdbc.datasource.DataSourceUtils: Returning JDBC Connection to DataSource

 

  五、数据库的相关知识

  1.数据库事务ACID特性

  • 原子性(Atomicity):整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节。事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像整个事务从来没被执行过一样。
  • 一致性(Consistency):指一个事务可以改变封装状态(除非它是一个只读的)。事务必须始终保持系统处于一致性的状态,不管在任何给定的时间并发事务有多少。
  • 隔离性(Isolation):指两个事务之间的隔离程度
  • 持久性(Durability):在事务完成以后,该事物对数据库所做的更改便持久保存在数据库之中了,并不会被回滚。

  

  2.丢失更新

  在互联网中存在着抢购、秒杀等高并发场景,使得数据库在一个多事务的环境中运行,多个事务的并发会产生一系列的问题,主要的问题之一就是丢失更新,丢失更新分为两类:

  假设一个账户同时存在互联网消费和刷卡消费两种形式,而一对夫妻共同使用这个账户。

  • 第一类丢失更新

    在最后的T6时刻,老婆回滚事务,却恢复了原来的初始值余额10000元,但是老公已经消费了1000元,这显然是不对的。

    这样的两个事务并发,一个回滚,一个提交成功导致不一致,称为第一类丢失更新。大部分数据库基本都已经消灭了这类丢失更新。

  • 第二类丢失更新

  

  两个事务并发,两者都提交了事务,由于在不同的事务中,无法探知其他事务的操作,导致不一致,称为第二类丢失更新。

  为了克服第二类丢失更新即保证事务之间协助的一致性,数据库中顶一个事务之间的隔离级别,来不同程度上减少出现丢失更新的可能。

 

  3.隔离级别

  按照SQL的标准规范,把隔离级别定义为4层:脏读(dirty read)、读/写提交(read commit)、可重复读(repeatable read)和序列化(Serializable)

  各类的隔离级别和产生的现象:

  

  (1)脏读(dirty read)  

    脏读是最低的隔离级别,其含义是允许一个事务去读取另一个事务中未提交的数据。

    

  在T3时刻老婆启动了消费,导致余额为9000元,老公在T4时刻消费,因为用了脏读,所以能够读取老婆消费后的余额(这个余额是事务二未提交的)为9000元,这样余额就为8000元,然后T5时刻老公提交了事务,余额变成了8000元。

  但是,老婆在T6时刻回滚事务,由于数据库已经克服了第一类丢失更新,所以余额依旧为8000元。

  这是由于,事务一可以读取事务二未提交的事务,这样的场景被称为脏读。

   (2)读/写提交

  为了克服脏读,SQL提出了第二个隔离级别--读/写提交。读/写提交,就是一个事务只能读取另一个事务已经提交的数据。

  

  在T3时刻,由于事务采取读/写提交的隔离级别,所以老公无法读取老婆未提交的9000元余额,只能读到10000元的余额,于是在T5提交事务后余额变为9000元。而T6时刻老婆回滚事务,结果也是正确的9000元。

  脏读可以引发其他的问题:

  

  由于T7时刻事务一知道事务二提交的结果--余额为1000元,导致老公没有钱买单。对于老公而言,他并不知道老婆做了什么事情,但是账户余额却莫名其妙地从10000元变为了1000元,对他来说账户余额是不能重复读取的,而是一个会变化的值,这样的场景称为不可重复读,这是读/写提交存在的问题。

  (3)可重复读

  可重复读是针对数据库同一条记录而言的,即可重复读会使得同一条数据库记录的读/写按照一个序列化进行操作,不会产生交叉情况,这样就能保证同一条数据的一致性,进而保证上述场景的正确性。但是由于数据库并不是只能针对一条记录进行读/写操作,在很多场景,数据库需要同时对多条记录进行读/写,这个时候就会产生幻读。  

  按照下面的例子,可重复读的意思就是,在T1、T2、T3和T4时刻,都只有一条操作,也就是操作的序列化。

  

  但是,老婆在T1查询到10条记录,到T4打印记录时,并不知道老公在T2和T3时刻进行了消费,导致多一条(可重复读是针对同一条记录而言的,而这里不是同一条记录)消费记录的产生,她会质疑这条多出来的记录是不是不存在的,这样的场景称为幻读。

  (4)序列化

  为了克服幻读,SQL又提出了序列化的隔离级别。它是一种让SQL按照顺序读/写的方式,能够消除数据库事务之间并发产生数据不一致的问题。

  

  4.传播行为

  传播行为是指方法之间的调用事务策略的问题。在大部分的情况下,我们都希望事务能够同时成功或者同时失败。但是也会有例外,假设现在需要信用卡的还款功能,有一个总的调用代码逻辑--RepaymentBatchService的batch方法,那么它要实现的是记录还款成功的总卡数和对应完成的信息,而每一张卡的还款则是通过RepaymentService的repay方法完成的。

  

  如果只有一条业务,那么当调用repay方法对某一张信用卡进行还款时,如果发生了异常,如果将这条事务回滚,就会造成所有的数据操作都会被回滚,那些已经正常还款的用户也会还款失败。

  但是,如果batch方法调用repay方法时,它会为repay方法创建一条新的事务。当这个方法产生异常时,只会回滚它自身的事务,而不会影响主事务和其他事务,这样就能避免上面遇到的问题。

  一个方法调用另外一个方法时,可以对事务的特性进行传播配置,称为传播行为。

 

  5.选择隔离级别和传播行为

(1)选择隔离级别

  在互联网应用中,不但要考虑数据库的一致性,还要考虑系统的性能。一般而言,从脏读到序列化,系统性能直线下降。因此设置高的级别,比如序列化,会严重压制并发,从而引发大量的线程挂起,直到获得锁才能进一步操作,而恢复时有需要大量的等待时间。大部分场景下,企业会选择读/写提交的方式设置事务,这样既有助于提高并发,又压制了脏读,但是对于数据一致性问题并没有解决。

  并不是所有的业务都在高并发下完成,当业务并发量不是很大或者根本不需要考虑的情况下,使用序列化隔离级别用以保证数据的一致性,也是一个不错的选择。

  在实际工作中,@Transactional隔离级别的默认值为Isolation.DEFAULT,随数据库默认值的变化而变化,必须MySQl支持4种隔离级别,默认的是可重复读的隔离级别;而Oracle只能支持读/写提交和序列化两种隔离级别,默认为读/写提交。

  

  (2)选择传播行为

  在Spring中传播行为的类型,是通过一个枚举类型定义的,这个枚举类是org.springframework.transaction.annotation.Propagation,其中定义了七种传播行为:

  最常用的是REQUIRED,也是默认的传播行为。

  

    

  

  六、Spring数据库事务管理

  数据库事务是企业应用最为重要的内容之一,与之密切关联的就是Spring中最著名的注解之一--@Transactional注解。

  互联网系统时时面对着高并发,在互联网系统中同时跑着成百上千条线程都是十分常见的,导致数据库在一个多事务访问的环境中,从而引发数据库丢失更新和数据一致性的问题,同时也会给服务器带来很大压力,甚至发生数据库系统死锁和瘫痪进而导致系统宕机。

  在大部分情况下,我们会认为数据库事务要么同时成功,要么同时失败,但是也存在着不同的要求。比如银行的信用卡还款,有个跑批量的事务,而这个批量事务又包含了对各个信用卡的还款业务的处理,我哦们补鞥因为其中一张卡的事务失败了,而把其他卡的事务也回滚,这样就会导致因为一个客户的异常,造成多个客户还款失败,即正常还款的用户,也被认为是不正常的还款,这样会引发严重的金融信誉问题,Spring事务带来了比较方便的解决方案。

  1.Spring数据库事务管理器的设计

  在Spring中的数据库事务是通过PlatformTransactionManager进行管理的,在之前已经知道JdbcTemplate是无法支持事务的,而能够支持事务的是org.springframework.transaction.support.TranscctionTemplate模板,它是Spring所提供的事务管理器的模板。

  通过阅读TranscctionTemplate的源码,可以发现事务的创建、提交和回滚都是通过PlatformTransactionManager接口来完成的;并且当事务产生异常时会回滚事务,在默认的实现中所有的异常都会回滚;当无异常时,会提交事务。

  在Spring中,有多种事务管理器:

  

  常用的是DataSourceTransactionManager,它继承抽象事务管理器AbstractPlatformTransactionManager,而AbstractPlatformTransactionManager又实现了PlatformTransactionManager接口。

  (1)配置事务管理器

  首先定义了数据库连接池,然后使用DataSourceTransactionManager去定义数据库事务管理器,并且注入了数据库连接池。这样Spring就知道你已经将数据库事务委托给事务管理器transactionManager管理了。

    <!-- 数据库连接池 -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver" />
        <property name="url" value="jdbc:mysql://localhost:3306/chapter13"/>
        <property name="username" value="root" />
        <property name="password" value="123456" />
        <property name="maxActive" value="255" />
        <property name="maxIdle" value="5" />
        <property name="maxWait" value="10000" />
    </bean>

    <!-- 事务管理器配置数据源事务 -->
    <bean id="transactionManager"
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
    </bean>

  2.声明式事务

  声明式事务是一种约定型的事务,在大部分情况下,当使用数据库事务时,大部分的场景是在代码中发生了异常时,需要回滚事务,而不发生异常时则提交事务,从而保证数据库数据的一致性。从这点出发,Spring给了一个约定,如果使用的是声明式事务,那么当你的业务方法不发生异常时(或者发生异常,但该异常也被配置信息允许提交事务),Spring就会让事务管理器提交事务,而发生异常(并且该异常不被你的配置信息所允许提交事务)时,则让事务管理器回滚事务。

  声明式事务允许自定义事务接口--TransactionDefinition,它可以由XML或者注解@Transactional进行配置。

  Transactional配置项:propagation表示传播行为,isolation表示隔离级别。这些属性会被Spring放到事务定义类TransactionDefinition中。

  事务定义器TransactionDefinition类中,将REQUIRED、SUPPORTS、MANDATORY、REQUIRES_NEW、NOT_SUPPORTED、NEVER和NESTED七个隔离级别分别设置为常量0-6

  使用声明式事务需要配置注解驱动,只需要加入下面的配置就可以使用@Transactional配置事务了。

    <!-- 使用注解定义事务 -->
    <tx:annotation-driven transaction-manager="transactionManager" />

  3.声明式事务的约定流程

  @Transaction注解可以使用在方法或类上面,在Spring IoC容器初始化时,Spring会读入这个注解或者XML配置的事务信息,并且保存到一个事务定义类里面(TransactionDefinition接口的子类),以备将来使用。当运行时会让Spring拦截注解标注的某一个方法或类的所有方法。Spring利用AOP将代码织入到AOP流程中,然后给出它的约定。

  

  约定流程为:首先Spring通过事务管理器(PlatformTransactionManager的子类)创建事务,与此同时会把事务定义中的隔离级别、超时时间等属性根据配置内容往事务上设置。而根据传播行为配置采取一种特定的策略,只需配置,无须编码。然后,启动开发者提供的业务代码,Spring会通过反射的方式调度开发者的业务代码,但是反射的结果可能是正常返回或者产生异常的返回,那么它给的约定是只要发生异常,并且符合事务定义类的回滚条件,Spring就会将数据库事务回滚,否则将数据库事务提交,这也会Spring自己完成的。

  例如:下面的代码中,只需在insertRole方法上使用@Transactional注解就可以完成数据库事务。

  对比于JDBC代码,这里没有数据库资源的打开和释放代码,也没有数据库提交的代码,只有注解@Transactional。

  这样就可以实现,当insertRole方法抛出异常时,Spring就会回滚事务,如果成功,就提交事务。

  这里的实现原理是Spring AOP技术,而其底层的实现原理是动态代理。

    @Autowired
    private RoleMapper roleMapper = null;

    @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_COMMITTED)
    public int insertRole(Role role) {
        return roleMapper.insertRole(role);
    }

 

  七、在Spring+MyBatis中使用数据库事务

  

  1.运行环境XML配置

  首先配置Spring+MyBatis环境,即Spring配置文件spring-cfg.xml

<?xml version='1.0' encoding='UTF-8' ?>
<!-- was: <?xml version="1.0" encoding="UTF-8"?> -->
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
    xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
       http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
       http://www.springframework.org/schema/aop 
       http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
       http://www.springframework.org/schema/tx 
       http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
       http://www.springframework.org/schema/context 
       http://www.springframework.org/schema/context/spring-context-4.0.xsd">
    <!--启用扫描机制,并指定扫描对应的包-->
    <context:annotation-config />
    <context:component-scan base-package="com.ssm.chapter13.*" />
    <!-- 数据库连接池 -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver" />
        <property name="url" value="jdbc:mysql://localhost:3306/chapter13"/>
        <property name="username" value="root" />
        <property name="password" value="123456" />
        <property name="maxActive" value="255" />
        <property name="maxIdle" value="5" />
        <property name="maxWait" value="10000" />
    </bean>

    <!-- 集成MyBatis -->
    <bean id="SqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
         <!--指定MyBatis配置文件-->
        <property name="configLocation" value="classpath:/mybatis/mybatis-config.xml" />
    </bean>

    <!-- 事务管理器配置数据源事务 -->
    <bean id="transactionManager"
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
    </bean>

    <!-- 使用注解定义事务 -->
    <tx:annotation-driven transaction-manager="transactionManager" />

    <!-- 采用自动扫描方式创建mapper bean -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
       <property name="basePackage" value="com.ssm.chapter13" />
       <property name="SqlSessionFactory" ref="SqlSessionFactory" />
       <property name="annotationClass" value="org.springframework.stereotype.Repository" />
    </bean>
    
</beans>

  分析:

    dataSourceSqlSessionFactoryMapperScannerConfigurer用来支持Spring+Mybatis

    transactionManager是为了配置事务管理器,同时将dataSource数据库连接池注入到事务管理器

    tx:annotation-driven是为了配置注解驱动,这样才能够使用@Transactional注解配置事务

  2.MyBatis相关配置

  数据库表映射的POJO类Role.java

package com.ssm.chapter13.pojo;

public class Role {
    private Long id;
    private String roleName;
    private String note;
  
    /**getter and setter**/
}

  与之对应的是MyBatis映射文件mybatis-config.xml,建立SQL与POJO的映射关系:这里只配置了一个简单的映射器Mapper,需要配置一个接口就可以了

<?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>
    <mappers>
        <mapper resource="com/ssm/chapter13/sqlMapper/RoleMapper.xml"/>
    </mappers>
</configuration>

  映射器Mapper文件RoleMapper.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.ssm.chapter13.mapper.RoleMapper">
    <insert id="insertRole" parameterType="com.ssm.chapter13.pojo.Role">
        insert into t_role (role_name, note) values(#{roleName}, #{note})
    </insert>
</mapper>

  与之对应,还需要有一个RoleMapper接口:

package com.ssm.chapter13.mapper;

import com.ssm.chapter13.pojo.Role;
import org.springframework.stereotype.Repository;

@Repository
public interface RoleMapper {
    public int insertRole(Role role);
}

  3.服务(Service)类:

  业务接口1:RoleService.java。

package com.ssm.chapter13.service;
import com.ssm.chapter13.pojo.Role;

public interface RoleService {
    public int insertRole(Role role);  
}

  业务接口2:RoleListService.java。其中的insertRoleList方法可以对角色列表进行插入。

package com.ssm.chapter13.service;

import java.util.List;

import com.ssm.chapter13.pojo.Role;

public interface RoleListService {
    public int insertRoleList(List<Role> roleList);
}

  业务实现类1:insertRole方法可以对单个角色进行插入。其隔离级别设置为读/写提交,传播行为为REQUIRES_NEW,表示无论是否在当前事务,方法都会在新的事务中运行。

package com.ssm.chapter13.service.impl;

@Service
public class RoleServiceImpl implements RoleService {

    @Autowired
    private RoleMapper roleMapper = null;
    
    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_COMMITTED)
    public int insertRole(Role role) {
        return roleMapper.insertRole(role);
    }
}

  业务实现类2:insertRoleList方法调用了RoleService接口的insertRole方法,可以对角色列表进行插入。其隔离级别设置为读/写提交,传播行为设置为REQUIRE,表示当方法调用时,如果不存在当前事务,那么就创建事务;如果之前的方法已经存在了事务,那么就沿用之前的事务。

package com.ssm.chapter13.service.impl;

@Service
public class RoleListServiceImpl implements RoleListService {
    @Autowired
    private RoleService roleService = null;
    Logger log = Logger.getLogger(RoleListServiceImpl.class);
    @Override
    @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED)
    public int insertRoleList(List<Role> roleList) {
        int count = 0;
        for (Role role : roleList) {
            try {
                count += roleService.insertRole(role);
            } catch (Exception ex) {
                log.info(ex);
            }
        }
        return count;
    }
}

  4.测试类 

package com.ssm.chapter13.main;
public class Chapter13Main {
    public static void main(String [] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext ("spring-cfg.xml");
        RoleListService roleListService = ctx.getBean(RoleListService. class);
        List<Role> roleList = new ArrayList<Role>();
        for (int i=1; i<=2; i++) {
            Role role = new Role();
            role.setRoleName("role_name_" + i);
            role.setNote("note_" + i);
            roleList.add(role);
        }
        int count = roleListService.insertRoleList(roleList);
       System.out.println(count);
    }
}

  5.测试结果


 DEBUG 2018-10-09 23:21:29,550 org.springframework.transaction.support.AbstractPlatformTransactionManager: Creating new transaction with name [com.ssm.chapter13.service.impl.RoleListServiceImpl.insertRoleList]: PROPAGATION_REQUIRED,ISOLATION_READ_COMMITTED; ''
 DEBUG 2018-10-09 23:21:29,763 org.springframework.jdbc.datasource.DataSourceTransactionManager: Acquired Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL-AB JDBC Driver] for JDBC transaction
 DEBUG 2018-10-09 23:21:29,766 org.springframework.jdbc.datasource.DataSourceUtils: Changing isolation level of JDBC Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL-AB JDBC Driver] to 2
 DEBUG 2018-10-09 23:21:29,767 org.springframework.jdbc.datasource.DataSourceTransactionManager: Switching JDBC Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL-AB JDBC Driver] to manual commit
DEBUG 2018-10-09 23:21:29,767 org.springframework.transaction.support.AbstractPlatformTransactionManager: Suspending current transaction, creating new transaction with name [com.ssm.chapter13.service.impl.RoleServiceImpl.insertRole] DEBUG 2018-10-09 23:21:29,782 org.springframework.jdbc.datasource.DataSourceTransactionManager: Acquired Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL-AB JDBC Driver] for JDBC transaction DEBUG 2018-10-09 23:21:29,782 org.springframework.jdbc.datasource.DataSourceUtils: Changing isolation level of JDBC Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL-AB JDBC Driver] to 2 DEBUG 2018-10-09 23:21:29,783 org.springframework.jdbc.datasource.DataSourceTransactionManager: Switching JDBC Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL-AB JDBC Driver] to manual commit
DEBUG 2018-10-09 23:21:29,787 org.mybatis.spring.SqlSessionUtils: Creating a new SqlSession DEBUG 2018-10-09 23:21:29,791 org.mybatis.spring.SqlSessionUtils: Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3214ee6] DEBUG 2018-10-09 23:21:29,796 org.mybatis.spring.transaction.SpringManagedTransaction: JDBC Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL-AB JDBC Driver] will be managed by Spring DEBUG 2018-10-09 23:21:29,800 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ==> Preparing: insert into t_role (role_name, note) values(?, ?) DEBUG 2018-10-09 23:21:29,824 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ==> Parameters: role_name_1(String), note_1(String) DEBUG 2018-10-09 23:21:29,826 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: <== Updates: 1 DEBUG 2018-10-09 23:21:29,827 org.mybatis.spring.SqlSessionUtils: Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3214ee6] DEBUG 2018-10-09 23:21:29,827 org.mybatis.spring.SqlSessionUtils$SqlSessionSynchronization: Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3214ee6] DEBUG 2018-10-09 23:21:29,827 org.mybatis.spring.SqlSessionUtils$SqlSessionSynchronization: Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3214ee6] DEBUG 2018-10-09 23:21:29,827 org.mybatis.spring.SqlSessionUtils$SqlSessionSynchronization: Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3214ee6]
DEBUG
2018-10-09 23:21:29,827 org.springframework.transaction.support.AbstractPlatformTransactionManager: Initiating transaction commit DEBUG 2018-10-09 23:21:29,828 org.springframework.jdbc.datasource.DataSourceTransactionManager: Committing JDBC transaction on Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL-AB JDBC Driver] DEBUG 2018-10-09 23:21:29,830 org.springframework.jdbc.datasource.DataSourceUtils: Resetting isolation level of JDBC Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL-AB JDBC Driver] to 4 DEBUG 2018-10-09 23:21:29,831 org.springframework.jdbc.datasource.DataSourceTransactionManager: Releasing JDBC Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL-AB JDBC Driver] after transaction DEBUG 2018-10-09 23:21:29,831 org.springframework.jdbc.datasource.DataSourceUtils: Returning JDBC Connection to DataSource DEBUG 2018-10-09 23:21:29,832 org.springframework.transaction.support.AbstractPlatformTransactionManager: Resuming suspended transaction after completion of inner transaction
DEBUG
2018-10-09 23:21:29,832 org.springframework.transaction.support.AbstractPlatformTransactionManager: Suspending current transaction, creating new transaction with name [com.ssm.chapter13.service.impl.RoleServiceImpl.insertRole] DEBUG 2018-10-09 23:21:29,832 org.springframework.jdbc.datasource.DataSourceTransactionManager: Acquired Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL-AB JDBC Driver] for JDBC transaction DEBUG 2018-10-09 23:21:29,833 org.springframework.jdbc.datasource.DataSourceUtils: Changing isolation level of JDBC Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL-AB JDBC Driver] to 2 DEBUG 2018-10-09 23:21:29,834 org.springframework.jdbc.datasource.DataSourceTransactionManager: Switching JDBC Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL-AB JDBC Driver] to manual commit
DEBUG 2018-10-09 23:21:29,834 org.mybatis.spring.SqlSessionUtils: Creating a new SqlSession DEBUG 2018-10-09 23:21:29,835 org.mybatis.spring.SqlSessionUtils: Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7b50df34] DEBUG 2018-10-09 23:21:29,835 org.mybatis.spring.transaction.SpringManagedTransaction: JDBC Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL-AB JDBC Driver] will be managed by Spring DEBUG 2018-10-09 23:21:29,835 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ==> Preparing: insert into t_role (role_name, note) values(?, ?) DEBUG 2018-10-09 23:21:29,836 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ==> Parameters: role_name_2(String), note_2(String) DEBUG 2018-10-09 23:21:29,836 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: <== Updates: 1 DEBUG 2018-10-09 23:21:29,836 org.mybatis.spring.SqlSessionUtils: Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7b50df34] DEBUG 2018-10-09 23:21:29,837 org.mybatis.spring.SqlSessionUtils$SqlSessionSynchronization: Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7b50df34] DEBUG 2018-10-09 23:21:29,837 org.mybatis.spring.SqlSessionUtils$SqlSessionSynchronization: Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7b50df34] DEBUG 2018-10-09 23:21:29,837 org.mybatis.spring.SqlSessionUtils$SqlSessionSynchronization: Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7b50df34]
DEBUG
2018-10-09 23:21:29,837 org.springframework.transaction.support.AbstractPlatformTransactionManager: Initiating transaction commit DEBUG 2018-10-09 23:21:29,837 org.springframework.jdbc.datasource.DataSourceTransactionManager: Committing JDBC transaction on Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL-AB JDBC Driver] DEBUG 2018-10-09 23:21:29,839 org.springframework.jdbc.datasource.DataSourceUtils: Resetting isolation level of JDBC Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL-AB JDBC Driver] to 4 DEBUG 2018-10-09 23:21:29,840 org.springframework.jdbc.datasource.DataSourceTransactionManager: Releasing JDBC Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL-AB JDBC Driver] after transaction DEBUG 2018-10-09 23:21:29,840 org.springframework.jdbc.datasource.DataSourceUtils: Returning JDBC Connection to DataSource DEBUG 2018-10-09 23:21:29,840 org.springframework.transaction.support.AbstractPlatformTransactionManager: Resuming suspended transaction after completion of inner transaction DEBUG 2018-10-09 23:21:29,840 org.springframework.transaction.support.AbstractPlatformTransactionManager: Initiating transaction commit DEBUG 2018-10-09 23:21:29,841 org.springframework.jdbc.datasource.DataSourceTransactionManager: Committing JDBC transaction on Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL-AB JDBC Driver] DEBUG 2018-10-09 23:21:29,841 org.springframework.jdbc.datasource.DataSourceUtils: Resetting isolation level of JDBC Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL-AB JDBC Driver] to 4 DEBUG 2018-10-09 23:21:29,842 org.springframework.jdbc.datasource.DataSourceTransactionManager: Releasing JDBC Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL-AB JDBC Driver] after transaction DEBUG 2018-10-09 23:21:29,842 org.springframework.jdbc.datasource.DataSourceUtils: Returning JDBC Connection to DataSource 2

   结合测试方法中insertRoleList方法两次调用insertRole方法,分析有关事务操作的流程:

  1.由于insertRoleList方法的隔离级别为读/写提交,传播行为为REQUIRED,而初始时没有当前事务,因此要首先创建insertRoleList方法的事务:

 DEBUG 2018-10-09 23:21:29,550 org.springframework.transaction.support.AbstractPlatformTransactionManager: 
  Creating new transaction with name [com.ssm.chapter13.service.impl.RoleListServiceImpl.insertRoleList]: PROPAGATION_REQUIRED,ISOLATION_READ_COMMITTED; ''

  然后DataSourceTransactionManager获取JDBC连接事务,并且调整JDBC连接事务传播级别为级别2,对应MANDATORY,即方法必须在事务内运行。

 DEBUG 2018-10-09 23:21:29,763 org.springframework.jdbc.datasource.DataSourceTransactionManager: 
  Acquired Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL-AB JDBC Driver] for JDBC transaction DEBUG 2018-10-09 23:21:29,766 org.springframework.jdbc.datasource.DataSourceUtils:
  Changing isolation level of JDBC Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL-AB JDBC Driver] to 2 DEBUG 2018-10-09 23:21:29,767 org.springframework.jdbc.datasource.DataSourceTransactionManager:
  Switching JDBC Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL-AB JDBC Driver] to manual commit

  2.在insertRoleList方法中,两次调用了RoleServiceImpl类的insertRole方法,而insertRole方法的事务定义为读/写提交和REQUIRES_NEW,以第一次调用为例,由于insertRoleList方法的传播行为定义为REQUIRED,因此需要暂时挂起insertRoleList事务,然后创建新的insertRole事务:

 DEBUG 2018-10-09 23:21:29,767 org.springframework.transaction.support.AbstractPlatformTransactionManager: 
  Suspending current transaction, creating new transaction with name [com.ssm.chapter13.service.impl.RoleServiceImpl.insertRole]

  同理,获取JDBC连接事务,然后设置传播级别为2,调整为手动提交

 DEBUG 2018-10-09 23:21:29,782 org.springframework.jdbc.datasource.DataSourceTransactionManager: 
  Acquired Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL-AB JDBC Driver] for JDBC transaction DEBUG 2018-10-09 23:21:29,782 org.springframework.jdbc.datasource.DataSourceUtils:
  Changing isolation level of JDBC Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL-AB JDBC Driver] to 2 DEBUG 2018-10-09 23:21:29,783 org.springframework.jdbc.datasource.DataSourceTransactionManager:
  Switching JDBC Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL-AB JDBC Driver] to manual commit

  3.进入SQL执行过程:

Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3214ee6]
JDBC Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL-AB JDBC Driver] will be managed by Spring
 ==>  Preparing: insert into t_role (role_name, note) values(?, ?) 
 ==> Parameters: role_name_1(String), note_1(String)
 <==    Updates: 1Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3214ee6]
Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3214ee6]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3214ee6]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3214ee6]

  4.如果没有异常,则进行事务提交,然后进行JDBC事务提交,然后重置JDBC事务级别为默认的4,即NOT_SUPPORTED即不支持事务,也就是不在事务中也可以运行。然后释放JDBC数据库连接,然后将数据库连接返还到数据库连接池,然后恢复之前挂起的insertRoleList事务,即将进行第二次调用insertRole方法。

Initiating transaction commit
Committing JDBC transaction on Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL-AB JDBC Driver]
Resetting isolation level of JDBC Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL-AB JDBC Driver] to 4
Releasing JDBC Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL-AB JDBC Driver] after transaction
Returning JDBC Connection to DataSource
Resuming suspended transaction after completion of inner transaction

  5.第二次调用insertRole时,重复2-4即可。

  

  八、Spring数据库事务的一些问题

  1.@Transactional的自调用失效问题

  注解@Transactional的底层实现是Spring AOP技术,而Spring AOP技术使用的是动态代理技术。

  这就意味着对于静态(static)和非public方法,注解@Transactional是失效的。而且,自调用也是使用过程中容易犯的错误。

  自调用就是,一个类的一个方法去调用自身另外一个方法的过程。

  修改RoleService接口,增加insertRoleList方法:

package com.ssm.chapter13.service;

import java.util.List;

import com.ssm.chapter13.pojo.Role;

public interface RoleService {
    
    public int insertRole(Role role);
    
    public int insertRoleList(List<Role> roleList);
    
}

  然后在实现类中实现这个方法,其中,insertRoleList方法中调用的是同一个类中的insertRole方法,在两个方法上保持原来的事务设置。

package com.ssm.chapter13.service.impl;

@Service
public class RoleServiceImpl implements RoleService {
    
    @Autowired
    private RoleMapper roleMapper = null;
    
    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_COMMITTED)
    public int insertRole(Role role) {
        return roleMapper.insertRole(role);
    }
 
    @Override
    @Transactional(propagation = Propagation.REQUIRED, isolation=Isolation.READ_COMMITTED)
    public int insertRoleList(List<Role> roleList) {
        int count = 0;
        for (Role role : roleList) {
            try {
                // 调用自身类的insertRole方法,产生自调用问题
                insertRole(role);
                count++;
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
        return count;
    }
}

  修改测试Main方法并运行:

package com.ssm.chapter13.main;
public class Chapter13Main {
    public static void main(String [] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext ("spring-cfg.xml");
//        RoleListService roleListService = ctx.getBean(RoleListService. class);
        RoleService roleService = ctx.getBean(RoleService.class);
        List<Role> roleList = new ArrayList<Role>();
        for (int i=1; i<=2; i++) {
            Role role = new Role();
            role.setRoleName("role_name_" + i);
            role.setNote("note_" + i);
            roleList.add(role);
        }
        int count = roleService.insertRoleList(roleList);
       System.out.println(count);
    }
}

  结果分析:从下面的结果可以看出,两次插入都只创建了一个SqlSession,也就是说,两次插入都使用了同一事务,即在insertRole方法上进行@Transactional标注失效了。

 ...
 DEBUG 2018-10-10 21:22:58,512 org.springframework.transaction.support.AbstractPlatformTransactionManager: Creating new transaction with name [com.ssm.chapter13.service.impl.RoleServiceImpl.insertRoleList]: PROPAGATION_REQUIRED,ISOLATION_READ_COMMITTED; ''
 DEBUG 2018-10-10 21:22:58,750 org.springframework.jdbc.datasource.DataSourceTransactionManager: Acquired Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL-AB JDBC Driver] for JDBC transaction
 DEBUG 2018-10-10 21:22:58,754 org.springframework.jdbc.datasource.DataSourceUtils: Changing isolation level of JDBC Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL-AB JDBC Driver] to 2
 DEBUG 2018-10-10 21:22:58,755 org.springframework.jdbc.datasource.DataSourceTransactionManager: Switching JDBC Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL-AB JDBC Driver] to manual commit
 DEBUG 2018-10-10 21:22:58,759 org.mybatis.spring.SqlSessionUtils: Creating a new SqlSession
DEBUG
2018-10-10 21:22:58,763 org.mybatis.spring.SqlSessionUtils: Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@e3b3b2f] DEBUG 2018-10-10 21:22:58,772 org.mybatis.spring.transaction.SpringManagedTransaction: JDBC Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL-AB JDBC Driver] will be managed by Spring DEBUG 2018-10-10 21:22:58,775 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ==> Preparing: insert into t_role (role_name, note) values(?, ?) DEBUG 2018-10-10 21:22:58,794 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ==> Parameters: role_name_1(String), note_1(String) DEBUG 2018-10-10 21:22:58,797 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: <== Updates: 1 DEBUG 2018-10-10 21:22:58,797 org.mybatis.spring.SqlSessionUtils: Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@e3b3b2f] DEBUG 2018-10-10 21:22:58,797 org.mybatis.spring.SqlSessionUtils: Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@e3b3b2f] from current transaction DEBUG 2018-10-10 21:22:58,797 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ==> Preparing: insert into t_role (role_name, note) values(?, ?) DEBUG 2018-10-10 21:22:58,798 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ==> Parameters: role_name_2(String), note_2(String) DEBUG 2018-10-10 21:22:58,798 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: <== Updates: 1
DEBUG 2018-10-10 21:22:58,798 org.mybatis.spring.SqlSessionUtils: Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@e3b3b2f] DEBUG 2018-10-10 21:22:58,799 org.mybatis.spring.SqlSessionUtils$SqlSessionSynchronization: Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@e3b3b2f] DEBUG 2018-10-10 21:22:58,799 org.mybatis.spring.SqlSessionUtils$SqlSessionSynchronization: Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@e3b3b2f] DEBUG 2018-10-10 21:22:58,799 org.mybatis.spring.SqlSessionUtils$SqlSessionSynchronization: Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@e3b3b2f] DEBUG 2018-10-10 21:22:58,799 org.springframework.transaction.support.AbstractPlatformTransactionManager: Initiating transaction commit DEBUG 2018-10-10 21:22:58,799 org.springframework.jdbc.datasource.DataSourceTransactionManager: Committing JDBC transaction on Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL-AB JDBC Driver] DEBUG 2018-10-10 21:22:58,830 org.springframework.jdbc.datasource.DataSourceUtils: Resetting isolation level of JDBC Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL-AB JDBC Driver] to 4 DEBUG 2018-10-10 21:22:58,831 org.springframework.jdbc.datasource.DataSourceTransactionManager: Releasing JDBC Connection [jdbc:mysql://localhost:3306/chapter6?useSSL=false, UserName=root@, MySQL-AB JDBC Driver] after transaction DEBUG 2018-10-10 21:22:58,831 org.springframework.jdbc.datasource.DataSourceUtils: Returning JDBC Connection to DataSource 2

  自调用引起@Transactional失效的根本原因在于AOP的实现原理。由于@Transactional的实现原理是AOP,而AOP的实现原理是动态代理。如果同一个类中的不同方法之间相互调用,那么就不存在代理对象的调用,这样就不会产生AOP去为我们设置@Transactional配置的参数了,这样就出现了自调用注解失效的问题。

  为了克服这个问题,第一种方法是像之前一样把两个方法分别位于两个不同的类中,这样Spring IoC容器中自动生成了RoleService的代理对象,这样就可以使用AOP;第二种方法是可以直接从容器中获取RoleService的代理对象,可以改写insertRoleList方法,从IoC容器中获取RoleService的代理对象。

  此时,需要将代码修改成下面的内容,由于需要通过应用上下文ctx的getBean方法获取到Bean,因此类需要实现ApplicationContextAware方法并且增加ctx字段。

package com.ssm.chapter13.service.impl;

@Service
public class RoleServiceImpl implements RoleService, ApplicationContextAware {
    
    @Autowired
    private RoleMapper roleMapper = null;
    
    private ApplicationContext ctx = null;

    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_COMMITTED)
    public int insertRole(Role role) {
        return roleMapper.insertRole(role);
    }
    // 直接从Spring IoC容器中获取到RoleService的代理对象
    @Override
    @Transactional(propagation = Propagation.REQUIRED, isolation= Isolation.READ_COMMITTED)
    public int insertRoleList2(List<Role> roleList) {
        int count = 0;
        // 从IoC容器中获取了RoleService的Bean,也就是一个代理对象
        RoleService service = ctx.getBean(RoleService.class);
        for (Role role : roleList) {
            try {
                service.insertRole(role);
                count++;
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
        return count;
    }

    // 增加setApplicationContext方法获取ctx
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        ctx = applicationContext;
    }
}

  2.错误使用Service

  假如想要在Controller中同时插入两个角色,且必须在同一个事务中处理,其中insertRole方法是带有@Transactional标注的方法

  当一个Controller使用Service方法时,如果这个Service标注有@Transactional,那么它就会启动一个事务,而一个Service方法完成后,它就会释放该事务,所以前后两个insertRole的方法是在两个不同的事务中完成的。如果第一个插入成功了,而第二个插入失败了,就会是数据库不完全同时成功或者失败,可能产生严重的数据不一致的问题,给生产带来严重的损失。

package com.test.errorUseService

@Controller
public class RoleController {

    @Autowired 
    private RoleService roleService = null;

    public void errorUseServices() {
        Role role1 = new Role();
        role1.setRoleName("role_name_1");
        role1.setNote("role_note_1");
        roleService.insertRole(role1);

        Role role2 = new Role();
        role2.setRoleName("role_name_2");
        role2.setNote("role_note_2");
        roleService.insertRole(role2);
    }
}

  3.过长时间占用事务

  在企业的生产系统中,数据库事务资源是最宝贵的资源之一,使用了数据库事务只有,要及时释放数据库事务。

  假设在插入角色之后还需要操作一个文件,而操作文件的方法是一个与数据库事务无关的操作:

    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_COMMITTED)
    public int insertRole(Role role) {
int result = roleMapper.insertRole(role);
doSomethingWithoutTranaction();
return return; }

  由于在insertRole方法上进行了@Transactional标注,因此当insertRole方法结束后Spring才会释放数据库事务资源,也就是说,必须等到doSomethingWithoutTranaction方法执行完成之后才可以释放数据库事务资源。如果doSomethingWithoutTranaction方法所消耗的时间特别长,那么导致数据库事务将长期得不到释放,如果此时发生高并发的需求,会造成大量的并发请求得不到数据库的事务资源而导致系统宕机。因此应该调整doSomethingWithoutTranaction方法的位置,使其放置在insertRole方法之外。

  4.错误捕捉异常

  Spring的事务中已经存在针对于异常的捕捉,即只要出现异常就会回滚事务。

  但是,当前需要将产品减库存和保存交易在同一个事务里面,要么同时成功,要么同时失败。假设减库存和保存交易的传播行为都为REQUIRED,那么下面的代码会出现:Spring在整个数据库事务所约定的流程中再也得不到任何的异常信息了。加入当库存减少成功了,但是保存交易信息是却出现了异常,此时由于catch语句的原因,Spring由于得不到保存交易信息这个过程的异常,这个时候就会出现库存减少,但是没有交易信息的情况。

@Autowired
private ProductService productService;

@Autowired
private TransactionService transactionService;

@Override
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITED)
public int doTransaction(TransactionBean trans) {
    int result =0;
    try {
        // 执行减少库存操作
        int result = productService.decreseStock(trans.getProductId(), trans.getQuantity());
        // 如果减少库存成功,则保存记录
        if(result > 0)  transactionService.save(trans);
    } catch(Exception ex) {
        // 自行捕获异常并且处理异常
        // 记录异常日志
        log.info(ex);
    }
    return result;
}
      

  解决办法是,捕获到异常后,再自行抛出异常交由上级处理,让Spring事务管理流程捕获到异常,然后进行正确的事务管理。

@Autowired
private ProductService productService;

@Autowired
private TransactionService transactionService;

@Override
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITED)
public int doTransaction(TransactionBean trans) {
    int result =0;
    try {
        // 执行减少库存操作
        int result = productService.decreseStock(trans.getProductId(), trans.getQuantity());
        // 如果减少库存成功,则保存记录
        if(result > 0)  transactionService.save(trans);
    } catch(Exception ex) {
        // 自行捕获异常并且处理异常
        // 记录异常日志
        log.info(ex);
        throw new RuntimeException(ex);
    }
    return result;
}

 

posted @ 2018-10-09 18:23  BigJunOba  阅读(1367)  评论(0编辑  收藏  举报