hibernate-orm/reference/zh-cn/modules/example_parentchild.xml

306 lines
14 KiB
XML
Raw Normal View History

<chapter id="example-parentchild">
<title>示例:父子关系(Parent Child Relationships)</title>
<para>
刚刚接触Hibernate的人大多是从父子关系parent / child type relationship的建模入手的。父子关系的建模有两种方法。由于种种原因最方便的方法是把<literal>Parent</literal><literal>Child</literal>都建模成实体类,并创建一个从<literal>Parent</literal>指向<literal>Child</literal>&lt;one-to-many&gt;关联,对新手来说尤其如此。还有一种方法,就是将<literal>Child</literal>声明为一个<literal>&lt;composite-element&gt;</literal>(组合元素)。 事实上在Hibernate中one to many关联的默认语义远没有composite element贴近parent / child关系的通常语义。下面我们会阐述如何使用<emphasis>带有级联的双向一对多关联(bidirectional one to many association with cascades)</emphasis>去建立有效、优美的parent / child关系。这一点也不难
</para>
<sect1 id="example-parentchild-collections">
<title>关于collections需要注意的一点</title>
<para>
Hibernate collections被当作其所属实体而不是其包含实体的一个逻辑部分。这非常重要它主要体现为以下几点
</para>
<itemizedlist>
<listitem>
<para>
当删除或增加collection中对象的时候collection所属者的版本值会递增。
</para>
</listitem>
<listitem>
<para>
如果一个从collection中移除的对象是一个值类型(value type)的实例比如composite element那么这个对象的持久化状态将会终止其在数据库中对应的记录会被删除。同样的向collection增加一个value type的实例将会使之立即被持久化。
</para>
</listitem>
<listitem>
<para>
另一方面如果从一对多或多对多关联的collection中移除一个实体在缺省情况下这个对象并不会被删除。这个行为是完全合乎逻辑的改变一个实体的内部状态不应该使与它关联的实体消失掉同样的向collection增加一个实体不会使之被持久化。
</para>
</listitem>
</itemizedlist>
<para>
实际上向Collection增加一个实体的缺省动作只是在两个实体之间创建一个连接而已同样移除的时候也只是删除连接。这种处理对于所有的情况都是合适的。对于父子关系则是完全不适合的在这种关系下子对象的生存绑定于父对象的生存周期。
</para>
</sect1>
<sect1 id="example-parentchild-bidir">
<title>双向的一对多关系(Bidirectional one-to-many)</title>
<para>
假设我们要实现一个简单的从Parent到Child的&lt;one-to-many&gt;关联。
</para>
<programlisting><![CDATA[<set name="children">
<key column="parent_id"/>
<one-to-many class="Child"/>
</set>]]></programlisting>
<para>
如果我们运行下面的代码
</para>
<programlisting><![CDATA[Parent p = .....;
Child c = new Child();
p.getChildren().add(c);
session.save(c);
session.flush();]]></programlisting>
<para>
Hibernate会产生两条SQL语句
</para>
<itemizedlist>
<listitem>
<para>一条<literal>INSERT</literal>语句,为<literal>c</literal>创建一条记录</para>
</listitem>
<listitem>
<para>
一条<literal>UPDATE</literal>语句,创建从<literal>p</literal><literal>c</literal>的连接
</para>
</listitem>
</itemizedlist>
<para>
这样做不仅效率低,而且违反了列<literal>parent_id</literal>非空的限制。我们可以通过在集合类映射上指定<literal>not-null="true"</literal>来解决违反非空约束的问题:
</para>
<programlisting><![CDATA[<set name="children">
<key column="parent_id" not-null="true"/>
<one-to-many class="Child"/>
</set>]]></programlisting>
<para>
然而,这并非是推荐的解决方法。
</para>
<para> 这种现象的根本原因是从<literal>p</literal><literal>c</literal>的连接外键parent_id没有被当作<literal>Child</literal>对象状态的一部分因而没有在INSERT语句中被创建。因此解决的办法就是把这个连接添加到Child的映射中。
</para>
<programlisting><![CDATA[<many-to-one name="parent" column="parent_id" not-null="true"/>]]></programlisting>
<para>
(我们还需要为类<literal>Child</literal>添加<literal>parent</literal>属性)
</para>
<para>
现在实体<literal>Child</literal>在管理连接的状态为了使collection不更新连接我们使用<literal>inverse</literal>属性。
</para>
<programlisting><![CDATA[<set name="children" inverse="true">
<key column="parent_id"/>
<one-to-many class="Child"/>
</set>]]></programlisting>
<para>
下面的代码是用来添加一个新的<literal>Child</literal>
</para>
<programlisting><![CDATA[Parent p = (Parent) session.load(Parent.class, pid);
Child c = new Child();
c.setParent(p);
p.getChildren().add(c);
session.save(c);
session.flush();]]></programlisting>
<para>
现在,只会有一条<literal>INSERT</literal>语句被执行!
</para>
<para>
为了让事情变得井井有条,可以为<literal>Parent</literal>加一个<literal>addChild()</literal>方法。
</para>
<programlisting><![CDATA[public void addChild(Child c) {
c.setParent(this);
children.add(c);
}]]></programlisting>
<para>
现在,添加<literal>Child</literal>的代码就是这样
</para>
<programlisting><![CDATA[Parent p = (Parent) session.load(Parent.class, pid);
Child c = new Child();
p.addChild(c);
session.save(c);
session.flush();]]></programlisting>
</sect1>
<sect1 id="example-parentchild-cascades">
<title>级联生命周期Cascading lifecycle</title>
<para>
需要显式调用<literal>save()</literal>仍然很麻烦,我们可以用级联来解决这个问题。
</para>
<programlisting><![CDATA[<set name="children" inverse="true" cascade="all">
<key column="parent_id"/>
<one-to-many class="Child"/>
</set>]]></programlisting>
<para>
这样上面的代码可以简化为:
</para>
<programlisting><![CDATA[Parent p = (Parent) session.load(Parent.class, pid);
Child c = new Child();
p.addChild(c);
session.flush();]]></programlisting>
<para>
同样的,保存或删除<literal>Parent</literal>对象的时候并不需要遍历其子对象。
下面的代码会删除对象<literal>p</literal>及其所有子对象对应的数据库记录。
</para>
<programlisting><![CDATA[Parent p = (Parent) session.load(Parent.class, pid);
session.delete(p);
session.flush();]]></programlisting>
<para>
然而,这段代码
</para>
<programlisting><![CDATA[Parent p = (Parent) session.load(Parent.class, pid);
Child c = (Child) p.getChildren().iterator().next();
p.getChildren().remove(c);
c.setParent(null);
session.flush();]]></programlisting>
<para>
不会从数据库删除<literal>c</literal>;它只会删除与<literal>p</literal>之间的连接(并且会导致违反<literal>NOT NULL</literal>约束,在这个例子中)。你需要显式调用<literal>delete()</literal>来删除<literal>Child</literal><!--因为Hibernate并没有设计垃圾回收器代码如下-->
</para>
<programlisting><![CDATA[Parent p = (Parent) session.load(Parent.class, pid);
Child c = (Child) p.getChildren().iterator().next();
p.getChildren().remove(c);
session.delete(c);
session.flush();]]></programlisting>
<para>
在我们的例子中如果没有父对象子对象就不应该存在如果将子对象从collection中移除实际上我们是想删除它。要实现这种要求就必须使用<literal>cascade="all-delete-orphan"</literal>
</para>
<programlisting><![CDATA[<set name="children" inverse="true" cascade="all-delete-orphan">
<key column="parent_id"/>
<one-to-many class="Child"/>
</set>]]></programlisting>
<para>
注意即使在collection一方的映射中指定<literal>inverse="true"</literal>级联仍然是通过遍历collection中的元素来处理的。如果你想要通过级联进行子对象的插入、删除、更新操作就必须把它加到collection中只调用<literal>setParent()</literal>是不够的。
</para>
</sect1>
<sect1 id="example-parentchild-update">
<title>级联与<literal>未保存值</literal>Cascades and <literal>unsaved-value</literal></title>
<para>
假设我们从<literal>Session</literal>中装入了一个<literal>Parent</literal>对象用户界面对其进行了修改然后希望在一个新的Session里面调用<literal>update()</literal>来保存这些修改。对象<literal>Parent</literal>包含了子对象的集合由于打开了级联更新Hibernate需要知道哪些Child对象是新实例化的哪些代表数据库中已经存在的记录。我们假设<literal>Parent</literal><literal>Child</literal>对象的标识属性都是自动生成的,类型为<literal>java.lang.Long</literal>。Hibernate会使用标识属性的值和version 或 timestamp 属性,来判断哪些子对象是新的。(参见<xref linkend="objectstate-saveorupdate"/>.) <emphasis>在 Hibernate3 中,显式指定<literal>unsaved-value</literal>不再是必须的了。</emphasis>
</para>
<para>
下面的代码会更新<literal>parent</literal><literal>child</literal>对象,并且插入<literal>newChild</literal>对象。
</para>
<programlisting><![CDATA[//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();]]></programlisting>
<para>
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.
</para>
<para>
这对于自动生成标识的情况是非常好的但是自分配的标识和复合标识怎么办呢这是有点麻烦因为Hibernate没有办法区分新实例化的对象标识被用户指定了和前一个Session装入的对象。在这种情况下Hibernate会使用timestamp或version属性或者查询第二级缓存或者最坏的情况查询数据库来确认是否此行存在。</para>
<!-- undocumenting
<para>
还有一种可能情况,有一个名为<literal>isUnsaved()</literal><literal>新的拦截器Interceptor</literal>方法,它允许应用程序自己实现新实例的判断。比如,你可以自己定义一个持久类的祖先类:
</para>
<programlisting><![CDATA[public class Persistent {
private boolean _saved = false;
public void onSave() {
_saved=true;
}
public void onLoad() {
_saved=true;
}
......
public boolean isSaved() {
return _saved;
}
}]]></programlisting>
<para>
<literal>saved</literal>属性是不会被持久化的。)
现在在<literal>onLoad()</literal><literal>onSave()</literal>外,还要实现<literal>isUnsaved()</literal>
</para>
<programlisting><![CDATA[public Boolean isUnsaved(Object entity) {
if (entity instanceof Persistent) {
return new Boolean( !( (Persistent) entity ).isSaved() );
}
else {
return null;
}
}
public boolean onLoad(Object entity,
Serializable id,
Object[] state,
String[] propertyNames,
Type[] types) {
if (entity instanceof Persistent) ( (Persistent) entity ).onLoad();
return false;
}
public boolean onSave(Object entity,
Serializable id,
Object[] state,
String[] propertyNames,
Type[] types) {
if (entity instanceof Persistent) ( (Persistent) entity ).onSave();
return false;
}]]></programlisting>
<para>
Don't worry; in Hibernate3 you don't need to write any of this kind of code if you don't want to.
别担心在Hibernate3中假若你不愿意你不需要编写任何这类代码。
</para>
-->
</sect1>
<sect1 id="example-parentchild-conclusion">
<title>结论</title>
<para> 这里有不少东西需要融会贯通可能会让新手感到迷惑。但是在实践中它们都工作地非常好。大部分Hibernate应用程序都会经常用到父子对象模式。
</para>
<para>
在第一段中我们曾经提到另一个方案。上面的这些问题都不会出现在<literal>&lt;composite-element&gt;</literal>映射中,它准确地表达了父子关系的语义。很不幸复合元素还有两个重大限制:复合元素不能拥有collections并且除了用于惟一的父对象外它们不能再作为其它任何实体的子对象。
</para>
</sect1>
</chapter>