Spring JPA保存TacoOrder时Taco列表清空引发约束违反问题
问题描述
调用processOrder方法保存TacoOrder实体时触发jakarta.validation.ConstraintViolationException,错误提示为You must choose at least 1 ingredient。但日志显示,保存前表单绑定的TacoOrder对象及会话中的TacoOrder对象均包含带有有效配料的Taco列表;执行orderRepository.save(order)后,返回的TacoOrder对象中Taco的createdAt、name字段为空,ingredients列表变为空集合。即使移除TacoOrder中tacos字段的CascadeType.ALL配置,保存后的TacoOrder仍无Taco数据。需排查JPA的save()方法清空TacoOrder中Taco列表的原因,并解决约束违反问题。
相关代码与日志
错误日志
jakarta.validation.ConstraintViolationException: Validation failed for classes [com.example.tacohouse.entities.Taco] during persist time for groups [jakarta.validation.groups.Default, ] List of constraint violations:[ ConstraintViolationImpl{interpolatedMessage='You must choose at least 1 ingredient', propertyPath=ingredients, rootBeanClass=class com.example.tacohouse.entities.Taco, messageTemplate='You must choose at least 1 ingredient'} ]
processOrder方法
@Transactional @PostMapping("/current") public String processOrder(@ModelAttribute("order") @Valid TacoOrder order, Errors errors, @AuthenticationPrincipal User user, RedirectAttributes redirectAttributes) { if(errors.hasErrors()){ return "orderForm"; } order.setUser(user); order.setPlacedAt(new Date()); log.info(" {} ", sessionScopedTacoOrderManager.getTacoOrder()); //fine log.info(" {} ", order); //fine TacoOrder tacoOrder = orderRepository.save(order); //problem in this user.addTacoOrder(tacoOrder); userRepository.save(user); log.info("Order submitted: {}", tacoOrder); return "redirect:/orders"; }
保存前后日志
2024-03-18 19:26:01.932 [http-nio-8080-exec-9] INFO c.e.t.controllers.OrderController -TacoOrder(id=94ef1789-4757-41db-8196-bf88117b498e, placedAt=Mon Mar 18 19:26:01 IST 2024, deliveryName=a, deliveryStreet=a, deliveryCity=a, deliveryState=a, deliveryZip=a, ccNumber=4417123456789113, ccExpiration=11/2024, ccCVV=111, tacos=[Taco(id=null, createdAt=Mon Mar 18 19:25:45 IST 2024, name=firstTaco, ingredients=[Ingredient(id=CHED, name=Cheddar, type=CHEESE)])]) 2024-03-18 19:26:01.932 [http-nio-8080-exec-9] INFO c.e.t.controllers.OrderController - TacoOrder(id=94ef1789-4757-41db-8196-bf88117b498e, placedAt=Mon Mar 18 19:26:01 IST 2024, deliveryName=a, deliveryStreet=a, deliveryCity=a, deliveryState=a, deliveryZip=a, ccNumber=4417123456789113, ccExpiration=11/2024, ccCVV=111, tacos=[Taco(id=null, createdAt=Mon Mar 18 19:25:45 IST 2024, name=firstTaco, ingredients=[Ingredient(id=CHED, name=Cheddar, type=CHEESE)])]) 2024-03-18 19:26:01.932 [http-nio-8080-exec-9] INFO c.e.t.controllers.OrderController - Order submitted: TacoOrder(id=94ef1789-4757-41db-8196-bf88117b498e, placedAt=2024-03-18 19:26:01.908, deliveryName=a, deliveryStreet=a, deliveryCity=a, deliveryState=a, deliveryZip=a, ccNumber=4417123456789113, ccExpiration=11/2024, ccCVV=111, tacos=[Taco(id=null, createdAt=null, name=null, ingredients=[])])
实体类代码
TacoOrder
@Data @Entity @Getter @Setter public class TacoOrder{ @Id //@GeneratedValue(strategy = GenerationType.IDENTITY) private final String id = UUID.randomUUID().toString(); private Date placedAt; @NotBlank(message = "Delivery name is required") private String deliveryName; @NotBlank private String deliveryStreet; @NotBlank(message = "City is required") private String deliveryCity; @NotBlank(message = "State is required") private String deliveryState; @NotBlank(message = "Zip Code is required") private String deliveryZip; @CreditCardNumber(message = "Invalid Credit Card Number") private String ccNumber; @Pattern(regexp = "^(0[1-9]|1[0-2])/20(2[3-9]|[3-9][0-9])$",message = "Must be of format MM/YYYY") private String ccExpiration; @NotNull @Digits(integer=3, fraction=0, message="Invalid CVV") private String ccCVV; @ManyToOne @ToString.Exclude private User user; @OneToMany(mappedBy = "tacoOrder", cascade = CascadeType.ALL, fetch = FetchType.EAGER) private List<Taco> tacos = new ArrayList<>(); public void addTaco(Taco taco) { taco.setTacoOrder(this); this.tacos.add(taco); } }
Taco
@Data @Entity @NoArgsConstructor //if we make a cunstructor @data wont make a noArgConstructor iteelf, so we define it public class Taco{ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private Date createdAt; @NotBlank @Size(min = 5, message = "Name must be at least 5 characters long") private String name; @ManyToMany @JoinTable( name = "ingredient_ref", joinColumns = @JoinColumn(name = "taco_id"), inverseJoinColumns = @JoinColumn(name = "ingredient_id")) @NotNull @Size(min=1, message="You must choose at least 1 ingredient") private List<Ingredient> ingredients = new ArrayList<>(); @ManyToOne @JoinColumn(name = "taco_order_id") @ToString.Exclude private TacoOrder tacoOrder; public Taco(Date createdAt, String name, List<Ingredient> ingredients) { this.createdAt = createdAt; this.name = name; this.ingredients = ingredients; } }
Ingredient
@Data @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED,force = true) @RequiredArgsConstructor public class Ingredient { @Id private final String id; private final String name; @Enumerated(EnumType.STRING) private final Type type; public enum Type { WRAP, PROTEIN, VEGGIES, CHEESE, SAUCE, EXTRA } }
问题分析与解决方案
核心原因
- Ingredient实体未被托管:表单提交的
Ingredient只有ID,未被EntityManager托管,JPA无法识别关联关系,最终清空Taco的ingredients列表。 - TacoOrder的ID生成逻辑错误:
TacoOrder的ID是手动赋值的final字段,JPA会将其视为已持久化实体,执行merge而非persist操作,导致关联的Taco未被正确级联保存。 - 验证时机触发异常:JPA在持久化
Taco时触发验证,此时ingredients已被清空,触发约束违反错误。
解决方案
1. 修复TacoOrder的ID生成方式
将ID改为JPA托管的UUID生成策略,避免手动赋值导致的merge逻辑:
@Id @GeneratedValue(strategy = GenerationType.UUID) private String id;
(移除原有的final修饰和直接赋值代码)
2. 确保Ingredient实体被托管
在保存TacoOrder前,从数据库查询对应ID的Ingredient实体,替换表单绑定的对象:
// 在processOrder方法中添加以下代码,保存前处理Taco的配料 for (Taco taco : order.getTacos()) { List<Ingredient> managedIngredients = new ArrayList<>(); for (Ingredient ing : taco.getIngredients()) { // 查询托管的Ingredient实体,确保JPA能识别关联 managedIngredients.add(ingredientRepository.findById(ing.getId()).orElseThrow()); } taco.setIngredients(managedIngredients); // 确保双向绑定正确 taco.setTacoOrder(order); }
3. 保留正确的级联配置
保留TacoOrder中@OneToMany的CascadeType.ALL,确保Taco能被级联保存;同时保证addTaco方法的双向绑定逻辑正确(当前代码已实现,无需修改)。
4. 可选:提前验证Taco约束
若希望在表单提交阶段就验证Taco的约束,可手动触发验证:
@Autowired private Validator validator; // 在保存前验证每个Taco for (Taco taco : order.getTacos()) { Set<ConstraintViolation<Taco>> violations = validator.validate(taco); if (!violations.isEmpty()) { throw new ConstraintViolationException(violations); } }
验证效果
修改后,保存TacoOrder时:
Taco的ingredients会关联到托管的Ingredient实体,不会被清空- JPA会正确执行
persist操作,级联保存Taco及其关联数据 - 验证不会触发约束违反,保存后的
TacoOrder将保留完整的Taco列表
内容的提问来源于stack exchange,提问作者Alpha
相关产品推荐
相关产品推荐

