@GeneratedValue级联保存时MySQL主键重复异常及手动设ID可行原因解析
这问题我做关联映射时也踩过坑,核心差异在于Hibernate处理自动生成ID和手动指定ID的实体时,持久化逻辑完全不同,结合你的场景来拆解:
先还原你的场景
你给两个Student都关联了同一个University(id=1L, name="pw"),并且在Student的@ManyToOne上设置了cascade=CascadeType.ALL——这意味着保存Student时,会把同样的持久化操作级联到关联的University上。
为什么用@GeneratedValue会抛主键重复异常?
当Student的ID用@GeneratedValue生成时,Hibernate会默认判定这个Student是全新的瞬时态(transient)实体,直接执行INSERT语句完成保存。
而因为级联了CascadeType.ALL,Hibernate对关联的University也会做同样的判断:虽然你手动给University设置了ID,但Hibernate此时不会先去数据库校验这个ID是否存在,而是直接尝试执行INSERT语句插入该University。
当你保存第二个Student时,又会触发一次对同一个University的INSERT操作,数据库里已经存在ID=1L的记录,自然就抛出了MySQLIntegrityConstraintViolationException: Duplicate entry '1' for key 'PRIMARY'异常。
为什么手动指定Student的ID就没问题?
当你手动给Student设置了ID后,Hibernate会把这个实体判定为托管态(managed)或游离态(detached),它会先执行SELECT语句去数据库检查这个Student是否已经存在。
同时,这个检查逻辑也会级联到关联的University:Hibernate会先查数据库里有没有ID=1L的University,发现已经存在后,就会执行UPDATE语句(哪怕University的数据没有变化),而不是再次执行INSERT,所以不会出现主键重复的问题。
可行的解决方案
如果想保留@GeneratedValue生成StudentID的方式,同时避免University重复插入,可以选以下两种方式:
- 调整级联类型:把
cascade=CascadeType.ALL改成cascade={CascadeType.PERSIST, CascadeType.MERGE},这样只有当University是全新的瞬时态时才会插入,否则会执行合并更新操作 - 提前托管
University:在保存Student之前,先通过entityManager.merge(university)或者Repository的save(university)方法,把University变成托管态,让Hibernate知晓它已经存在于数据库中
内容的提问来源于stack exchange,提问作者sebaszczen

