Hibernate Many-to-Many with join-class Cascading issue
Asked Answered
S

3

6

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
}
Spodumene answered 21/9, 2018 at 11:30 Comment(4)
Actually the orphan removal is the only thing that should work in this case because you are removing the FooBar reference from the collections of Foo and Bar, making that FooBar an "orphan".Castello
Was thinking about this and i just realized that not all side of the relation are in sync, try to add fooBar.setFoo(null); and fooBar.setBar(null); to the remove methodCastello
@Castello oh I see what you are getting at and I think that is a promising approach. I will test it right after my lunch and will ping you again with the result. Thanks for the suggestion!Spodumene
@Castello that did not work. I almost thought we did it :(Spodumene
R
2

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

Reputable answered 24/9, 2018 at 21:55 Comment(6)
Thank you for taking time to help me! Sadly, this doesn't work, I just tried that again. I will checkout your little project and see if it works there. The biggest difference between your code and mine is that my repositories extend JpaRepository while yours extend CrudRepository. And that in your models, the constructors are not there. Together with @zeromus' comment, I can now see that it should indeed be the orphanRemoval that fixes this. I will give an update, as soon as I have run your code.Spodumene
I also can't/don't do the entityManager.flush() and entityManager.clear() after the repository save.Spodumene
Indeed this seems to work! This (and other answers) make me believe that there must be something else that messes with my orphanRemoval, because in my project, adding orphanRemoval=true does not work. Thank you so much for your effort.Spodumene
Although my Project still doesn't run, you answered my question well, and even provided me a testproject, proving that your answer is correct. I may have follow-up answers ("What is breaking my orphanRemoval?") but it would not be fair to change this post to that question. Therefore, I gave the bounty to you. Thanks again for your effort!Spodumene
Thanks for the feedback. I should have explained the flush/clear() trick in the test to be fair. The flush() sends the current changes to the DB, then the clear() starts the Hibernate session (in the entityManager) over. This has the same effect as running the sections of code before/after in different transactions, but is just simpler to code in a test case - I wouldn't recommend this elsewhere. Have updated the example for future reference.Reputable
Regarding your first comment, I wouldn't expect the constructors to be relevant - so long as there is an implicit or explicit default constructor Hibernate should be happy. And JpaRepository just extends CrudRepository, so it seems unlikely to be the cause. The quickest way i can think to narrow the search would be a breakpoint in org.hibernate.engine.internal.Cascade.deleteOrphans(). You should be able to trap this being called in the test case - see the screenshot added to the answer. Then it should be possible to spot where your case takes a different turn in the Cascade class.Reputable
R
1

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.
Robins answered 25/9, 2018 at 13:4 Comment(1)
I made these modifications, but still the FooBar remains. Thank you for your suggestions. I suspect there has to be something in my code or model that messes with my orphanRemoval, since you are now the second answerer who said they made it work.Spodumene
R
0

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).

Robeson answered 21/9, 2018 at 11:40 Comment(1)
Yes, the foo and the bar are already in DB. The FooBar entry also exists in db when I want to foo.removeBar(bar);Spodumene

© 2022 - 2024 — McMap. All rights reserved.