Hibernate多对多关系删除Post实体成功却抛出异常问题
先理清楚问题背景
你这边Post和Category是多对多关系,JPA映射逻辑没问题:
Post实体代码
@ManyToMany(cascade = {PERSIST, MERGE}) private Set<Category> categories = new HashSet<>();
Category实体代码
@ManyToMany(mappedBy = "categories") private Set<Post> posts = new HashSet<>();
但执行删除Post的方法时,明明删除操作实际成功了,Hibernate却抛出了ObjectOptimisticLockingFailureException(根源是StaleStateException),同时SQL语句还重复执行了两次:
Hibernate: delete from post_categories where posts_id=? Hibernate: delete from post_categories where posts_id=? Hibernate: delete from post_likers where favorites_id=? Hibernate: delete from post_likers where favorites_id=? Hibernate: delete from post where id=? Hibernate: delete from post where id=?
你的删除方法如下:
@CacheEvict(cacheNames = "pinnedPosts", allEntries = true) public void deletePost(long postId) { Optional<Post> optionalPost = postRepository.findById(postId); if (optionalPost.isPresent()) { Post post = optionalPost.get(); for (Category category : post.getCategories()) { long newScore = post.getAuthor().getScore() - post.getLikesCount() * likeScore; post.getAuthor().setScore(newScore); category.getPosts().remove(post); } postRepository.deleteById(postId); } else { throw new PostNotFoundException(); } }
问题根源:重复SQL就是异常的罪魁祸首
没错,重复执行的SQL直接导致了这个异常,原因很清晰:
- 你手动从
category.getPosts()里移除了post,Hibernate在事务flush阶段,会自动清理post_categories关联表的对应记录 - 紧接着调用
postRepository.deleteById(postId),由于Post是多对多关系的拥有方(Category侧用了mappedBy="categories"),Hibernate会再次尝试清理关联表并删除Post本身 - 第二次执行删除SQL时,目标记录已经被删掉了,返回的受影响行数是0,但Hibernate预期是1,于是抛出
StaleStateException,最终包装成ObjectOptimisticLockingFailureException
另外还有个小细节要注意:你循环里修改了作者的积分,但没有显式保存Author实体,虽然这不是当前异常的直接原因,但可能会导致积分修改不生效。
解决方案:避免手动维护和JPA自动维护的冲突
这里提供两种靠谱的解决方式,你可以按需选择:
方式一:移除手动维护关联的代码,让JPA自动处理
既然Post是多对多关系的拥有方,删除Post时JPA会自动帮你清理关联表的记录,完全不需要手动去Category的posts集合里移除Post。修改后的代码如下:
@CacheEvict(cacheNames = "pinnedPosts", allEntries = true) @Transactional // 确保整个操作在同一个事务中,避免数据不一致 public void deletePost(long postId) { // 用orElseThrow简化空值判断逻辑 Post post = postRepository.findById(postId) .orElseThrow(PostNotFoundException::new); // 更新作者积分并显式保存,确保修改生效 long newScore = post.getAuthor().getScore() - post.getLikesCount() * likeScore; post.getAuthor().setScore(newScore); authorRepository.save(post.getAuthor()); // 直接删除已加载的Post实体,Hibernate会自动清理关联表 postRepository.delete(post); }
关键点说明:
- 用
delete(post)代替deleteById(postId),因为已经加载了Post实体,Hibernate能更准确地处理关联清理逻辑 - 加上
@Transactional(如果当前方法不在事务上下文内),保证所有操作的原子性 - 显式保存Author实体,确保积分修改能持久化到数据库
方式二:如果必须手动维护关联,先清空Post的categories集合
要是你因为业务需求必须手动处理关联关系,可以先清空Post的categories集合,这样Hibernate就不会重复处理关联表了:
@CacheEvict(cacheNames = "pinnedPosts", allEntries = true) @Transactional public void deletePost(long postId) { Post post = postRepository.findById(postId) .orElseThrow(PostNotFoundException::new); // 更新作者积分并保存 long newScore = post.getAuthor().getScore() - post.getLikesCount() * likeScore; post.getAuthor().setScore(newScore); authorRepository.save(post.getAuthor()); // 复制一份categories集合,避免循环时修改原集合导致并发修改异常 Set<Category> categories = new HashSet<>(post.getCategories()); for (Category category : categories) { category.getPosts().remove(post); } // 清空Post的categories集合,让Hibernate只处理一次关联清理 post.getCategories().clear(); postRepository.delete(post); }
这种方式通过先清空拥有方的集合,让Hibernate只执行一次关联表清理和Post删除操作,避免了重复SQL的问题。
额外检查:二级缓存是否干扰
如果你的Post、Category实体开启了二级缓存(加了@Cacheable),可能会导致Hibernate重复加载实体,进而重复执行SQL。你已经用@CacheEvict清理了pinnedPosts缓存,这部分是对的,但可以检查下实体本身的缓存是否需要在删除后同步清理。
总结
重复执行SQL确实是导致异常的直接原因,核心矛盾是你手动维护关联和JPA自动维护关联的冲突。通过移除手动维护代码让JPA自动处理,或者先清空Post的categories集合,都能解决这个问题,同时记得确保所有操作在同一个事务中完成,避免数据不一致。
内容的提问来源于stack exchange,提问作者Mahozad

