Spring测试Propagation.NEVER方法时如何回滚MariaDB变更
问题场景
现有如下Spring服务实现:
@Service public class MyService { @Transactional(propagation = Propagation.NEVER) public void doStuff(UUID id) { // 例如调用外部HTTP接口,可能耗时较长 // 内部通过TransactionTemplate等方式操作数据库更新 } }
方法上@Transactional(propagation = Propagation.NEVER)的作用是:调用该方法时不允许存在活跃事务,避免等待外部服务响应时长时间占用数据库连接。
对该方法做测试时存在矛盾:
- 若测试类上加
@Transactional注解实现测试后自动回滚,会因为被测试方法的NEVER传播配置直接抛出异常,测试代码示例如下:
@SpringBootTest @Transactional public class MyServiceTest { @Autowired private MyService myService; public void testDoStuff() { putMyTestDataInDb(); myService.doStuff(); // 此处抛出异常:不允许存在活跃事务 assertThat(myData).isTheWayIExpectedItToBe(); } }
- 若移除测试类上的
@Transactional注解,测试产生的数据会残留在数据库中,无法保证后续测试的数据库状态一致性。
目前临时方案是在JUnit的@AfterEach回调中截断清空所有数据库表,但表数量较多时执行速度慢,维护成本高。
核心诉求
在不使用表截断操作、不依赖测试类上的@Transactional注解的前提下,实现测试执行后自动回滚数据库变更。
额外要求:
- 当前测试用Testcontainers启动MariaDB实例,仅适配MariaDB/MySQL的方案即可,数据库无关的通用方案更佳
- 方案需要兼容另一个测试场景:验证生产代码的事务边界配置是否正确,避免因生产代码漏加
@Transactional导致运行时懒加载异常
当前技术栈:
- 持久层:JPA + Hibernate
- 数据库初始化:应用启动时通过Liquibase执行DDL脚本
已尝试不可行的方案
@DirtiesContext:重建Spring应用上下文的开销远高于清空全表,执行速度过慢- MariaDB SAVEPOINT:仅支持回滚单个事务内部的状态,无法实现跨事务的全局回滚
- 手动操作数据源连接:测试前直接从数据源获取连接执行
START TRANSACTION、测试后执行ROLLBACK,侵入性极强,且因为业务代码会从连接池获取独立连接,事务不共享,回滚无法生效
可行解决方案
方案1:基于MariaDB 10.5+ 数据库克隆特性(适配当前场景,性能最优)
这个方案完全符合要求,不需要截断表,不需要测试类加事务,性能极高。
实现逻辑:
- 调整Testcontainers配置,等容器启动、Spring上下文初始化完成(Liquibase跑完所有建表脚本、加载完基础初始化数据)后,创建一个基准库的快照:
这个克隆是MariaDB内核级的文件拷贝,几十毫秒就能完成,耗时和库表数量无关。CREATE DATABASE baseline_db CLONE 你的业务库名; - 写一个JUnit 5扩展,在每个测试方法执行前:
- 先删除已经存在的业务库
- 从基准库重新克隆出干净的业务库
DROP DATABASE IF EXISTS 你的业务库名; CREATE DATABASE 你的业务库名 CLONE baseline_db; - 测试执行过程中所有的写入都在全新克隆的业务库上执行,不需要做任何回滚操作,下一个测试执行前会重新从基准库克隆,完全不会有数据残留。
优点:
- 性能极强,单测试方法的环境重置耗时在10ms级别,远快于全表截断、重建上下文
- 完全不侵入业务代码,不需要修改测试的事务配置,可以正常验证生产代码的事务传播行为
- 不需要手动维护表列表,表结构变更后不需要调整重置逻辑
注意点:确保Testcontainers用的MariaDB镜像版本 >= 10.5即可,不需要额外引入其他依赖。
方案2:基于连接代理的跨事务回滚(数据库无关通用方案)
这个方案不依赖数据库特定语法,核心逻辑是通过代理数据源,记录测试执行过程中所有的写操作,测试结束后自动生成反向回滚语句执行。
实现逻辑:
- 测试配置中引入轻量JDBC代理,拦截所有非查询类的SQL语句(INSERT/UPDATE/DELETE)
- 测试执行前创建回滚日志上下文,所有被拦截的写操作,自动生成对应的反向补偿SQL:
- INSERT语句对应生成DELETE语句,根据主键定位插入的行
- UPDATE语句执行前先查询该行的原始值,生成反向UPDATE语句把字段改回原始值
- DELETE语句执行前先查询被删除的行完整数据,生成对应的INSERT语句
- 测试结束后,按照SQL执行的反向顺序执行所有补偿SQL,就能把数据库恢复到测试前的状态。
优点:
- 不依赖数据库特定语法,所有支持JDBC的数据库都能用
- 不需要修改测试的事务配置,可以正常验证事务边界
缺点: - 代理逻辑有一定侵入性,批量写操作时生成补偿SQL会有少量性能开销
- 对于复杂SQL(比如批量更新、多表关联更新)的反向SQL生成需要做额外适配
方案3:Testcontainers 文件系统快照(通用容器方案)
如果使用的MariaDB版本低于10.5不支持克隆特性,可以用Testcontainers自带的快照能力:
- 等容器启动、Liquibase初始化完干净的数据库后,调用Testcontainers的容器快照方法,把当前容器的文件系统状态打一个快照
- 每个测试方法执行前,直接恢复容器到初始化完成的干净状态
这个恢复是容器层的文件系统增量回滚,速度远快于重建上下文或者全表截断,缺点是恢复时会重置整个容器的状态,比数据库克隆的开销稍高,但也在可接受范围内。
内容的提问来源于stack exchange,提问作者Sébastien Nussbaumer
相关产品推荐
相关产品推荐

