使用自增ID的实体时,JpaRepository.save()是否适合执行UPSERT操作?
JpaRepository.saveAll() UPSERT重复记录问题解答
问题1逻辑验证
你的判断完全正确。
Spring Data JPA的save()方法默认通过主键判断实体是否为新对象:
- 当传入的实体主键值为空时,会直接判定为新实体,走插入逻辑。配合你使用的
@GeneratedValue(GenerationType.IDENTITY)注解,插入时会触发MySQL自增规则生成新ID,全程不会触发更新逻辑,必然产生重复记录。 - 只有当传入的实体携带了数据库中已存在的主键值时,
save()才会先执行查询判断存在性,再执行更新操作。你的UPSERT逻辑是依靠业务唯一键判断实体是否存在,和默认的主键判断逻辑完全不匹配,所以无法达到预期效果。
问题2方案评估及可选方案
自定义查询方案可行性
该方案完全可行,是业务唯一键UPSERT场景下的主流实现方式,你可以按以下逻辑优化实现:
- 先在Repository层编写批量查询方法,根据业务唯一键一次性捞出当前批次所有已存在的库内记录
- 内存中对比新实体和库内记录的属性,拆分出三类数据:纯新增实体、需要更新的实体、无变更实体
- 纯新增实体直接调用
saveAll()批量插入,需要更新的实体手动赋值已有主键后调用saveAll()批量更新,无变更实体直接跳过 - 数据量较大时可以开启JDBC批量提交参数提升执行效率。
其他可选方案
方案1:重写实体的新对象判断逻辑
让JPA不再按主键判断实体是否为新对象,改为匹配你的业务逻辑:
- 实体类实现
Persistable接口,重写isNew()方法,逻辑改为根据业务唯一键判断是否为新实体(也可添加transient字段标记实体的新/旧状态) - 注意该方案需要配合业务唯一键的数据库唯一索引使用,避免并发场景下出现重复插入问题。
方案2:使用MySQL原生UPSERT语法
直接编写原生SQL调用INSERT INTO ... ON DUPLICATE KEY UPDATE实现真正的批量UPSERT,性能远高于JPA默认的逐行判断逻辑:
- 前提是业务唯一键已经在MySQL表中建立了唯一索引
- 只需在Repository层编写带
@Modifying、@Query注解的原生SQL,参数传入批量实体的属性列表即可,一步完成插入/更新/无变更跳过的操作,不需要额外的前置查库对比。
方案3:使用Hibernate原生@NaturalId注解
用Hibernate提供的@NaturalId注解标记你的业务唯一键字段,后续可通过Session.byNaturalId()方法快速查询已有记录,再进行属性对比和更新操作,比自定义查询的规范度更高。
内容的提问来源于stack exchange,提问作者Simeon Yachev
相关产品推荐
相关产品推荐

