有關使用和部署 Java 永續性體系結構 (JPA) 的案例研究
2006 年夏天釋出的 EJB 3.0 規範提供了一個大大簡化但功能更為強大的 EJB 框架,該框架演示了批註與傳統 EJB 2.x 部署描述符相比的顯著優勢。J2SE 5.0 中引入的批註是修飾符,可以在類、欄位、方法、引數、本地變數、構造符、列舉和程式包中使用。大量 EJB 3.0 新特性中都強調了批註的使用,這些特性包括:基於普通舊式 Java 物件的 EJB 類、EJB 管理器類的相關性注入、引入可以攔截其他業務方法呼叫的攔截器或方法,以及顯著增強的 Java 永續性 API (JPA) 等。
為了說明 JPA 的概念,我們來看一個實際示例。最近,我的辦公室需要實施稅務登記系統。與大多數系統一樣,該系統具有自己的複雜性和挑戰性。由於其特殊的挑戰涉及了資料訪問和物件關係對映 (ORM),因此我們決定在實施該系統的同時試用新的 JPA。
在該專案期間,我們面臨以下幾個挑戰:
- 應用程式中使用的實體之間存在多種關係。
- 應用程式支援對關係資料進行復雜搜尋。
- 應用程式必須確保資料完整性。
- 應用程式在持久儲存資料之前需要對其進行驗證。
- 需要批量操作。
資料模型
首先來看看我們的關係資料模型的簡化版本,該版本足以解釋 JPA 的細微之處。從業務角度而言,主申請人提交稅務登記申請。申請人可以有零個或多個合夥人。申請人和合夥人必須指定兩個地址,即註冊地址和經營地址。主申請人還必須宣告和描述其過去受到的所有處罰。
定義實體。我們通過將實體對映到單獨的表定義了以下實體:
實體 | 對映到的表 |
Registration | REGISTRATION |
Party | PARTY |
Address | ADDRESS |
Penalty | PENALTY |
CaseOfficer | CASE_OFFICER |
表 1. 實體-表對映
識別要對映到資料庫表和列的實體很容易。下面是一個簡化的 Registration 實體示例。(我將在後面介紹該實體的其他對映和配置。)
@Entity @Table(name="REGISTRATION") public class Registration implements Serializable{ @Id private int id; @Column(name="REFERENCE_NUBER") private String referenceNumber; .......... }
對我們而言,使用 JPA 實體的主要好處是我們感覺就像對常規的 Java 類進行編碼一樣:無需再使用複雜的生命週期方法。我們可以使用批註將永續性特性分配給實體。我們發現無需使用其他資料傳輸物件 (DTO) 層,並且可以重用實體以便在層之間移動。資料的可移動性突然變得更好了。
支援多型性。通過檢視我們的資料模型,我們注意到我們使用了 PARTY 表同時儲存申請人和合夥人記錄。這些記錄不但具有一些相同的屬性,而且還具有各自特有的屬性。
我們希望在繼承層次中對此模型進行建模。利用 EJB 2.x,我們只能使用一個 Party 實體 bean,然後通過在程式碼內實施邏輯來根據 party 型別建立申請人或合夥人物件。另一方面,JPA 使我們可以在實體級別指定繼承層次。
我們決定通過一個抽象的實體 Party 和兩個具體的實體 Partner 和 Applicant 對繼承層次進行建模:
@Entity @Table(name="PARTY_DATA") @Inheritance(strategy= InheritanceType.SINGLE_TABLE) @DiscriminatorColumn(name="PARTY_TYPE") public abstract class Party implements Serializable{ @Id protected int id; @Column(name="REG_ID") protected int regID; protected String name; ......... }
兩個具體的類 Partner 和 Applicant 現在將繼承抽象的 Party 類的特徵。
@Entity @DiscriminatorValue("0") public class Applicant extends Party{ @Column(name="TAX_REF_NO") private String taxRefNumber; @Column(name="INCORP_DATE") private String incorporationDate; ........
}
如果 party_type 列的值為 0,則永續性提供程式將返回一個 Applicant 實體的例項;如果該列的值為 1,永續性提供程式將返回一個 Partner 實體的例項。
構建關係。我們的應用程式資料模型中的 PARTY 表包含 REGISTRATION 表的一個外來鍵列 (reg_id)。在該結構中,Party 實體成為實體的擁有方或關係的源,因為我們在其中指定連線列。Registration 成為關係的目標。
每個 ManyToOne 關係都很可能是雙向的;即兩個實體之間還存在 OneToMany 關係。下表顯示我們的關係定義:
關係 | 擁有方 | 多重性/對映 |
Registration->CaseOfficer | CaseOfficer | OneToOne |
Registration->Party | Party | ManyToOne |
Party->Address | Address | ManyToOne |
Party->Penalty | Penalty | ManyToOne |
反向關係 | ||
Registration->CaseOfficer | OneToOne | |
Registration->Party | OneToMany | |
Party->Address | OneToMany | |
Party->Penalty | OneToMany |
表 2.關係
public class Registration implements Serializable{ .... @OneToMany(mappedBy = "registration") private Collection<Party> parties; .... } public abstract class Party implements Serializable{ .... @ManyToOne @JoinColumn(name="REG_ID") private Registration registration; ....
注意:mappedBy 元素指明連線列是在關係的另一端指定的。
接下來,我們需要考慮由 JPA 規範定義、永續性提供程式實施的關係的行為。我們希望如何獲取相關資料,EAGER 還是 LAZY?我們查看了由 JPA 定義的關係的預設 FETCH 型別,然後向表 2 中添加了額外的一列以包括我們的發現:
關係 | 擁有方 | 多重性/對映 | 預設的 FETCH 型別 |
Registration->CaseOfficer | CaseOfficer | OneToOne | EAGER |
Party->Registration | Party | ManyToOne | EAGER |
Address->Party | Address | ManyToOne | EAGER |
Penalty->Party | Penalty | ManyToOne | EAGER |
反向關係 | |||
Registration->Party | OneToMany | LAZY | |
Party->Address | OneToMany | LAZY | |
Party->Penalty | OneToMany | LAZY |
表 3. 設定預設的 FETCH 型別
通過檢視業務要求,似乎當我們獲得 Registration 詳細資訊後,我們總是需要顯示與該登記相關聯的 Party 的詳細資訊。如果將 FETCH 型別設定為 LAZY,我們需要反覆呼叫資料庫以獲取資料。這意味著,如果將 Registration->Party 關係的 FETCH 型別改為 EAGER,我們會獲得更好的效能。在該設定下,永續性提供程式將相關資料作為單個 SQL 的一部分返回。
同樣,當我們在螢幕上顯示 Party 詳細資訊時,我們需要顯示其相關聯的 Address。因此,將 Party-Address 關係改為使用 EAGER 獲取型別是很有幫助的。
另一方面,我們可以將 Party->Penalty 關係的 FETCH 型別設為 LAZY,因為我們不需要顯示處罰的詳細資訊,除非使用者這樣要求。如果我們使用了 EAGER 獲取型別,當 m 個當事人每人有 n 個處罰時,我們最終就要載入 m*n 個 Penalty 實體,這會產生不必要的大物件圖形,從而降低效能。
public class Registration implements Serializable{ @OneToMany(mappedBy = "registration", fetch = FetchType.EAGER) private Collection<Party> parties; ..... } public abstract class Party implements Serializable{ @OneToMany (mappedBy = "party", fetch = FetchType.EAGER) private Collection<Address> addresses; @OneToMany (mappedBy = "party", fetch=FetchType.LAZY) private Collection<Penalty> penalties; ..... }
訪問惰性關係。考慮使用惰性載入方法,請考慮永續性上下文的範圍。您可以在 EXTENDED 永續性上下文或 TRANSACTION 範圍內的永續性上下文之間進行選擇。EXTENDED 永續性上下文在事務之間保持活動狀態,作用非常類似會話狀態的會話 bean。
由於我們的應用程式不是會話式的,永續性上下文不需要在事務之間可持續;因此,我們決定使用 TRANSACTION 範圍內的永續性上下文。但是,這帶來了惰性載入的問題。獲取了實體並結束了事務之後,就可以分離實體了。在我們的應用程式中,嘗試載入任何以惰性方式載入的關係資料將產生未定義的行為。
大多數情況下,當辦事員檢索登記資料時,我們不需要顯示處罰記錄。但是對於管理員,我們需要額外顯示處罰記錄。考慮到大多數情況下,我們不需要顯示處罰記錄,將關係的 FETCH 型別更改為 EAGER 就沒什麼意義了。相反,我們可以通過檢測經營者使用系統的時間來觸發關係資料的惰性載入。這會使關係資料在實體已分離時也可用,並可以在以後進行訪問。下面的示例解釋了這個概念:
Registration registration = em.find(Registration.class, regID); Collection<Party> parties = registration.getParties(); for (Iterator<Party> iterator = parties.iterator(); iterator.hasNext();) { Party party = iterator.next(); party.getPenalties().size(); } return registration;
在上面的示例中,我們只調用 Party 實體的處罰集合的 size() 方法。這樣做確實有效並且觸發了惰性載入,即使在 Registration 實體分離時,所有集合也會填充並可用。(或者,您可以使用 JP-QL 的一個名為 FETCH JOIN 的特殊特性,我們會在本文的後面對此進行討論。)
關係和永續性
接下來,我們需要考慮關係在持久儲存資料的上下文中的行為方式。本質上講,如果對關係資料進行了任何更改,我們希望在物件級別進行同樣的更改並通過永續性提供程式持久儲存這些更改。在 JPA 中,我們可以使用 CASCADE 型別控制永續性行為。
JPA 中定義了四種 CASCADE 型別:
- PERSIST:持久儲存擁有方實體時,也會持久儲存該實體的所有相關資料。
- MERGE:將分離的實體重新合併到活動的永續性上下文時,也會合並該實體的所有相關資料。
- REMOVE:刪除一個實體時,也會刪除該實體的所有相關資料。
- ALL:以上都適用。
建立實體。 我們決定在所有情況下,當我們新建一個父實體時,我們希望其所有相關的子實體也自動持久儲存。這簡化了編碼:我們只需正確設定關係資料,而無需在每個實體上單獨呼叫 persist() 操作。這意味著簡化了編碼,因為我們只需正確設定關係資料,而無需在每個實體上單獨呼叫 persist() 操作。
因此,級聯型別 PERSIST 是對我們最具吸引力的選項。我們將所有關係定義重新調整為使用該選項。
更新實體 在事務內獲取資料,然後在事務外對實體進行更改並持久儲存更改,這是很常見的。例如,在我們的應用程式中,使用者可以檢索現有的登記,更改主申請人的地址。當我們獲取一個現有的 Registration 實體並因此獲取了該實體在特定事務內的所有相關資料時,事務在此處結束,資料被髮送到表示層。此時,該 Registration 以及所有其他相關的實體例項與永續性上下文相分離。
在 JPA 中,為了持久儲存分離實體上的更改,我們使用 EntityManager 的 merge() 操作。此外,為將更改傳播到關係資料,所有關係定義必須包括 CASCADE 型別 MERGE 以及關係對映的配置中定義的任何其他 CASCADE 型別。
在該背景下,我們確保了為所有關係定義指定了正確的 CASCADE 型別。
刪除實體。 接下來,我們需要確定刪除某些實體時會發生什麼。例如,如果我們刪除一個 Registration,我們可以安全地刪除與該 Registration 相關聯的所有 Party。但是反過來卻不是這樣。此處的技巧是通過在關係上級聯 remove() 操作以避免意外刪除實體。正如您將在下一部分中看到的那樣,由於引用完整性約束,這樣的操作可能不會成功。
我們得出以下結論:在遵循 OnetoMany 的清晰的父子關係中(如 Party 和 Address 或 Party 和 Penalty),僅在關係的父 (ONE) 方指定 CASCADE 型別 REMOVE 是安全的。然後,我們對關係定義進行了相應的重新調整。
public abstract class Party implements Serializable{ @OneToMany (mappedBy = "party", fetch = FetchType.EAGER, cascade =
{CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE}) private Collection<Address> addresses; @OneToMany (mappedBy = "party", fetch=FetchType.LAZY, cascade =
{CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE}) private Collection<Penalty> penalties; ..... }
管理關係
根據 JPA,管理關係是程式設計師的唯一職責。永續性提供程式不承擔有關關係資料狀態的任何事情,因此它們不嘗試管理關係。
假定了該事實,我們重新檢查了我們用來管理關係和查明潛在問題區域的策略。我們發現:
- 如果我們嘗試設定父級和級子之間的關係,但父級不再存在於資料庫中(可能被其他使用者刪除),這將導致資料完整性問題。
- 如果我們嘗試刪除一條父記錄而沒有首先刪除其子記錄,將違反引用完整性。
因此,我們規定了以下編碼原則:
- 如果我們獲得一個實體以及該實體在某個事務內的相關實體,在該事務外部更改關係,然後嘗試在新的事務內持久儲存更改,那麼最好重新獲取父實體。
- 如果我們嘗試刪除一條父記錄而不刪除子記錄,那麼我們必須將所有子記錄的外來鍵欄位設定為 NULL,然後再刪除該父記錄。
考慮 CaseWorker 和 Registration 之間的 OneToOne 關係。刪除特定的登記時,我們並不刪除辦事員;因此,我們需要先將 reg_id 外來鍵設定為空,然後才能刪除任何登記。
@Stateless public class RegManager { ..... public void deleteReg(int regId){ Registration reg = em.find(Registration.class, regId); CaseOfficer officer =reg.getCaseOfficer(); officer.setRegistration(null); em.remove(reg); } }
資料完整性
一個使用者檢視某條登記記錄時,另一個使用者可能正在對同一應用程式進行更改。如果第一個使用者隨後對該申請進行了其他更改,他可能面臨在不知情的情況下用舊資料覆蓋該應用程式的風險。
為了解決此問題,我們決定使用“樂觀鎖定”。在 JPA 中,實體可以定義一個版本列,我們可以用該列實施樂觀鎖定。
public class Registration implements Serializable{ @Version private int version; ..... }
永續性提供程式會將版本列的記憶體中值與資料庫中的該值進行匹配。如果兩個值不同,永續性提供程式將報告異常。
驗證
當我們說主申請人至少必須有一個地址且地址至少必須包含首行和郵政編碼時,我們是對 Party 和 Address 實體應用業務規則。然而,如果我們說每個地址行必須始終少於 100 個字元時,該驗證是 Address 實體固有的。
在我們的應用程式中,由於大多數工作流和麵向流程的邏輯都在會話 Bean 層進行編碼,因此我們決定實施到該層的跨物件/業務規則型別驗證。然而,我們在實體內放置了固有驗證。使用 JPA,我們可以將任何方法與實體的生命週期事件相關聯。
以下例項驗證了 Address 行包含的字元不能超過 100 個,並在持久儲存 Address 實體之前呼叫該方法(通過 @PrePersist 批註)。出現故障時,該方法將向呼叫者丟擲業務異常(擴充套件自 RuntimeException 類),然後可以使用該異常向使用者傳遞一條訊息。
public class Address implements Serializable{ ..... @PrePersist public void validate() if(addressLine1!=null && addressLine1.length()>1000){ throw new ValidationException("Address Line 1 is longer than 1000 chars."); } }
搜尋
我們的稅務登記應用程式提供了一個搜尋工具,用來查詢有關特定登記的詳細資訊、其當事人以及其他詳細資訊。提供一個有效的搜尋工具涉及很多挑戰,如編寫有效的查詢以及為了瀏覽大型結果列表而實施分頁。JPA 指定了一個 Java 永續性查詢語言 (JP-QL),與實體一同使用以實施資料訪問。這是對 EJB 2.x EJB QL 的主要改進。我們成功地使用 JP-QL 提供了有效的資料訪問機制。
查詢
在 JPA 中,我們可以選擇動態建立查詢或定義靜態查詢。這些靜態或命名查詢支援引數;引數值在執行時指定。由於我們的查詢範圍定義得相當好,因此我們決定將命名查詢與引數結合使用。命名查詢也更為有效,因為永續性提供程式可以快取轉換的 SQL 查詢,以供將來使用。
我們的應用程式為此提供了一個簡單的使用案例:使用者輸入一個申請引用號以檢索登記詳細資訊。我們在 Registration 實體上提供了一個命名查詢,如下所示:
@Entity @Table(name="REGISTRATION") @NamedQuery(name="findByRegNumber", query = "SELECT r FROM REGISTRATION r WHERE r.appRefNumber=?1") public class Registration implements Serializable{ ..... }
例如,我們應用程式內的一個搜尋要求需要特別注意:用於檢索所有當事人及其罰款總額的報告查詢。由於該應用程式允許存在無處罰的當事人,因此簡單的 JOIN 操作不會列出無處罰的當事人。 為解決此問題,我們使用了 JP-QL 的 OUTER JOIN 工具。我們還可以使用 GROUP BY 子句累積處罰。我們在 Party 實體中添加了另一個命名查詢,如下所示:
@Entity @Table(name="PARTY_DATA") @Inheritance(strategy= InheritanceType.SINGLE_TABLE) @DiscriminatorColumn(name="PARTY_TYPE") @NamedQueries({@NamedQuery(name="generateReport", query=" SELECT NEW com.ssg.article.ReportDTO(p.name, SUM(pen.amount))
FROM Party p LEFT JOIN p.penalties pen GROUP BY p.name""), @NamedQuery(name="bulkInactive", query="UPDATE PARTY P SET p.status=0 where p.registrationID=?1")}) public abstract class Party { ..... }
注意,在上面的命名查詢“generateReport”示例中,我們例項化了該查詢本身內的一個新 ReportDTO 物件。這仍然是 JPA 的一個十分強大的功能。
我們可以批量操作嗎?
在我們的應用程式中,官員可以檢索登記並使其處於非活動狀態。在這種情況下,我們還應該將所有與該 Registration 相關聯的 Party 都設定為非活動狀態。這通常意味著將 PARTY 表中的 Status 列設定為 0。為了提高效能,我們將使用批量更新,而不是針對每個 Party 執行單獨的 SQL。
幸運的是,JPA 提供了進行此操作的方法:
@NamedQuery(name="bulkInactive", query="UPDATE PARTY p SET p.status=0 where p.registrationID=?1") public abstract class Party implements Serializable{ ..... }
注意:批量操作直接向資料庫發出 SQL,這意味著並不更新持就性上下文以反映更改。使用超出單個事務範圍的擴充套件的永續性上下文時,快取的實體可能包含陳的資料。
及早獲取。
另一個挑戰性的要求是選擇性資料顯示。例如,如果管理員搜尋登記,我們需要顯示登記方記錄的所有處罰。然而,該資訊並不提供給普通辦事員。對於某些登記,我們需要顯示登記方記錄的所有處罰。然而,該資訊並不提供給普通辦事員。
Party 和 Penalty 之間的關係是 OneToMany。前面提到過,此關係的預設 FETCH 型別為 LAZY。但為了滿足這個搜尋選擇性顯示要求,將 Penalty 詳細資訊作為單個 SQL 獲取以避免多個 SQL 呼叫是很有意義的。
JP-QL 中的 FETCH Join 特性幫我們解決了這個問題。如果我們希望暫時覆蓋 LAZY 獲取型別,可以使用 Fetch Join。然而,如果頻繁使用該特性,考慮將 FETCH 型別重新調整為 EAGER 是很明智的。
@NamedQueries({@NamedQuery(name="generateReport", query=" SELECT NEW com.ssg.article.ReportDTO(p.name, SUM(pen.amount))
FROM Party p LEFT JOIN p.penalties pen GROUP BY p.name""), @NamedQuery(name="bulkInactive", query="UPDATE PARTY P SET p.status=0 where p.registrationID=?1"), @NamedQuery(name="getItEarly", query="SELECT p FROM Party p JOIN FETCH p.penalties")}) public abstract class Party { ..... }
結論
總的說來,JPA 簡化了永續性編碼。我們發現它功能齊備且十分有效。它豐富的查詢介面和極大改進的查詢語言簡化了複雜關係情況的處理。它的繼承支援幫助我們在永續性級別保持邏輯域模型,我們可以跨層重新用相同的實體。JPA 的所有優點使其成為大家今後明確的選擇