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

У меня есть два объекта, которые формируют отношения между родителями и дочерними элементами, которые имеют отношение "многие ко многим". Следуя рекомендациям справочного руководства 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 сам.

Ниже приведена unit test, которая показывает это поведение:

@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_speakers, где 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.Final, кстати.

Ответ 1

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

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

Поэтому поведение спящего режима, которое вы описали, является правильным (хорошо, "как описано" ); хотя в идеальном мире он бы предупредил вас о том, что DELETE_ORPHAN является незаконным для многих-ко многим во время компиляции 2-го прохода.

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

Обновление: Здесь выдержка из Hibernate docs (глава 11.11, сразу после серого примечания на CascadeType.ALL), основные моменты мои:

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

Далее вниз:

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