Spring Junit 对 JDBC Test的支持, 如下设置可以方便的对数据库操作进行回滚,以免扰乱数据库数据。
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:application-context.xml") @Transactional(value = "txManager", rollbackFor = { Exception.class }) public class StudentDaoImplTest { @Autowired StudentDao studentDao; /** * Test method for * {@link com.chengmaoning.jroad.jdbc.dao.impl.StudentDaoImpl#findAllStudents()}. */ @Test public void testFindAllStudents() { List<Student> students = studentDao.findAllStudents(); System.out.println(students); Assert.isTrue(true, null); } ...所有test方法默认都是@Rollback(true),可以在类定义前或者方法定义前通过@Commit改变回滚策略。
ps:事务回滚除了需要指定事务管理器txManager,还需要StudentDao studentDao的实现类是基于JdbcTemplate的,更严格的说,JDBC操作所在的数据库连接Connection是关联到事务的,或者说被txManager管理的()Low-level synchronization approach。
所以,如果我们的代码是直接使用the traditional JDBC approach of calling the getConnection() method on the DataSource,那么这部分代码对数据库的操作是不会被回滚的,因为数据库连接没有被事务管理器管理,怎么才能得到被事务管理器管理起来的数据库连接呢?you instead use Spring’s org.springframework.jdbc.datasource.DataSourceUtils class as follows:
Connection conn = DataSourceUtils.getConnection(dataSource);如下例子,批量插入并返回批量id列表(因为JdbcTemplate 不支持批量插入并批量返回id列表 :disappointed:):
public List<Long> batchInsert(List<Student> students) { List<Long> ids = new ArrayList<>(); Connection connection = null; try { // 这种方式才能做事务,关联到txManager connection = DataSourceUtils.getConnection(jdbcTemplate.getDataSource()); PreparedStatement statement = connection.prepareStatement("insert into student(name, age) values (?, ?)", new String[] { "id" }); for (Student student : students) { statement.setString(1, student.getName()); statement.setInt(2, student.getAge()); statement.addBatch(); } statement.executeBatch(); ResultSet rs = statement.getGeneratedKeys(); while (rs.next()) { ids.add(rs.getLong(1)); } } catch (Exception e) { e.printStackTrace(); } finally { /** * 不必手动关闭connection,测试框架会在扫尾工作中关闭connection */ // if (connection != null) { // try { // connection.close(); // } catch (SQLException e) { // e.printStackTrace(); // } // } } return ids; }注意代码最后的注释部分,如果在这里手动关闭连接(不注释)的话,在测试用例中就会报错:
@Test public void testBatchInsert() { List<Student> students = new ArrayList<>(); for (int i = 0; i < 2; i++) { Student student = new Student(); student.setName("student_" + i); student.setId(8 + i); students.add(student); } List<Long> ids = studentDao.batchInsert(students); System.out.println(ids); }异常堆栈如下,但是查看数据库确实最终没有写入,也就是说事务是回滚了的。
Aug 07, 2017 12:08:11 AM org.springframework.test.context.TestContextManager afterTestMethod WARNING: Caught exception while allowing TestExecutionListener [org.springframework.test.context.transaction.TransactionalTestExecutionListener@2d6e8792] to process 'after' execution for test: method [public void com.chengmaoning.jroad.jdbc.dao.impl.StudentDaoImplTest.testBatchInsert()], instance [com.chengmaoning.jroad.jdbc.dao.impl.StudentDaoImplTest@1b2abca6], exception [null] org.springframework.transaction.TransactionSystemException: Could not roll back JDBC transaction; nested exception is java.sql.SQLException: Connection is null. at org.springframework.jdbc.datasource.DataSourceTransactionManager.doRollback(DataSourceTransactionManager.java:331) at org.springframework.transaction.support.AbstractPlatformTransactionManager.processRollback(AbstractPlatformTransactionManager.java:853) at org.springframework.transaction.support.AbstractPlatformTransactionManager.rollback(AbstractPlatformTransactionManager.java:830) at org.springframework.test.context.transaction.TransactionContext.endTransaction(TransactionContext.java:125) at org.springframework.test.context.transaction.TransactionalTestExecutionListener.afterTestMethod(TransactionalTestExecutionListener.java:227) at org.springframework.test.context.TestContextManager.afterTestMethod(TestContextManager.java:319) at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:94) at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84) at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:252) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:94) at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288) at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268) at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61) at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70) at org.junit.runners.ParentRunner.run(ParentRunner.java:363) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:191) at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86) at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:678) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192) Caused by: java.sql.SQLException: Connection is null. at org.apache.commons.dbcp2.DelegatingConnection.checkOpen(DelegatingConnection.java:612) at org.apache.commons.dbcp2.DelegatingConnection.rollback(DelegatingConnection.java:490) at org.springframework.jdbc.datasource.DataSourceTransactionManager.doRollback(DataSourceTransactionManager.java:328) ... 25 more根据异常信息,只需要把最后手动关闭数据库连接的代码注释即可。但是问题又来了,如果不考虑测试用例,正常代码执行逻辑中,connection不是应该在finally中释放么? 这可能是测试框架的一个bug?未来得及深入debug跟进,大神们怎么看?
