JPA保存TacoOrder时外键与关联表数据缺失问题排查
问题:Spring MVC提交TacoOrder后关联数据未正确保存
问题描述
使用Spring MVC控制器通过两页视图提交TacoOrder表单对象,调用orderRepo.save()方法保存。日志显示提交的TacoOrder包含ID及关联的Taco数据,但H2数据库中出现两个异常:
taco表的taco_order_id外键全为nullingredient_ref关联表无数据
Ingredient表与TacoOrder表数据正常,已通过data.sql预加载Ingredient表,相关实体类及配置如下:
实体类代码
Ingredient类
@Data @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED, force = true) public class Ingredient { @Id private final String id; private final String name; @Enumerated(EnumType.STRING) private final Type type; @OneToMany(mappedBy = "ingredient") private List<IngredientRef> ingredientRefs = new ArrayList<>(); public enum Type { WRAP, PROTEIN, VEGGIES, CHEESE, SAUCE } }
TacoOrder类
@Data @Entity public class TacoOrder implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private Date placedAt = new Date(); @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; @OneToMany(mappedBy = "tacoOrder", cascade = CascadeType.ALL) private List<Taco> tacos = new ArrayList<>(); public void addTaco(Taco taco) { this.tacos.add(taco); } }
Taco类
@Data @Entity public class Taco { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private Date createdAt = new Date(); @NotBlank @Size(min = 5, message = "Name must be at least 5 characters long") private String name; @ManyToMany private List<Ingredient> ingredients; @ManyToOne @JoinColumn(name = "taco_order_id") private TacoOrder tacoOrder; @OneToMany(mappedBy = "taco", cascade = CascadeType.ALL) private List<IngredientRef> ingredientRefs = new ArrayList<>(); }
IngredientRef类
@Data @Entity public class IngredientRef { @Id @ManyToOne @JoinColumn(name = "ingredient_id") private Ingredient ingredient; @ManyToOne @JoinColumn(name = "taco_id") private Taco taco; }
日志输出
2023-08-31T02:30:35.257+05:30 INFO 16268 --- [io-8080-exec-10] com.example.tacohouse.OrderController : Order submitted: TacoOrder(id=1, placedAt=Thu Aug 31 02:29:51 IST 2023, deliveryName=a, deliveryStreet=b, deliveryCity=b, deliveryState=b, deliveryZip=b, ccNumber=4417123456789113, ccExpiration=11/2024, ccCVV=111, tacos=[Taco(id=1, createdAt=Thu Aug 31 02:29:56 IST 2023, name=firstTaco, ingredients=[Ingredient(id=COTO, name=Corn Tortilla, type=WRAP, ingredientRefs=[])], tacoOrder=null, ingredientRefs=[]), Taco(id=2, createdAt=Thu Aug 31 02:30:02 IST 2023, name=secondTaco, ingredients=[Ingredient(id=CARN, name=Carnitas, type=PROTEIN, ingredientRefs=[])], tacoOrder=null, ingredientRefs=[]), Taco(id=3, createdAt=Thu Aug 31 02:30:07 IST 2023, name=thirdTaco, ingredients=[Ingredient(id=CHED, name=Cheddar, type=CHEESE, ingredientRefs=[]), Ingredient(id=JACK, name=Monterrey Jack, type=CHEESE, ingredientRefs=[])], tacoOrder=null, ingredientRefs=[])])
H2数据库数据
ingredient表:数据正常taco_order表:数据正常taco表:
| CREATED_AT | ID | TACO_ORDER_ID | NAME |
|---|---|---|---|
| 2023-08-31 02:29:51 | 1 | null | firstTaco |
| 2023-08-31 02:30:02 | 2 | null | secondTaco |
| 2023-08-31 02:30:07 | 3 | null | thirdTaco |
ingredient_ref表:无数据
应用配置(application.properties)
spring.datasource.generate-unique-name=false spring.datasource.name=tacocloud spring.jpa.hibernate.ddl-auto=create spring.jpa.defer-datasource-initialization=true
问题原因及修复方案
1. Taco表taco_order_id为null的原因及修复
在双向一对多关联中,拥有外键的一方(Taco)是关联的维护端,TacoOrder是被维护端。当前addTaco方法仅将Taco加入列表,但未设置Taco的tacoOrder引用,导致Hibernate无法更新外键。
修改TacoOrder的addTaco方法:
public void addTaco(Taco taco) { this.tacos.add(taco); taco.setTacoOrder(this); // 新增该行,维护双向关联 }
2. ingredient_ref表无数据的原因及修复
(1)主键定义错误
IngredientRef使用两个@ManyToOne作为主键,但JPA无法直接识别这种复合主键,需要通过@EmbeddedId定义复合主键:
创建复合主键类:
@Data @Embeddable public class IngredientRefId implements Serializable { private String ingredient; // 对应Ingredient的id private Long taco; // 对应Taco的id }
修改IngredientRef类:
@Data @Entity public class IngredientRef { @EmbeddedId private IngredientRefId id; @ManyToOne @MapsId("ingredient") // 映射到复合主键的ingredient字段 @JoinColumn(name = "ingredient_id") private Ingredient ingredient; @ManyToOne @MapsId("taco") // 映射到复合主键的taco字段 @JoinColumn(name = "taco_id") private Taco taco; }
(2)关联维护冲突
当前Taco同时使用@ManyToMany和IngredientRef关联Ingredient,两者冲突。需移除@ManyToMany,改为手动维护IngredientRef关联:
修改Taco类:
@Data @Entity public class Taco { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private Date createdAt = new Date(); @NotBlank @Size(min = 5, message = "Name must be at least 5 characters long") private String name; @ManyToOne @JoinColumn(name = "taco_order_id") private TacoOrder tacoOrder; @OneToMany(mappedBy = "taco", cascade = CascadeType.ALL, orphanRemoval = true) private List<IngredientRef> ingredientRefs = new ArrayList<>(); // 新增方法,添加配料并维护关联 public void addIngredient(Ingredient ingredient) { IngredientRef ref = new IngredientRef(); IngredientRefId refId = new IngredientRefId(); refId.setIngredient(ingredient.getId()); refId.setTaco(this.getId()); ref.setId(refId); ref.setIngredient(ingredient); ref.setTaco(this); this.ingredientRefs.add(ref); } }
在创建Taco时,调用addIngredient方法添加配料,即可正确保存到ingredient_ref表中。
内容的提问来源于stack exchange,提问作者alpha
相关产品推荐
相关产品推荐

