面向對象和關系型數據庫
Java 語言天生就是一門面向對象的編程語言,在 Java 世界中,被處理的內 容都被組織成一個一個的對象,對象和對象之間存在著繼承、引用關系,這樣的 關系無法通過簡單的方式直接映射到關系型數據庫中。因此在關系型數據庫與面 向對象之間存在著阻抗失諧。
我們通過一個簡單的例子來說明這種阻抗失諧給企業應用開發者帶來的困難。 假設在企業應用中存在三個 Java 類:Animal、Fish 和 Dog。Animal 僅僅具備 兩個屬性:id 和 name。Fish 是一種 Animal,但是人們比較關注它的生活區域 是在海裡還是在河裡,因此它除了繼承自 Animal 之外,還有自己獨特的屬性 territory。Dog 也是一種 Animal,人們比較關注它的性別,因此它除了繼承自 Animal 之外,還有自己獨特的屬性 sex。我們可以用下面這個圖描述三者之間的 關系模型。
圖 1. Animal、Fish 和 Dog 對象模型
在 Java 應用中,由於動態綁定的支持,Fish、Dog 都可以被作為 Animal 對 象處理。但是如果我們換到關系型數據庫的視角,情況發生了改變: 通常情況下 ,Animal、Fish、Dog 毫無關聯,它們都保存在各自對應的表中,假設分別對應 Animal 表、Fish 表和 Dog 表,如果要維護 Animal 和 Fish 的繼承關系,我們 就需要使用 SQL 的聯合查詢語句查出 Animal 的所有屬性和 Fish 的所有屬性, 這樣就必須使用某種外鍵進行關聯:
Select animal.*,fish.* form animal,fish where animal.id = fish.id
從這個簡單的例子中我們就可以看出,一個企業應用開發者使用 Java 實現企 業應用時需要同時掌握面向對象和關系型數據庫的兩種思想,而且還必須保證它 們之間的映射是正確的,否則無法保證企業應用的正確性,這對於企業應用開發 者是個的挑戰,因此 Java 社區一直在尋求如何將面向對象和關系型數據庫思想 統一起來的簡單途徑,這方面的努力促進了持久化技術的發展。
OpenJPA 是最新的嘗試,它能夠將對象繼承關系的持久化透明化,企業應用開 發者僅需要處理對象模型,而不需要處理和關系型數據庫有關的內容,極大地降 低了對象繼承關系持久化的難度。下面我們來了解 OpenJPA 中持久化對象繼承關 系的幾種方式。
持久化對象繼承關系的方式
我們從關系數據庫角度看對象繼承關系的持久化這個問題域:對象繼承通常意 味著子類比父類提供更多的屬性,持久化對象繼承關系的實質就是如何根據對象 的類型動態的處理這些多出來的屬性。OpenJPA 框架支持使用三種不同的策略處 理對象繼承關系:
1. 類及其子類保存在一張數據庫表中
在這種情況下,類及其子類都保存在同一張數據表中,該表提供足夠的字段保 存類及其子類的所有屬性,同時提供一個特別字段保存當前記錄對應類的實際類 名(默認名 DTYPE,也可以在開發時指定其它名稱)。在企業應用運行過程中, OpenJPA 框架根據 Java 對象的實際類型和數據庫表進行綁定。
以上一章節中提到的對象模型為例: Animal、Fish、Dog 三個類的所有對象 實例都被保存在 Animal 數據表中,該表將會有 5 個屬性,其中 ID,NAME 字段 對應 ANIMAL 類的兩個屬性,ID、NAME、SEX 對應 Dog 類的屬性,ID、NAME、 STERRITORY 對應 Fish 類的屬性。DTYPE 是 OpenJPA 加入的字段,用於確定當 前記錄的實際類類型,在這裡例子中,它的內容是“ANIMAL”、“FISH”或者是 “DOG”。
圖 2. 第一種策略的數據庫表現
2. 類和子類分別保存在不同的數據庫表中,互相之間沒有關聯
這種情況下,開發者不理會類之間是否存在繼承關系,為每一個類的持久化使 用唯一的表,父類對象保存在父類對應的表中,子類對象的信息保存在子類對應 的表中,這也是通常的持久化框架采用的方式。下面這個圖顯示了這種情況下對 象繼承關系數據庫中的表現。
以上一章節中提到的對象模型為例: Animal、Fish、Dog 三個類的對象實例 都被保存在各自對應的數據表中。下面這個圖顯示了這種情況下對象繼承關系數 據庫中的表現。
圖 3. 第二種策略的數據庫表現
3. 類和子類分別保存在不同的數據庫表中,子類中不保存父類中已有的屬性 ,僅通過主鍵進行關聯
這種情況下,父類和子類對應不同的表,但是子類對應的表中不再保存父類對 應表中已經存在的字段信息,兩個表之間通過關鍵字段關聯起來,也就是數據庫 技術中通常所說的外健。這種實現方式是最理想化的一種,既能夠處理對象之間 的繼承,又滿足了關系數據庫中對於設計范式的要求。
以上一章節中提到的對象模型為例: Animal、Fish、Dog 三個類的對象實例 都被在 Animal 表中有記錄;而 Fish 對象的 TERRITORY 屬性者被 FISH 表所保 存,FISH 表通過 ID 和 Animal 表中的數據進行關聯;而 Dog 對象的 SEX 屬性 者被 Dog 表所保存,Dog 表通過 ID 和 Animal 表中的數據進行關聯。下面這個 圖顯示了這種情況下對象繼承關系數據庫中的表現。
圖 4. 第三種策略的數據庫表現
這三種方式的處理對於開發者而言是透明的,無論選擇哪一種,僅僅影響數據 在關系數據庫中的保存方式,對於開發者而言,只需要按照面向對象的方式操作 對象既可,OpenJPA 框架在處理持久化操作的時候,會動態地判斷當前對象的實 際類類型(後期綁定),從而確定持久化到哪個表中。在一個企業應用的實現中 ,開發者可以根據需要選擇這三種方式的一種或者幾種來處理對象之間的繼承關 系。
Inheritance 注釋
OpenJPA 是一個基於注釋的持久化框架,對持久化的大多數元信息都只需要為 實體類提供相應的注釋。開發者使用注釋描述實體和數據庫表之間的映射,也采 用注釋描述對象繼承關系的持久化。javax.persistence.Inheritance 注釋用來 指定對象繼承關系持久化的方式。它的 strategy 屬性用於指定持久化對象繼承 關系在關系數據庫中的表現形式,可選擇項包括 SINGLE_TABLE、JOINED 和 TABLE_PER_CLASS。它們三個都是 javax.persistence.InheritanceType 中定義 的常量。
SINGLE_TABLE
strategy 設置為 SINGLE_TABLE 選項表示所有類及其子類保存在同一個數據 庫表中,對象的類型使用表中的特殊字段 DTYPE 進行識別。
TABLE_PER_CLASS
strategy 設置為該選項表示每個類使用一個表。
JOINED
strategy 設置為該選項表示父類和子類分別保存在不同的數據庫表中,子類 中不保存父類對應數據庫表中已有的屬性,僅通過主鍵進行關聯。
javax.persistence.Inheritance 注釋是類級別的注釋。需要為每一個成為父 類的實體類提供 javax.persistence.Inheritance 注釋並且指定 strategy 屬性 。在同一個企業應用中,開發者可以根據實際情況選擇這三種策略中的一種,或 者是幾種同時使用。
對象繼承關系的持久化和查詢
上面的章節中,我們已經介紹了 OpenJPA 中處理對象繼承的方法,下面我們 通過一些簡短的代碼來演示如何實現 Animal、Fish、Dog 及其繼承關系的持久化 ,同時介紹如何將這種對象繼承關系從數據庫中還原出來。
演示中,我們選擇使用實現第三種方式:JOINED。這也是 OpenJPA 中持久化 對象繼承的最佳實踐,既符合 Java 開發者面向對象的習慣,也能夠符合關系數 據庫設計的范式要求,而且數據庫中的數據冗余最小。TABLE_PER_CLASS 和 SINGLE_TABLE 方式下的對象繼承關系持久化的例子請讀者參考下面的步驟自己完 成。
實體類 Animal
首先我們創建實體類 Animal,它是 Fish 和 Dog 的父類。因此必須為它處理 提供 javax.persistence.Inheritance 注釋。我們選擇使用 JOINED 策略處理繼 承關系,因此設置 javax.persistence.Inheritance 注釋的 strategy 屬性為 InheritanceType.JOINED。
清單 1 Animal.java, 繼承關系的父類
1. package chapter04.entity;
2. import javax.persistence.Entity;
3. import javax.persistence.Id;
4. import javax.persistence.Inheritance;
5. import javax.persistence.InheritanceType;
6.
7. @Entity
8. @Inheritance(strategy = InheritanceType.JOINED)
9. public class Animal {
10. @Id
11. private int id;
12.
13. private String name;
14.
15. public Animal() {
16. }
17.
18. public Animal(int id, String name) {
19. this.id = id;
20. this.name = name;
21. }
22.
23. public int getId() {
24. return id;
25. }
26.
27. public void setId(int id) {
28. this.id = id;
29. }
30.
31. public String getName() {
32. return name;
33. }
34.
35. public void setName(String name) {
36. this.name = name;
37. }
38.
39. }
實體類 Fish
實體類 Fish 是 Animal 的子類,因此它必須繼承自 Animal。同時根據 OpenJPA 的要求,每一個實體必須使用 javax.persistence.Entity 進行注釋, 因此在 Fish 類聲明之前提供 javax.persistence.Entity 注釋。
清單 2 Fish.java, 繼承關系中的子類
1. package chapter04.entity;
2.
3. import javax.persistence.Entity;
4.
5. @Entity
6. public class Fish extends Animal {
7. /* 魚的活動范圍,比如江、河、湖、海 */
8. private String territory;
9.
10. public Fish() {
11. }
12.
13. public Fish(int id, String name, String territory) {
14. super(id, name);
15. this.territory = territory;
16. }
17.
18. public String getTerritory() {
19. return territory;
20. }
21.
22. public void setTerritory(String territory) {
23. this.territory = territory;
24. }
25.
26. }
實體類 Dog
實體類 Dog 是 Animal 的子類,因此它必須繼承自 Animal。和 Fish 類一樣 ,我們需要在 Dog 類聲明之前提供 javax.persistence.Entity 注釋。
清單 3 Dog.java, 繼承關系中的子類
1. package chapter04.entity;
2.
3. import javax.persistence.Entity;
4.
5. @Entity
6. public class Dog extends Animal {
7. /* 性別 */
8. private String sex;
9.
10. public Dog() {
11. }
12.
13. public Dog(int id, String name, String sex) {
14. super(id, name);
15. this.sex = sex;
16. }
17.
18. public String getSex() {
19. return sex;
20. }
21.
22. public void setSex(String sex) {
23. this.sex = sex;
24. }
25.
26. }
創建合適的數據庫表
我們可以使用下面的語句創建數據庫表:
CREATE TABLE ANIMAL(ID INTEGER NOT NULL PRIMARY KEY,NAME VARCHAR(255))
CREATE TABLE DOG(ID INTEGER NOT NULL PRIMARY KEY,SEX VARCHAR (255))
CREATE TABLE FISH(ID INTEGER NOT NULL PRIMARY KEY,TERRITORY VARCHAR(255))
[注] 使用 OpenJPA 中的 MappingTool 工具可以很容易的保持 Entity 和數 據庫之間的一致性,也可以使用 MappingTool 工具生成的數據庫定義文件(DDL )創建應用正常運行所需要的數據庫結構。請參考 OpenJPA 的幫助文檔中關於 MappingTool 部分的內容。
持久化實體
使用 OpenJPA 持久化實體的繼承關系時,開發者只需要按照面向對象的思想 操縱實體即可,無需為實體的繼承關系作多余的工作。下面的章節中我們將了解 持久化實體 Animal、Fish、Dog 時開發者需要完成的工作以及 OpenJPA 轉化後 在關系數據庫中的實現細節。
持久化 Animal
我們可以使用下面的代碼段來持久化一個新的 Animal 對象:
1. // 通過 Persistence 創建 EntityManagerFactory
2. EntityManagerFactory factory = Persistence.createEntityManagerFactory(
3. "jpa-unit", System.getProperties());
4.
5. // 從 EntityManagerFactory 中創建 EntityManager
6. EntityManager em = factory.createEntityManager();
7.
8. // 開始持久化實體的事務
9. em.getTransaction().begin();
10.
11. // 使用相同的方式持久化實體
12. em.persist(new Animal(1,"honey"));
13.
14. // 提交持久化實體的事務
15. em.getTransaction().commit();
16.
17. // 關閉EntityManager
18. em.close();
當我們執行這段代碼時,OpenJPA 會將它轉化為關系數據庫對應的 SQL 語句 :
INSERT INTO Animal (id, name) VALUES (1, 'honey')
[注] 如果您還不知道如何使用 OpenJPA 持久化對象,請閱讀 本系列 前面的 文章,了解 OpenJPA 開發的基本知識。
持久化 Fish
Fish 對象的持久化和 Animal 實體的持久化過程沒有任何的不同,只不過 persist 方法的參數變成了 Fish 對象。我們可以使用下面的代碼段來持久化一 個新的 Fish 對象,請注意下面代碼中加粗的部分。
1. // 通過 Persistence 創建 EntityManagerFactory
2. EntityManagerFactory factory = Persistence.createEntityManagerFactory(
3. "jpa-unit", System.getProperties());
4.
5. // 從 EntityManagerFactory 中創建 EntityManager
6. EntityManager em = factory.createEntityManager();
7.
8. // 開始持久化實體的事務
9. em.getTransaction().begin();
10.
11. // 使用相同的方式持久化實體
12. em.persist(new Fish(2,"mermaid","SEA"));
13.
14. // 提交持久化實體的事務
15. em.getTransaction().commit();
16.
17. // 關閉EntityManager
18. em.close();
由於 Fish 對象的屬性保存在兩個表中,因此當我們執行這段代碼時, OpenJPA 會將它轉化為對應的兩條 SQL 語句:
INSERT INTO Animal (id, name) VALUES (2, 'mermaid')
INSERT INTO Fish (id, territory) VALUES (2, 'SEA')
持久化 Dog
持久化 Dog 對象和持久化 Fish 對象的過程幾乎一樣,區別是 persist 方法 的參數變成了 Dog 對象。
em.persist(new Dog(3,"ba guai","MALE"));
和持久化 Fish 對象時一樣,Dog 對象的屬性也保存在兩個表中,因此當我們 執行這段代碼時,OpenJPA 會將它轉化為對應的兩條 SQL 語句:
INSERT INTO Animal (id, name) VALUES (3, 'ba guai')
INSERT INTO Dog (id, sex) VALUES (3, 'MALE')
從數據庫中查詢實體對象
在上一章節中我們了解了如何持久化存在繼承關系的實體內,並且介紹了 OpenJPA 在處理繼承關系時的細節行為,接下來我們將介紹如何從數據庫中獲取 實體,以及 OpenJPA 在這個過程中對於繼承關系處理的細節。
獲取所有 Animal 對象
我們通過 OpenJPA 中的 Query 接口和 JPQL(Java Persistence Query Language)語言來獲取數據庫中的記錄並且轉換為相應的 Java 對象,因此開發 者只需要處理 Java 對象模型即可。下面的代碼可以從數據庫中獲取所有的 Animal 對象,請注意其中粗體的部分。
1. // 通過 Persistence 創建 EntityManagerFactory
2. EntityManagerFactory factory = Persistence.createEntityManagerFactory(
3. "jpa-unit", System.getProperties());
4. // 創建新的 EntityManager
5. EntityManager em2 = factory.createEntityManager();
6.
7. // 查詢所有 Animal 對象
8. Query q = em2.createQuery("select m from Animal m");
9.
10. // 直接處理 Animal 對象,打印 Animal 對象的信息
11. for (Animal m : (List<Animal>) q.getResultList()) {
12. System.out.println("Animal Object:");
13. System.out.println(" id:" + m.getId());
14. System.out.println(" name:" + m.getName());
15. }
16.
17. // 關閉 EntityManager 和 EntityManagerFactory
18. em2.close();
19. factory.close();
當我們執行這段代碼時,OpenJPA 會將它轉化為關系數據庫對應的 SQL 查詢 語句:
SELECT t0.id, t1.id, t2.id, t0.name, t1.sex, t2.territory
FROM Animal t0
LEFT OUTER JOIN Dog t1 ON t0.id = t1.id
LEFT OUTER JOIN Fish t2 ON t0.id = t2.id
在查詢結果返回後,OpenJPA 會將查詢結果影射到相關的 Animal 對象上,整 個過程是透明的,開發者只需要處理對象模型即可。
獲取所有 Fish 對象
Fish 對象的獲取和 Animal 對象的獲取在 OpenJPA 中大同小異,唯一的區別 是使用 JPQL 不相同,查詢 Fish 對象時使用“select fish from Fish fish” 。下面的代碼可以從數據庫中獲取所有的 Fish 對象,請注意其中粗體的部分。
1. // 通過 Persistence 創建 EntityManagerFactory
2. EntityManagerFactory factory = Persistence.createEntityManagerFactory(
3. "jpa-unit", System.getProperties());
4. // 創建新的 EntityManager
5. EntityManager em2 = factory.createEntityManager();
6.
7. // 查詢所有 Fish 對象
8. Query q1 = em2.createQuery("select fish from Fish fish");
9.
10. // 打印 Fish 對象的信息
11. for (Fish fish : (List<Fish>) q1.getResultList()) {
12. System.out.println("Fish Object:");
13. System.out.println(" id:" + fish.getId());
14. System.out.println(" name:" + fish.getName());
15. System.out.println(" territory:" + fish.getTerritory ());
16. }
17.
18. // 關閉 EntityManager 和 EntityManagerFactory
19. em2.close();
20. factory.close();
當我們執行這段代碼時,OpenJPA 會將它轉化為關系數據庫對應的 SQL 查詢 語句:
SELECT t1.id, t0.id, t1.name, t0.territory
FROM Fish t0
INNER JOIN Animal t1 ON t0.id = t1.id
在查詢結果返回後,OpenJPA 會將查詢結果影射到相關的Fish對象上,整個過 程是透明的,開發者只需要處理對象模型即可。
獲取所有 Dog 對象
獲取 Dog 對象的過程和獲取 Fish 對象的過程一致,開發者只需要將 Query 接口使用的 JPQL 語句改為“select dog from Dog dog”。
Query q1 = em2.createQuery("select dog from Dog dog ");
當我們執行這段代碼時,OpenJPA 會將它轉化為關系數據庫對應的 SQL 查詢 語句:
SELECT t1.id, t0.id, t1.name, t0.sex
FROM Dog t0
INNER JOIN Animal t1 ON t0.id = t1.id
在查詢結果返回後,OpenJPA 會將查詢結果影射到相關的Fish對象上,整個過 程是透明的,開發者只需要處理對象模型即可。
總結
對象繼承關系在關系數據庫中的表現是對象持久化中難於實現的部分, OpenJPA 為開發者提供了一種透明的實現。在 OpenJPA 中提供了 SINGLE_TABLE 、JOINED 和 TABLE_PER_CLASS 三種實現方式處理實體繼承關系,開發者需要做 的僅僅是為實體類提供 javax.persistence.Inheritance 注釋,同時設置它的 strategy 屬性,確定使用哪種對象繼承關系即可,和關系數據庫交互的部分由 OpenJPA 框架完成。
在下一期文章中,讓我們來了解如何使用 OpenJPA 處理實體之間的關聯關系 ,如一對一、一對多、多對一、多對多關系,我們將介紹相關的注釋,並學習如 何在代碼中使用它,敬請期待!
原文: http://www.ibm.com/developerworks/cn/java/j-lo-openjpa3/