kscherrer
kscherrer

Reputation: 5766

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:


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
}

Upvotes: 6

Views: 389

Answers (3)

df778899
df778899

Reputation: 10931

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

Upvotes: 2

Andrei Pietrusel
Andrei Pietrusel

Reputation: 556

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.

Upvotes: 1

Slava Vedenin
Slava Vedenin

Reputation: 60104

Java Persistence 2.1. Chapter 3.2.3

Operation remove

• If X is a new entity, it is ignored by the remove operation. However, the remove operation is cascaded to entities referenced by X, if the relationship from X to these other entities is annotated with the cascade=REMOVE or cascade=ALL annotation element value.

• If X is a managed entity, the remove operation causes it to become removed. The remove operation is cascaded to entities referenced by X, if the relationships from X to these other entities is annotated with the cascade=REMOVE or cascade=ALL annotation element value.

Check that you already use operation persist for you Entities Foo(or FooBar or Bar).

Upvotes: 0

Related Questions