有關使用和部署 Java 持久性體系結構 (JPA) 的案例研究
2006 年 11 月發(fā)布
2006 年夏天發(fā)布的 EJB 3.0 規(guī)范提供了一個大大簡化但功能更為強大的 EJB 框架,該框架演示了批注與傳統(tǒng) EJB 2.x 部署描述符相比的顯著優(yōu)勢。J2SE 5.0 中引入的批注是修飾符,可以在類、字段、方法、參數(shù)、本地變量、構造符、枚舉和程序包中使用。大量 EJB 3.0 新特性中都強調(diào)了批注的使用,這些特性包括:基于普通舊式 Java 對象的 EJB 類、EJB 管理器類的相關性注入、引入可以攔截其他業(yè)務方法調(diào)用的攔截器或方法,以及顯著增強的 Java 持久性 API (JPA) 等。
為了說明 JPA 的概念,我們來看一個實際示例。最近,我的辦公室需要實施稅務登記系統(tǒng)。與大多數(shù)系統(tǒng)一樣,該系統(tǒng)具有自己的復雜性和挑戰(zhàn)性。由于其特殊的挑戰(zhàn)涉及了數(shù)據(jù)訪問和對象關系映射 (ORM),因此我們決定在實施該系統(tǒng)的同時試用新的 JPA。
在該項目期間,我們面臨以下幾個挑戰(zhàn):
應用程序中使用的實體之間存在多種關系。 應用程序支持對關系數(shù)據(jù)進行復雜搜索。 應用程序必須確保數(shù)據(jù)完整性。 應用程序在持久保存數(shù)據(jù)之前需要對其進行驗證。 需要批量操作。
數(shù)據(jù)模型
首先來看看我們的關系數(shù)據(jù)模型的簡化版本,該版本足以解釋 JPA 的細微之處。從業(yè)務角度而言,主申請人提交稅務登記申請。申請人可以有零個或多個合伙人。申請人和合伙人必須指定兩個地址,即注冊地址和經(jīng)營地址。主申請人還必須聲明和描述其過去受到的所有處罰。
定義實體。我們通過將實體映射到單獨的表定義了以下實體:
實體 映射到的表
Registration REGISTRATION
Party PARTY
Address ADDRESS
Penalty PENALTY
CaseOfficer CASE_OFFICER
表 1. 實體-表映射
識別要映射到數(shù)據(jù)庫表和列的實體很容易。下面是一個簡化的 Registration 實體示例。(我將在后面介紹該實體的其他映射和配置。)
@Entity@Table(name="REGISTRATION")public class Registration implements Serializable{@Idprivate int id;@Column(name="REFERENCE_NUBER")private String referenceNumber;..........}
對我們而言,使用 JPA 實體的主要好處是我們感覺就像對常規(guī)的 Java 類進行編碼一樣:無需再使用復雜的生命周期方法。我們可以使用批注將持久性特性分配給實體。我們發(fā)現(xiàn)無需使用其他數(shù)據(jù)傳輸對象 (DTO) 層,并且可以重用實體以便在層之間移動。數(shù)據(jù)的可移動性突然變得更好了。
支持多態(tài)性。通過查看我們的數(shù)據(jù)模型,我們注意到我們使用了 PARTY 表同時存儲申請人和合伙人記錄。這些記錄不但具有一些相同的屬性,而且還具有各自特有的屬性。
我們希望在繼承層次中對此模型進行建模。利用 EJB 2.x,我們只能使用一個 Party 實體 bean,然后通過在代碼內(nèi)實施邏輯來根據(jù) party 類型創(chuàng)建申請人或合伙人對象。另一方面,JPA 使我們可以在實體級別指定繼承層次。
我們決定通過一個抽象的實體 Party 和兩個具體的實體 Partner 和 Applicant 對繼承層次進行建模:
@Entity@Table(name="PARTY_DATA")@Inheritance(strategy= InheritanceType.SINGLE_TABLE)@DiscriminatorColumn(name="PARTY_TYPE")public abstract class Party implements Serializable{@Idprotected int id;@Column(name="REG_ID")protected int regID;protected String name;.........}
兩個具體的類 Partner 和 Applicant 現(xiàn)在將繼承抽象的 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 實體的實例。
構建關系。我們的應用程序數(shù)據(jù)模型中的 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 規(guī)范定義、持久性提供程序實施的關系的行為。我們希望如何獲取相關數(shù)據(jù),EAGER 還是 LAZY?我們查看了由 JPA 定義的關系的默認 FETCH 類型,然后向表 2 中添加了額外的一列以包括我們的發(fā)現(xiàn):
關系 擁有方 多重性/映射 默認的 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 類型
通過查看業(yè)務要求,似乎當我們獲得 Registration 詳細信息后,我們總是需要顯示與該登記相關聯(lián)的 Party 的詳細信息。如果將 FETCH 類型設置為 LAZY,我們需要反復調(diào)用數(shù)據(jù)庫以獲取數(shù)據(jù)。這意味著,如果將 Registration->Party 關系的 FETCH 類型改為 EAGER,我們會獲得更好的性能。在該設置下,持久性提供程序將相關數(shù)據(jù)作為單個 SQL 的一部分返回。
同樣,當我們在屏幕上顯示 Party 詳細信息時,我們需要顯示其相關聯(lián)的 Address。因此,將 Party-Address 關系改為使用 EAGER 獲取類型是很有幫助的。
另一方面,我們可以將 Party->Penalty 關系的 FETCH 類型設為 LAZY,因為我們不需要顯示處罰的詳細信息,除非用戶這樣要求。如果我們使用了 EAGER 獲取類型,當 m 個當事人每人有 n 個處罰時,我們最終就要加載 m*n 個 Penalty 實體,這會產(chǎn)生不必要的大對象圖形,從而降低性能。
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 范圍內(nèi)的持久性上下文之間進行選擇。EXTENDED 持久性上下文在事務之間保持活動狀態(tài),作用非常類似會話狀態(tài)的會話 bean。
由于我們的應用程序不是會話式的,持久性上下文不需要在事務之間可持續(xù);因此,我們決定使用 TRANSACTION 范圍內(nèi)的持久性上下文。但是,這帶來了惰性加載的問題。獲取了實體并結束了事務之后,就可以分離實體了。在我們的應用程序中,嘗試加載任何以惰性方式加載的關系數(shù)據(jù)將產(chǎn)生未定義的行為。
大多數(shù)情況下,當辦事員檢索登記數(shù)據(jù)時,我們不需要顯示處罰記錄。但是對于管理員,我們需要額外顯示處罰記錄。考慮到大多數(shù)情況下,我們不需要顯示處罰記錄,將關系的 FETCH 類型更改為 EAGER 就沒什么意義了。相反,我們可以通過檢測經(jīng)營者使用系統(tǒng)的時間來觸發(fā)關系數(shù)據(jù)的惰性加載。這會使關系數(shù)據(jù)在實體已分離時也可用,并可以在以后進行訪問。下面的示例解釋了這個概念:
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;
在上面的示例中,我們只調(diào)用 Party 實體的處罰集合的 size() 方法。這樣做確實有效并且觸發(fā)了惰性加載,即使在 Registration 實體分離時,所有集合也會填充并可用。(或者,您可以使用 JP-QL 的一個名為 FETCH JOIN 的特殊特性,我們會在本文的后面對此進行討論。)
關系和持久性
接下來,我們需要考慮關系在持久保存數(shù)據(jù)的上下文中的行為方式。本質上講,如果對關系數(shù)據(jù)進行了任何更改,我們希望在對象級別進行同樣的更改并通過持久性提供程序持久保存這些更改。在 JPA 中,我們可以使用 CASCADE 類型控制持久性行為。
JPA 中定義了四種 CASCADE 類型:
PERSIST:持久保存擁有方實體時,也會持久保存該實體的所有相關數(shù)據(jù)。 MERGE:將分離的實體重新合并到活動的持久性上下文時,也會合并該實體的所有相關數(shù)據(jù)。 REMOVE:刪除一個實體時,也會刪除該實體的所有相關數(shù)據(jù)。 ALL:以上都適用。
創(chuàng)建實體。 我們決定在所有情況下,當我們新建一個父實體時,我們希望其所有相關的子實體也自動持久保存。這簡化了編碼:我們只需正確設置關系數(shù)據(jù),而無需在每個實體上單獨調(diào)用 persist() 操作。這意味著簡化了編碼,因為我們只需正確設置關系數(shù)據(jù),而無需在每個實體上單獨調(diào)用 persist() 操作。
因此,級聯(lián)類型 PERSIST 是對我們最具吸引力的選項。我們將所有關系定義重新調(diào)整為使用該選項。
更新實體 在事務內(nèi)獲取數(shù)據(jù),然后在事務外對實體進行更改并持久保存更改,這是很常見的。例如,在我們的應用程序中,用戶可以檢索現(xiàn)有的登記,更改主申請人的地址。當我們獲取一個現(xiàn)有的 Registration 實體并因此獲取了該實體在特定事務內(nèi)的所有相關數(shù)據(jù)時,事務在此處結束,數(shù)據(jù)被發(fā)送到表示層。此時,該 Registration 以及所有其他相關的實體實例與持久性上下文相分離。
在 JPA 中,為了持久保存分離實體上的更改,我們使用 EntityManager 的 merge() 操作。此外,為將更改傳播到關系數(shù)據(jù),所有關系定義必須包括 CASCADE 類型 MERGE 以及關系映射的配置中定義的任何其他 CASCADE 類型。
在該背景下,我們確保了為所有關系定義指定了正確的 CASCADE 類型。
刪除實體。 接下來,我們需要確定刪除某些實體時會發(fā)生什么。例如,如果我們刪除一個 Registration,我們可以安全地刪除與該 Registration 相關聯(lián)的所有 Party。但是反過來卻不是這樣。此處的技巧是通過在關系上級聯(lián) remove() 操作以避免意外刪除實體。正如您將在下一部分中看到的那樣,由于引用完整性約束,這樣的操作可能不會成功。
我們得出以下結論:在遵循 OnetoMany 的清晰的父子關系中(如 Party 和 Address 或 Party 和 Penalty),僅在關系的父 (ONE) 方指定 CASCADE 類型 REMOVE 是安全的。然后,我們對關系定義進行了相應的重新調(diào)整。
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;.....}管理關系
根據(jù) JPA,管理關系是程序員的唯一職責。持久性提供程序不承擔有關關系數(shù)據(jù)狀態(tài)的任何事情,因此它們不嘗試管理關系。
假定了該事實,我們重新檢查了我們用來管理關系和查明潛在問題區(qū)域的策略。我們發(fā)現(xiàn):
如果我們嘗試設置父級和級子之間的關系,但父級不再存在于數(shù)據(jù)庫中(可能被其他用戶刪除),這將導致數(shù)據(jù)完整性問題。 如果我們嘗試刪除一條父記錄而沒有首先刪除其子記錄,將違反引用完整性。
因此,我們規(guī)定了以下編碼原則:
如果我們獲得一個實體以及該實體在某個事務內(nèi)的相關實體,在該事務外部更改關系,然后嘗試在新的事務內(nèi)持久保存更改,那么最好重新獲取父實體。 如果我們嘗試刪除一條父記錄而不刪除子記錄,那么我們必須將所有子記錄的外鍵字段設置為 NULL,然后再刪除該父記錄。
考慮 CaseWorker 和 Registration 之間的 OneToOne 關系。刪除特定的登記時,我們并不刪除辦事員;因此,我們需要先將 reg_id 外鍵設置為空,然后才能刪除任何登記。
@Statelesspublic class RegManager {.....public void deleteReg(int regId){Registration reg = em.find(Registration.class, regId);CaseOfficer officer =reg.getCaseOfficer();officer.setRegistration(null);em.remove(reg);}}
數(shù)據(jù)完整性
一個用戶查看某條登記記錄時,另一個用戶可能正在對同一應用程序進行更改。如果第一個用戶隨后對該申請進行了其他更改,他可能面臨在不知情的情況下用舊數(shù)據(jù)覆蓋該應用程序的風險。
為了解決此問題,我們決定使用“樂觀鎖定”。在 JPA 中,實體可以定義一個版本列,我們可以用該列實施樂觀鎖定。
public class Registration implements Serializable{@Versionprivate int version;.....}
持久性提供程序會將版本列的內(nèi)存中值與數(shù)據(jù)庫中的該值進行匹配。如果兩個值不同,持久性提供程序將報告異常。
驗證
當我們說主申請人至少必須有一個地址且地址至少必須包含首行和郵政編碼時,我們是對 Party 和 Address 實體應用業(yè)務規(guī)則。然而,如果我們說每個地址行必須始終少于 100 個字符時,該驗證是 Address 實體固有的。
在我們的應用程序中,由于大多數(shù)工作流和面向流程的邏輯都在會話 Bean 層進行編碼,因此我們決定實施到該層的跨對象/業(yè)務規(guī)則類型驗證。然而,我們在實體內(nèi)放置了固有驗證。使用 JPA,我們可以將任何方法與實體的生命周期事件相關聯(lián)。
以下實例驗證了 Address 行包含的字符不能超過 100 個,并在持久保存 Address 實體之前調(diào)用該方法(通過 @PrePersist 批注)。出現(xiàn)故障時,該方法將向調(diào)用者拋出業(yè)務異常(擴展自 RuntimeException 類),然后可以使用該異常向用戶傳遞一條消息。
public class Address implements Serializable{.....@PrePersistpublic void validate()if(addressLine1!=null && addressLine1.length()>1000){throw new ValidationException("Address Line 1 is longer than 1000 chars.");}}搜索
我們的稅務登記應用程序提供了一個搜索工具,用來查找有關特定登記的詳細信息、其當事人以及其他詳細信息。提供一個有效的搜索工具涉及很多挑戰(zhàn),如編寫有效的查詢以及為了瀏覽大型結果列表而實施分頁。JPA 指定了一個 Java 持久性查詢語言 (JP-QL),與實體一同使用以實施數(shù)據(jù)訪問。這是對 EJB 2.x EJB QL 的主要改進。我們成功地使用 JP-QL 提供了有效的數(shù)據(jù)訪問機制。
查詢
在 JPA 中,我們可以選擇動態(tài)創(chuàng)建查詢或定義靜態(tài)查詢。這些靜態(tài)或命名查詢支持參數(shù);參數(shù)值在運行時指定。由于我們的查詢范圍定義得相當好,因此我們決定將命名查詢與參數(shù)結合使用。命名查詢也更為有效,因為持久性提供程序可以緩存轉換的 SQL 查詢,以供將來使用。
我們的應用程序為此提供了一個簡單的使用案例:用戶輸入一個申請引用號以檢索登記詳細信息。我們在 Registration 實體上提供了一個命名查詢,如下所示:
@Entity@Table(name="REGISTRATION")@NamedQuery(name="findByRegNumber", query = "SELECT r FROM REGISTRATION r WHERE r.appRefNumber=?1")public class Registration implements Serializable{.....}
例如,我們應用程序內(nèi)的一個搜索要求需要特別注意:用于檢索所有當事人及其罰款總額的報告查詢。由于該應用程序允許存在無處罰的當事人,因此簡單的 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”示例中,我們實例化了該查詢本身內(nèi)的一個新 ReportDTO 對象。這仍然是 JPA 的一個十分強大的功能。
我們可以批量操作嗎?
在我們的應用程序中,官員可以檢索登記并使其處于非活動狀態(tài)。在這種情況下,我們還應該將所有與該 Registration 相關聯(lián)的 Party 都設置為非活動狀態(tài)。這通常意味著將 PARTY 表中的 Status 列設置為 0。為了提高性能,我們將使用批量更新,而不是針對每個 Party 執(zhí)行單獨的 SQL。
幸運的是,JPA 提供了進行此操作的方法:
@NamedQuery(name="bulkInactive", query="UPDATE PARTY p SET p.status=0 where p.registrationID=?1")public abstract class Party implements Serializable{.....}
注意:批量操作直接向數(shù)據(jù)庫發(fā)出 SQL,這意味著并不更新持就性上下文以反映更改。使用超出單個事務范圍的擴展的持久性上下文時,緩存的實體可能包含陳的數(shù)據(jù)。
及早獲取。
另一個挑戰(zhàn)性的要求是選擇性數(shù)據(jù)顯示。例如,如果管理員搜索登記,我們需要顯示登記方記錄的所有處罰。然而,該信息并不提供給普通辦事員。對于某些登記,我們需要顯示登記方記錄的所有處罰。然而,該信息并不提供給普通辦事員。
Party 和 Penalty 之間的關系是 OneToMany。前面提到過,此關系的默認 FETCH 類型為 LAZY。但為了滿足這個搜索選擇性顯示要求,將 Penalty 詳細信息作為單個 SQL 獲取以避免多個 SQL 調(diào)用是很有意義的。
JP-QL 中的 FETCH Join 特性幫我們解決了這個問題。如果我們希望暫時覆蓋 LAZY 獲取類型,可以使用 Fetch Join。然而,如果頻繁使用該特性,考慮將 FETCH 類型重新調(diào)整為 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 簡化了持久性編碼。我們發(fā)現(xiàn)它功能齊備且十分有效。它豐富的查詢界面和極大改進的查詢語言簡化了復雜關系情況的處理。它的繼承支持幫助我們在持久性級別保持邏輯域模型,我們可以跨層重新用相同的實體。JPA 的所有優(yōu)點使其成為大家今后明確的選擇。