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会尝试插入重复主键的记录,触发重复行异常。
解决步骤
先查询已有评分再处理
保存前先通过复合主键查询是否存在该用户对该漫画的评分,存在则更新,不存在则新建。修改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); }补充无参构造函数
JPA要求实体类和可嵌入主键类必须有无参构造,否则查询实例化时可能出错。给RatePK和Rate添加:// RatePK中添加 public RatePK() {} // Rate中添加 public Rate() {}优化复合主键的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
相关产品推荐
相关产品推荐

