使用SpringBoot和Hibernate与复合pks的双向@OneToMany关系

我的应用程序中现有的父子关系最近变得更加复杂,因为我们在父级和子级的主键上添加了“类型”列。 在此之后,添加,阅读和修改儿童效果很好,但删除它们是一种痛苦。

使用Vlad Mihalcea在本文中给出的关于@OneToMany关系的建议以及关于复合键的各种示例,我尝试了类似于以下模型的实现。 但是,删除孩子仍然无法正常工作,我现在有一个奇怪的错误消息作为奖励。

我使用的是Spring Boot 1.4.1和Hibernate 5.1.9.Final。

案子

Parent实体具有@EmbeddedId ParentPK,其中包含两个字段以及Cascade.ALLorphanRemoval设置为true的orphanRemoval

 @Entity @Table(name = "z_parent") public class Parent { @EmbeddedId private ParentPK pk; @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true) @JoinColumns({ @JoinColumn(name = "parent_code", referencedColumnName = "code"), @JoinColumn(name = "parent_type", referencedColumnName = "type") }) List children = new ArrayList(); public Parent() { } public Parent(String code, String type) { this.pk = new ParentPK(code, type); } public void addChild(Child child){ child.setParent(this); children.add(child); } public void removeChild(Child child){ child.setParent(null); children.remove(child); } //getters and setters, including delegate getters and setters @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Parent)) return false; Parent parent = (Parent) o; return pk.equals(parent.pk); } @Override public int hashCode() { return pk.hashCode(); } } 

ParentPK

 @Embeddable public class ParentPK implements Serializable { @Column(name = "code") private String code; @Column(name = "type") private String type; public ParentPK() { } public ParentPK(String code, String type) { this.code = code; this.type = type; } //getters and setters @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof ParentPK)) return false; ParentPK parentPK = (ParentPK) o; if (!getCode().equals(parentPK.getCode())) return false; return getType().equals(parentPK.getType()); } @Override public int hashCode() { int result = getCode().hashCode(); result = 31 * result + getType().hashCode(); return result; } } 

Child实体具有其自己的code标识符,该标识符与标识父节点的两个字符串一起形成另一个复合主键。 与Parent的关系是双向的,因此Child也有一个用@ManyToOne注释的parent字段。

儿童

 @Entity @Table(name = "z_child") public class Child { @EmbeddedId private ChildPk pk = new ChildPk(); //The two columns of the foreign key are also part of the primary key @ManyToOne(fetch = FetchType.LAZY) @JoinColumns({ @JoinColumn(name = "parent_code", referencedColumnName = "code", insertable = false, updatable = false), @JoinColumn(name = "parent_type", referencedColumnName = "type", insertable = false, updatable = false) }) private Parent parent; public Child() { } public Child(String code, String parentCode, String parentType) { this.pk = new ChildPk(code, parentCode, parentType); } //getters and setters, including delegate getters and setters @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Child)) return false; Child child = (Child) o; return pk.equals(child.pk); } @Override public int hashCode() { return pk.hashCode(); } } 

ChildPK

 @Embeddable class ChildPk implements Serializable { @Column(name = "code") private String code; @Column(name = "parent_code") private String parentCode; @Column(name = "parent_type") private String parentType; public ChildPk() { } public ChildPk(String code, String parentCode, String parentType) { this.code = code; this.parentCode = parentCode; this.parentType = parentType; } //getters and setters @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof ChildPk)) return false; ChildPk childPk = (ChildPk) o; if (!getCode().equals(childPk.getCode())) return false; if (!getParentCode().equals(childPk.getParentCode())) return false; return getParentType().equals(childPk.getParentType()); } @Override public int hashCode() { int result = getCode().hashCode(); result = 31 * result + getParentCode().hashCode(); result = 31 * result + getParentType().hashCode(); return result; } } 

由于我使用Spring,我已经为Parent声明了一个简单的CRUD存储库:

 @Repository public interface ParentRepository extends JpaRepository { } 

问题

假设我已经在数据库中有一个有两个孩子的父:

z_Parent

“代码”,“类型”

“父母”,“收养”

z_child

“code”,“parent_code”,“parent_type”

“Child1”,“Parent”,“Adoptive”

“Child2”,“Parent”,“Adoptive”

,我必须坚持更新版本的父版本,只包含第一个孩子:

 public Parent mapFromUpperLayer(){ Parent updatedParent =new Parent("Parent", "Adoptive"); List children = new ArrayList(); Child child1 = new Child("Child1", updatedParent); child1.setParent(updatedParent); children.add(child1); updatedParent.setChildren(children); return updatedParent; } 

如果我只是用一个孩子保存实体:

 @Autowired private ParentRepository parentRepository; @Test @Commit public void saveUpdate(){ Parent updatedParent = mapFromUpperLayer(); parentRepository.save(updatedParent); } 

然后我有以下结果(我已经清除了一点日志):

 Hibernate: select parent0_.code as code1_50_1_, parent0_.type as type2_50_1_, children1_.parent_code as parent_c2_49_3_, children1_.parent_type as parent_t3_49_3_, children1_.code as code1_49_3_, children1_.code as code1_49_0_, children1_.parent_code as parent_c2_49_0_, children1_.parent_type as parent_t3_49_0_ from z_parent parent0_ left outer join z_child children1_ on parent0_.code=children1_.parent_code and parent0_.type=children1_.parent_type where parent0_.code=? and parent0_.type=? TRACE 12412 --- : binding parameter [1] as [VARCHAR] - [Parent] TRACE 12412 --- : binding parameter [2] as [VARCHAR] - [Adoptive] Hibernate: update z_child set parent_code=null, parent_type=null where parent_code=? and parent_type=? and code=? TRACE 12412 --- : binding parameter [1] as [VARCHAR] - [Parent] TRACE 12412 --- : binding parameter [2] as [VARCHAR] - [Adoptive] TRACE 12412 --- : binding parameter [3] as [VARCHAR] - [Child2] TRACE 12412 --- : binding parameter [4] as [VARCHAR] - [Parent] INFO 12412 --- : HHH000010: On release of batch it still contained JDBC statements WARN 12412 --- : SQL Error: 0, SQLState: 22023 ERROR 12412 --- : L'indice de la colonne est hors limite : 4, nombre de colonnes : 3. 

这里有两个问题。 Hibernate正确识别要从父项中删除Child2生成更新而不是删除查询。 我完全使用双向关系以避免这种情况,但似乎我还没有完全理解它是如何工作的。 当然,它生成的更新包含三列的四个参数(“Parent”出现两次),这很奇怪。

我已经尝试过了什么

首先,我从数据库中检索了实体,删除了它的子节点并将它们的父节点设置为nullremoveChild方法)并添加了新的列表,同时注意每次将父节点设置为我要保存的实例( addChild方法) )。

 @Test @Commit public void saveUpdate2(){ Parent updatedParent = mapFromUpperLayer(); Parent persistedParent = parentRepository.findOne(new ParentPK(updatedParent.getCode(), updatedParent.getType())); //remove all the children and add the new collection, both one by one (new ArrayList(persistedParent.getChildren())) .forEach(child -> persistedParent.removeChild(child)); updatedParent.getChildren().forEach(child -> persistedParent.addChild(child)); parentRepository.save(persistedParent); } 

其次我尝试了这个问题的解决方案,就是我已经在ChildPK中直接声明了@ManyToOne部分的关系:

 @Embeddable class ChildPk implements Serializable { @Column(name = "code") private String code; @ManyToOne(fetch = FetchType.LAZY) @JoinColumns({ @JoinColumn(name = "parent_code", referencedColumnName = "code"), @JoinColumn(name = "parent_type", referencedColumnName = "type") }) private Parent parent; public ChildPk() { } public ChildPk(String code, Parent parent) { this.code = code; this.parent = parent; } .... 

在这两种情况下,我都会得到相同的生成查询和相同的错误。

问题

  1. 如何构建我的父子关系,以便在保存新版本的父版本时Hibernate能够删除已删除的子项? 理想情况下,我不想过多地更改数据库的结构 – 例如,连接表实现起来相当费时。

  2. 不太重要但有趣:为什么Hibernate试图绑定四个参数“[Parent],[Adoptive],[Child2],[Parent]”来识别更新查询中的Child2?

感谢您的耐心等待!

Parent.children上的注释是问题的根源。 添加mappedBy ,删除父端的mappedBy

设置它的正确方法:

 @OneToMany(mappedBy = "parent", fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true) List children = new ArrayList<>(); 

我相信为删除生成的查询是期望的结果。

 Hibernate: delete from z_child where code=? and parent_code=? and parent_type=? 

此外, removeChild可以简化 – 不需要将child的父级设置为null – 无论如何都会处理它。 这不会影响生成的查询。

  public void removeChild(Child child){ // child.setParent(null); No need to do that children.remove(child); }