Stephenmu 2012-03-06
之前为了练练手自己用spring3集成mybaits3,采用spring的声明式事务,发现数据的插入没有问题,但是异常时不能回滚,一开始的代码如下(是按mybatis-spring-1.0.2-reference.pdf):
dao层代码mybatis版本的:
public void insertBlog(Blog blog) { SqlSession sqlSession = sqlSessionFactory.openSession(); BlogMapper blogMapper = sqlSession.getMapper(BlogMapper.class); blogMapper.insertBlog(blog); } public void addTag(long blogId, Tag tag) { SqlSession sqlSession = sqlSessionFactory.openSession(); BlogMapper blogMapper = sqlSession.getMapper(BlogMapper.class); Long tagId = blogMapper.findTagId(tag.getTagName()); if (tagId != null ) blogMapper.addTagForBlog(new Tuple<Long, Long>(blogId, tagId)); else { blogMapper.insertTag(tag); blogMapper .addTagForBlog(new Tuple<Long, Long>(blogId, tag.getId())); } }
dao层jdbc版本的实例,代码太长,不罗列了:
@Override public void insertBlog(Blog blog) throws Exception { Connection connection = dataSource.getConnection(); String sql = "insert into blog (title," + "create_date, last_modify)" + " values(?,now(),now())"; PreparedStatement a = connection.prepareStatement(sql); a.setString(1, blog.getTitle()); a.execute(); String getBlogId = "select LAST_INSERT_ID() as id"; ResultSet rsid = a.executeQuery(getBlogId); if(rsid.first()) blog.setBlogId(rsid.getLong(1)); }
bean配置:
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource" /> <property name="configLocation" value="classpath:common/MybatisConfig.xml"></property> </bean> <bean id="blogService" class="service.impl.BlogService"> <property name="blogDao" ref="blogDaoMybatis" /> </bean> <bean id="blogDaoMybatis" class="dao.BlogDaoWithMybatis"> <property name="sqlSessionFactory" ref="sqlSessionFactory" /> </bean>
事务配置:
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="com.mysql.jdbc.Driver" /> <property name="url" value="jdbc:mysql://localhost:3306/test" /> <property name="username" value="root" /> <property name="password" value="****" /> </bean> <!-- init the spring transactionManager --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource" /> </bean> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <tx:method name="*Tsc" propagation="REQUIRED" rollback-for="java.lang.Exception" /> <tx:method name="*" propagation="SUPPORTS" /> </tx:attributes> </tx:advice>
service方法:
public void insertBlog(Blog blog) throws Exception { blogDao.insertBlog(blog); } public void addTagToBlog(long blogId, Tag tag) throws Exception { blogDao.addTag(blogId, tag); // throw new RuntimeException("hhh"); } public void addBlogTestTsc(Blog blog, Tag tag) throws Exception { insertBlog(blog); addTagToBlog(blog.getBlogId(), tag); }
blog能正常插入,误以为事务工作正常,同样的配置,自己尝试了在service方法addTagToBlog里面抛出异常,发现blog和标签都正常插入了……后来自己又用纯jdbc写了一个插入问题依旧(后面会提到),在网上一直搜寻答案都未果(这也是为什么在这里写出来的原因)
问题的关键不是事务配置错误
首先datasource属性的问题,dbcp默人行为是自动提交,所以插入完数据库就自动提交了,于是改配置为:
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="com.mysql.jdbc.Driver" /> <property name="url" value="jdbc:mysql://localhost:3306/test" /> <property name="username" value="root" /> <property name="password" value="****" /> <property name="defaultAutoCommit" value="false"></property> </bean>发现使用上面的代码后数据怎么都插不进去了,看过spring官方文档事务部分,里面的dbcp数据源配置没有这句:
<property name="defaultAutoCommit" value="false">。mybatis集成spring的文档也就是强调datasource要和spring事务里面配置的datasource一致,这时候思维就混乱了。昨天又看着自己的jdbc版本的代码,就想:如果是我用spring的aop来实现事务,我怎么能能在动态生成的代理对象中获取到被代理对象方法里面获取的那个connection对象再去commit和rollback呢?脑袋愚钝,一下子没有想明白。后来就在spring的DataSourceTransactionManager代码里面doCommit方法和doRollBack方法上各打了一个断点,发现不抛异常时执行了doCommit,抛异常时执行了doRollBack。这回可以肯定的是我的配置没有错,也证明我怀疑是对的,spring在代理类中获取的connection和我在dao层代码中获取的不是一个。看了下spring的org.springframework.jdbc.[版本].jar中的类,发现了DataSourceUtils这个类有一个static方法
public static Connection doGetConnection(DataSource dataSource) throws SQLException顿时豁然开朗,改掉自己jdbc版本的dao层代码获取connection的代码为:
Connection connection = DataSourceUtils.getConnection(dataSource);
再测试,一切ok,搞定。
个人理解:使用DataSourceUtils的getConnection方法时把该获取到的这个链接在当前线程环境中注册到spring事务的context中去了,这样在生成的代理对象中就可以获取到这个connection然后commit或者rollback之(这其中还是有不是很清楚的地方:整么区分一个线程环境中多个地方获取connection,暂时没有敢去看spring的源代码)
如法炮制,在mybatis-spring-1.0.2.jar包中找到了SqlSessionUtils,换取所有获取mybatisSqlSession代码为:
SqlSession sqlSession = SqlSessionUtils.getSqlSession(sqlSessionFactory)
经过测试也正常。