(譯者注:在閱讀本章的時候,以后整個手冊的閱讀過程中,我們都會面臨一個名詞方面的問題,那就是“集合”。"Collections"和"Set"在中文里對應(yīng)都被翻譯為“集合”,但是他們的含義很不一樣。Collections是一個超集,Set是其中的一種。大部分情況下,本譯稿中泛指的未加英文注明的“集合”,都應(yīng)當(dāng)理解為“Collections”。在有些二者同時出現(xiàn),可能造成混淆的地方,我們用“集合類”來特指“Collecions”,“集合(Set)”來指"Set",一般都會在后面的括號中給出英文。希望大家在閱讀時聯(lián)系上下文理解,不要造成誤解。 與此同時,“元素”一詞對應(yīng)的英文“element”,也有兩個不同的含義。其一為集合的元素,是內(nèi)存中的一個變量;另一含義則是XML文檔中的一個標(biāo)簽所代表的元素。也請注意區(qū)別。 本章中,特別是后半部分是需要反復(fù)閱讀才能理解清楚的。如果遇到任何疑問,請記住,英文版本的reference是惟一標(biāo)準(zhǔn)的參考資料。)
Hibernate要求持久化集合值字段必須聲明為接口,比如:
public class Product { private String serialNumber; private Set parts = new HashSet(); public Set getParts() { return parts; } void setParts(Set parts) { this.parts = parts; } public String getSerialNumber() { return serialNumber; } void setSerialNumber(String sn) { serialNumber = sn; } }
實(shí)際的接口可能是java.util.Set, java.util.Collection, java.util.List, java.util.Map, java.util.SortedSet, java.util.SortedMap 或者...任何你喜歡的類型!("任何你喜歡的類型" 代表你需要編寫 org.hibernate.usertype.UserCollectionType的實(shí)現(xiàn).)
注意我們是如何用一個HashSet實(shí)例來初始化實(shí)例變量的.這是用于初始化新創(chuàng)建(尚未持久化)的類實(shí)例中集合值屬性的最佳方法。當(dāng)你持久化這個實(shí)例時——比如通過調(diào)用persist()——Hibernate 會自動把HashSet替換為Hibernate自己的Set實(shí)現(xiàn)。觀察下面的錯誤:
Cat cat = new DomesticCat(); Cat kitten = new DomesticCat(); .... Set kittens = new HashSet(); kittens.add(kitten); cat.setKittens(kittens); session.persist(cat); kittens = cat.getKittens(); //Okay, kittens collection is a Set (HashSet) cat.getKittens(); //Error!
根據(jù)不同的接口類型,被Hibernate注射的持久化集合類的表現(xiàn)類似HashMap, HashSet, TreeMap, TreeSet or ArrayList。
集合類實(shí)例具有值類型的通常行為。當(dāng)被持久化對象引用后,他們會自動被持久化,當(dāng)不再被引用后,自動被刪除。假若實(shí)例被從一個持久化對象傳遞到另一個,它的元素可能從一個表轉(zhuǎn)移到另一個表。兩個實(shí)體不能共享同一個集合類實(shí)例的引用。因?yàn)榈讓雨P(guān)系數(shù)據(jù)庫模型的原因,集合值屬性無法支持空值語義;Hibernate對空的集合引用和空集合不加區(qū)別。
你不需要過多的為此擔(dān)心。就如同你平時使用普通的Java集合類一樣來使用持久化集合類。只是要確認(rèn)你理解了雙向關(guān)聯(lián)的語義(后文討論)。
用于映射集合類的Hibernate映射元素取決于接口的類型。比如, <set> 元素用來映射Set類型的屬性。
<class name="Product"> <id name="serialNumber" column="productSerialNumber"/> <set name="parts"> <key column="productSerialNumber" not-null="true"/> <one-to-many class="Part"/> </set> </class>
除了<set>,還有<list>, <map>, <bag>, <array> 和 <primitive-array> 映射元素。<map>具有代表性:
<map name="propertyName" (1) table="table_name" (2) schema="schema_name" (3) lazy="true|false" (4) inverse="true|false" (5) cascade="all|none|save-update|delete|all-delete-orphan" (6) sort="unsorted|natural|comparatorClass" (7) order-by="column_name asc|desc" (8) where="arbitrary sql where condition" (9) fetch="join|select|subselect" (10) batch-size="N" (11) access="field|property|ClassName" (12) optimistic-lock="true|false" (13) node="element-name|." embed-xml="true|false" > <key .... /> <map-key .... /> <element .... /> </map>
(1) |
name 集合屬性的名稱 |
(2) |
table (可選——默認(rèn)為屬性的名稱)這個集合表的名稱(不能在一對多的關(guān)聯(lián)關(guān)系中使用) |
(3) |
schema (可選) 表的schema的名稱, 他將覆蓋在根元素中定義的schema |
(4) |
lazy (可選--默認(rèn)為true) 可以用來關(guān)閉延遲加載,指定一直使用預(yù)先抓取(對數(shù)組不適用) |
(5) |
inverse (可選——默認(rèn)為false) 標(biāo)記這個集合作為雙向關(guān)聯(lián)關(guān)系中的方向一端。 |
(6) |
cascade (可選——默認(rèn)為none) 讓操作級聯(lián)到子實(shí)體 |
(7) |
sort(可選)指定集合的排序順序, 其可以為自然的(natural)或者給定一個用來比較的類。 |
(8) |
order-by (可選, 僅用于jdk1.4) 指定表的字段(一個或幾個)再加上asc或者desc(可選), 定義Map,Set和Bag的迭代順序 |
(9) |
where (可選) 指定任意的SQL where條件, 該條件將在重新載入或者刪除這個集合時使用(當(dāng)集合中的數(shù)據(jù)僅僅是所有可用數(shù)據(jù)的一個子集時這個條件非常有用) |
(10) |
fetch (可選, 默認(rèn)為select) 用于在外連接抓取、通過后續(xù)select抓取和通過后續(xù)subselect抓取之間選擇。 |
(11) |
batch-size (可選, 默認(rèn)為1) 指定通過延遲加載取得集合實(shí)例的批處理塊大小("batch size")。 |
(12) |
access(可選-默認(rèn)為屬性property):Hibernate取得屬性值時使用的策略 |
(12) |
樂觀鎖 (可選 - 默認(rèn)為 true): 對集合的狀態(tài)的改變會是否導(dǎo)致其所屬的實(shí)體的版本增長。 (對一對多關(guān)聯(lián)來說,關(guān)閉這個屬性常常是有理的) |
集合實(shí)例在數(shù)據(jù)庫中依靠持有集合的實(shí)體的外鍵加以辨別。此外鍵作為集合關(guān)鍵字段(collection key column)(或多個字段)加以引用。集合關(guān)鍵字段通過<key> 元素映射。
在外鍵字段上可能具有非空約束。對于大多數(shù)集合來說,這是隱含的。對單向一對多關(guān)聯(lián)來說,外鍵字段默認(rèn)是可以為空的,因此你可能需要指明 not-null="true"。
<key column="productSerialNumber" not-null="true"/>
外鍵約束可以使用ON DELETE CASCADE。
<key column="productSerialNumber" on-delete="cascade"/>
對<key> 元素的完整定義,請參閱前面的章節(jié)。
集合幾乎可以包含任何其他的Hibernate類型,包括所有的基本類型、自定義類型、組件,當(dāng)然還有對其他實(shí)體的引用。存在一個重要的區(qū)別:位于集合中的對象可能是根據(jù)“值”語義來操作(其聲明周期完全依賴于集合持有者),或者它可能是指向另一個實(shí)體的引用,具有其自己的生命周期。在后者的情況下,被作為集合持有的狀態(tài)考慮的,只有兩個對象之間的“連接”。
被包容的類型被稱為集合元素類型(collection element type)。集合元素通過<element>或<composite-element>映射,或在其是實(shí)體引用的時候,通過<one-to-many> 或<many-to-many>映射。前兩種用于使用值語義映射元素,后兩種用于映射實(shí)體關(guān)聯(lián)。
所有的集合映射,除了set和bag語義的以外,都需要指定一個集合表的索引字段(index column)——用于對應(yīng)到數(shù)組索引,或者List的索引,或者Map的關(guān)鍵字。通過<map-key>,Map 的索引可以是任何基礎(chǔ)類型;若通過<map-key-many-to-many>,它也可以是一個實(shí)體引用;若通過<composite-map-key>,它還可以是一個組合類型。數(shù)組或列表的索引必須是integer類型,并且使用 <list-index>元素定義映射。被映射的字段包含有順序排列的整數(shù)(默認(rèn)從0開始)。
<map-key column="column_name" (1) formula="any SQL expression" (2) type="type_name" (3) node="@attribute-name" length="N"/>
(1) |
column(可選):保存集合索引值的字段名。 |
(2) |
formula (可選): 用于計算map關(guān)鍵字的SQL公式 |
(3) |
type (可選,默認(rèn)為整型integer):集合索引的類型。 |
<map-key-many-to-many column="column_name" (1) formula="any SQL expression" (2)(3) class="ClassName" />
(1) |
column(可選):集合索引值中外鍵字段的名稱 |
(2) |
formula (可選): 用于計算map關(guān)鍵字的外鍵的SQL公式 |
(3) |
class (必需):集合的索引使用的實(shí)體類。 |
假若你的表沒有一個索引字段,當(dāng)你仍然希望使用List作為屬性類型,你應(yīng)該把此屬性映射為Hibernate <bag>。從數(shù)據(jù)庫中獲取的時候,bag不維護(hù)其順序,但也可選擇性的進(jìn)行排序。
從集合類可以產(chǎn)生很大一部分映射,覆蓋了很多常見的關(guān)系模型。我們建議你試驗(yàn)schema生成工具,來體會一下不同的映射聲明是如何被翻譯為數(shù)據(jù)庫表的。
任何值集合或者多對多關(guān)聯(lián)需要專用的具有一個或多個外鍵字段的collection table、一個或多個collection element column,以及還可能有一個或多個索引字段。
對于一個值集合, 我們使用<element>標(biāo)簽。
<element column="column_name" (1) formula="any SQL expression" (2) type="typename" (3) length="N" precision="N" scale="N" not-null="true|false" unique="true|false" node="element-name" />
(1) |
column(可選):保存集合元素值的字段名。 |
(2) |
formula (可選): 用于計算元素的SQL公式 |
(3) |
type (必需):集合元素的類型 |
多對多關(guān)聯(lián)(many-to-many association) 使用 <many-to-many>元素定義.
<many-to-many column="column_name" (1) formula="any SQL expression" (2) class="ClassName" (3) fetch="select|join" (4) unique="true|false" (5) not-found="ignore|exception" (6) entity-name="EntityName" (7) node="element-name" embed-xml="true|false" />
(1) |
column(可選): 這個元素的外鍵關(guān)鍵字段名 |
(2) |
formula (可選): 用于計算元素外鍵值的SQL公式. |
(3) |
class (必需): 關(guān)聯(lián)類的名稱 |
(3) |
outer-join (可選 - 默認(rèn)為auto): 在Hibernate系統(tǒng)參數(shù)中hibernate.use_outer_join被打開的情況下,該參數(shù)用來允許使用outer join來載入此集合的數(shù)據(jù)。 |
(4) |
為此關(guān)聯(lián)打開外連接抓取或者后續(xù)select抓取。這是特殊情況;對于一個實(shí)體及其指向其他實(shí)體的多對多關(guān)聯(lián)進(jìn)全預(yù)先抓取(使用一條單獨(dú)的SELECT),你不僅需要對集合自身打開join,也需要對<many-to-many>這個內(nèi)嵌元素打開此屬性。 |
(5) |
對外鍵字段允許DDL生成的時候生成一個惟一約束。這使關(guān)聯(lián)變成了一個高效的一對多關(guān)聯(lián)。(此句存疑:原文為This makes the association multiplicity effectively one to many.) |
(6) |
not-found (可選 - 默認(rèn)為 exception): 指明引用的外鍵中缺少某些行該如何處理: ignore 會把缺失的行作為一個空引用處理。 |
(7) |
entity-name (可選): 被關(guān)聯(lián)的類的實(shí)體名,作為class的替代。 |
例子:首先, 一組字符串:
<set name="names" table="NAMES"> <key column="GROUPID"/> <element column="NAME" type="string"/> </set>
包含一組整數(shù)的bag(還設(shè)置了order-by參數(shù)指定了迭代的順序):
<bag name="sizes" table="item_sizes" order-by="size asc"> <key column="item_id"/> <element column="size" type="integer"/> </bag>
一個實(shí)體數(shù)組,在這個案例中是一個多對多的關(guān)聯(lián)(注意這里的實(shí)體是自動管理生命周期的對象(lifecycle objects),cascade="all"):
<array name="addresses" table="PersonAddress" cascade="persist"> <key column="personId"/> <list-index column="sortOrder"/> <many-to-many column="addressId" class="Address"/> </array>
一個map,通過字符串的索引來指明日期:
<map name="holidays" table="holidays" schema="dbo" order-by="hol_name asc"> <key column="id"/> <map-key column="hol_name" type="string"/> <element column="hol_date" type="date"/> </map>
一個組件的列表:(下一章討論)
<list name="carComponents" table="CarComponents"> <key column="carId"/> <list-index column="sortOrder"/> <composite-element class="CarComponent"> <property name="price"/> <property name="type"/> <property name="serialNumber" column="serialNum"/> </composite-element> </list>
一對多關(guān)聯(lián)通過外鍵連接兩個類對應(yīng)的表,而沒有中間集合表。 這個關(guān)系模型失去了一些Java集合的語義:
-
一個被包含的實(shí)體的實(shí)例只能被包含在一個集合的實(shí)例中
-
一個被包含的實(shí)體的實(shí)例只能對應(yīng)于集合索引的一個值中
一個從Product到Part的關(guān)聯(lián)需要關(guān)鍵字字段,可能還有一個索引字段指向Part所對應(yīng)的表。 <one-to-many>標(biāo)記指明了一個一對多的關(guān)聯(lián)。
<one-to-many class="ClassName" (1) not-found="ignore|exception" (2) entity-name="EntityName" (3) node="element-name" embed-xml="true|false" />
(1) |
class(必須):被關(guān)聯(lián)類的名稱。 |
(2) |
not-found (可選 - 默認(rèn)為exception): 指明若緩存的標(biāo)示值關(guān)聯(lián)的行缺失,該如何處理: ignore 會把缺失的行作為一個空關(guān)聯(lián)處理。 |
(3) |
entity-name (可選): 被關(guān)聯(lián)的類的實(shí)體名,作為class的替代。 |
例子
<set name="bars"> <key column="foo_id"/> <one-to-many class="org.hibernate.Bar"/> </set>
注意:<one-to-many>元素不需要定義任何字段。 也不需要指定表名。
重要提示:如果一對多關(guān)聯(lián)中的外鍵字段定義成NOT NULL,你必須把<key>映射聲明為not-null="true",或者使用雙向關(guān)聯(lián),并且標(biāo)明inverse="true"。參閱本章后面關(guān)于雙向關(guān)聯(lián)的討論。
下面的例子展示一個Part實(shí)體的map,把name作為關(guān)鍵字。( partName 是Part的持久化屬性)。注意其中的基于公式的索引的用法。
<map name="parts" cascade="all"> <key column="productId" not-null="true"/> <map-key formula="partName"/> <one-to-many class="Part"/> </map>
Hibernate支持實(shí)現(xiàn)java.util.SortedMap和java.util.SortedSet的集合。你必須在映射文件中指定一個比較器:
<set name="aliases" table="person_aliases" sort="natural"> <key column="person"/> <element column="name" type="string"/> </set> <map name="holidays" sort="my.custom.HolidayComparator"> <key column="year_id"/> <map-key column="hol_name" type="string"/> <element column="hol_date" type="date"/> </map>
sort屬性中允許的值包括unsorted,natural和某個實(shí)現(xiàn)了java.util.Comparator的類的名稱。
分類集合的行為事實(shí)上象java.util.TreeSet或者java.util.TreeMap。
如果你希望數(shù)據(jù)庫自己對集合元素排序,可以利用set,bag或者map映射中的order-by屬性。這個解決方案只能在jdk1.4或者更高的jdk版本中才可以實(shí)現(xiàn)(通過LinkedHashSet或者 LinkedHashMap實(shí)現(xiàn))。 它是在SQL查詢中完成排序,而不是在內(nèi)存中。
<set name="aliases" table="person_aliases" order-by="lower(name) asc"> <key column="person"/> <element column="name" type="string"/> </set> <map name="holidays" order-by="hol_date, hol_name"> <key column="year_id"/> <map-key column="hol_name" type="string"/> <element column="hol_date" type="date"/> </map>
注意: 這個order-by屬性的值是一個SQL排序子句而不是HQL的!
關(guān)聯(lián)還可以在運(yùn)行時使用集合filter()根據(jù)任意的條件來排序。
sortedUsers = s.createFilter( group.getUsers(), "order by this.name" ).list();
雙向關(guān)聯(lián)允許通過關(guān)聯(lián)的任一端訪問另外一端。在Hibernate中, 支持兩種類型的雙向關(guān)聯(lián):
- 一對多(one-to-many)
-
Set或者bag值在一端, 單獨(dú)值(非集合)在另外一端
- 多對多(many-to-many)
-
兩端都是set或bag值
要建立一個雙向的多對多關(guān)聯(lián),只需要映射兩個many-to-many關(guān)聯(lián)到同一個數(shù)據(jù)庫表中,并再定義其中的一端為inverse(使用哪一端要根據(jù)你的選擇,但它不能是一個索引集合)。
這里有一個many-to-many的雙向關(guān)聯(lián)的例子;每一個category都可以有很多items,每一個items可以屬于很多categories:
<class name="Category"> <id name="id" column="CATEGORY_ID"/> ... <bag name="items" table="CATEGORY_ITEM"> <key column="CATEGORY_ID"/> <many-to-many class="Item" column="ITEM_ID"/> </bag> </class> <class name="Item"> <id name="id" column="CATEGORY_ID"/> ... <!-- inverse end --> <bag name="categories" table="CATEGORY_ITEM" inverse="true"> <key column="ITEM_ID"/> <many-to-many class="Category" column="CATEGORY_ID"/> </bag> </class>
如果只對關(guān)聯(lián)的反向端進(jìn)行了改變,這個改變不會被持久化。 這表示Hibernate為每個雙向關(guān)聯(lián)在內(nèi)存中存在兩次表現(xiàn),一個從A連接到B,另一個從B連接到A。如果你回想一下Java對象模型,我們是如何在Java中創(chuàng)建多對多關(guān)系的,這可以讓你更容易理解:
category.getItems().add(item); // The category now "knows" about the relationship item.getCategories().add(category); // The item now "knows" about the relationship session.persist(item); // The relationship won''t be saved! session.persist(category); // The relationship will be saved
非反向端用于把內(nèi)存中的表示保存到數(shù)據(jù)庫中。
要建立一個一對多的雙向關(guān)聯(lián),你可以通過把一個一對多關(guān)聯(lián),作為一個多對一關(guān)聯(lián)映射到到同一張表的字段上,并且在"多"的那一端定義inverse="true"。
<class name="Parent"> <id name="id" column="parent_id"/> .... <set name="children" inverse="true"> <key column="parent_id"/> <one-to-many class="Child"/> </set> </class> <class name="eg.Child"> <id name="id" column="id"/> .... <many-to-one name="parent" class="Parent" column="parent_id" not-null="true"/> </class>
在“一”這一端定義inverse="true"不會影響級聯(lián)操作,二者是正交的概念!
有三種可能的途徑來映射一個三重關(guān)聯(lián)。第一種是使用一個Map,把一個關(guān)聯(lián)作為其索引:
<map name="contracts"> <key column="employer_id" not-null="true"/> <map-key-many-to-many column="employee_id" class="Employee"/> <one-to-many class="Contract"/> </map>
<map name="connections"> <key column="incoming_node_id"/> <map-key-many-to-many column="outgoing_node_id" class="Node"/> <many-to-many column="connection_id" class="Connection"/> </map>
第二種方法是簡單的把關(guān)聯(lián)重新建模為一個實(shí)體類。這使我們最經(jīng)常使用的方法。
最后一種選擇是使用復(fù)合元素,我們會在后面討論
如果你完全信奉我們對于“聯(lián)合主鍵(composite keys)是個壞東西”,和“實(shí)體應(yīng)該使用(無機(jī)的)自己生成的代用標(biāo)識符(surrogate keys)”的觀點(diǎn),也許你會感到有一些奇怪,我們目前為止展示的多對多關(guān)聯(lián)和值集合都是映射成為帶有聯(lián)合主鍵的表的!現(xiàn)在,這一點(diǎn)非常值得爭辯;看上去一個單純的關(guān)聯(lián)表并不能從代用標(biāo)識符中獲得什么好處(雖然使用組合值的集合可能會獲得一點(diǎn)好處)。不過,Hibernate提供了一個(一點(diǎn)點(diǎn)試驗(yàn)性質(zhì)的)功能,讓你把多對多關(guān)聯(lián)和值集合應(yīng)得到一個使用代用標(biāo)識符的表去。
<idbag> 屬性讓你使用bag語義來映射一個List (或Collection)。
<idbag name="lovers" table="LOVERS"> <collection-id column="ID" type="long"> <generator class="sequence"/> </collection-id> <key column="PERSON1"/> <many-to-many column="PERSON2" class="eg.Person" outer-join="true"/> </idbag>
你可以理解,<idbag>人工的id生成器,就好像是實(shí)體類一樣!集合的每一行都有一個不同的人造關(guān)鍵字。但是,Hibernate沒有提供任何機(jī)制來讓你取得某個特定行的人造關(guān)鍵字。
注意<idbag>的更新性能要比普通的<bag>高得多!Hibernate可以有效的定位到不同的行,分別進(jìn)行更新或刪除工作,就如同處理一個list, map或者set一樣。
在目前的實(shí)現(xiàn)中,還不支持使用identity標(biāo)識符生成器策略來生成<idbag>集合的標(biāo)識符。
在前面的幾個章節(jié)的確非常令人迷惑。 因此讓我們來看一個例子。這個類:
package eg; import java.util.Set; public class Parent { private long id; private Set children; public long getId() { return id; } private void setId(long id) { this.id=id; } private Set getChildren() { return children; } private void setChildren(Set children) { this.children=children; } .... .... }
這個類有一個Child的實(shí)例集合。如果每一個子實(shí)例至多有一個父實(shí)例, 那么最自然的映射是一個one-to-many的關(guān)聯(lián)關(guān)系:
<hibernate-mapping> <class name="Parent"> <id name="id"> <generator class="sequence"/> </id> <set name="children"> <key column="parent_id"/> <one-to-many class="Child"/> </set> </class> <class name="Child"> <id name="id"> <generator class="sequence"/> </id> <property name="name"/> </class> </hibernate-mapping>
在以下的表定義中反應(yīng)了這個映射關(guān)系:
create table parent ( id bigint not null primary key ) create table child ( id bigint not null primary key, name varchar(255), parent_id bigint ) alter table child add constraint childfk0 (parent_id) references parent
如果父親是必須的, 那么就可以使用雙向one-to-many的關(guān)聯(lián)了:
<hibernate-mapping> <class name="Parent"> <id name="id"> <generator class="sequence"/> </id> <set name="children" inverse="true"> <key column="parent_id"/> <one-to-many class="Child"/> </set> </class> <class name="Child"> <id name="id"> <generator class="sequence"/> </id> <property name="name"/> <many-to-one name="parent" class="Parent" column="parent_id" not-null="true"/> </class> </hibernate-mapping>
請注意NOT NULL的約束:
create table parent ( id bigint not null primary key ) create table child ( id bigint not null primary key, name varchar(255), parent_id bigint not null ) alter table child add constraint childfk0 (parent_id) references parent
另外,如果你絕對堅(jiān)持這個關(guān)聯(lián)應(yīng)該是單向的,你可以對<key>映射聲明NOT NULL約束:
<hibernate-mapping> <class name="Parent"> <id name="id"> <generator class="sequence"/> </id> <set name="children"> <key column="parent_id" not-null="true"/> <one-to-many class="Child"/> </set> </class> <class name="Child"> <id name="id"> <generator class="sequence"/> </id> <property name="name"/> </class> </hibernate-mapping>
另外一方面,如果一個子實(shí)例可能有多個父實(shí)例, 那么就應(yīng)該使用many-to-many關(guān)聯(lián):
<hibernate-mapping> <class name="Parent"> <id name="id"> <generator class="sequence"/> </id> <set name="children" table="childset"> <key column="parent_id"/> <many-to-many class="Child" column="child_id"/> </set> </class> <class name="Child"> <id name="id"> <generator class="sequence"/> </id> <property name="name"/> </class> </hibernate-mapping>
表定義:
create table parent ( id bigint not null primary key ) create table child ( id bigint not null primary key, name varchar(255) ) create table childset ( parent_id bigint not null, child_id bigint not null, primary key ( parent_id, child_id ) ) alter table childset add constraint childsetfk0 (parent_id) references parent alter table childset add constraint childsetfk1 (child_id) references child