Session.saveOrUpdate导致ID递增2而非1的原因及解决办法
这问题我之前帮好几个开发者排查过,核心原因基本都和主键生成策略、Hibernate内部的ID预分配逻辑有关,咱们一步步拆解:
常见原因
1. 主键生成器的预分配设置问题
如果你的实体用了SEQUENCE类型的主键生成策略,并且默认/显式设置了allocationSize=2,Hibernate会每次从数据库序列中预取2个ID值,导致实际插入时ID间隔增长2。而你之前的自定义方法可能直接用了数据库自增(IDENTITY)或者手动控制ID,所以不会出现这个跳增问题。
另外,老版本Hibernate中使用hilo生成器(Hibernate特定的高低位算法)时,本身就会导致ID按固定间隔增长,这也是常见的坑点。
2. saveOrUpdate的内部校验触发额外ID获取
当调用saveOrUpdate时,Hibernate会先判断实体是瞬时态(新实体)还是游离态(已有实体)。对于无ID的新实体,部分老版本Hibernate会先执行一次ID预获取操作,之后真正插入时又再获取一次,导致两次消耗ID,最终表现为递增2。
3. 数据库序列与表数据不一致
如果之前手动插入过实体ID,或者修改过数据库序列的当前值,导致序列的当前值和表中最大ID不匹配,Hibernate在获取序列值时会跳过部分ID,也可能出现跳增的情况。
修复方案
1. 调整主键生成器的分配大小
如果使用SEQUENCE策略,显式设置allocationSize=1,强制Hibernate每次只获取一个ID:
@Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "your_entity_seq") @SequenceGenerator( name = "your_entity_seq", sequenceName = "YOUR_ENTITY_SEQ", // 对应数据库的序列名 allocationSize = 1 // 关键:设置为1 ) private Long id;
如果是MySQL数据库,更推荐直接用GenerationType.IDENTITY(基于自增主键),避免Hibernate模拟序列带来的额外问题:
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
2. 检查Hibernate核心配置
确保hibernate.id.new_generator_mappings=true(Hibernate 5+默认是开启的),这个参数控制是否使用新的生成器实现,老的生成器逻辑更容易出现ID预分配的问题。如果你的配置文件里设为false,改成true即可。
3. 替换saveOrUpdate为更明确的方法
如果能提前区分实体是新的还是已存在的,直接用persist()保存新实体、update()更新已有实体,避免saveOrUpdate的自动判断逻辑带来的额外操作:
// 保存新实体 session.persist(newEntity); // 更新已有实体 session.update(existingEntity);
如果必须保留自动判断逻辑,可以尝试用merge()替代saveOrUpdate,它的内部逻辑更严谨,大概率不会触发额外的ID获取。
4. 重置数据库序列(如果需要)
如果是序列与表数据不一致导致的问题,手动重置序列的当前值,让它比表中最大ID大1即可。比如PostgreSQL的命令:
ALTER SEQUENCE YOUR_ENTITY_SEQ RESTART WITH (SELECT MAX(id) + 1 FROM your_entity_table);
按照上面的步骤排查,基本能解决ID跳增的问题。如果还是不行,可以把你的实体映射代码和Hibernate配置贴出来,我再帮你细查。
内容的提问来源于stack exchange,提问作者Vadym Borys

