12

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.

matt b
  • 138,234
  • 66
  • 282
  • 345

1 Answers1

16

DELETE_ORPHAN cascade mode is not defined for many-to-many relationship - only for one-to-many (the latter sports a "orphanRemoval=true|false" attribute within JPA standard @OneToMany annotation, so you don't have to resort to proprietary Hibernate annotation).

The reason for this is exactly as you've described - there's no way for Hibernate to figure out whether "orphaned" end of the many-to-many relationship is truly orphaned without running a query against the database which is both counter-intuitive and can (potentially) have serious performance implications.

Hibernate behavior you've described is therefore correct (well, "as documented"); though in a perfect world it would have alerted you to the fact that DELETE_ORPHAN is illegal on many-to-many during 2nd pass mappings compilation.

I can't think of a good way of achieving what you want to do, to be honest. The easiest (but database-specific) way would likely be to define a trigger on deletion from conference_speakers that would check whether this speaker is "truly" orphaned and delete it from speakers if so. The database-independent option is to do the same thing manually in DAO or listener.

Update: Here's an excerpt from Hibernate docs (Chapter 11.11, right after gray Note on CascadeType.ALL), highlights are mine:

A special cascade style, delete-orphan, applies only to one-to-many associations, and indicates that the delete() operation should be applied to any child object that is removed from the association.

Further down:

It does not usually make sense to enable cascade on a many-to-one or many-to-many association. In fact the @ManyToOne and @ManyToMany don't even offer a orphanRemoval attribute. Cascading is often useful for one-to-one and one-to-many associations.

ChssPly76
  • 99,456
  • 24
  • 206
  • 195
  • Thanks for confirming what I was thinking about Hibernate having to issue another query to know if it was truly orphaned or not. Would you happen to know where in the documentation this is mentioned though, about many-to-many and delete-orphan? I racked my brain searching for any mention of combining the two. – matt b Sep 30 '11 at 18:00