spring-data-jpa踩坑 - delete-then-save唯一索引冲突问题

5,256 阅读2分钟

问题背景

  • 开发过程中,为了保证数据的唯一性,对某个旧表建立一个联合索引unique_format_content_language(format_content_id,language_code)。加了唯一索引之后,有个MQ联动的业务场景,消费一直报错:
org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; constraint [unique_format_content_language] ...
  • 该业务场景MQ消费的基本逻辑(以下步骤是在一个事务中):
    • 接收数据变更的MQ
    • 增量翻译数据
    • 删除旧翻译
    • 保存新翻译
  • dao使用的技术为spring-data-jpa,使用的方式如下:
@Repository
public interface KnowledgeContentTranslationRepository extends JpaRepository<ContentTranslationItem, String>{
}

问题定位过程及解决方案

  • 尝试用debug模式,在出错的地方打了个断点,从数据来看是没什么问题的。
    • knowledgeContentTranslationRepository.delete(deleteContentTranslationItems) 删除了资源的翻译
    • knowledgeContentTranslationRepository.save(addContentTranslationItems) 新增了资源的翻译
  • 不过结合hibernate的sql日志可以发现:insert语句先执行了。在insert语句执行前,并没有执行delete方法
  • 所以可以猜测应该是jpa框架的一些特性导致的
  • 通过google关键字检索,找到github上对应问题的一个issue
    • 该issue中很多人也遇到了delete then save时,唯一索引报错问题
  • 通过查看该issue的讨论记录,可找到对应的两个解决方案(经过验证,这两个方案都可行)
    • 使用deleteInBatch代替delete
    • delete之后,手动调用flush方法

总结

  • spring-data-jpa在一个事务中,先调用delete方法,再调用save方法时,事务提交时,并不会先执行delete的语句,而是直接执行insert语句
    • 在这种情况下,如果表有唯一索引,就有可能出现唯一索引冲突。异常信息为:
    org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; constraint [unique_format_content_language] ...
    
  • 对于delete then save,唯一索引报错的场景,可以考虑通过一下两种方案来避免报错
    • 使用deleteInBatch代替delete
      • 推荐。但deleteInBatch底层是将id通过or的形式拼接成sql,通过形如:delete from [table_name] where id=? or id=?...的sql进行执行的。当删除的数据过大时,可能会出现java.lang.StackOverflowError异常
    • delete之后,手动调用flush方法
      • 不怎么推荐。因为flush方法会该方法之前的所有数据都同步到DB,可能会有性能问题

遗留问题

  • spring-data-jpa的delete then save操作,为啥delete不执行?jpa为啥要如此设计?