Имеет ли cascade = "all-delete-orphan" какое-либо значение в однонаправленной ассоциации "многие ко многим" с таблицей соединений Hibernate? - PullRequest
10 голосов
/ 30 сентября 2011

У меня есть два объекта, которые образуют отношения родитель-ребенок, которые имеют отношение многие ко многим. Следуя рекомендациям справочного руководства по Hibernate, я сопоставил это с помощью таблицы соединений:

<class name="Conference" table="conferences">
    ...
    <set name="speakers" table="conference_speakers" cascade="all">
        <key column="conference_id"/>
        <many-to-many class="Speaker" column="speaker_id"/>
    </set>
</class>

<class name="Speaker" table="speakers">
    <id name="id" column="id">
        <generator class="native"/>
    </id>
    <property name="firstName"/>
    <property name="lastName"/>
</class>

Мое желание заключается в том, чтобы один докладчик мог быть связан со многими различными конференциями, но также и то, что любой оратор, на который больше не ссылается какая-либо конференция, удаляется из таблицы speakers (поскольку докладчик без связанных конференций не имеют большое значение в моем проекте).

Однако я обнаружил, что если я использую cascade="all-delete-orphan", то, если динамик, связанный с несколькими конференциями, удаляется из только одной из них , Hibernate пытается удалить сам экземпляр Speaker.

Ниже приведен модульный тест, демонстрирующий это поведение:

@Test
public void testRemoveSharedSpeaker() {

    int initialCount = countRowsInTable("speakers");

    Conference c1 = new Conference("c1");
    Conference c2 = new Conference("c2");

    Speaker s = new Speaker("John", "Doe");

    c1.getSpeakers().add(s);
    c2.getSpeakers().add(s);

    conferenceDao.saveOrUpdate(c1);
    conferenceDao.saveOrUpdate(c2);
    flushHibernate();

    assertEquals(initialCount + 1, countRowsInTable("speakers"));
    assertEquals(2, countRowsInTable("conference_speakers"));

    // the remove:
    c1 = conferenceDao.get(c1.getId());
    c1.getSpeakers().remove(s);

    flushHibernate();

    assertEquals("count should stay the same", initialCount + 1, countRowsInTable("speakers"));
    assertEquals(1, countRowsInTable("conference_speakers"));

    c1 = conferenceDao.get(c1.getId());
    c2 = conferenceDao.get(c2.getId());

    assertEquals(0, c1.getSpeakers().size());
    assertEquals(1, c2.getSpeakers().size());
}

При обработке удаления s из c1.speakers выдается ошибка, поскольку Hibernate удаляет как строку в таблице объединения, так и строку таблицы speakers:

DEBUG org.hibernate.SQL - удалить из конференц-спикеров, где conference_id =? и speaker_id =?
DEBUG org.hibernate.SQL - удалить из динамиков, где id =?

Если я изменю cascade="all-delete-orphan" на cascade="all", тогда этот тест будет работать, как и ожидалось, хотя и приведет к нежелательному поведению, в результате которого я получу потерянные строки в моей таблице speakers.

Это заставляет меня задуматься - возможно ли, чтобы Hibernate знал, когда удалять осиротевшие объекты из дочерних отношений, но только тогда, когда другие родители не ссылаются на ребенка (независимо от того, являются ли эти родители в текущем Session)? Возможно, я неправильно использую cascade="all-delete-orphan"?

Я получаю точно такое же поведение, если использую аннотации JPA вместо сопоставления XML, например:

@ManyToMany(cascade = CascadeType.ALL)
@JoinTable(name = "conference_speakers",
        joinColumns = @JoinColumn(name = "conference_id"),
        inverseJoinColumns = @JoinColumn(name = "speaker_id"))
@org.hibernate.annotations.Cascade(org.hibernate.annotations.CascadeType.DELETE_ORPHAN)
private Set<Speaker> speakers = new HashSet<Speaker>();

Это с Hibernate 3.6.7. Кстати, между прочим.

1 Ответ

13 голосов
/ 30 сентября 2011

DELETE_ORPHAN каскадный режим не определен для отношения «многие ко многим» - только для «один ко многим» (последний содержит атрибут «orphanRemoval = true | false» в стандарте JPA @OneToMany, поэтому вы неприходится прибегать к проприетарной аннотации Hibernate).

Причина этого точно такая, как вы описали - Hibernate не может выяснить, действительно ли «осиротевший» конец отношения «многие ко многим» действительноосиротевший, не выполнив запрос к базе данных, которая является одновременно нелогичной и может (потенциально) может иметь серьезные последствия для производительности.

Описанное вами поведение гибернации, следовательно, является правильным (ну, «как задокументировано»);хотя в идеальном мире это предупредило бы вас о том, что DELETE_ORPHAN недопустимо для многих ко многим во время компиляции сопоставлений 2-го прохода.

Я не могу придумать хороший способ достижения того, что выхочу сделать, если честно.Самый простой (но зависящий от базы данных) способ, вероятно, состоит в том, чтобы определить триггер на удаление из conference_speakers, который бы проверял, является ли этот оратор "действительно" осиротевшим, и удалял его из speakers, если это так.Независимый от базы данных вариант - сделать то же самое вручную в DAO или слушателе.

Обновление: Вот выдержка из Документы Hibernate (Глава 11.11, сразу после серого)Обратите внимание на CascadeType.ALL), мои блики:

Специальный каскадный стиль, delete-orphan, применяется только к ассоциациям один-ко-многим и указывает, что удалениеОперация () должна применяться к любому дочернему объекту, который удален из ассоциации.

Далее:

Обычно не имеет смысла включать каскад для многихАссоциация "один к одному" или "многие ко многим".Фактически @ManyToOne и @ManyToMany даже не предлагают атрибут orphanRemoval.Каскадирование часто полезно для однозначных и однозначных ассоциаций.

...