zoukankan      html  css  js  c++  java
  • 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;
    }
  • 相关阅读:
    Java类型转换.
    搭建jenkins集群node结点
    java Lambda
    @Autowired使用说明
    Disruptor底层实现讲解与RingBuffer数据结构讲解
    Disruptor并发框架简介
    并发编程中的读写锁分离锁的使用
    并发编程过程中的重入锁
    互联网进行限流策略的Semaphore信号量使用
    并发编程中Future和Callable使用
  • 原文地址:https://www.cnblogs.com/BigJunOba/p/9762001.html
Copyright © 2011-2022 走看看