Spring Boot Test中H2数据库主键约束冲突问题排查求助
H2内存数据库插入数据触发主键约束冲突排查
生产环境使用PostgreSQL时,数据库脚本、实体类及Repository均正常运行。搭建Spring Boot测试环境采用H2内存数据库,配置与代码如下:
相关配置与代码
schema-h2.sql(src/test/resources)
CREATE TABLE IF NOT EXISTS project.general_types( id SMALLINT PRIMARY KEY NOT NULL, name VARCHAR(15) NOT NULL);
data.sql(src/test/resources)
INSERT INTO project.general_types(id, name) VALUES (0, 'Text'), (1, 'Binary');
application-test.yml
spring: datasource: url: jdbc:h2:mem:testdb;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;DATABASE_TO_UPPER=false; username: sa password: driver-class-name: org.h2.Driver initialization-mode: always platform: h2 jpa: generate-ddl: false defer-datasource-initialization: true show-sql: true open-in-view: false hibernate: ddl-auto: none properties: hibernate: format_sql: true default_schema: project dialect: org.hibernate.dialect.H2Dialect globally_quoted_identifiers: true
实体类
@Entity(name = "general_types") public class GeneralTypesEntity { @Id private short id; @Basic private String name; // getters/setters omitted... }
测试用例
@SpringBootTest @ActiveProfiles("test") class GeneralTypesDbTest { @Autowired private GeneralTypesRepository generalTypesRepository; @Test void test() { assertNotNull(generalTypesRepository); assertEquals(2, generalTypesRepository.count()); } }
错误现象
仅使用schema-h2.sql时测试正常,但添加data.sql执行插入语句后,出现主键约束冲突:
Caused by: org.h2.jdbc.JdbcSQLIntegrityConstraintViolationException: Unique index or primary key violation: "PRIMARY KEY ON project.general_types(id) [0, 'Text']"; SQL statement: INSERT INTO project.general_types(id, name) VALUES (0, 'Text'), (1, 'Binary') [23505-200] at org.h2.message.DbException.getJdbcSQLException(DbException.java:459) ~[h2-1.4.200.jar:1.4.200] at org.h2.message.DbException.getJdbcSQLException(DbException.java:429) ~[h2-1.4.200.jar:1.4.200] at org.h2.message.DbException.get(DbException.java:205) ~[h2-1.4.200.jar:1.4.200] at org.h2.message.DbException.get(DbException.java:181) ~[h2-1.4.200.jar:1.4.200] at org.h2.mvstore.db.MVPrimaryIndex.add(MVPrimaryIndex.java:127) ~[h2-1.4.200.jar:1.4.200] at org.h2.mvstore.db.MVTable.addRow(MVTable.java:531) ~[h2-1.4.200.jar:1.4.200] at org.h2.command.dml.Insert.insertRows(Insert.java:195) ~[h2-1.4.200.jar:1.4.200] at org.h2.command.dml.Insert.update(Insert.java:151) ~[h2-1.4.200.jar:1.4.200] at org.h2.command.CommandContainer.update(CommandContainer.java:198) ~[h2-1.4.200.jar:1.4.200] at org.h2.command.Command.executeUpdate(Command.java:251) ~[h2-1.4.200.jar:1.4.200] at org.h2.jdbc.JdbcStatement.executeInternal(JdbcStatement.java:228) ~[h2-1.4.200.jar:1.4.200] at org.h2.jdbc.JdbcStatement.execute(JdbcStatement.java:201) ~[h2-1.4.200.jar:1.4.200] at com.zaxxer.hikari.pool.ProxyStatement.execute(ProxyStatement.java:94) ~[HikariCP-4.0.3.jar:na] at com.zaxxer.hikari.pool.HikariProxyStatement.execute(HikariProxyStatement.java) ~[HikariCP-4.0.3.jar:na] at org.springframework.jdbc.datasource.init.ScriptUtils.executeSqlScript(ScriptUtils.java:261) ~[spring-jdbc-5.3.18.jar:5.3.18] ... 96 common frames omitted
将插入语句移至schema-h2.sql中表创建语句后仍报错。
原因分析
核心问题是初始化脚本被重复执行,导致相同主键数据被插入两次:
- 配置中同时使用了
spring.datasource.initialization-mode: always和spring.jpa.defer-datasource-initialization: true,前者触发Spring JDBC执行初始化脚本,后者延迟数据源初始化至Hibernate启动后,可能再次触发脚本执行; - Spring Boot 2.5+版本中,
spring.datasource.initialization-mode已被废弃,改用spring.sql.init.*配置,旧配置与新的延迟初始化特性叠加,容易引发重复执行问题; - 即使将插入语句合并到
schema-h2.sql,只要脚本被重复执行,就会触发主键冲突。
解决方案
方案1:修改插入语句为幂等操作
使用H2支持的MERGE语句替代INSERT,确保即使重复执行也不会报错:
MERGE INTO project.general_types(id, name) KEY(id) VALUES (0, 'Text'), (1, 'Binary');
方案2:调整Spring Boot配置,避免脚本重复执行
将旧的数据源初始化配置替换为新版spring.sql.init配置,并关闭重复触发的可能:
spring: datasource: url: jdbc:h2:mem:testdb;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;DATABASE_TO_UPPER=false; username: sa password: driver-class-name: org.h2.Driver sql: init: mode: always platform: h2 jpa: generate-ddl: false defer-datasource-initialization: true show-sql: true open-in-view: false hibernate: ddl-auto: none properties: hibernate: format_sql: true default_schema: project dialect: org.hibernate.dialect.H2Dialect globally_quoted_identifiers: true
移除spring.datasource.initialization-mode和spring.datasource.platform,改用spring.sql.init.*配置,确保脚本仅执行一次。
方案3:禁用延迟数据源初始化(如果不需要)
如果不需要延迟数据源初始化到Hibernate之后,可以将spring.jpa.defer-datasource-initialization设为false,避免脚本被Hibernate再次触发:
spring: jpa: defer-datasource-initialization: false
内容的提问来源于stack exchange,提问作者Stephen Paulin
相关产品推荐
相关产品推荐

