Spring Data集成测试@Rollback仅部分回滚 JdbcAggregateTemplate数据未回滚
问题场景
开展集成测试验证存储过程执行逻辑时,标准测试流程如下:
- 插入测试前置准备数据
- 执行目标存储过程
- 查询存储过程写入的下游仓储数据做结果校验
当前测试流程可正常执行完成,但回滚阶段出现异常:仅存储过程写入的数据行被成功回滚,通过JdbcAggregateTemplate插入的前置数据未被回滚。手动在测试方法末尾删除残留数据可临时解决问题,初步怀疑是@Transactional或@Rollback注解配置疏漏。
测试类代码如下:
@SpringBootTest @Transactional @Rollback @TestInstance(TestInstance.Lifecycle.PER_CLASS) class JobServiceIntegrationTest @Autowired constructor( private val repo: JobExecutorService, private val template: JdbcAggregateTemplate, private val generatedDataRepo: GeneratedDataRepo, ) { @Nested inner class ExecuteMyStoredProc { @Test fun `job is executed`() { // arrange val supportingData = supportingData() // act // 该部分数据无法按预期回滚 val expected = template.insert(supportingData) // 该部分数据可正常回滚 repo.doExecuteMyStoredProc() val actual = generatedDataRepo.findAll().first() assertEquals(expected.supportingDataId, actual.supportingDataId) } } fun supportingData() : SupportingData { // 省略测试数据构造逻辑 } }
按照事务逻辑预期,外层事务回滚时其覆盖的所有操作都应被一并回滚。此前编写的其他同技术栈集成测试回滚逻辑均符合预期,这类常规测试仅执行业务逻辑写入数据库,场景更简单。本次测试与常规测试的唯一差异是调用了内部包含独立事务逻辑的存储过程。
运行环境信息:
- 数据库:SQL Server
- 技术栈:Spring JDBC + Kotlin
根因分析
该问题和注解配置无关,核心是SQL Server的事务机制与Spring事务管理的交互冲突:
SQL Server本身不支持真正的嵌套事务,仅通过@@TRANCOUNT计数模拟事务嵌套:BEGIN TRANSACTION会将计数+1,COMMIT会将计数-1,只有计数归0时才会真正提交所有变更;而ROLLBACK会直接将计数置0,回滚所有层级的变更。
- 测试启动时Spring为
@Transactional标注的测试类开启外层事务,将当前绑定连接的@@TRANCOUNT置为1,关闭自动提交 JdbcAggregateTemplate插入前置数据的操作在该事务上下文中执行,数据未持久化- 调用包含独立事务逻辑的存储过程时,存储过程内部的
COMMIT操作会将@@TRANCOUNT减为0,直接提交之前插入的前置数据,此时连接上已无Spring管控的活跃事务 - 存储过程自身写入的数据如果是在内部事务提交后执行,会被后续Spring感知到连接无活跃事务时的回滚操作覆盖,因此表现为可正常回滚
解决方案
方案1:调整存储过程事务逻辑(优先推荐)
移除存储过程内部的显式COMMIT/ROLLBACK事务控制语句,让存储过程完全复用外层Spring开启的事务,所有事务的提交、回滚动作统一交给Spring事务管理器管控。
如果存储过程需要被非Spring托管的场景复用,可以新增一个布尔类型入参(比如@p_outer_trans_managed bit = 0),当参数值为1时跳过存储过程内部的事务逻辑,仅在测试、上层应用已管控事务的场景传入该标记即可。
方案2:显式绑定统一事务管理器
部分场景下Spring Data JDBC的JdbcAggregateTemplate可能与自定义JDBC Repository使用不同的事务管理器实例,导致两个组件的数据库操作不在同一个本地事务中。可以在@Transactional注解上显式指定Spring Boot自动配置的JDBC事务管理器,确保所有操作使用同一个连接、同一个事务上下文:
@SpringBootTest @Transactional("transactionManager") // 显式指定JDBC事务管理器 @Rollback @TestInstance(TestInstance.Lifecycle.PER_CLASS)
方案3:兜底清理方案
如果短时间内无法修改存储过程逻辑或调整事务配置,可以在测试类中添加@AfterEach生命周期方法,在每个测试方法执行完成后手动删除插入的测试前置数据,避免测试数据污染数据库。
内容的提问来源于stack exchange,提问作者Justin Grahn

