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

Spring JPA复合主键实体更新时重复行异常解决方案咨询

问题

我正在开发一个基于Spring的REST API,支持用户注册漫画并对已注册的漫画进行评分。API核心功能之一是用户可为漫画打分,为此我创建了基于复合主键(用户+漫画)的JPA实体Rate存储评分数据,并编写了注册和修改评分的服务。但遇到问题:用户首次给某漫画评分时一切正常,尝试修改评分时抛出异常:

org.hibernate.HibernateException: Duplicate row was found and `ASSERT` was specified
    at org.hibernate.sql.results.spi.ListResultsConsumer.consume(ListResultsConsumer.java:187) ~[hibernate-core-6.2.6.Final.jar:6.2.6.Final]
    ...(省略后续栈信息)

相关代码如下:

RatePK

@Embeddable
public class RatePK implements Serializable {
    private static final long serialVersionUID = 1L;
    
    @ManyToOne
    @JoinColumn(name="user_id")
    private User user;
    
    @ManyToOne
    @JoinColumn(name="comic_id")
    private Comic comic;

    public RatePK(User user, Comic comic) {
        this.user = user;
        this.comic = comic;
    }

    public User getUser() {
        return user;
    }

    public Comic getComic() {
        return comic;
    }

    public void setUser(User user) {
        this.user = user;
    }

    public void setComic(Comic comic) {
        this.comic = comic;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((comic == null) ? 0 : comic.hashCode());
        result = prime * result + ((user == null) ? 0 : user.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        RatePK other = (RatePK) obj;
        if (comic == null) {
            if (other.comic != null)
                return false;
        } else if (!comic.equals(other.comic))
            return false;
        if (user == null) {
            if (other.user != null)
                return false;
        } else if (!user.equals(other.user))
            return false;
        return true;
    }
    
}

Rate

@Entity
@Table(name="TB_RATE")
public class Rate implements Serializable {
    private static final long serialVersionUID = 1L;
    
    @EmbeddedId
    private RatePK id = new RatePK();
    
    private Integer rate;

    public Rate(User user, Comic comic, Integer rate) {
        super();
        this.id.setUser(user);
        this.id.setComic(comic);
        this.rate = rate;
    }

    public RatePK getId() {
        return id;
    }

    public Integer getRate() {
        return rate;
    }

    public void setId(RatePK id) {
        this.id = id;
    }

    public void setRate(Integer rate) {
        this.rate = rate;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((id == null) ? 0 : id.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Rate other = (Rate) obj;
        if (id == null) {
            if (other.id != null)
                return false;
        } else if (!id.equals(other.id))
            return false;
        return true;
    }
    
}

RateRepository

@Repository
public interface RateRepository extends JpaRepository<Rate, RatePK> {

}

RateService

public void save(Long comicId, RateNewDTO rateDto) {        
    UserSS userAuthenticated = UserService.authenticated(); 
        
    User user = userService.findById(userAuthenticated.getId());
        
    Comic comic = comicService.findById(comicId);
        
    Rate rate = new Rate(user, comic, rateDto.getRate());

    rateRepository.save(rate);  
}

解决方案

问题根源

每次修改评分时,你直接创建新的Rate对象调用save()方法,但JPA的save()逻辑是:若实体是未被持久化上下文管理的新实例,执行插入;若为已持久化的实例,才会执行更新。

用户再次评分时,新创建的Rate对象主键虽与数据库现有记录一致,但不在当前持久化上下文中,JPA会尝试插入重复主键的记录,触发重复行异常。

解决步骤

  1. 先查询已有评分再处理
    保存前先通过复合主键查询是否存在该用户对该漫画的评分,存在则更新,不存在则新建。修改RateService的save方法:

    public void save(Long comicId, RateNewDTO rateDto) {        
        UserSS userAuthenticated = UserService.authenticated(); 
        User user = userService.findById(userAuthenticated.getId());
        Comic comic = comicService.findById(comicId);
        
        // 构建复合主键
        RatePK ratePK = new RatePK(user, comic);
        // 查询已有评分
        Optional<Rate> existingRate = rateRepository.findById(ratePK);
        
        Rate rate;
        if (existingRate.isPresent()) {
            // 存在则更新评分值
            rate = existingRate.get();
            rate.setRate(rateDto.getRate());
        } else {
            // 不存在则新建
            rate = new Rate(user, comic, rateDto.getRate());
        }
        
        rateRepository.save(rate);  
    }
    
  2. 补充无参构造函数
    JPA要求实体类和可嵌入主键类必须有无参构造,否则查询实例化时可能出错。给RatePK和Rate添加:

    // RatePK中添加
    public RatePK() {}
    
    // Rate中添加
    public Rate() {}
    
  3. 优化复合主键的equals/hashCode(可选但建议)
    当前RatePK的equals/hashCode依赖User和Comic对象的equals实现,若实体状态变化可能导致主键判断出错。建议改为依赖ID字段:

    @Embeddable
    public class RatePK implements Serializable {
        private static final long serialVersionUID = 1L;
        
        @Column(name="user_id")
        private Long userId;
        
        @Column(name="comic_id")
        private Long comicId;
    
        // 关联关系设为懒加载,且不参与插入更新
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name="user_id", insertable = false, updatable = false)
        private User user;
        
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name="comic_id", insertable = false, updatable = false)
        private Comic comic;
    
        public RatePK() {}
        
        public RatePK(Long userId, Long comicId) {
            this.userId = userId;
            this.comicId = comicId;
        }
    
        // getter、setter省略...
    
        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + ((comicId == null) ? 0 : comicId.hashCode());
            result = prime * result + ((userId == null) ? 0 : userId.hashCode());
            return result;
        }
    
        @Override
        public boolean equals(Object obj) {
            if (this == obj) return true;
            if (obj == null || getClass() != obj.getClass()) return false;
            RatePK other = (RatePK) obj;
            return Objects.equals(comicId, other.comicId) && Objects.equals(userId, other.userId);
        }
    }
    

    对应的Rate构造函数调整为:

    public Rate(User user, Comic comic, Integer rate) {
        this.id.setUserId(user.getId());
        this.id.setComicId(comic.getId());
        this.rate = rate;
    }
    

内容的提问来源于stack exchange,提问作者Gustavo Cruz

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.10 17:12:31