记一次搭建基于H2内存数据库的集成测试踩到的transaction的坑
前言
最近在项目里搭建了一套DAO层的测试框架(也可以说是集成测试框架),数据库使用的H2,数据连接池与线上一样使用的Druid,为了保证test case之间彼此状态独立,需要保证transaction在test method后被rollback而不是commit,听起来很简单,但是过程还是遇到了一些H2的DataSource和Druid的DataSource的实现上的坑。
插一句
DML是需要保证clean state的,但是DDL大部分情况下是跨test methods共享的,所以需要单独起一个connection执行DDL并且commit,但是H2的memory mode下默认会在最后一个connection关掉之后shutdown掉数据库,需要在url上加参数“DB_CLOSE_DELAY=-1;”,否则就会遇到DDL的时候报错找不到表,这个问题其实在官方文档上写的很清楚,一开始看的时候没注意踩到了这个小坑。
基于Mybatis的interceptor
刚开始的想法是不用Spring的TransactionManager,直接写interceptor拦截Executor.commit,当close SqlSession的时候调用到PooledJdbcConnection.close:
public synchronized void close() throws SQLException {
if (!isClosed) {
try {
rollback();
setAutoCommit(true);
} catch (SQLException e) {
// ignore
}
closedHandle();
isClosed = true;
}
}
其默认会调用其父类JdbcConnection.setAutoCommit:
public synchronized void setAutoCommit(boolean autoCommit) throws SQLException {
try {
if (isDebugEnabled()) {
debugCode("setAutoCommit(" + autoCommit + ");");
}
checkClosed();
if (autoCommit && !session.getAutoCommit()) {
commit();
}
session.setAutoCommit(autoCommit);
} catch (Exception e) {
throw logAndConvert(e);
}
}
然后惊喜的发现JdbcConnection在SqlSession.close的时候自动“帮我”commit了,然而由于H2的DataSource实现不支持设置autoCommit参数,所以这条路走不通了
基于Spring tests的@Rollback
接着考虑引入TransactionManager,父类继承自AbstractTransactionalTestNGSpringContextTests而不再是AbstractTestNGSpringContextTests,这两个类的主要区别在于TransactionalTestExecutionListener,其beforeTestMethod会在每个test method之前设置好TransactionContext,TransactionContext会基于@Rollback设置flag,后续的时序图如下: 于是这条路也走到了JdbcConnection.setAutoCommit
基于DruidDataSource的setDefaultAutoCommit
前面两个方法都卡在了Connection.setAutoCommit,于是决定采用DruidDataSource作为test的DataSource实现,这样可以支持设置Connection的autoCommit,但是这种模式下遇到一个问题,只要设置了autoCommit=false,即使加上@Commit也没法持久化数据,debug的时候发现问题出现在Mybatis的SpringManagedTransaction,时序图如下:
其中SpringManagedTransaction.commit:
public void commit() throws SQLException {
if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Committing JDBC Connection [" + this.connection + "]");
}
this.connection.commit();
}
}
SpringManagedTransaction.rollback:
public void rollback() throws SQLException {
if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Rolling back JDBC Connection [" + this.connection + "]");
}
this.connection.rollback();
}
}
其中autoCommit的值取自Connection,所以这里this.autoCommit=false,而isConnectionTransactional取自DataSourceUtils.isConnectionTransactional,这里为this.isConnectionTransactional=true,所以SpringManagedTransaction的commit和rollback都不会真正被执行,而DruidPooledConnection被release的时候会调用JdbcConnection.rollback,解决这个的办法就是设置DruidDataSource.setDefaultAutoCommit(true)。