Search code examples
javahibernatejpacascadehibernate-onetomany

Hibernate Many-to-Many with join-class Cascading issue


I have a Many-to-Many relationship between the class Foo and Bar. Because I want to have additional information on the helper table, I had to make a helper class FooBar as explained here: The best way to map a many-to-many association with extra columns when using JPA and Hibernate

I created a Foo, and created some bars (saved to DB). When I then add one of the bars to the foo using

foo.addBar(bar);            // adds it bidirectionally
barRepository.save(bar);    // JpaRepository

then the DB-entry for FooBar is created - as expected.

But when I want to remove that same bar again from the foo, using

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

then the earlier created FooBar-entry is NOT deleted from the DB. With debugging I saw that the foo.removeBar(bar); did indeed remove bidirectionally. No Exceptions are thrown.

Am I doing something wrong? I am quite sure it has to do with Cascading options, since I only save the bar.


What I have tried:

  • adding orphanRemoval = true on both @OneToMany - annotations, which did not work. And I think that's correct, because I don't delete neither Foo nor Bar, just their relation.

  • excluding CascadeType.REMOVE from the @OneToMany annotations, but same as orphanRemoval I think this is not for this case.


Edit: I suspect there has to be something in my code or model that messes with my orphanRemoval, since there are now already 2 answers who say that it works (with orphanRemoval=true).

The original question has been answered, but if anybody knows what could cause my orphanRemoval not to work I would really appreciate your input. Thanks


Code: 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
}

Solution

  • I tried this out from the example code. With a couple of 'sketchings in' this reproduced the fault.

    The resolution did turn out to be as simple as adding the orphanRemoval = true you mentioned though. On Foo.getFooBars() :

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

    It seemed easiest to post that reproduction up to GitHub - hopefully there's a further subtle difference or something I missed in there.

    This is based around Spring Boot and an H2 in-memory database so should work with no other environment - just try mvn clean test if in doubt.

    The FooRepositoryTest class has the test case. It has a verify for the removal of the linking FooBar, or it may just be easier to read the SQL that gets logged.


    Edit

    This is the screenshot mentioned in a comment below: deleteOrphans() breakpoint