You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Hibernate多对多关系删除Post实体成功却抛出异常问题

多对多关联下删除Post时的Hibernate重复SQL与异常问题解决

先理清楚问题背景

你这边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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 07:36:31