当前位置: 首页 > 面试题库 >

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

阎德宇
2023-03-14
问题内容

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

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

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

案子

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

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

}

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用@ManyToOne注释的字段。

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

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<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方法)。

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


 类似资料:
  • 问题内容: 假设我有两个实体: 然后,我要保留“客户”实体,然后,参照先前添加的“客户”保留“订单”实体。当我从数据库中检索此客户并调用getOrders时,它将返回空集。这是正常行为吗?如果是,当我添加新的Order实体时,我该怎么做以自动刷新此集合? 问题答案: Jpa不会为您维护关系,因此应用程序需要设置双向关系的两端,以使它们与数据库保持同步。当您设置了order-> customer关系

  • 拥有2个实体:订单和产品。1个订单可以有多个产品,多个产品可以属于1个订单(每个产品只属于1个订单)。 尝试了@JsonIgnore。这不会返回子元素或父元素。尝试了@JsonManagedReference和@JsonBackReference-仍然没有成功。 请给我指点一下

  • 我在将带有@ManyToOne关系的实体(雇员)映射到带有@OneToMany关系的实体(社区)时遇到了问题。 当我将一个实体(社区)分配给一个实体(员工),其中社区的值为空时,它工作正常。 问题出现了,如果雇员已经为社区分配了价值。当我更改该值并保存更改时,该员工为社区获得了新的值,并且这个新社区在集合中获得了该员工。唯一的问题是,老社区仍然有这个员工在收集,但它应该被删除。这只有在数据库重新启

  • 我正在使用JPA(Hibernate)并试图用childs和复合键持久化整个新实体,但当持久化childs时,我在键中得到了null。表结构: 映射:

  • 问题内容: 我有一个具有单向一对多关系的类,如下所示: 通常,获取此订单的内容很简单: 但是无论出于何种原因,我都可能希望以某种方式过滤结果并仅以最快的方式检索部分商品集合,例如高于一定价格,低于一定库存的所有商品(不返回然后全部过滤)然后)。为此,我将运行HQL查询以检索特定订单的商品,并将更多内容添加到我的where子句或查询对象中。 凭直觉我会想要这种行为(这是完全错误的): 但是当然这是错

  • 我定义了具有@OneToOne双向关系的实体。 关系的所有者站点(子类): 其他站点(父类): 当我试图坚持有子集的父母(不坚持),然后我得到了例外 TransientPropertyValueException:对象引用未保存的临时实例-在刷新之前保存临时实例 这看起来可以理解。 但当我定义具有双向一对一/多对一关联的实体时,如: 此关系的所有者站点: 关系的另一面: 并尝试持久化具有子集合集(