Hibernate對象關聯--UML基礎知識、XDoclet---- 4 inverse And Cascading
inverse 和 Cascading 在對象關聯時,非常重要,官方文檔把它分散在幾個地方講解。下面搜集了多方文章,集中討論這個問題:
重點是理解:UML中的多向性、導向性與inverse ,cascading之間的關系;以及inverse,cascading對集合行為的影響力。
一、引用Huihoo翻譯Hibernate官方文檔
2.3. 第二部分 - 關聯映射
我們已經映射了一個持久化實體類到一個表上。讓我們在這個基礎上增加一些類之間的關聯性。 首先我們往我們程序里面增加人(people)的概念,并存儲他們所參與的一個Event列表。 (譯者注:與Event一樣,我們在后面的教程中將直接使用person來表示“人”而不是它的中文翻譯)
最初的Person類是簡單的:
public class Person {
private Long id;
private int age;
private String firstname;
private String lastname;
Person() {}
// Accessor methods for all properties, private setter for 'id'
}
Create a new mapping file called Person.hbm.xml:
<hibernate-mapping>
<class name="Person" table="PERSON">
<id name="id" column="PERSON_ID">
<generator class="increment"/>
</id>
<property name="age"/>
<property name="firstname"/>
<property name="lastname"/>
</class>
</hibernate-mapping>
Finally, add the new mapping to Hibernate's configuration:
<mapping resource="Event.hbm.xml"/>
<mapping resource="Person.hbm.xml"/>
我們現在將在這兩個實體類之間創建一個關聯。顯然,person可以參與一系列Event,而Event也有不同的參加者(person)。 設計上面我們需要考慮的問題是關聯的方向(directionality),階數(multiplicity)和集合(collection)的行為。
我們將向Person類增加一組Event。這樣我們可以輕松的通過調用aPerson.getEvents() 得到一個Person所參與的Event列表,而不必執行一個顯式的查詢。我們使用一個Java的集合類:一個Set,因為Set 不允許包括重復的元素而且排序和我們無關。
目前為止我們設計了一個單向的,在一端有許多值與之對應的關聯,通過Set來實現。 讓我們為這個在Java類里編碼并映射這個關聯:
public class Person {
private Set events = new HashSet();
public Set getEvents() {
return events;
}
public void setEvents(Set events) {
this.events = events;
}
}
在我們映射這個關聯之前,先考慮這個關聯另外一端。很顯然的,我們可以保持這個關聯是單向的。如果我們希望這個關聯是雙向的, 我們可以在Event里創建另外一個集合,例如:anEvent.getParticipants()。 這是留給你的一個設計選項,但是從這個討論中我們可以很清楚的了解什么是關聯的階數(multiplicity):在這個關聯的兩端都是“多”。 我們叫這個為:多對多(many-to-many)關聯。因此,我們使用Hibernate的many-to-many映射:
<class name="Person" table="PERSON">
<id name="id" column="PERSON_ID">
<generator class="increment"/>
</id>
<property name="age"/>
<property name="firstname"/>
<property name="lastname"/>
<set name="events" table="PERSON_EVENT">
<key column="PERSON_ID"/>
<many-to-many column="EVENT_ID" class="Event"/>
</set>
</class>
Hibernate支持所有種類的集合映射,<set>是最普遍被使用的。對于多對多(many-to-many)關聯(或者叫n:m實體關系), 需要一個用來儲存關聯的表(association table)。表里面的每一行代表從一個person到一個event的一個關聯。 表名是由set元素的table屬性值配置的。關聯里面的標識字段名,person的一端,是 由<key>元素定義,event一端的字段名是由<many-to-many>元素的 column屬性定義的。你也必須告訴Hibernate集合中對象的類(也就是位于這個集合所代表的關聯另外一端的類)。
這個映射的數據庫表定義如下:
_____________ __________________
| | | | _____________
| EVENTS | | PERSON_EVENT | | |
|_____________| |__________________| | PERSON |
| | | | |_____________|
| *EVENT_ID | <--> | *EVENT_ID | | |
| EVENT_DATE | | *PERSON_ID | <--> | *PERSON_ID |
| TITLE | |__________________| | AGE |
|_____________| | FIRSTNAME |
| LASTNAME |
|_____________|
讓我們把一些people和event放到EventManager的一個新方法中:
private void addPersonToEvent(Long personId, Long eventId) {
Session session = HibernateUtil.currentSession();
Transaction tx = session.beginTransaction();
Person aPerson = (Person) session.load(Person.class, personId);
Event anEvent = (Event) session.load(Event.class, eventId);
aPerson.getEvents().add(anEvent);
tx.commit();
HibernateUtil.closeSession();
}
在加載一個Person和一個Event之后,簡單的使用普通的方法修改集合。 如你所見,沒有顯式的update()或者save(), Hibernate自動檢測到集合已經被修改 并需要保存。這個叫做automatic dirty checking,你也可以嘗試修改任何對象的name或者date的參數。 只要他們處于persistent狀態,也就是被綁定在某個Hibernate Session上(例如:他們 剛剛在一個單元操作從被加載或者保存),Hibernate監視任何改變并在后臺隱式執行SQL。同步內存狀態和數據庫的過程,通常只在 一個單元操作結束的時候發生,這個過程被叫做flushing。
你當然也可以在不同的單元操作里面加載person和event。或者在一個Session以外修改一個 不是處在持久化(persistent)狀態下的對象(如果該對象以前曾經被持久化,我們稱這個狀態為脫管(detached))。 在程序里,看起來像下面這樣:
private void addPersonToEvent(Long personId, Long eventId) {
Session session = HibernateUtil.currentSession();
Transaction tx = session.beginTransaction();
Person aPerson = (Person) session.load(Person.class, personId);
Event anEvent = (Event) session.load(Event.class, eventId);
tx.commit();
HibernateUtil.closeSession();
aPerson.getEvents().add(anEvent); // aPerson is detached
Session session2 = HibernateUtil.currentSession();
Transaction tx2 = session.beginTransaction();
session2.update(aPerson); // Reattachment of aPerson
tx2.commit();
HibernateUtil.closeSession();
}
對update的調用使一個脫管對象(detached object)重新持久化,你可以說它被綁定到 一個新的單元操作上,所以任何你對它在脫管(detached)狀態下所做的修改都會被保存到數據庫里。
這個對我們當前的情形不是很有用,但是它是非常重要的概念,你可以把它設計進你自己的程序中。現在,加進一個新的 選項到EventManager的main方法中,并從命令行運行它來完成這個練習。如果你需要一個person和 一個event的標識符 - save()返回它。*******這最后一句看不明白
上面是一個關于兩個同等地位的類間關聯的例子,這是在兩個實體之間。像前面所提到的那樣,也存在其它的特別的類和類型,這些類和類型通常是“次要的”。 其中一些你已經看到過,好像int或者String。我們稱呼這些類為值類型(value type), 它們的實例依賴(depend)在某個特定的實體上。這些類型的實例沒有自己的身份(identity),也不能在實體間共享 (比如兩個person不能引用同一個firstname對象,即使他們有相同的名字)。當然,value types并不僅僅在JDK中存在 (事實上,在一個Hibernate程序中,所有的JDK類都被視為值類型),你也可以寫你自己的依賴類,例如Address, MonetaryAmount。
你也可以設計一個值類型的集合(collection of value types),這個在概念上與實體的集合有很大的不同,但是在Java里面看起來幾乎是一樣的。
我們把一個值類型對象的集合加入Person。我們希望保存email地址,所以我們使用String, 而這次的集合類型又是Set:
private Set emailAddresses = new HashSet();
public Set getEmailAddresses() {
return emailAddresses;
}
public void setEmailAddresses(Set emailAddresses) {
this.emailAddresses = emailAddresses;
}
Set的映射
<set name="emailAddresses" table="PERSON_EMAIL_ADDR">
<key column="PERSON_ID"/>
<element type="string" column="EMAIL_ADDR"/>
</set>
比較這次和較早先的映射,差別主要在element部分這次并沒有包括對其它實體類型的引用,而是使用一個元素類型是 String的集合(這里使用小寫的名字是向你表明它是一個Hibernate的映射類型或者類型轉換器)。 和以前一樣,set的table參數決定用于集合的數據庫表名。key元素 定義了在集合表中使用的外鍵。element元素的column參數定義實際保存String值 的字段名。
看一下修改后的數據庫表定義。
_____________ __________________
| | | | _____________
| EVENTS | | PERSON_EVENT | | | ___________________
|_____________| |__________________| | PERSON | | |
| | | | |_____________| | PERSON_EMAIL_ADDR |
| *EVENT_ID | <--> | *EVENT_ID | | | |___________________|
| EVENT_DATE | | *PERSON_ID | <--> | *PERSON_ID | <--> | *PERSON_ID |
| TITLE | |__________________| | AGE | | *EMAIL_ADDR |
|_____________| | FIRSTNAME | |___________________|
| LASTNAME |
|_____________|
你可以看到集合表(collection table)的主鍵實際上是個復合主鍵,同時使用了2個字段。這也暗示了對于同一個 person不能有重復的email地址,這正是Java里面使用Set時候所需要的語義(Set里元素不能重復)。
你現在可以試著把元素加入這個集合,就像我們在之前關聯person和event的那樣。Java里面的代碼是相同的。
下面我們將映射一個雙向關聯(bi-directional association)- 在Java里面讓person和event可以從關聯的 任何一端訪問另一端。當然,數據庫表定義沒有改變,我們仍然需要多對多(many-to-many)的階數(multiplicity)。一個關系型數據庫要比網絡編程語言 更加靈活,所以它并不需要任何像導航方向(navigation direction)的東西 - 數據可以用任何可能的方式進行查看和獲取。
首先,把一個參與者(person)的集合加入Event類中:
private Set participants = new HashSet();
public Set getParticipants() {
return participants;
}
public void setParticipants(Set participants) {
this.participants = participants;
}
在Event.hbm.xml里面也映射這個關聯。
<set name="participants" table="PERSON_EVENT" inverse="true">
<key column="EVENT_ID"/>
<many-to-many column="PERSON_ID" class="Person"/>
</set>
如你所見,2個映射文件里都有通常的set映射。注意key和many-to-many 里面的字段名在兩個映射文件中是交換的。這里最重要的不同是Event映射文件里set元素的 inverse="true"參數。
這個表示Hibernate需要在兩個實體間查找關聯信息的時候,應該使用關聯的另外一端 - Person類。 這將會極大的幫助你理解雙向關聯是如何在我們的兩個實體間創建的。
首先,請牢記在心,Hibernate并不影響通常的Java語義。 在單向關聯中,我們是怎樣在一個Person和一個Event之間創建聯系的? 我們把一個Event的實例加到一個Person類內的Event集合里。所以,顯然如果我們要讓這個關聯可以雙向工作, 我們需要在另外一端做同樣的事情 - 把Person加到一個Event類內的Person集合中。 這“在關聯的兩端設置聯系”是絕對必要的而且你永遠不應該忘記做它。
許多開發者通過創建管理關聯的方法來保證正確的設置了關聯的兩端,比如在Person里:
protected Set getEvents() {
return events;
}
protected void setEvents(Set events) {
this.events = events;
}
public void addToEvent(Event event) {
this.getEvents().add(event);
event.getParticipants().add(this);
}
public void removeFromEvent(Event event) {
this.getEvents().remove(event);
event.getParticipants().remove(this);
}
注意現在對于集合的get和set方法的訪問控制級別是protected - 這允許在位于同一個包(package)中的類以及繼承自這個類的子類 可以訪問這些方法,但是禁止其它的直接外部訪問,避免了集合的內容出現混亂。你應該盡可能的在集合所對應的另外一端也這樣做。
inverse映射參數究竟表示什么呢?對于你和對于Java來說,一個雙向關聯僅僅是在兩端簡單的設置引用。然而僅僅這樣 Hibernate并沒有足夠的信息去正確的產生INSERT和UPDATE語句(以避免違反數據庫約束), 所以Hibernate需要一些幫助來正確的處理雙向關聯。把關聯的一端設置為inverse將告訴Hibernate忽略關聯的 這一端,把這端看成是另外一端的一個鏡子(mirror)。這就是Hibernate所需的信息,Hibernate用它來處理如何把把 一個數據導航模型映射到關系數據庫表定義。 你僅僅需要記住下面這個直觀的規則:所有的雙向關聯需要有一端被設置為inverse。在一個一對多(one-to-many)關聯中 它必須是代表多(many)的那端。而在多對多(many-to-many)關聯中,你可以任意選取一端,兩端之間并沒有差別。
22.1. 關于collections需要注意的一點
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屬性,或者查詢第二級緩存,或 者最壞的情況,查詢數據庫,來確認是否此行存在。
1、inverse類似UML圖中的導向性.one-many時,如果你設計的類是先整體,后部分,那么整體一端應該inverse為true。
many-many時,如果其中一端是業務語義的根,另一端的存在依附于根,那么根這一端的inverse為true,否則配置為false.如果兩端都配置為true,關聯表中就無法寫入數據;如果兩端都配置為false,你就不能同時a.addB(b),b.addA(a)的方式互相放入對方的集合中,否則會發生沖突。
2、Cascading配置了類之間的行為影響程度。本文引用的第二大節講的很清楚。在實際使用時,還是要注意其用法與業務的關聯:
如果只刪除關聯關系,通常配置save-update / none
如果把子端全部刪除,配置為all。
3、Set在使用時。many-one,刪除one時,cascading必須為all。如果是save-update,none都會出現“違反完整約束條件”的錯誤。
many-many(A-B)時,A方cascading為all時,刪除A類,B、AB關聯信息都會被刪除。
A方cascading為save-update時,刪除A類,AB關聯信息會被刪除,B保留。
A方cascading為none時,刪除A類,B、AB關聯信息都會保留。
4、List使用時,首先必須確保index的值必須設置,否則違背了List順序位置的概念,無法完成刪除邏輯的正確性,且會throw“違反完整約束條件”的錯誤。
在正確給定了index后,其行為與Set相同。
posted on 2006-03-12 11:16 西部村里人 閱讀(838) 評論(0) 編輯 收藏 所屬分類: Hibernate