简体   繁体   中英

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
}

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()断点

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.

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM