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; } }
从代码可以看出,即使是执行一条简单的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; } }
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; }
四、MyBatis-Spring项目
目前大部分的互联网项目中都使用SSM搭建平台的。使用Spring IoC可以有效管理各类Java资源,达到即插即拔的功能;通过AOP框架,数据库事务可以委托给Spring处理,消除很大一部分的事务代码,配合MyBatis的高灵活、可配置、可优化SQL等特性,完全可以构建高性能的大型网站。
在Spring环境中使用MyBatis也更加简单,节省了不少代码,甚至可以不用SqlSessionFactory、SqlSession等对象。因为MyBatis-Spring为我们封装了它们。
配置MyBatis-Spring项目需要下面几步:
- 配置数据源
- 配置SqlSessionFactory
- 可以选择的配置由SqlSessionTemplate,在同时配置SqlSessionTemplate和SqlSessionFactory的情况下,优先采用SqlSessionTemplate
- 配置Mapper,可以配置单个Mapper,也可以通过扫描的方法生成Mapper,比较灵活。此时Spring IoC会生成对应接口的实例,这样就可以通过注入的方式来获取资源。
- 事务管理。
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配置文件:包括dataSource、sqlSessionFactory和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>
分析:
dataSource、SqlSessionFactory和MapperScannerConfigurer用来支持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; }