Гибернация "многие ко многим" с проблемой каскадирования в объединенном классе

У меня есть Many-to-Many отношения между классом Foo а также Bar, Поскольку я хочу получить дополнительную информацию о вспомогательной таблице, мне пришлось создать вспомогательный класс FooBar как объяснено здесь: лучший способ отобразить связь "многие ко многим" с дополнительными столбцами при использовании JPA и Hibernate

Я создал Foo и создал несколько баров (сохраненных в БД). Когда я затем добавляю один из баров в Foo, используя

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

Затем создается запись DB для FooBar- как и ожидалось.

Но когда я хочу снова удалить тот же самый бар из foo, используя

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

тогда ранее созданная FooBar-запись НЕ удаляется из БД. С отладкой я увидел, что foo.removeBar(bar); действительно удалить двунаправленно. Никаких исключений не выбрасывается.

Я делаю что-то неправильно? Я совершенно уверен, что это связано с опциями каскадирования, так как я сохраняю только панель.


Что я пробовал:

  • добавление orphanRemoval = true на обоих @OneToMany - аннотации, которые не работали. И я думаю, что это правильно, потому что я не удаляю ни Foo, ни Bar, только их связь.

  • исключая CascadeType.REMOVE из аннотаций @OneToMany, но так же, как orphanRemoval, я думаю, что это не для этого случая.


Изменить: я подозреваю, что в моем коде или модели должно быть что-то, что портит мой orphanRemoval, так как теперь уже есть 2 ответа, которые говорят, что это работает (с orphanRemoval=true).

На оригинальный вопрос ответили, но если кто-нибудь знает, что может привести к тому, что мой orphanRemoval не будет работать, я был бы очень признателен за ваш вклад. Спасибо


Код: Foo, Бар, 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
}

3 ответа

Решение

Я попробовал это из примера кода. С парой "набросков" это воспроизвело ошибку.

Решение оказалось таким простым, как добавление orphanRemoval = true Вы упомянули, хотя. На Foo.getFooBars():

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

Казалось, проще всего опубликовать это воспроизведение до GitHub - надеюсь, есть еще одна тонкая разница или кое-что, что я пропустил там.

Это основано на Spring Boot и базе данных H2 в памяти, поэтому должно работать без какой-либо другой среды - просто попробуйте mvn clean test если сомневаешься.

FooRepositoryTest класс имеет контрольный пример. У него есть проверка для удаления ссылки FooBarИли, может быть, проще читать SQL, который заносится в журнал.


редактировать

Это скриншот, упомянутый в комментарии ниже: deleteOrphans () точка останова

Я протестировал ваш сценарий и сделал следующие три модификации, чтобы он работал:

  1. Добавлен orphanRemoval=true для обоих методов @OneToMany getFooBars () из Foo и Bar. Для вашего конкретного сценария было бы достаточно добавить его в Foo, но вы, вероятно, захотите того же эффекта, когда удаляете foo из бара.
  2. Заключен вызов метода foo.removeBar(bar) внутри метода, аннотированного с помощью Spring's @Transactional. Вы можете поместить этот метод в новый класс @Service FooService.
    Причина: для работы orphanRemoval требуется активный транзакционный сеанс.
  3. Удален вызов barRepository.save(bar) после вызова foo.removeBar(bar).
    Теперь это излишне, потому что внутри транзакционного сеанса изменения сохраняются автоматически.

Сохранение Java 2.1. Глава 3.2.3

Операция удаления

• Если X - новый объект, он игнорируется операцией удаления. Однако операция удаления каскадно относится к объектам, на которые ссылается X, если связь между X и этими другими объектами аннотируется значением элемента аннотации cascade=REMOVE или cascade=ALL.

• Если X является управляемым объектом, операция удаления приводит к его удалению. Операция удаления каскадно относится к объектам, на которые ссылается X, если отношения между X и этими другими объектами аннотируются значением элемента аннотации cascade=REMOVE или cascade=ALL.

Проверьте, что вы уже используете операцию persist для вас сущностей Foo(или же FooBar или же Bar).

Другие вопросы по тегам