我的应用程序中已有一个父子关系,但由于我们在父子的主键中都添加了“类型”列,因此该关系最近变得更加复杂。在此之后,添加,阅读和修改儿童效果很好,但是删除它们很痛苦。
使用Vlad Mihalcea在本文中针对@OneToMany关系给出的建议以及组合键上的各种示例,我尝试了一种类似于以下模型的实现。但是,删除孩子仍然无法正常工作,现在我得到了一个奇怪的错误消息作为奖励。
我正在使用Spring Boot 1.4.1和Hibernate 5.1.9.Final。
父实体具有一个@EmbeddedId ParentPK,其中包含两个字段以及一个children集合,该集合具有Cascade.ALL和orphanRemoval设置为true。
children
Cascade.ALL
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<Child> 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(); } }
@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用@ManyToOne注释的字段。
Child
code
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(); } }
@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<Parent, ParentPK> { }
假设我已经有一个在数据库中有两个孩子的Parent:
z_Parent
“代码”,“类型”
“父母”,“收养”
z_child
“代码”,“父母代码”,“父母类型”
“ Child1”,“父母”,“收养”
“ Child2”,“父母”,“收养”
,并且我必须保留父版本的更新版本,其中仅包含第一个孩子:
public Parent mapFromUpperLayer(){ Parent updatedParent =new Parent("Parent", "Adoptive"); List<Child> 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的原因 是生成更新而不是删除查询。为了避免这种情况,我完全使用了双向关系,但是似乎我还不完全了解它是如何工作的。而且,当然,它生成的更新包含三列的四个参数(“父”出现两次),这很奇怪。
首先,我从数据库中检索了该实体,删除了它的两个孩子,并将其父对象设置为 null (removeChild方法),并添加了新列表,同时还要注意每次将父对象设置为要保存的实例时(addChild方法)。
removeChild
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; } ....
在两种情况下,我都会得到相同的生成查询和相同的错误。
如何构造我的父子关系,以便在保存新版本的父级时Hibernate能够删除已删除的子级?理想情况下,我不想过多地更改数据库的结构-例如,连接表的实现将非常耗时。
不太重要但很有趣:为什么Hibernate试图绑定四个参数“ [Parent],[Adoptive],[Child2],[Parent]”以在更新查询中标识Child2?
感谢您的耐心等待!
注释Parent.children是问题的根源。添加mappedBy,@JoinColumns在父侧删除。
Parent.children
mappedBy
@JoinColumns
正确的设置方式:
@OneToMany(mappedBy = "parent", fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true) List<Child> 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); }