Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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
}
like image 766
kscherrer Avatar asked Sep 21 '18 11:09

kscherrer


2 Answers

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

like image 87
df778899 Avatar answered Sep 21 '22 17:09

df778899


I've tested your scenario and did the following three modifications to make it work:

  1. Added orphanRemoval=true to both of the @OneToMany getFooBars() methods from Foo and Bar. For your specific scenario adding it in Foo would be enough, but you probably want the same effect for when you remove a foo from a bar as well.
  2. Enclosed the foo.removeBar(bar) call inside a method annotated with Spring's @Transactional. You can put this method in a new @Service FooService class.
    Reason: orphanRemoval requires an active transactional session to work.
  3. Removed call to barRepository.save(bar) after calling foo.removeBar(bar).
    This is now redundant, because inside a transactional session changes are saved automatically.
like image 43
Andrei Pietrusel Avatar answered Sep 22 '22 17:09

Andrei Pietrusel