刚刚接触Hibernate的人大多是从父子关系(parent / child type relationship)的建模入手的。父子关系的建模有两种方法。由于种种原因,最方便的方法是把Parent和Child都建模成实体类,并创建一个从Parent指向Child的<one-to-many>关联,对新手来说尤其如此。还有一种方法,就是将Child声明为一个<composite-element>(组合元素)。 事实上在Hibernate中one to many关联的默认语义远没有composite element贴近parent / child关系的通常语义。下面我们会阐述如何使用带有级联的双向一对多关联(bidirectional one to many association with cascades)去建立有效、优美的parent / child关系。这一点也不难!
Hibernate collections被当作其所属实体而不是其包含实体的一个逻辑部分。这非常重要!它主要体现为以下几点:
当删除或增加collection中对象的时候,collection所属者的版本值会递增。
如果一个从collection中移除的对象是一个值类型(value type)的实例,比如composite element,那么这个对象的持久化状态将会终止,其在数据库中对应的记录会被删除。同样的,向collection增加一个value type的实例将会使之立即被持久化。
另一方面,如果从一对多或多对多关联的collection中移除一个实体,在缺省情况下这个对象并不会被删除。这个行为是完全合乎逻辑的--改变一个实体的内部状态不应该使与它关联的实体消失掉!同样的,向collection增加一个实体不会使之被持久化。
实际上,向Collection增加一个实体的缺省动作只是在两个实体之间创建一个连接而已,同样移除的时候也只是删除连接。这种处理对于所有的情况都是合适的。对于父子关系则是完全不适合的,在这种关系下,子对象的生存绑定于父对象的生存周期。
假设我们要实现一个简单的从Parent到Child的<one-to-many>关联。
<set name="children"> <key column="parent_id"/> <one-to-many class="Child"/> </set>
如果我们运行下面的代码
Parent p = .....; Child c = new Child(); p.getChildren().add(c); session.save(c); session.flush();
Hibernate会产生两条SQL语句:
一条INSERT语句,为c创建一条记录
一条UPDATE语句,创建从p到c的连接
这样做不仅效率低,而且违反了列parent_id非空的限制。我们可以通过在集合类映射上指定not-null="true"来解决违反非空约束的问题:
<set name="children"> <key column="parent_id" not-null="true"/> <one-to-many class="Child"/> </set>
然而,这并非是推荐的解决方法。
这种现象的根本原因是从p到c的连接(外键parent_id)没有被当作Child对象状态的一部分,因而没有在INSERT语句中被创建。因此解决的办法就是把这个连接添加到Child的映射中。
<many-to-one name="parent" column="parent_id" not-null="true"/>
(我们还需要为类Child添加parent属性)
现在实体Child在管理连接的状态,为了使collection不更新连接,我们使用inverse属性。
<set name="children" inverse="true"> <key column="parent_id"/> <one-to-many class="Child"/> </set>
下面的代码是用来添加一个新的Child
Parent p = (Parent) session.load(Parent.class, pid); Child c = new Child(); c.setParent(p); p.getChildren().add(c); session.save(c); session.flush();
现在,只会有一条INSERT语句被执行!
为了让事情变得井井有条,可以为Parent加一个addChild()方法。
public void addChild(Child c) { c.setParent(this); children.add(c); }
现在,添加Child的代码就是这样
Parent p = (Parent) session.load(Parent.class, pid); Child c = new Child(); p.addChild(c); session.save(c); session.flush();
需要显式调用save()仍然很麻烦,我们可以用级联来解决这个问题。
<set name="children" inverse="true" cascade="all"> <key column="parent_id"/> <one-to-many class="Child"/> </set>
这样上面的代码可以简化为:
Parent p = (Parent) session.load(Parent.class, pid); Child c = new Child(); p.addChild(c); session.flush();
同样的,保存或删除Parent对象的时候并不需要遍历其子对象。 下面的代码会删除对象p及其所有子对象对应的数据库记录。
Parent p = (Parent) session.load(Parent.class, pid); session.delete(p); session.flush();
然而,这段代码
Parent p = (Parent) session.load(Parent.class, pid); Child c = (Child) p.getChildren().iterator().next(); p.getChildren().remove(c); c.setParent(null); session.flush();
不会从数据库删除c;它只会删除与p之间的连接(并且会导致违反NOT NULL约束,在这个例子中)。你需要显式调用delete()来删除Child。
Parent p = (Parent) session.load(Parent.class, pid); Child c = (Child) p.getChildren().iterator().next(); p.getChildren().remove(c); session.delete(c); session.flush();
在我们的例子中,如果没有父对象,子对象就不应该存在,如果将子对象从collection中移除,实际上我们是想删除它。要实现这种要求,就必须使用cascade="all-delete-orphan"。
<set name="children" inverse="true" cascade="all-delete-orphan"> <key column="parent_id"/> <one-to-many class="Child"/> </set>
注意:即使在collection一方的映射中指定inverse="true",级联仍然是通过遍历collection中的元素来处理的。如果你想要通过级联进行子对象的插入、删除、更新操作,就必须把它加到collection中,只调用setParent()是不够的。
假设我们从Session中装入了一个Parent对象,用户界面对其进行了修改,然后希望在一个新的Session里面调用update()来保存这些修改。对象Parent包含了子对象的集合,由于打开了级联更新,Hibernate需要知道哪些Child对象是新实例化的,哪些代表数据库中已经存在的记录。我们假设Parent和Child对象的标识属性都是自动生成的,类型为java.lang.Long。Hibernate会使用标识属性的值,和version 或 timestamp 属性,来判断哪些子对象是新的。(参见第 11.7 节 “自动状态检测”.) 在 Hibernate3 中,显式指定unsaved-value不再是必须的了。
下面的代码会更新parent和child对象,并且插入newChild对象。
//parent and child were both loaded in a previous session parent.addChild(child); Child newChild = new Child(); parent.addChild(newChild); session.update(parent); session.flush();
Well, that's all very well for the case of a generated identifier, but what about assigned identifiers and composite identifiers? This is more difficult, since Hibernate can't use the identifier property to distinguish between a newly instantiated object (with an identifier assigned by the user) and an object loaded in a previous session. In this case, Hibernate will either use the timestamp or version property, or will actually query the second-level cache or, worst case, the database, to see if the row exists.
这对于自动生成标识的情况是非常好的,但是自分配的标识和复合标识怎么办呢?这是有点麻烦,因为Hibernate没有办法区分新实例化的对象(标识被用户指定了)和前一个Session装入的对象。在这种情况下,Hibernate会使用timestamp或version属性,或者查询第二级缓存,或者最坏的情况,查询数据库,来确认是否此行存在。