拓冰建站拓冰建站
首页 / 资讯中心 / 正文

@MybatisPlusTest自动插入报错排查:数据源、事务与SQL初始化

写Mapper层单测的时候我一开始真的是被MybatisPlusTest这个注解坑得够呛。明明业务代码在Spring Boot启动后跑得好好的数据也能正常插入换成MybatisPlusTest一跑单元测试自动插入就报错而且错误乱七八糟有Failed to configure a DataSource的有Table doesnt exist的还有BAD SQL GRAMMAR的。更烦的是这些报错往往不是直接出现在你写的那条insert语句上而是出现在Spring容器启动阶段、MyBatis的缓存清理阶段甚至出现在某个PostConstruct初始化逻辑里堆栈绕来绕去定位一张表的数据插入就能折腾一下午。这篇内容就把MybatisPlusTest自动插入报错这件事彻底拆开。我会先讲这个注解的加载机制和隔离边界再说一次完整的排查链路然后逐个列出我实测遇过的高频根因和修复方案最后把TestNG、PostConstruct、事务回滚这三个特别容易和它纠缠在一起的坑也一并说清楚。适合正在用Spring Boot MyBatis-Plus写单元测试、被MybatisPlusTest折腾过的同学这段内容能让你少走不少弯路。1. 两种典型报错现场H2内存库与MySQL双环境对比MybatisPlusTest自动插入报错绝大多数场景分两类。一类是测试环境用H2内存数据库另一类是测试环境直接连真实的MySQL。两种环境的报错特征不一样排查思路也完全不一样放在一起对比看会非常直观。1.1 H2环境下建表脚本没执行却已经调用了MapperH2内存库的特点是启动快、依赖轻适合跑纯逻辑层的单测。但H2不会自动创建表结构必须靠schema.sql或data.sql初始化。很多项目在application-test.yml里配置了spring: datasource: url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY-1;MODEMySQL driver-class-name: org.h2.Driver username: sa password: sql: init: schema-locations: classpath:db/schema-h2.sql >Caused by: org.h2.jdbc.JdbcSQLSyntaxErrorException: Table USER not found如果你查看的表名是大写、小写、驼峰H2在MySQL模式下对大小写敏感还有一套自己的规则这又会叠加一层混乱。User实体映射的user表在H2里可能就被解析成了USER然后提示找不到。很多人在这个环节就开始怀疑实体类注解配错了其实根本不是。1.2 MySQL环境下数据源绕过了测试配置连上了生产库第二类更危险。有些项目图省事测试类里没单独指定数据源MybatisPlusTest直接使用了application.yml里的MySQL连接。虽然能连上也能插入但至少有三个隐患测试数据写进真实表跑完不清理污染开发库如果表的字段有唯一约束第二次跑同样的测试用例直接报Duplicate entry更隐蔽的是如果连的是生产库风险就不是报错这么简单了。我见过一个案例是测试类里用了MybatisPlusTest但又继承了某个抽象基类基类上标了SpringBootTest。测试注解被继承后加载的是完整应用上下文而不是切片上下文。数据源自然就走到生产配置了。最后解决办法是删掉继承关系或者把公共配置提取到独立工具类里而不是通过测试类继承。这两种环境在报错信息上有个明显区别H2环境下多是JdbcSQLSyntaxErrorException、表不存在、语法不兼容真实MySQL环境下多是主键冲突、唯一约束冲突、或者连接串相关异常。先确认当前测试走的是哪个数据源再开始排错能省掉一大半无效排查。2. MybatisPlusTest加载链路拆解为什么自动插入会翻车要彻底搞清楚自动插入为什么报错必须先弄明白MybatisPlusTest启动时到底加载了什么、跳过了什么。2.1 这个注解到底加载了哪些BeanMybatisPlusTest本身是一个组合注解核心元注解包含下面这些ExtendWith(SpringExtension.class)JUnit 5场景下BootstrapWith(SpringBootTestContextBootstrapper.class)AutoConfigureCache、AutoConfigureMybatisPlus、AutoConfigureTestDatabase等标记Transactional默认开启事务回滚它本质上是一个SpringBootTest的切片变体只加载MyBatis-Plus相关的自动配置类包括MybatisPlusAutoConfiguration、MybatisPlusLanguageDriverAutoConfiguration等。也就是说容器里只有数据源、SqlSessionFactory、Mapper接口代理、TransactionManager这一层的东西。Service、Controller、Component这些业务Bean默认不会被扫描更不会注册进容器。这设计本来很好测试隔离性高、启动快。但代价是一旦你的业务代码里出现了间接依赖比如PostConstruct里调用了Mapper或者某个Bean在初始化阶段就要操作数据表切片的上下文根本hold不住。2.2 事务自动回滚的实现边界MybatisPlusTest默认自带事务测试方法跑完数据自动回滚。这个“自动回滚”是依赖TestTransaction和事务管理器实现的。但注意它只对通过PlatformTransactionManager管理的事务有效。MyBatis-Plus底层拿到的是SqlSession自动提交开关默认关闭事务靠Spring的DataSourceTransactionManager来提交/回滚。只要测试方法里没有手动Commit也没有嵌套新事务数据就不会落库。但在实际项目里我踩过几个回滚不生效的坑Mapper接口的方法上贴了Transactional(propagation Propagation.REQUIRES_NEW)一旦REQUIRES_NEW触发内部事务就脱离了外部测试事务的管理会独立提交测试结束后的回滚根本管不到它。代码里有直接调SqlSessionTemplate.getConnection().setAutoCommit(true)这种底层操作自动提交一开事务管理器后续的rollback也就失效了。数据源配置了HikariCP的auto-commit初始值时如果设为true同样会影响。所以“自动插入报错”表面上是SQL语法或数据源问题底层却可能是事务边界没搞清楚。2.3 缓存清理时的“二次插入”现象还有一个很容易误导人的报错模式堆栈出现在flushing cache and retry附近。这句话看着像是插入之后清一级缓存时出的问题实际上是因为MyBatis执行insert时会先进入一级缓存判断然后在真正执行SQL之前如果发现statement发生了变化或者批量操作没有结束就会触发flush。如果前面有一条SQL已经写入了数据但事务没有正确声明后面再插入时主键冲突此时清缓存重试反而掩盖了真正出错的那一步。我在一个项目里遇到过这样的报错### Error flushing statements. Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Duplicate entry 12345 for key PRIMARY第一反应是主键重复。但查了一圈才发现同一个测试方法里有两个Mapper方法第一个方法通过PostConstruct已经提前执行过一次插入到了测试方法执行阶段又插入同一条id的数据于是主键冲突。而这个PostConstruct根本不在MybatisPlusTest的设计预期里它导致数据自动插入发生在容器初始化阶段直接把这个锅甩给了“自动插入报错”这个现象。3. 一次完整排查记录从堆栈第一行到根因落锤光说理论不够这里记录一次我印象特别深的排查全过程。现象是项目从H2切换到真实MySQL后MybatisPlusTest里的insert直接报错堆栈如下java.lang.IllegalStateException: Failed to load ApplicationContext at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext at org.springframework.test.context.junit.jupiter.SpringExtension.postProcessTestInstance Caused by: java.lang.IllegalArgumentException: Property sqlSessionFactory or sqlSessionTemplate are required at org.mybatis.spring.SqlSessionTemplate.init Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name sqlSessionFactory Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name dataSource Caused by: org.springframework.boot.autoconfigure.jdbc.DataSourceProperties$DataSourceBeanCreationException: Failed to determine a suitable driver class3.1 第一步先判断是不是数据源配置没生效Failed to determine a suitable driver class是无数据源配置的典型报错。当时测试类长这样MybatisPlusTest class UserMapperTest { Autowired private UserMapper userMapper; }而测试资源目录下只有application-test.yml类上却没有激活testprofile。MybatisPlusTest不会自动加载application-test.yml它只会加载默认的application.yml如果默认配置里没有数据源那就直接完蛋。解决办法是在测试类上加ActiveProfiles(test)。顺便也可以加上AutoConfigureTestDatabase(replace Replace.NONE)避免测试框架尝试把数据源替换成内嵌数据库。3.2 第二步看SQL打印是根本没执行还是执行了报错数据源修好后又出现新的现象test方法内insert没报错但事务提交时报错堆栈里能看到MySQL的Table testdb.user doesnt exist。这时候光看堆栈已经不够了得看SQL执行情况。在application-test.yml里打开SQL日志logging: level: com.example.project.mapper: debug或者mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl开了日志之后发现insert SQL确实打印出来了表名和字段名都对但MySQL里根本没有这张表。这就不是SQL问题了而是schema没初始化。项目为了兼容H2和MySQL建表语句分了两套H2用的是db/schema-h2.sqlMySQL用的是db/schema-mysql.sql。测试环境切换后spring.sql.init.schema-locations还指向H2的脚本MySQL环境下执行那么一套VARCHAR、AUTO_INCREMENT的写法很容易不兼容。3.3 第三步用Sql注解主动建表确认根因后最终采用的方式是在测试类直接指定初始化脚本MybatisPlusTest AutoConfigureTestDatabase(replace AutoConfigureTestDatabase.Replace.NONE) ActiveProfiles(test) Sql(scripts /db/schema-mysql.sql, executionPhase Sql.ExecutionPhase.BEFORE_TEST_METHOD) class UserMapperTest { Autowired private UserMapper userMapper; }这里要特别注意Sql默认在测试方法之前执行如果你有多个测试方法每个方法前都会跑一遍DROP TABLE IF EXISTS CREATE TABLE确保数据干净。但代价是增加了测试耗时。3.4 第四步从“报错位置”反推真正出错的Mapper有时候堆栈指向的类和真正出错的类不是同一个。比如报错堆栈在UserMapper.insert但实际是RoleMapper在插入时先执行了某个schema校验。MyBatis-Plus在解析实体类时会根据TableName注解映射表但这个映射和实际数据库表结构不一致时insert生成的SQL里就会出现多余的字段。我那次最终定位到的真正原因是实体类里新增了一个数据库里还没有的字段MyBatis-Plus默认按全字段插入INSERT INTO user (id, name, email, new_column) VALUES (...)而MySQL表里没new_column这一列数据库直接报Unknown column。解决方式有几种在实体类新字段上加TableField(exist false)告诉MyBatis-Plus这不是表字段用TableField(insertStrategy FieldStrategy.NEVER)只影响插入策略或者同步数据库表结构。这个排查路径看起来简单但当时绕了很久就是因为被堆栈顶部的flushing cache带偏了方向。所以给个建议看到自动插入报错先打开SQL日志确认“实际执行了什么SQL”再确认“SQL执行在哪个环节失败”最后才动手改代码。4. 六个高频根因逐一拆解与修复配置下面是这段时间里实测遇到过的报错根因汇总按出现频率排序每一条都有对应的修复方式。虽然不能覆盖所有项目但基本能把MybatisPlusTest自动插入报错的坑踩个七八成。4.1 根因一字段策略和主键策略不匹配MyBatis-Plus的ID生成策略有三种常见配置AUTO数据库自增、ASSIGN_ID雪花算法、INPUT手动输入。在MybatisPlusTest场景下经常出现的问题是实体类配置AUTO但测试库表结构里主键不是自增列导致insert报错实体类配置ASSIGN_ID但实体主键字段类型是Long雪花算法生成的ID超出某些MySQL表中int列的范围直接报Data truncation: Out of range value批量插入时ASSIGN_ID和AUTO混用导致部分行主键为空。我当时遇到的一个案例是团队内部约定统一用ASSIGN_ID但表结构从另一个系统迁移过来时主键还是int。插入单条看不出问题一旦批量插入SnowflakeIdWorker生成的ID不稳定地超出范围。修复方式一般是在测试的数据准备阶段指定一个固定ID或者在实体类主键上用TableId(type IdType.AUTO)。4.2 根因二批量插入方法不是MyBatis-Plus内置方法MyBatis-Plus的BaseMapper默认没有insertBatchSomeColumn这是MybatisPlusMethod扩展能力的一部分。如果你项目里有人引用了类似userMapper.insertBatchSomeColumn(userList);但测试切片里没有配置对应的SQL注入器运行时就报Invalid bound statement (not found): com.xxx.UserMapper.insertBatchSomeColumn或者提示BindingException。此时需要在测试环境里通过Import把自定义SQL注入器配置进来MybatisPlusTest Import(MybatisPlusConfig.class) class UserMapperTest { ... }这里MybatisPlusConfig里定义MybatisPlusSqlInjector这个Bean里面注册InsertBatchSomeColumn方法。不配置的话测试里一调用批量插入就挂而且报错信息很容易让人误以为SQL写错了。4.3 根因三TableField映射的列名和数据库关键字冲突这个坑很经典。实体类字段叫order、desc、group这类SQL关键字时生成的insert SQL就是INSERT INTO user (id, order) VALUES (?, ?)MySQL直接报语法错误。H2在MySQL模式下对关键字的容忍度略有差异所以经常出现“H2环境下测试通过切到MySQL就报错”的情况。修复方式有两种在实体类字段上显式指定别名TableField(order) private String order;或者在MyBatis-Plus配置里开启自动加反引号mybatis-plus: global-config: db-config: column-format: %s后者影响全局谨慎使用。4.4 根因四MapperScan配置没有覆盖测试切片MybatisPlusTest虽然会加载MyBatis-Plus自动配置但它不会主动扫描所有Mapper接口。它只会注册测试类里Autowired标注的Mapper如果有多个Mapper之间相互依赖或者MyBatis的一级缓存和延迟加载机制需要额外的Mapper就可能在解析时出现Bean没找到。最简单的修复方式是在测试类上显式指定扫描包MybatisPlusTest MapperScan(com.example.project.mapper) class UserMapperTest { ... }或者用MybatisPlusTest(scanBasePackages com.example.project.mapper)。如果项目Mapper分布在多个包用MapperScan逐个列出即可。4.5 根因五测试环境和生产环境用了同一套实体类配置这个属于设计层面的问题。有些项目的实体类上直接写了数据库相关的TableName、TableField如果没有通过抽象层隔离测试环境一旦连了别的库这些映射关系可能和H2自动建表逻辑完全不匹配。推荐的做法是在test目录下准备一套独立的实体类或单独的应用配置。如果复用同一套实体类至少保证数据库表结构和H2的schema脚本完全一致。实体类里尽量少用数据库特有的类型比如json、enum等否则在H2下类型映射会很难看。4.6 根因六SQL初始化脚本执行时机太晚除了Sql注解spring.sql.init.mode这个配置也经常坑人。Spring Boot 2.5之后SQL初始化默认只对嵌入式数据库生效如果你用的是真实MySQL需要显式设置spring: sql: init: mode: always否则MySQL环境下根本不会执行建表脚本。但要注意mode: always意味着每次应用上下文启动都会执行脚本脚本里如果没有DROP TABLE IF EXISTS第二次启动建表就会报Table already exists。综合来看遇到MybatisPlusTest自动插入报错不要急着改业务代码先在测试环境里把下面三件事确认清楚测试数据源到底是H2还是MySQL建表脚本是否真的执行了执行的是哪一份脚本SQL日志是否打印出了真正的插入语句。这三个点确定了问题基本就能定位到根因。5. 和TestNG、PostConstruct、事务注解纠缠的那些坑热搜词里出现一批跟单元测试相关的关键词其中testng、PostConstruct、事务注解这三个是我在实践中确实踩过的位置非常特殊单独拉出来说。5.1 TestNG和MybatisPlusTest的兼容问题如果项目里已经集成了TestNG但测试类还在用MybatisPlusTest多半会出问题。原因很直接MybatisPlusTest默认的扩展机制是基于JUnit 5的ExtendWith(SpringExtension.class)设计的而TestNG有自己的测试生命周期管理方式和Spring的测试上下文框架不是一套东西。具体表现是Spring容器能正常启动Mapper能注入成功但测试方法里的自动插入要么不执行要么执行后事务不生效更常见的现象是报IllegalStateException: Could not find SpringExtension。可行的做法是如果团队用TestNG那就不要用MybatisPlusTest改用SpringBootTest 手动配置Mapper扫描的方式或者直接用SpringJUnit4ClassRunner这种兼容运行器如果坚持用MybatisPlusTest那就把TestNG从测试依赖里隔离出去给MyBatis-Plus相关测试单独用JUnit 5。这里没有特别优雅的二合一方案。Spring官方测试框架的重心在JUnit和TestNG两者之间有取舍MybatisPlusTest显然选了JUnit 5。5.2 PostConstruct里执行insert为什么报错我遇到过一个很奇怪的现象测试类里什么都没做只是Autowired了一个Mapper但一启动测试报错堆栈指向一个PostConstruct方法里面调用了另一个Mapper的insert。原因在于MybatisPlusTest加载的Spring容器虽然没有业务Bean如果你的测试类或者导入的配置类中存在PostConstruct初始化方法并且这个方法调用了Mapper那么它会在容器启动阶段执行一次插入。此时如果事务管理器的初始化还没完成或者DataSource代理还没完全就绪自动插入就会出问题。更常见的场景是配置类里的PostConstructTestConfiguration public class TestDataInitializer { Autowired private UserMapper userMapper; PostConstruct public void initData() { userMapper.insert(new User(...)); } }这种写法的本意是提前准备测试数据但它忽略了MybatisPlusTest的阶段划分。数据准备应该在测试方法内部完成或者用BeforeEach、Sql、TestDataPrepare这类明确命名的机制。PostConstruct在容器初始化阶段执行的数据库插入极容易在事务边界之外操作数据测试结束后还可能不清理造成下一轮测试数据残留。5.3 事务注解在Mapper方法上的限制MybatisPlusTest会为测试方法套上一层事务但这层事务并不影响Mapper方法上的事务注解。如果Mapper方法上写了Transactional(propagation Propagation.REQUIRES_NEW) int insertUser(User user);那么该方法的插入操作会挂起外层事务开启一个新事务。测试方法结束后外层事务回滚但已经提交的新事务不受影响数据直接落库。等下轮测试再跑同样的数据又插入一次如果数据库有唯一索引必然报Duplicate entry或者主键冲突。排查技巧如果发现测试里执行insert没问题但第二次运行测试就报错优先怀疑Mapper方法上的独立事务注解。解决办法一般是测试用的Mapper接口单独拆一个出来不标注事务相关注解或者测试类统一使用Transactional并调整隔离级别或者在BeforeEach里执行DELETE FROM user清理数据。6. 让Mapper层测试少踩坑的实操习惯最后这部分算不上什么高深理论全是我自己试过之后总结出来的操作习惯照着做能省不少排查时间。6.1 测试类保持最小依赖不继承业务基类有段时间同事为了复用公共配置在测试类上继承了一个业务BaseService结果MybatisPlusTest的切片配置被SpringBootTest元注解覆盖导致整个应用上下文全部加载整个单元测试跑了将近一分钟。这已经完全失去了MybatisPlusTest“只测Mapper”的意义。所以测试类的继承结构要尽量扁平公共配置写在src/test/java下的基础测试类里只放数据源、SQL初始化、事务回滚这些测试专用配置不要混入任何业务逻辑。6.2 数据准备和断言分开MybatisPlusTest的定位是Mapper层测试不是业务层测试。数据准备最好用两种方式之一固定SQL脚本用Sql注解挂在测试方法上测试方法内先delete再insert最后select断言。我个人的习惯是准备数据时不用被测Mapper自己的insert方法因为这样万一insert方法本身有bug用例根本测不出来。更稳妥的做法是单独写一个测试数据专用Mapper或在schema.sql里直接预置数据让被测方法只负责自己的逻辑判断。6.3 给测试数据源和业务数据源彻底分开如果条件允许每个开发者本地都在Docker里跑一个MySQL测试实例连的库名统一叫testdb建表脚本维护在src/test/resources/db下。这样既不依赖H2的兼容性问题也不会有连到真实库的风险。测试完甚至可以直接用DROP TABLE清理反正库里没有真实业务数据。6.4 关注MyBatis-Plus和Spring Boot版本兼容性版本问题虽然不直接算“自动插入报错”的根因但它的影响非常隐蔽。比如MyBatis-Plus 3.4.x和3.5.x在批量插入的SQL注入器注册方式上就有差异从3.5.x版本开始自定义SQL注入器的包路径变了测试切片里如果还按旧包名Import直接启动失败报错却是No suitable driver排查起来非常混淆。建议在项目里统一管理版本至少保证mybatis-plus-boot-starter和Spring Boot的兼容矩阵在官方文档范围内。不要一个升级一个不升级。6.5 测试方法内主动开启SQL日志断言打开MyBatis-Plus的SQL日志然后断言SQL执行结果这一步在排查阶段尤其重要。断言SQL可以用AssertJ或JUnit的assertThrows。比如你预期某个字段超长会导致插入失败那就不要默默吞掉异常而是明确断言它抛出了DataIntegrityViolationException。这样测试失败时才不会只看到一句“插入报错”而是能看到具体原因。MybatisPlusTest自动插入报错的排查说到底就是两个关键词边界和顺序。边界是指测试切片的Bean范围顺序是指SQL初始化、数据准备、Mapper执行、事务提交的先后关系。把这两个维度理清了绝大多数报错都能快速归因。如果你也在某个奇奇怪怪的报错里卡了很久先别改业务代码从测试环境的数据源和SQL日志开始查起大概率问题就出在那两个地方。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门