【问题标题】:Hibernate Many-to-Many with join-class Cascading issue休眠多对多与加入类级联问题
【发布时间】:2019-02-25 19:25:03
【问题描述】:

我在 FooBar 类之间有 Many-to-Many 关系。因为我想获得有关辅助表的更多信息,所以我必须创建一个辅助类 FooBar,如下所述:The best way to map a many-to-many association with extra columns when using JPA and Hibernate

我创建了一个 Foo,并创建了一些条(保存到 DB)。然后当我使用

将其中一个条添加到 foo 时
foo.addBar(bar);            // adds it bidirectionally
barRepository.save(bar);    // JpaRepository

然后创建 FooBar 的 DB 条目 - 正如预期的那样。

但是当我想再次从 foo 中删除同一个栏时,使用

foo.removeBar(bar);         // removes it bidirectionally
barRepository.save(bar);    // JpaRepository

那么早先创建的 FooBar-entry 会从数据库中删除。 通过调试,我看到foo.removeBar(bar); 确实是双向删除的。不抛出异常。

我做错了吗? 我很确定它与级联选项有关,因为我只保存栏。


我尝试过的:

  • 在两个 @OneToMany 上添加 orphanRemoval = true - 注释,这不起作用。我认为这是正确的,因为我没有删除 Foo 和 Bar,只是它们的关系。

  • 从 @OneToMany 注释中排除 CascadeType.REMOVE,但与 orphanRemoval 相同,我认为这不适用于这种情况。


编辑:我怀疑我的代码或模型中一定有一些东西与我的 orphanRemoval 混淆,因为现在已经有 2 个答案说它有效(orphanRemoval=true)。

原始问题已得到解答,但如果有人知道什么可能导致我的 orphanRemoval 无法正常工作,我将非常感谢您的意见。谢谢


代码:Foo、Bar、FooBar

public class Foo {

    private Collection<FooBar> fooBars = new HashSet<>();

    // constructor omitted for brevity

    @OneToMany(cascade = CascadeType.ALL, mappedBy = "foo", fetch = FetchType.EAGER)
    public Collection<FooBar> getFooBars() {
        return fooBars;
    }

    public void setFooBars(Collection<FooBar> fooBars) {
        this.fooBars = fooBars;
    }

    // use this to maintain bidirectional integrity
    public void addBar(Bar bar) {
        FooBar fooBar = new FooBar(bar, this);

        fooBars.add(fooBar);
        bar.getFooBars().add(fooBar);
    }

    // use this to maintain bidirectional integrity
    public void removeBar(Bar bar){
        // I do not want to disclose the code for findFooBarFor(). It works 100%, and is not reloading data from DB
        FooBar fooBar = findFooBarFor(bar, this); 

        fooBars.remove(fooBar);
        bar.getFooBars().remove(fooBar);
    }

}

public class Bar {

    private Collection<FooBar> fooBars = new HashSet<>();

    // constructor omitted for brevity

    @OneToMany(fetch = FetchType.EAGER, mappedBy = "bar", cascade = CascadeType.ALL)
    public Collection<FooBar> getFooBars() {
        return fooBars;
    }

    public void setFooBars(Collection<FooBar> fooBars) {
        this.fooBars = fooBars;
    }
}

public class FooBar {

    private FooBarId id; // embeddable class with foo and bar (only ids)
    private Foo foo;
    private Bar bar;

    // this is why I had to use this helper class (FooBar), 
    // else I could have made a direct @ManyToMany between Foo and Bar
    private Double additionalInformation; 

    public FooBar(Foo foo, Bar bar){
        this.foo = foo;
        this.bar = bar;
        this.additionalInformation = .... // not important
        this.id = new FooBarId(foo.getId(), bar.getId());
    }

    @EmbeddedId
    public FooBarId getId(){
        return id;
    }

    public void setId(FooBarId id){
        this.id = id;
    }

    @ManyToOne
    @MapsId("foo")
    @JoinColumn(name = "fooid", referencedColumnName = "id")
    public Foo getFoo() {
        return foo;
    }

    public void setFoo(Foo foo) {
        this.foo = foo;
    }

    @ManyToOne
    @MapsId("bar")
    @JoinColumn(name = "barid", referencedColumnName = "id")
    public Bar getBar() {
        return bar;
    }

    public void setBar(Bar bar) {
        this.bar = bar;
    }

    // getter, setter for additionalInformation omitted for brevity
}

