Hibernate与JPA祖孙实体映射问题:grand_parent_id为空排查
我使用Spring、Hibernate和JPA开发Dropwizard应用(非SpringBoot),存储采用MySQL,数据库未使用外键。注:我认同子表存储祖父ID并非最佳实践,但这是无法修改的遗留场景。
现有JPA实体如下:
GrandParent实体
@Entity public class GrandParent { @GeneratedValue(strategy = GenerationType.IDENTITY) @Id @Column(name = "id", columnDefinition = "int unsigned not null") private Long id; //... some other fields... getters and setters @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true ) private Set<Parent> parents; public void addParent(Parent parent) { if (this.parents == null) { this.parents = new HashSet<>(); } this.parents.add(parent); parent.setGrandParent(this); } public void removeParent(Parent parent) { this.parents.remove(parent); parent.setGrandParent(null); } }
Parent实体
@Entity public class Parent { @GeneratedValue(strategy = GenerationType.IDENTITY) @Id @Column(name = "id", columnDefinition = "int unsigned") private Long id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "grand_parent_id", referencedColumnName = "id", columnDefinition = "int unsigned") private GrandParent grandParent; @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true ) private Set<Child> children; // other fields, getters and setters public void addChild(Child child) { if (this.fields == null) { this.fields = new HashSet<>(); } this.children.add(child); child.setParent(this); } public void removeChild(Child child) { this.children.remove(child); child.setParent(null); } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Parent )) return false; return id != null && id.equals(((Parent) o).getId()); } @Override public int hashCode() { return getClass().hashCode(); } }
Child实体
@Entity public class Child { @GeneratedValue(strategy = GenerationType.IDENTITY) @Id @Column(name = "id", columnDefinition = "int unsigned") private Long id; @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "grand_parent_id", columnDefinition = "int unsigned") private GrandParent grandParent; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "parent_id", columnDefinition = "int unsigned") private Parent parent; // other fields, getters and setters }
我按如下方式创建实体实例:
Child child = Child.builder() .someField("1") .someOtherField(1) .build(); Parent parent = Parent.builder() .someField("1") .someOtherField(1) .build(); parent.addChild(child); GrandParent grandParent = GrandParent.builder() .someField("1") .someOtherField(1) .build(); grandParent.addParent(parent);
通过hibernateSession.save(grandParent)保存GrandParent实体后,所有行均成功存储,但child的grand_parent_id仍为Null。
我尝试修改Parent的addChild方法,显式设置child的GrandParent:
public void addChild(Child child) { if (this.children == null) { this.children = new HashSet<>(); } this.children.add(child); child.setParent(this); //let's try... child.setGrandParent(getGrandParent()); }
但child.grand_parent_id依然为Null,请问问题出在哪,该如何解决?
- 实例创建顺序错误:你先把Child添加到Parent,之后才把Parent关联到GrandParent。此时调用
parent.addChild(child)时,Parent的grandParent属性还未被设置(是null),所以child.setGrandParent(getGrandParent())实际上给Child的grandParent设了null,后续Parent关联GrandParent后,没有重新更新Child的grandParent属性,Hibernate自然不会更新数据库字段。 - Parent实体方法笔误:原
addChild方法里写的是this.fields == null,这是个低级错误,会导致children集合未被正确初始化,不过你修改后的方法已经修正了这个问题,但顺序问题依然存在。 - 关联维护端规则:Child的
grandParent字段是@OneToOne且带有@JoinColumn,说明Child是这个关联的维护端,只有当Child的grandParent属性被正确设置并被Hibernate检测到变化时,才会更新数据库的grand_parent_id字段。
方案一:调整实例创建顺序(推荐)
先创建GrandParent,再创建Parent并关联到GrandParent,最后创建Child并添加到Parent。这样在调用parent.addChild(child)时,Parent的grandParent已经存在,能正确设置Child的grandParent:
GrandParent grandParent = GrandParent.builder() .someField("1") .someOtherField(1) .build(); Parent parent = Parent.builder() .someField("1") .someOtherField(1) .build(); grandParent.addParent(parent); Child child = Child.builder() .someField("1") .someOtherField(1) .build(); parent.addChild(child); hibernateSession.save(grandParent);
方案二:在GrandParent的addParent方法中批量更新Child的GrandParent
如果无法调整创建顺序,可以修改GrandParent的addParent方法,在关联Parent时,自动更新该Parent下所有Child的grandParent属性:
public void addParent(Parent parent) { if (this.parents == null) { this.parents = new HashSet<>(); } this.parents.add(parent); parent.setGrandParent(this); // 更新该Parent下所有Child的grandParent if (parent.getChildren() != null) { for (Child child : parent.getChildren()) { child.setGrandParent(this); } } }
方案三:手动触发Child的属性更新
在把Parent添加到GrandParent后,手动遍历Parent的Child并设置grandParent:
Child child = Child.builder() .someField("1") .someOtherField(1) .build(); Parent parent = Parent.builder() .someField("1") .someOtherField(1) .build(); parent.addChild(child); GrandParent grandParent = GrandParent.builder() .someField("1") .someOtherField(1) .build(); grandParent.addParent(parent); // 手动更新Child的grandParent child.setGrandParent(grandParent); hibernateSession.save(grandParent);
无论采用哪种方案,都要确保Child的grandParent属性在Hibernate持久化前被正确设置,因为Hibernate只会根据维护端(Child)的属性值来更新数据库字段。
内容的提问来源于stack exchange,提问作者mesh

