為什么對象-關系數據庫的映射對于現代開發者是一件大事呢?一方面,對象技術(例如 Java 技術)是應用于新軟件系統開發的最常見的環境。另外,關系數據庫仍然是許多人都青睞的持久信息存儲方法,并且在較長時間內這種情況不太會改變。請繼續讀下去,了解如何使用這種技術。
為什么要寫有關對象-關系數據庫之間的映射的文章呢?因為在對象范例和關系范例之間“阻抗不匹配”。對象范例基于軟件工程的一些原理,例如耦合、聚合和封裝,而關系范例則基于數學原理,特別是集合論的原理。兩種不同的理論基礎導致各自有不同的優缺點。而且,對象范例側重于從包含數據和行為的對象中構建應用程序,而關系范例則主要針對數據的存儲。當為訪問而尋找一種合適的方法時,“阻抗不匹配”就成了主要矛盾:使用對象范例,您是通過它們的關系來訪問對象,而使用關系范例,則通過復制數據來聯接表中的行。這種基本的差異導致兩種范例的結合并不理想,不過話說回來,本來就預料到會有一些問題。使對象-關系數據庫之間的映射成功的一個秘訣就是理解這兩種范例和它們的差異,然后基于這些認識來進行明智的取舍。
本文應該能夠消除現今開發周期中一些普遍共有的誤解,對對象-關系數據庫之間映射所涉及到的一些問題提供了切合實際的看法。這些策略基于我的開發經驗,項目范圍從小到大,涉及金融、銷售、軍事、遠程通信和外購等行業。我已對使用 C++、 Smalltalk、Visual Basic 和 Java 語言編寫的應用程序應用了這些原則。
如何將對象映射成關系數據庫
在這一節中,我會描述一些將對象成功映射成關系數據庫所需的基本技術。
將屬性映射成列
類屬性將映射成關系數據庫中的零或幾列。要記住,并不是所有屬性都是持久的。例如, Invoice
類會有 grandTotal
屬性,這個屬性由其實例在計算時使用,但它不保存到數據庫中。而且,某些對象屬性本身就是對象,例如 Course
對象有一個作為屬性的 TextBook
實例,它映射為數據庫中的幾列(實際上,很有可能 TextBook
類本身就將映射成一個或多個表)。重要的是,這是一個遞歸定義:有時屬性將映射成零或者多列。也有可能將幾個屬性映射成表中的單一列。例如,代表美國郵遞區號代碼的類可以有三個數字屬性,每個都表示完整郵政編號代碼中的每一部分,而郵政編號代碼可以在地址表中作為單一的列存儲。
在關系數據庫中實現繼承
在將對象保存到關系數據庫中時,繼承的概念中發生幾個有趣的問題。(請參閱
圖 1. 簡單類層次結構的 UML 類示意圖

將類映射成表
類到表的映射通常不是直接的。除了非常簡單的數據庫以外,您不會有類到表的一對一映射。在以下章節中,我將討論為關系數據庫實現繼承結構的三種策略:
整個類層次結構使用一個數據實體
使用這種方法,您可以將一個完整類層次結構映射成一個數據實體,而層次結構中所有類的所有屬性都存儲在這個實體中。圖 2 描述了采取這個方法時圖 1 的類層次結構的持久模型。請注意,為表的主鍵引入了一個 personOID
列 - 我在所有解決方案中都使用 OID (沒有商業含義的標識,又稱替代鍵),只是為了保持一致和使用我所知道的向數據實體分配鍵的最好辦法。
圖 2. 將類層次結構映射成單一數據實體

這種方法的優點是簡單,因為所需的所有人員數據都可以在一張表中找到,所以在人們更改角色時支持多態性,并且使用這種方法,專門報告(為一小組用戶特定目的所執行的報告,這些用戶通常自己寫報告)也非常簡單。缺點是每次在類層次結構的任何地方添加一個新屬性時都必須將一個新屬性添加到表中。這增加了類層次結構中的耦合 - 如果在添加一個屬性時有任何錯誤,除獲得新屬性的類的子類外,還可能影響到層次結構中的所有類。它還可能浪費數據庫中的許多空間。我還必須添加 objectType
列來表明行代表的是學生、教授還是其它類型的人員。在人們具有單一角色時這種方法很有效,但如果他們有多個角色(例如,一個人既是學生又是教授),很快就會失效。
每個具體類使用一個數據實體
使用這種方法,每個數據實體就既包含屬性又包含它所表示的類繼承的屬性。圖 3 描述了采取這個方法時圖 1 的類層次結構的持久模型。有與 Student
類對應的和與 Professor
類對應的數據實體,因為它們是具體類,但沒有與 Person
類對應的數據實體,因為它是抽象類(它的名稱以斜體字表示)。為每個數據實體都分別分配了自己的主鍵, studentOID
和 professorOID
。
圖 3. 將每個具體類映射成單個數據實體

這種方法最大的好處是,它仍然能相當容易地執行專門報告,只要您所需的有關單一類的所有數據都只存儲在一張表中。但也有幾個缺點。一個是當修改類時,必須修改它的表和它所有子類的表。例如,如果要向 Person
類添加高度和重量,就需要同時更新兩個表,它會涉及很多工作。第二,無論何時,只要對象更改了它的角色 - 可能您聘用了您一個剛畢業的學生作為教授 - 則需要將數據復制到相應的表中,并為它指定一個新的 OID。這又涉及到很多工作。第三,很難在支持多個角色的同時仍維護數據完整性。(這種情況是可能的;只是比原先困難一點。)例如,您會在哪里存儲既是學生又是教授的人的姓名呢?
每個類使用一個數據實體
使用這種方法,為每個類創建一張表,它的屬性是 OID 和特定于該類的屬性。圖 4 描述了采取這個方法時圖 1 的類層次結構的持久模型。 請注意,將 personOID
用作了所有三個數據實體的主鍵。圖 4 的一個有趣的特性是,為 Professor
和 Student
中的 personOID
列都分配了兩個構造型,而這在標準建模語言 (UML) 中是不允許的。我的意見是,這是一個必須由 UML 持久性建模概要解決的問題,甚至可能在這個建模規則中也需要更改。(有關持久性模型的詳細信息,請參閱
圖 4. 將每個類映射成它自己的數據實體

這種方法的最大好處就是它能夠最好地適應面向對象的概念。它能夠很好地支持多態性,對于對象可能有的每個角色,只需要在相應的表中保存記錄。修改超類和添加新的子類也非常容易,因為您只需要修改或添加一張表。這種方法也有幾個缺點。第一,數據庫中有大量的表 -- 實際上每類都有一個(加上維護關系的表)。第二,使用這種技術讀取和寫入數據的時間比較長,因為您必須訪問多個表。如果通過將類層次結構中的每個表放入不同物理磁盤驅動器盤片(假設每個磁盤驅動器磁頭都單獨操作)上來智能地組織數據庫的話,就可以緩解這個問題。第三,有關數據庫的專門報告很困難,除非添加一些視圖來模擬所需的表。
比較映射策略
現在,請注意,每個映射策略怎樣產生不同的模型。要理解三種策略之間的設計優缺點,請考慮圖 5 中顯示的對我們的類層次結構做些簡單的更改:添加了 TenuredProfessor
,這是從 Professor
中繼承的。
圖 5. 擴展初始類層次結構

圖 6 顯示了一個更新過的持久性模型,用于將整個類層次結構映射成一個數據實體。盡管很明顯,數據庫中的空間浪費增加了,但請注意,按照這種策略操作,只需花非常小的代價就可以更新模型。
圖 6. 將擴展的層次結構映射成單一數據實體

圖 7 顯示了將每個具體類映射成數據實體時的持久性模型。使用這個策略,雖然因為我們從教授提升到終身教授,這樣對象和我們的關系就有了改變(學生變成教授),所以如何處理對象的這個問題更復雜了,但我只需要添加一個新表。
圖 7. 將擴展的層次結構的具體類映射成數據實體

圖 8 顯示了第三種映射策略的解決方案 -- 將單個類映射成單個數據實體。這需要我添加一個只包括 TenuredProfessor
類的新屬性的新表。這種方法的缺點是,要使用新類的實例,它需要好幾個數據庫訪問。
圖 8. 將擴展的層次結構的所有類映射成數據實體

要摒棄這樣一種觀點,即這些辦法都不夠好;每種辦法都有其優缺點。在下面的表 1 中對它們進行比較。
表 1. 比較映射繼承的各種辦法
考慮因素 |
每個層次結構一張表 |
每個具體類一張表 |
每個類一張表 |
專門報告 |
容易 |
中等 |
中等/困難 |
實現的難易程度 |
容易 |
中等 |
困難 |
數據訪問的難易程度 |
容易 |
容易 |
中等/容易 |
耦合 |
非常高 |
高 |
低 |
數據訪問速度 |
快 |
快 |
中等/快 |
對多態性的支持 |
中等 |
低 |
高 |
映射關聯、聚合和組成
不僅必須將對象映射到數據庫中,還必須將對象之間的關系進行映射,這樣才能在以后進行恢復。對象之間有四種類型的關系:繼承、關聯、聚合和組成。要有效地映射這些關系,必須理解它們之間的差異、如何實現一般的關系,以及如何實現特定的多對多關系。
關聯、聚合和組合之間的差異
從數據庫的角度看,關聯和聚合/組合關系之間的唯一不同是對象相互之間的綁定程度。對于聚合和組合,在數據庫中對整體所做的操作通常需要同時對部分進行操作,而關聯就不是這樣。
在圖 9 中有三個類,其中兩個在它們之間有簡單的關聯關系,有兩個共享聚合關系(實際上,組合可能是這種模型中更確切的說法)。(有關關系的詳細信息,請參閱
圖 9. 關聯和聚合/組合之間的差異

在關系數據庫中實現關系
關系數據庫中的關系是通過使用外鍵來維護的。外鍵是在一張表中出現的一個或多個數據屬性;它可以是另一張表的鍵的一部分,或者干脆碰巧就是另一張表的鍵。外鍵可以讓您將一張表中的一行與另一張表中的一行相關起來。要實現一對一和一對多的關系,您只需要將一張表的鍵包括在另一張表中。
在圖 10 中有三張表,它們的鍵 (OID) 和外鍵用于在它們之間實現關系。首先,在 Position
和 Employee
數據實體間有一個一對一的關聯。一對一關聯就是它的每個復合度的最大值都是 1 的這么一種關系。要實現這個關系,我在 Employee
數據實體中使用屬性 positionOID
,Position
數據實體的鍵。因為關聯是單向的 -- employee 那些行知道它們的位置行,但反過來就不行 -- 所以我必須這么做。如果這是個雙向的關聯,我還會在 Position
中添加一個名為 employeeOID
的外鍵。然后,使用相同的方法在 Employee
和 Task
之間實現了多對一關聯(又稱為一對多關聯),唯一的不同是將外鍵放在了 Task
中,因為它在關系的“多”方。
圖 10. 簡單人力資源數據庫的持久性模型。

實現多對多關聯
要實現多對多關系,需要關聯表的概念,它是一種數據實體,唯一目標是在關系數據庫中維護兩個或多個表之間的關聯。圖 10 中,在Employee
和 Benefit
之間有一個多對多關系。圖 11 中,可以看到如何使用關聯表來實現多對多關系。在關系數據庫中,關聯表中包含的屬性傳統上是關系中涉及到的表中的鍵組合。關聯表的名稱通常是它所關聯的表的名稱組合,或者是它實現的關聯的名稱。在這種情況下,我選擇 EmployeeBenefit
而不是 BenefitEmployee
和 has
,因為我覺得它可以更好地反映關聯的性質。
圖 11. 在關系數據庫中實現多對多關系

看一下圖 11 中應用程序的復合度。規則是,一旦引入了關聯表,復合度就“交叉”,如圖 12 所示。值為 '1' 的復合度總在外邊緣引入,如圖 11 和 12 中所示,以保留原始關聯的整體復合度。原始的關聯表明雇員有一種或多種福利,并且任何給定的福利都給予一個或多個雇員。在圖 11 中您可以看到,即使在有關聯表維護關聯的情況下仍然是這種情況。
圖 12. 關聯表簡介

有必要注明我選擇應用構造型“<<關聯表>>”而不是關聯類的說明 -- 將關聯類與它所描述的關聯連接的虛線行 -- 出于兩個原因。首先,關聯表的目的是實現關聯,而關聯類的目的是描述關聯。其次,圖 11 中采取的方法反映了為使用關系技術所需的實際實現策略。
結束