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

Posted

技术标签:

【中文标题】使用 SpringBoot 和 Hibernate 与复合 pk 的双向 @OneToMany 关系【英文标题】:Bidirectional @OneToMany relationship with composite pks using SpringBoot and Hibernate 【发布时间】:2018-03-21 18:21:47 【问题描述】:

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

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

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

案例

父实体有一个@EmbeddedId ParentPK,其中包含两个字段和一个children 集合,其中Cascade.ALLorphanRemoval 设置为true。

家长

@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 还有一个 parent 字段,用 @ManyToOne 注释。

孩子

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

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

问题

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

z_Parent

“代码”、“类型”

“父母”、“收养”

z_child

“代码”、“父代码”、“父类型”

“孩子1”、“父母”、“收养”

“孩子2”、“父母”、“收养”

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

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 将从父级中删除生成更新而不是删除查询。为了避免这种情况,我完全使用了双向关系,但似乎我还没有完全理解它是如何工作的。当然,它生成的更新包含三个列的四个参数(“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);

其次,我尝试了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;
    
    ....

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

问题

    如何构建我的父-子关系,以便在我保存新版本的父级时,Hibernate 能够删除已删除的子级?理想情况下,我不想过多地改变数据库的结构 - 例如,连接表的实现会相当耗时。

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

感谢您的耐心等待!

【问题讨论】:

【参考方案1】:

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 的 parent 设置为 null - 无论如何都会处理。这不会影响生成的查询。

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

【讨论】:

谢谢,你说得对!那么您认为第二个问题也与此有关吗?也就是Hibernate准备了四个参数,因为每边有两个@JoinColumn?

以上是关于使用 SpringBoot 和 Hibernate 与复合 pk 的双向 @OneToMany 关系的主要内容,如果未能解决你的问题,请参考以下文章

SpringMVC 项目中 创建SpringBoot,使用Hibernate和JPA

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

如何使用 Springboot 和 Hibernate 在 DTO 和 Aggentity 类中映射 Postgres JSON 数据类型

SpringBoot和Hibernate整合

Spring 5和hibernate 4兼容吗?

springboot+kotlin+gradle+hibernate学习笔记