Skip to main content
 首页 » 编程设计

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

2023年09月11日27jirigala

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

使用 this article 中给出的建议通过 Vlad Mihalcea 关于 @OneToMany 关系以及复合键的各种示例,我尝试了类似于以下模型的实现。但是,删除子项仍然不起作用,我现在收到一条奇怪的错误消息作为奖励。

我正在使用 Spring Boot 1.4.1 和 Hibernate 5.1.9.Final。

案例

Parent 实体有一个带两个字段的 @EmbeddedId ParentPK 和一个 Cascade.ALLorphanRemoval 设置为 true 的 children 集合。

parent

@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(); 
    } 
 
} 

父PK

@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 字段。

child

@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(); 
    }     
} 

子PK

@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,所以我已经为父级声明了一个简单的 CRUD 存储库:

@Repository 
public interface ParentRepository extends JpaRepository<Parent, ParentPK> { 
} 

问题

假设我在数据库中已经有一个有两个 child 的 parent :

z_Parent

“代码”、“类型”

“ parent ”、“领养”

z_child

"代码"、"parent_code"、"parent_type"

“ child 1”、“ parent ”、“收养”

“ child 2”、“ parent ”、“收养”

,并且我必须保留父级的更新版本,仅包含第一个子级:

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; 
} 

如果我简单地保存带有一个 child 的实体:

@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”出现两次),这很奇怪。

我已经尝试过的

首先,我从数据库中检索了实体,删除了它的两个子项并将它们的父项设置为 null(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); 
} 

其次,我尝试了 this question 中的解决方案,也就是说,我已经直接在 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 到底为什么要尝试绑定(bind)四个参数“[Parent]、[Adoptive]、[Child2]、[Parent]”来在更新查询中识别 Child2?

感谢您的耐心等待!

请您参考如下方法:

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 可以简化 - 不必将子项的父级设置为 null - 无论如何都会处理它。这不会影响生成的查询。

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