JPA的實施模式:延遲加載
JPA 1.0規范沒有深入地討論這一主題而僅僅是用大致同於以下的幾句話來進行描述是很令人遺憾的:
即 時策略(EAGER strategy)是持久性提供程序(persistence provider)運行時方面的一個需求,即數據必須被及時抓取(eagerly fetched),而對於持久性提供程序運行時來說,延遲策略(LAZY strategy)則是一個提示(hint),示意數據在首次被訪問時,其應該被延遲加載。這樣的實現是允許的,即允許及時加載已經指定了延遲策略提示的 數據。
截至撰寫本文時止,JPA 2.0規范的擬議最后草案並未在這一部分中添加任何內容,現在我們能做的就是閱讀JPA提供程序(JPA provider)的文檔並做一些實驗。
延遲加載何時會發生
@Basic、 @OneToMany、@ManyToOne、@OneToOne、@ManyToMany等所有的注解都有一個被稱為fetch的可選參數,如果該參數 被設置為FetchType.LAZY的話,那么對於JPA提供程序來說,它會被解釋成一個提示,示意可以延遲該域的加載直至其第一次被訪問:
在@Basic注解情況下是屬性的值
在@ManyToOne或者@OneToOne注解情況下是引用,或者
在@OneToMany或者@ManyToMany注解情況下是集合
在缺省情況下,即時加載屬性的值而延遲加載集合,如果你之前用過純Hibernate的話,那么以下情況是與你的預期相反的,即在缺省情況下,引用是被即時加載的。
在討論如何使用延遲加載之前,讓我們先來看看JPA提供程序有可能會如何來實現延遲加載。
構建時字節碼編入、運行時字節碼編入和運行時代理
為了使得延遲加載有效,JPA提供程序需要變變魔術,把一些並未在某個地方存在的對象顯現出來,使它們顯得就像在那里似的,JPA提供程序可以通過一些不同的方法來達到這一目的,其中最常用的方法是:
構 建時字節碼編入(Build-time bytecode instrumentation)—實體類在被編譯之后及打包運行前就被編入。這一做法的一個缺點是其需要修改構建過程以及(因此)不能總是保持與IDE 的兼容。被編入的類與其非編入版本之間可能會是二進制不兼容的,這可能會導致Java序列化問題以及其他類似問題,不過我還沒有獲悉有人提到過這方面的問 題。
運行時字節碼編入(Run-time bytecode instrumentation)—實體類還可以在運行時而不是構建時被編入,這就需要使用JDK 1.5或以上版本的-javaagent選項來安裝Java代理,運行在JDK 1.6或更高版本之下時則是使用類的重轉化(class retransformation),如果正在使用更舊版本的JDK的話,或者需要用到一些專有的方法。因此,雖然這種方法不需要修改構建過程,但卻是與 使用的JDK密切相關的。
運行時代理(Run-time proxies)—在這種情況下,類不需要被編入,不過JPA提供程序返回的對象是實際實體的代理,這些代理可以是動態代理類、由CGLIB創建的代理或 者是代理集合類。這種方法雖然要求的設置最少,但卻是可供JPA實現者使用的方法中最缺少透明度的一個,因此需要完全了解他們。
Hibernate的基於運行時代理的延遲加載
雖然Hibernate支持通過構建時的字節碼編入來啟用個別屬性的延遲加載,但是大多數的Hibernate用戶都會使用運行時代理,這是是默認的方法,並且大多數情況下都非常有效,所以,讓我們來探討一下Hibernate的運行時代理。
Hibernate創建兩種類型的代理:
1. 在通過延遲的多對一或者一對一關聯來延遲加載實體,或者通過調用EntityManager.getReference來延遲加載實體 時,Hibernate使用CGLIB來創建實體類的一個子類,該子類作為到真正實體的代理。代理中的任一方法首次被調用時,從數據庫中加載實體,並且把 方法調用傳遞給被加載的實體。我的同事Maarten Winkels去年曾在他的博客上講述了這些Hibernate代理存在的隱患。
2. 在通過一對多或者多對多關聯來延遲加載實體集合時,Hibernate返回諸如PersistentSet或者PersistentMap一類的實現了 PersistentCollection接口的類的一個實例,集合第一次被訪問的時候,它的成員會被加載,成員實體被當作平常的類來加載,因此之前提到 的Hibernate代理的隱患與這部分無關。
為了感受一下這里發生的事情,你可能希望在調試器中跟進一些簡單的JPA代碼來查看Hibernate創建的對象,如果這一機制涉及很多的話,這種做法會增進你理解。J
OpenJPA的運行時代碼編入
OpenJPA提供了許多增強方法(enhancement method),因為文檔中就是這樣稱呼它的,其中我發現運行時的字節碼編入是最容易設置的。
通 過調試器的步進,你能夠看到OpenJPA並未創建代理,作為代替,一些額外的域出現在每一個實體類中,它們有着類似pcStateManager或者 pcDetachedState這樣的名字。更重要的是,你可以看到被延遲加載的實體的域都被設置為0或者null,這樣的話它的內容只有在其方法被調用 的時候才加載,更確切地說,被配置為延遲加載的屬性只有在其getter方法被調用時才加載。
了解這一點是非常重要的,即直接訪問延遲加載 實體的域(或者是代表延遲加載屬性的域)並不會觸發對這一實體(或者域)的加載,此外,當會話(session)不再是可用的時,OpenJPA不會像 Hibernate那樣拋出異常,而只是讓值處於非初始化狀態,這在以后會引發我之前提過的NullPointException異常。
OpenJPA與Hibernate相比較
你可能會注意到這兩種方法之間的首要區別是被代理/編入的對象:
OpenJPA 對所有實體進行了編入,這意味着它能夠檢測到你何時訪問正在使用的實體的一個延遲的引用或者集合,這樣它就會返回實際的實體或者實際實體的集合。你只有在 使用EntityManager.getReference延遲加載實體時,或者已把屬性配置成延遲加載時,才會得到(部分)為空的實體。
就 延遲引用(或者是已使用EntityManager.getReference來延遲加載的實體)這一情況來說,Hibernate使用CGLIB來代理 延遲對象本身,這會帶來之前提到過的代理隱患。在使用延遲集合的時候,Hibernate的處理則像OpenJPA一樣是透明的。最后一點 是,Hibernate並不支持使用代理的延遲加載屬性。
如果把OpenJPA的編入和Hibernate的運行時代理相比較一下你就會發現,OpenJPA所采用的方法更加的透明化,遺憾的是,這一優勢卻因為OpenJPA較少強健的錯誤處理而有所縮小。
模式
既然現在已經知道了如何配置延遲加載以及它的工作方式,那么我們如何才能正確地使用它呢?
第 一步是檢查所有的關聯,查看哪些應該被延遲加載而哪些應該被即時加載。我的經驗法則是開始先把所有的*對一關聯都保留為即時的(這是缺省情況),他們在通 常情況下的查詢數目的合計無論如何都很難達到一個很大的數量,如果數量真的很大的話,我可以修改這些關聯。然后我會檢查所有的*對多關聯,任何它們中的關 聯如果其指向的實體總是被訪問因而總是被加載的話,我就會把他們配置成即時加載的,有時候我會使用Hibernate特有的 @CollectionOfElements注解來映射這種“值類型”的實體。
第二步是最重要的,為了防止所有的 LazyInitializationException或者NullPointerException異常,你需要確保所有對領域對象的訪問在同一個事 務內部發生。當領域對象在事務完成之后被訪問時,持久性上下文不能再被訪問以加載持久對象,因此會導致這些問題的出現。有兩種方法可用來化解這一矛盾:
1. 最純粹的方式是在服務之前放置一個服務門面(Service Facade)(如果你喜歡的話遠程門面(Remote Facade)也行),並通過傳輸對象(即Data Transfer Object,又名DTO)只與服務門面的客戶通信。門面負責把所有適當的值從領域對象拷貝到數據傳輸對象中,包括引用和集合的深度拷貝。應用的事務范圍 應包括服務門面這樣的工作模式,即給門面設定@Transactional注解,或者為它指定一個適當的@TransactionAttribute。
2. 如果你正在使用MVC框架來編寫模型2(Model 2)web應用的話,另一種被廣泛使用的辦法是在視圖模式中使用打開的EntityManager,在Spring中可配置一個Servlet過濾器或者 Web MVC攔截器,這樣當請求進來的時候,過濾器或者攔截器會打開實體管理器,並保持管理器的打開狀態直到請求的處理完成。這意味着在控制器 (controller)和視圖(view)(JSP或者其他方式)中活動的是相同的事務。或者一些純粹控會爭辯說,這種做法會導致表現層依賴於領域對 象,但對於簡單的web應用來說,這是一種令人感興趣的方式。
第三步是啟用JPA提供程序的SQL日志功能來檢查應用的一 些用例。這對於了解實體被訪問時哪些查詢被執行很有啟發作用。SQL日志還能夠提供性能優化的輸入,因此你能夠重新審視在步驟一中所作的決定並對數據庫進 行調整。最終延遲加載都會與性能有關,所以不要忘記了本步驟。
我希望本篇博客給你帶來了關於延遲加載如何工作以及如何在應 用中使用它的一些見解,在下一篇博客中我會深入探討DTO這一主題和服務門面模式。不過在結束之前,我要感謝來J-Spring 2009討論這一主題的每個人,我非常開心!看起來確實有很多人都想知道如何有效地使用JPA,因為我收到了很多問題。遺憾的是這些問題用光了我的所有時 間,下次我會更加注意舉着時間卡的女孩,並帶上我的手表,再次感謝你們的參與!