【问题讨论】:

  • 实际上,在这种情况下,孤儿删除是唯一可行的方法,因为您要从 Foo 和 Bar 的集合中删除 FooBar 引用,从而使 FooBar 成为“孤儿”。
  • 正在考虑这个问题,我才意识到并非关系的所有方面都是同步的,尝试添加 fooBar.setFoo(null);和 fooBar.setBar(null);到 remove 方法
  • @Zeromus 哦,我明白你的意思了,我认为这是一个很有前途的方法。我将在午餐后立即对其进行测试,并再次向您发送结果。感谢您的建议!
  • @Zeromus 没用。我差点以为我们做到了:(

标签: java hibernate jpa cascade hibernate-onetomany


【解决方案1】:

我在示例代码中对此进行了尝试。通过几个“草图”,这重现了错误。

解决方案确实就像添加您提到的orphanRemoval = true 一样简单。在Foo.getFooBars()

@OneToMany(cascade = CascadeType.ALL, mappedBy = "foo", fetch = FetchType.EAGER, orphanRemoval = true)
public Collection<FooBar> getFooBars() {
    return fooBars;
}

将复制品发布到GitHub 似乎最容易 - 希望那里有进一步的细微差别或我错过的东西。

这是基于 Spring Boot 和 H2 内存数据库,因此不应在其他环境下工作 - 如果有疑问,请尝试 mvn clean test

FooRepositoryTest 类有测试用例。它具有删除链接 FooBar 的验证,或者它可能更容易阅读记录的 SQL。


编辑

这是下面评论中提到的屏幕截图:

【讨论】:

  • 感谢您花时间帮助我!可悲的是,这不起作用,我又试了一次。我会检查你的小项目,看看它是否在那里工作。你的代码和我的代码之间最大的区别是我的存储库扩展了 JpaRepository 而你的扩展了 CrudRepository。而且在您的模型中,构造函数不存在。连同@zeromus 的评论,我现在可以看到它确实应该是解决此问题的 orphanRemoval。运行您的代码后,我将提供更新。
  • 在存储库保存后我也不能/不做entityManager.flush()entityManager.clear()
  • 确实这似乎有效!这个(和其他答案)让我相信一定有其他东西会影响我的 orphanRemoval,因为在我的项目中,添加 orphanRemoval=true 不起作用。非常感谢您的努力。
  • 虽然我的Project还是没有运行,但是你很好的回答了我的问题,还给我提供了一个testproject,证明你的回答是正确的。我可能有后续答案(“是什么破坏了我的 orphanRemoval?”),但将这篇文章更改为该问题是不公平的。因此,我把赏金给了你。再次感谢您的努力!
  • 感谢您的反馈。我应该公平地解释测试中的flush/clear() 技巧。 flush() 将当前更改发送到 DB,然后 clear() 启动 Hibernate 会话(在 entityManager 中)结束。这与在不同事务中运行之前/之后的代码部分具有相同的效果,但在测试用例中编写代码更简单——我不会在其他地方推荐这个。已更新示例以供将来参考。
【解决方案2】:

我已经测试了您的场景并进行了以下三项修改以使其正常工作:

  1. 在 Foo 和 Bar 的 @OneToMany getFooBars() 方法中添加了 orphanRemoval=true。对于您的特定场景,将其添加到 Foo 中就足够了,但是当您从 bar 中删除 foo 时,您可能也希望获得相同的效果。
  2. 将 foo.removeBar(bar) 调用封装在使用 Spring 的 @Transactional 注释的方法中。您可以将此方法放在一个新的 @Service FooService 类中。
    原因:orphanRemoval 需要一个活动的事务会话才能工作。
  3. 在调用 foo.removeBar(bar) 后删除了对 barRepository.save(bar) 的调用。
    这现在是多余的,因为在事务性会话中更改会自动保存。

【讨论】:

  • 我做了这些修改,但 FooBar 仍然存在。谢谢你的建议。我怀疑我的代码或模型中一定有一些东西与我的 orphanRemoval 混淆,因为你现在是第二个说他们让它工作的回答者。
【解决方案3】:

Java 持久性 2.1。第3.2.3章

删除操作

• 如果X 是一个新实体,则删除操作会忽略它。 但是,删除操作级联到 X 引用的实体, 如果从 X 到这些其他实体的关系被注释为 cascade=REMOVE 或 cascade=ALL 注释元素值。

• 如果 X 是 一个托管实体,删除操作会导致它被删除。 删除操作级联到 X 引用的实体,如果 从 X 到这些其他实体的关系用 cascade=REMOVE 或 cascade=ALL 注释元素值。

检查您是否已为您使用操作persist 实体Foo(或FooBarBar)。

【讨论】:

  • 是的,foo 和 bar 已经在 DB 中了。当我想foo.removeBar(bar); 时,FooBar 条目也存在于 db 中
猜你喜欢
  • 1970-01-01
  • 2019-07-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-13
  • 1970-01-01
  • 2011-05-20
相关资源
最近更新 更多