MyBatis事务回滚时序列未回滚问题求助
这事儿其实不是Spring或MyBatis的锅,而是数据库序列的固有特性导致的——序列的nextval调用是原子且非事务性的,一旦你调用了它,不管后续事务是提交还是回滚,这个序列值都被“消耗”了,数据库不会因为事务回滚而把序列值退回去。所以你的单元测试每次跑,哪怕事务回滚了,序列还是会一直递增。
针对测试场景的解决办法
1. 测试后手动重置序列
既然序列不会自动回滚,那我们就在每次测试结束后手动把序列重置到初始值,保证下一次测试的环境是干净的。
比如你的测试类可以加个@AfterEach方法,用JdbcTemplate执行序列重置的SQL:
@RunWith(SpringRunner.class) @ActiveProfiles(profiles = "local") @SpringBootTest @Slf4j @Transactional public class MultimediaMapperTest { @Autowired private MultimediaMapper multimediaMapper; @Autowired private JdbcTemplate jdbcTemplate; // 你的测试方法... @AfterEach void resetSequence() { // 根据你的数据库类型调整SQL: // Oracle 语法 jdbcTemplate.execute("ALTER SEQUENCE multimedia_seq RESTART WITH 1"); // PostgreSQL 语法 // jdbcTemplate.execute("ALTER SEQUENCE multimedia_seq RESTART WITH 1"); // 如果是MySQL自增主键(非序列) // jdbcTemplate.execute("ALTER TABLE multimedia AUTO_INCREMENT = 1"); } }
2. 改用MyBatis的useGeneratedKeys(如果数据库支持)
如果你的数据库支持JDBC的getGeneratedKeys机制(比如Oracle配合触发器、PostgreSQL的序列自增列),可以替换掉@SelectKey,让MyBatis自动处理主键生成,这样虽然序列还是会递增,但写法更规范,而且某些场景下可能减少手动调用nextval的次数:
比如映射器接口的方法可以改成:
@Insert("INSERT INTO multimedia (name) VALUES (#{name})") @Options(useGeneratedKeys = true, keyProperty = "id") int insert(Multimedia multimedia);
前提是你要在数据库端配置好序列和触发器(比如Oracle),或者把id列设为SERIAL/IDENTITY类型(PostgreSQL/MySQL),让数据库自动为列赋值,MyBatis通过JDBC获取生成的主键。
3. 测试使用嵌入式数据库(可选)
如果你的测试允许用嵌入式数据库(比如H2),可以在测试配置里用H2模拟生产数据库的序列行为,不过H2的序列在事务回滚时会回滚,刚好能满足你的测试需求。但要注意,嵌入式数据库和生产数据库的行为可能有差异,适合简单的单元测试场景。
总结
序列的递增不随事务回滚是数据库的设计决定,目的是保证序列生成的性能和唯一性。在测试场景下,手动重置序列是最直接且贴近生产环境的解决方案,能保证每次测试的独立性。
内容的提问来源于stack exchange,提问作者Jose A. Matarán




