I have two objects which form a parent-child relationship which have a many-to-many relationship. Following the recommendations in the Hibernate reference manual, I have mapped this using a join table:
<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>
My wish is that a single Speaker can be associated with many different Conferences, but also that any Speaker which is no longer referenced by any Conference is removed from the speakers
table (as a Speaker with no associated conferences doesn't have much meaning in my project).
However, I've found that if I use cascade="all-delete-orphan"
, then if a Speaker which is associated with multiple Conferences is removed from just one of them, Hibernate attempts to delete the Speaker instance itself.
Below is a unit test which shows this behavior:
@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());
}
An error is thrown when s
's removal from c1.speakers
is processed, because Hibernate is deleting both the row in the join table and the speakers
table row as well:
DEBUG org.hibernate.SQL - delete from conference_speakers where conference_id=? and speaker_id=?
DEBUG org.hibernate.SQL - delete from speakers where id=?
If I change cascade="all-delete-orphan"
to just cascade="all"
, then this test works as expected, although it leads to the undesired behavior where I will end up with orphaned rows in my speakers
table.
This leads me to wonder - is it even possible for Hibernate to know when to delete orphaned objects from the child-side of the relationship, but only when the child is not referenced by any other parents (whether or not those parents are in the current Session
)? Perhaps I am misusing cascade="all-delete-orphan"
?
I get the same exact behavior if I use JPA annotations instead of XML mapping such as:
@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>();
This is with Hibernate 3.6.7.Final, by the way.