Spring3+Hibernate6迁移时H2数据库主键约束冲突问题排查
问题背景
我们正从Spring 2.x.x(搭配Hibernate 5)迁移到Spring 3.x.x(搭配Hibernate 6),使用H2数据库做测试时遇到唯一键约束冲突问题,此前Spring2环境下运行正常。
相关代码与配置
实体类
@Getter @Setter @NoArgsConstructor @Entity @ToString @Table(name = "category_config") public class CategoryConfig extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @NotNull(groups = Existing.class) @Null(groups = New.class) private Long categoryConfigId; }
服务类
public CategoryConfig addCategory(CategoryConfig categoryConfig,String tenantId) { log.info(" info categoryConfig {} ", categoryConfig); if (categoryConfig == null||categoryConfig.getDeptNbr()==null) { throw new AllocationRuntimeException(ErrorCodes.CATEGORY_NUMBER_MANDATORY.getError()); } categoryConfig.setTenantId(tenantId); categoryConfig.setAllocationStatus(ItemSetupAllocationStatus.PROGRESS.getValue()); categoryConfig.setItemType(ItemType.INSEASON.getValue()); try { return categoryConfigRepository.save(categoryConfig); } catch (Exception e) { throw new AllocationRuntimeException(e, ErrorCodes.CATEGORY_NUMBER_ALREADY_CONFIGURED.getError(categoryConfig.getDeptNbr())); } }
H2配置
spring: cloud: gcp: core: enabled: false bigquery: datasetName: test project-id: test enabled: false azure: storage: blob: storage-url: https://test.blob.core.windows.net datasource: driver-class-name: org.h2.Driver url: jdbc:h2:mem:testdb;DB_CLOSE_ON_EXIT=FALSE username: password: jpa: defer-datasource-initialization: true database-platform: org.hibernate.dialect.H2Dialect show-sql: true properties: hibernate: format_sql: true ddl-auto: update
测试数据(H2初始化脚本)
INSERT INTO category_config(category_config_id, dept_nbr, dept_desc, created_by, created_on, last_updated_by, last_updated_on, sum_need_qty_cdf, sum_need_qty_legacy, allocated_items, wk_supply, wk_supply_legacy, allocation_description, inherit_global_decile, isVLTRequired, tenant_id, allocation_status, item_type, current_doh) VALUES(1, 202, 'edit cat test', 's0k06wm', '2022-08-26', 's0k06wm', '2022-08-26', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'sams_us', 'Allocation available', 'inseason,preseason',7) INSERT INTO category_config(category_config_id, dept_nbr, dept_desc, created_by, created_on, last_updated_by, last_updated_on, sum_need_qty_cdf, sum_need_qty_legacy, allocated_items, wk_supply, wk_supply_legacy, allocation_description, inherit_global_decile, isVLTRequired, tenant_id, allocation_status, item_type, current_doh) VALUES(2, 12, 'cat test 12', 's0k06wm', '2022-08-26', 's0k06wm', '2022-08-26', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'sams_mx', 'Allocation available', 'inseason',8) INSERT INTO category_config(category_config_id, dept_nbr, dept_desc, created_by, created_on, last_updated_by, last_updated_on, sum_need_qty_cdf, sum_need_qty_legacy, allocated_items, wk_supply, wk_supply_legacy, allocation_description, inherit_global_decile, isVLTRequired, tenant_id, allocation_status, item_type, current_doh) VALUES(3, 14, 'cat test 14', 's0k06wm', '2022-08-26', 's0k06wm', '2022-08-26', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'sams_mx', 'Allocation available', 'inseason,preseason',8) INSERT INTO category_config(category_config_id, dept_nbr, dept_desc, created_by, created_on, last_updated_by, last_updated_on, sum_need_qty_cdf, sum_need_qty_legacy, allocated_items, wk_supply, wk_supply_legacy, allocation_description, inherit_global_decile, isVLTRequired, tenant_id, allocation_status, item_type, current_doh) VALUES(4, 7, 'JUGUETES', 's0k06wm', '2022-08-26', 's0k06wm', '2022-08-26', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'sams_mx', 'Allocation available', 'inseason,preseason',8) INSERT INTO category_config(category_config_id, dept_nbr, dept_desc, created_by, created_on, last_updated_by, last_updated_on, sum_need_qty_cdf, sum_need_qty_legacy, allocated_items, wk_supply, wk_supply_legacy, allocation_description, inherit_global_decile, isVLTRequired, tenant_id, allocation_status, item_type, current_doh) VALUES(5, 1990, 'edit cat test', 's0r0e4g', '2023-05-30', 's0r0e4g', '2023-05-30', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'sams_us', 'Allocation available', 'inseason',8) INSERT INTO category_config(category_config_id, dept_nbr, dept_desc, created_by, created_on, last_updated_by, last_updated_on, sum_need_qty_cdf, sum_need_qty_legacy, allocated_items, wk_supply, wk_supply_legacy, allocation_description, inherit_global_decile, isVLTRequired, tenant_id, allocation_status, item_type, current_doh) VALUES(6, 5000, 'test cat', 's0k06wm', '2022-08-26', 's0k06wm', '2022-08-26', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'sams_mx', 'Allocation available', 'inseason,preseason',8)
报错信息
could not execute statement [Unique index or primary key violation: "PRIMARY KEY ON PUBLIC.CATEGORY_CONFIG(CATEGORY_CONFIG_ID) ( /* key:1 */ NULL, 7, NULL, 202, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, CAST(1 AS BIGINT), TIMESTAMP '2022-08-26 00:00:00', TIMESTAMP '2022-08-26 00:00:00', NULL, 'Allocation available', 's0k06wm', 'edit cat test', 'inseason,preseason', 's0k06wm', 'sams_us')"; SQL statement: insert into category_config (allocated_items,allocation_description,allocation_status,created_by,created_on,current_doh,current_stock_pct,dept_desc,dept_nbr,inherit_global_decile,isvltrequired,item_type,last_updated_by,last_updated_on,margin_onhand,profit,sales_amt,sales_qty,sum_need_qty_cdf,sum_need_qty_legacy,tenant_id,total_oh_qty,wk_supply,wk_supply_legacy,category_config_id) values (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,default) [23505-224]] [insert into category_config (allocated_items,allocation_description,allocation_status,created_by,created_on,current_doh,current_stock_pct,dept_desc,dept_nbr,inherit_global_decile,isvltrequired,item_type,last_updated_by,last_updated_on,margin_onhand,profit,sales_amt,sales_qty,sum_need_qty_cdf,sum_need_qty_legacy,tenant_id,total_oh_qty,wk_supply,wk_supply_legacy,category_config_id) values (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,default)]; SQL [insert into category_config (allocated_items,allocation_description,allocation_status,created_by,created_on,current_doh,current_stock_pct,dept_desc,dept_nbr,inherit_global_decile,isvltrequired,item_type,last_updated_by,last_updated_on,margin_onhand,profit,sales_amt,sales_qty,sum_need_qty_cdf,sum_need_qty_legacy,tenant_id,total_oh_qty,wk_supply,wk_supply_legacy,category_config_id) values (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,default)]; constraint [PRIMARY KEY]
疑问
- Hibernate 6中
GenerationType.IDENTITY是否存在兼容问题? - 调用save方法时传入的
categoryConfigId为null,且未在H2中显式创建表(之前版本无需此操作),是否与此问题相关?
问题分析与解决方案
核心原因
Hibernate 6对GenerationType.IDENTITY的处理逻辑有变化:
- Hibernate 5中,使用IDENTITY策略时,会让数据库自动生成主键值,插入语句不会显式包含主键字段;
- Hibernate 6则会在插入语句中把主键字段设为
default(如报错SQL里的category_config_id值为default),但H2数据库中,手动插入带主键的数据后,IDENTITY序列的起始值并未同步更新,导致新插入时数据库尝试生成的主键值(比如从1开始)和已存在的主键冲突。
另外,ddl-auto: update在Hibernate6中的行为更严格,虽然会自动创建表,但不会自动调整IDENTITY序列的起始值,这和Hibernate5的处理不同。
解决步骤
同步IDENTITY序列起始值
在初始化数据的SQL脚本末尾添加语句,手动设置H2表的IDENTITY序列起始值为现有最大主键+1:ALTER TABLE category_config ALTER COLUMN category_config_id RESTART WITH 7;(现有数据的最大
category_config_id是6,所以从7开始)调整Hibernate配置(可选)
如果不想手动维护序列起始值,可以添加Hibernate配置强制使用数据库的IDENTITY生成逻辑:spring: jpa: properties: hibernate: id: new_generator_mappings: false该配置会让Hibernate 6退回到类似Hibernate5的IDENTITY处理方式,避免显式在插入语句中包含主键字段。
验证表结构生成
可以通过H2控制台检查表结构,确认category_config_id字段确实被标记为IDENTITY(自增)。如果结构异常,可临时将ddl-auto改为create-drop测试,再改回update。
总结
这个问题本质是Hibernate6对IDENTITY策略的实现变化,加上H2数据库的自增序列未被初始化脚本同步更新导致的冲突。通过同步序列起始值或调整Hibernate配置即可解决。
内容的提问来源于stack exchange,提问作者PAMPA ROY

