我有两个对象,它们形成父子关系,具有多对多关系。按照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"
,那么如果与多个会议相关联的Speaker从中删除,只是其中一个, 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 - delete from conference_speakers where conference_id=?和speaker_id = ?
delete from speaker where 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。最后,顺便说一下
DELETE_ORPHAN级联模式不是为多对多关系定义的-仅用于一对多(后者在JPA标准@OneToMany
注释中具有"orphanRemoval=true|false"属性,因此您不必使用专有的Hibernate注释)。
这样做的原因正如你所描述的那样——Hibernate没有办法判断多对多关系的"孤立"端是否真的是孤立的,除非对数据库运行一个查询,这既违反直觉,又可能(潜在地)有严重的性能影响。
你所描述的Hibernate行为因此是正确的(好吧,"如文档所述");虽然在一个完美的世界里,它会提醒你一个事实,即DELETE_ORPHAN
在多对多映射编译期间是非法的。
conference_speakers
中删除时定义一个触发器,该触发器将检查该发言者是否"真正"为孤儿,如果是,则从speakers
中删除该发言者。与数据库无关的选项是在DAO或侦听器中手动执行相同的操作。
更新:这是Hibernate文档(第11.11章,在关于CascadeType.ALL的灰色注释之后)的摘录,亮点是我的:
进一步:一个特殊的级联样式,delete-orphan,只适用于一对多关联,并指示delete()操作应该被删除应用于从关联中删除的任何子对象。
在多对一或上启用级联通常没有意义多对多关联。事实上,@ManyToOne和@ manymany并没有甚至提供一个孤儿移除属性。级联通常用于一对一和一对多关联。