Spring JPA主键/唯一索引冲突问题及解决方法
错误信息
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.dao.DataIntegrityViolationException: could not execute statement [Unique index or primary key violation: "PUBLIC.CONSTRAINT_INDEX_3 ON PUBLIC.USER_TACO_ORDER_REF(TACO_ORDER_ID NULLS FIRST) VALUES ( /* 1 / '52aca972-aeac-4305-9021-ad17ea1a0b07' )"; SQL statement:
insert into user_taco_order_ref (user_id,taco_order_id) values (?,?) [23505-220]] [insert into user_taco_order_ref (user_id,taco_order_id) values (?,?)]; SQL [insert into user_taco_order_ref (user_id,taco_order_id) values (?,?)]; constraint ["PUBLIC.CONSTRAINT_INDEX_3 ON PUBLIC.USER_TACO_ORDER_REF(TACO_ORDER_ID NULLS FIRST) VALUES ( / 1 */ '52aca972-aeac-4305-9021-ad17ea1a0b07' )"
问题场景
- 第一次提交订单:处理正常,订单存入数据库后添加到User的
tacoOrders列表,因配置Cascade.ALL,关联记录自动保存,列表仅含该订单。 - 第二次提交订单:重置会话中的TacoOrder对象,处理后添加到User列表。持久化User时,Hibernate尝试保存整个列表(包含已持久化的旧订单和新订单),插入重复关联记录到
User_Taco_Order_Ref表,触发唯一索引冲突。
相关代码
控制器代码
@Transactional @PostMapping("/current") public String processOrder(@ModelAttribute("order") @Valid TacoOrder order, Errors errors, @AuthenticationPrincipal User user) { if(errors.hasErrors()){ return "orderForm"; } order.setUser(user); order.setPlacedAt(new Date()); user.addTacoOrder(order); log.info("4 {} {}", order, user); //LOG TacoOrder tacoOrder = orderRepository.save(order); userService.updateUserFromOrder(user, tacoOrder); userRepository.save(user); log.info("Order submitted: {}", tacoOrder); //LOG return "redirect:/orders"; }
User实体关联配置
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER) @JoinTable( name = "User_Taco_Order_Ref", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "tacoOrder_id")) private List<TacoOrder> tacoOrders = new ArrayList<>();
日志验证
第一次订单后User的tacoOrders列表:
User(id=1, username=admin, ..., tacoOrders=[TacoOrder(id=52aca972-aeac-4305-9021-ad17ea1a0b07, ...)])
第二次订单后User的tacoOrders列表:
User(id=1, username=admin, ..., tacoOrders=[TacoOrder(id=52aca972-aeac-4305-9021-ad17ea1a0b07, ...), TacoOrder(id=37808e6b-ed1d-4f38-8652-8c672b88d5e5, ...)])
解决方案
1. 调整关联关系的级联策略
当前CascadeType.ALL会触发所有操作的级联,包括对已持久化实体的重复插入。可以将级联类型改为CascadeType.PERSIST + CascadeType.MERGE,或者仅保留CascadeType.PERSIST(因为订单创建后通常不需要通过User修改):
@OneToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}, fetch = FetchType.EAGER) @JoinTable( name = "User_Taco_Order_Ref", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "tacoOrder_id")) private List<TacoOrder> tacoOrders = new ArrayList<>();
这样Hibernate只会对新的(未持久化)订单执行插入,对已存在的订单不会重复操作。
2. 优化控制器中的保存逻辑
不需要同时保存订单和用户,因为订单已经关联了User,且级联策略会处理关联:
@Transactional @PostMapping("/current") public String processOrder(@ModelAttribute("order") @Valid TacoOrder order, Errors errors, @AuthenticationPrincipal User user) { if(errors.hasErrors()){ return "orderForm"; } order.setUser(user); order.setPlacedAt(new Date()); // 不需要手动添加到user的列表,或者添加后无需重复保存user TacoOrder tacoOrder = orderRepository.save(order); userService.updateUserFromOrder(user, tacoOrder); // 移除userRepository.save(user); 因为订单保存后关联关系已通过级联处理 log.info("Order submitted: {}", tacoOrder); return "redirect:/orders"; }
如果必须更新User的其他字段,确保更新时Hibernate不会重复处理已存在的订单关联。
3. 检查User.addTacoOrder方法的实现
确保该方法不会重复添加相同的订单,或者在添加前判断订单是否已存在:
public void addTacoOrder(TacoOrder order) { if (order != null && !tacoOrders.contains(order)) { tacoOrders.add(order); order.setUser(this); // 双向关联维护 } }
同时要确保TacoOrder正确重写了equals和hashCode方法,基于主键ID判断是否为同一实体。
4. 修改关联表的唯一约束(备选)
如果业务允许一个订单属于多个用户(显然这里不允许),可以移除taco_order_id上的唯一索引。但更合理的是保持唯一约束,因为一个订单应该只属于一个用户,所以这个方案仅作为备选,优先调整代码逻辑。
内容的提问来源于stack exchange,提问作者Alpha

