天天看點

使用 Spring Data JPA 簡化 JPA 開發—Spring Data JPA 開發指南

從一個簡單的 JPA 示例開始

本文主要講述 Spring Data JPA,但是為了不至于給 JPA 和 Spring 的初學者造成較大的學習曲線,我們首先從 JPA 開始,簡單介紹一個 JPA 示例;接着重構該示例,并引入 Spring 架構,這兩部分不會涉及過多的篇幅,如果希望能夠深入學習 Spring 和 JPA,可以根據本文最後提供的參考資料進一步學習。

自 JPA 伴随 Java EE 5 釋出以來,受到了各大廠商及開源社群的追捧,各種商用的和開源的 JPA 架構如雨後春筍般出現,為開發者提供了豐富的選擇。它一改之前 EJB 2.x 中實體 Bean 笨重且難以使用的形象,充分吸收了在開源社群已經相對成熟的 ORM 思想。另外,它并不依賴于 EJB 容器,可以作為一個獨立的持久層技術而存在。目前比較成熟的 JPA 架構主要包括 Jboss 的 Hibernate EntityManager、Oracle 捐獻給 Eclipse 社群的 EclipseLink、Apache 的 OpenJPA 等。

本文的示例代碼基于 Hibernate EntityManager 開發,但是讀者幾乎不用修改任何代碼,便可以非常容易地切換到其他 JPA 架構,因為代碼中使用到的都是 JPA 規範提供的接口 / 類,并沒有使用到架構本身的私有特性。示例主要涉及七個檔案,但是很清晰:業務層包含一個接口和一個實作;持久層包含一個接口、一個實作、一個實體類;另外加上一個 JPA 配置檔案和一個測試類。相關類 / 接口代碼如下:

清單 1. 實體類 AccountInfo.java

清單 2. 業務層接口 UserService.java

清單 3. 業務層的實作類 UserServiceImpl.java

清單 4. 持久層接口

清單 5. 持久層的實作類

清單 6. JPA 标準配置檔案 persistence.xml

清單 7. 本文使用如下的 main 方法進行開發者測試

回頁首

簡述 Spring 架構對 JPA 的支援

接下來我們引入 Spring,以展示 Spring 架構對 JPA 的支援。業務層接口 UserService 保持不變,UserServiceImpl 中增加了三個注解,以讓 Spring 完成依賴注入,是以不再需要使用 new 操作符建立 UserDaoImpl 對象了。同時我們還使用了 Spring 的聲明式事務:

清單 8. 配置為 Spring Bean 的業務層實作

對于持久層,UserDao 接口也不需要修改,隻需修改 UserDaoImpl 實作,修改後的代碼如下:

清單 9. 配置為 Spring Bean 的持久層實作

清單 10. Spring 配置檔案

清單 11. 改造後的基于 Spring 的開發者測試代碼

通過對比重構前後的代碼,可以發現 Spring 對 JPA 的簡化已經非常出色了,我們可以大緻總結一下 Spring 架構對 JPA 提供的支援主要展現在如下幾個方面:

首先,它使得 JPA 配置變得更加靈活。JPA 規範要求,配置檔案必須命名為 persistence.xml,并存在于類路徑下的 META-INF 目錄中。該檔案通常包含了初始化 JPA 引擎所需的全部資訊。Spring 提供的 LocalContainerEntityManagerFactoryBean 提供了非常靈活的配置,persistence.xml 中的資訊都可以在此以屬性注入的方式提供。

其次,Spring 實作了部分在 EJB 容器環境下才具有的功能,比如對 @PersistenceContext、@PersistenceUnit 的容器注入支援。

第三,也是最具意義的,Spring 将 EntityManager 的建立與銷毀、事務管理等代碼抽取出來,并由其統一管理,開發者不需要關心這些,如前面的代碼所示,業務方法中隻剩下操作領域對象的代碼,事務管理和 EntityManager 建立、銷毀的代碼都不再需要開發者關心了。

更進一步:Spring Data JPA 讓一切近乎完美

通過前面的分析可以看出,Spring 對 JPA 的支援已經非常強大,開發者隻需關心核心業務邏輯的實作代碼,無需過多關注 EntityManager 的建立、事務處理等 JPA 相關的處理,這基本上也是作為一個開發架構而言所能做到的極限了。然而,Spring 開發小組并沒有止步,他們再接再厲,于最近推出了 Spring Data JPA 架構,主要針對的就是 Spring 唯一沒有簡化到的業務邏輯代碼,至此,開發者連僅剩的實作持久層業務邏輯的工作都省了,唯一要做的,就隻是聲明持久層的接口,其他都交給 Spring Data JPA 來幫你完成!

至此,讀者可能會存在一個疑問,架構怎麼可能代替開發者實作業務邏輯呢?畢竟,每一個應用的持久層業務甚至領域對象都不盡相同,架構是怎麼做到的呢?其實這背後的思想并不複雜,比如,當你看到 UserDao.findUserById() 這樣一個方法聲明,大緻應該能判斷出這是根據給定條件的 ID 查詢出滿足條件的 User 對象。Spring Data JPA 做的便是規範方法的名字,根據符合規範的名字來确定方法需要實作什麼樣的邏輯。

接下來我們針對前面的例子進行改造,讓 Spring Data JPA 來幫助我們完成業務邏輯。在着手寫代碼之前,開發者需要先 下載下傳Spring Data JPA 的釋出包(需要同時下載下傳 Spring Data Commons 和 Spring Data JPA 兩個釋出包,Commons 是 Spring Data 的公共基礎包),并把相關的依賴 JAR 檔案加入到 CLASSPATH 中。

首先,讓持久層接口 UserDao 繼承 Repository 接口。該接口使用了泛型,需要為其提供兩個類型:第一個為該接口處理的域對象類型,第二個為該域對象的主鍵類型。修改後的 UserDao 如下:

清單 12. Spring Data JPA 風格的持久層接口

然後删除 UserDaoImpl 類,因為我們前面說過,架構會為我們完成業務邏輯。最後,我們需要在 Spring 配置檔案中增加如下配置,以使 Spring 識别出需要為其實作的持久層接口:

清單 13. 在 Spring 配置檔案中啟用掃描并自動建立代理的功能

至此便大功告成了!執行一下測試代碼,然後看一下資料庫,新的資料已經如我們預期的添加到表中了。如果要再增加新的持久層業務,比如希望查詢出給 ID 的 AccountInfo 對象,該怎麼辦呢?很簡單,在 UserDao 接口中增加一行代碼即可:

清單 14. 修改後的持久層接口,增加一個方法聲明

下面總結一下使用 Spring Data JPA 進行持久層開發大緻需要的三個步驟:

聲明持久層的接口,該接口繼承 Repository,Repository 是一個标記型接口,它不包含任何方法,當然如果有需要,Spring Data 也提供了若幹 Repository 子接口,其中定義了一些常用的增删改查,以及分頁相關的方法。

在接口中聲明需要的業務方法。Spring Data 将根據給定的政策(具體政策稍後講解)來為其生成實作代碼。

在 Spring 配置檔案中增加一行聲明,讓 Spring 為聲明的接口建立代理對象。配置了 <jpa:repositories> 後,Spring 初始化容器時将會掃描 base-package 指定的包目錄及其子目錄,為繼承 Repository 或其子接口的接口建立代理對象,并将代理對象注冊為 Spring Bean,業務層便可以通過 Spring 自動封裝的特性來直接使用該對象。

此外,<jpa:repository> 還提供了一些屬性和子标簽,便于做更細粒度的控制。可以在 <jpa:repository> 内部使用 <context:include-filter>、<context:exclude-filter> 來過濾掉一些不希望被掃描到的接口。具體的使用方法見 Spring參考文檔。

應該繼承哪個接口?

前面提到,持久層接口繼承 Repository 并不是唯一選擇。Repository 接口是 Spring Data 的一個核心接口,它不提供任何方法,開發者需要在自己定義的接口中聲明需要的方法。與繼承 Repository 等價的一種方式,就是在持久層接口上使用 @RepositoryDefinition 注解,并為其指定 domainClass 和 idClass 屬性。如下兩種方式是完全等價的:

清單 15. 兩種等價的繼承接口方式示例

如果持久層接口較多,且每一個接口都需要聲明相似的增删改查方法,直接繼承 Repository 就顯得有些啰嗦,這時可以繼承 CrudRepository,它會自動為域對象建立增删改查方法,供業務層直接使用。開發者隻是多寫了 "Crud" 四個字母,即刻便為域對象提供了開箱即用的十個增删改查方法。

但是,使用 CrudRepository 也有副作用,它可能暴露了你不希望暴露給業務層的方法。比如某些接口你隻希望提供增加的操作而不希望提供删除的方法。針對這種情況,開發者隻能退回到 Repository 接口,然後到 CrudRepository 中把希望保留的方法聲明複制到自定義的接口中即可。

分頁查詢和排序是持久層常用的功能,Spring Data 為此提供了 PagingAndSortingRepository 接口,它繼承自 CrudRepository 接口,在 CrudRepository 基礎上新增了兩個與分頁有關的方法。但是,我們很少會将自定義的持久層接口直接繼承自 PagingAndSortingRepository,而是在繼承 Repository 或 CrudRepository 的基礎上,在自己聲明的方法參數清單最後增加一個 Pageable 或 Sort 類型的參數,用于指定分頁或排序資訊即可,這比直接使用 PagingAndSortingRepository 提供了更大的靈活性。

JpaRepository 是繼承自 PagingAndSortingRepository 的針對 JPA 技術提供的接口,它在父接口的基礎上,提供了其他一些方法,比如 flush(),saveAndFlush(),deleteInBatch() 等。如果有這樣的需求,則可以繼承該接口。

上述四個接口,開發者到底該如何選擇?其實依據很簡單,根據具體的業務需求,選擇其中之一。筆者建議在通常情況下優先選擇 Repository 接口。因為 Repository 接口已經能滿足日常需求,其他接口能做到的在 Repository 中也能做到,彼此之間并不存在功能強弱的問題。隻是 Repository 需要顯示聲明需要的方法,而其他則可能已經提供了相關的方法,不需要再顯式聲明,但如果對 Spring Data JPA 不熟悉,别人在檢視代碼或者接手相關代碼時會有疑惑,他們不明白為什麼明明在持久層接口中聲明了三個方法,而在業務層使用該接口時,卻發現有七八個方法可用,從這個角度而言,應該優先考慮使用 Repository 接口。

前面提到,Spring Data JPA 在背景為持久層接口建立代理對象時,會解析方法名字,并實作相應的功能。除了通過方法名字以外,它還可以通過如下兩種方式指定查詢語句:

Spring Data JPA 可以通路 JPA 命名查詢語句。開發者隻需要在定義命名查詢語句時,為其指定一個符合給定格式的名字,Spring Data JPA 便會在建立代理對象時,使用該命名查詢語句來實作其功能。

開發者還可以直接在聲明的方法上面使用 @Query 注解,并提供一個查詢語句作為參數,Spring Data JPA 在建立代理對象時,便以提供的查詢語句來實作其功能。

下面我們分别講述三種建立查詢的方式。

通過解析方法名建立查詢

通過前面的例子,讀者基本上對解析方法名建立查詢的方式有了一個大緻的了解,這也是 Spring Data JPA 吸引開發者的一個很重要的因素。該功能其實并非 Spring Data JPA 首創,而是源自一個開源的 JPA 架構 Hades,該架構的作者 Oliver Gierke 本身又是 Spring Data JPA 項目的 Leader,是以把 Hades 的優勢引入到 Spring Data JPA 也就是順理成章的了。

架構在進行方法名解析時,會先把方法名多餘的字首截取掉,比如 find、findBy、read、readBy、get、getBy,然後對剩下部分進行解析。并且如果方法的最後一個參數是 Sort 或者 Pageable 類型,也會提取相關的資訊,以便按規則進行排序或者分頁查詢。

在建立查詢時,我們通過在方法名中使用屬性名稱來表達,比如 findByUserAddressZip ()。架構在解析該方法時,首先剔除 findBy,然後對剩下的屬性進行解析,詳細規則如下(此處假設該方法針對的域對象為 AccountInfo 類型):

先判斷 userAddressZip (根據 POJO 規範,首字母變為小寫,下同)是否為 AccountInfo 的一個屬性,如果是,則表示根據該屬性進行查詢;如果沒有該屬性,繼續第二步;

從右往左截取第一個大寫字母開頭的字元串(此處為 Zip),然後檢查剩下的字元串是否為 AccountInfo 的一個屬性,如果是,則表示根據該屬性進行查詢;如果沒有該屬性,則重複第二步,繼續從右往左截取;最後假設 user 為 AccountInfo 的一個屬性;

接着處理剩下部分( AddressZip ),先判斷 user 所對應的類型是否有 addressZip 屬性,如果有,則表示該方法最終是根據 "AccountInfo.user.addressZip" 的取值進行查詢;否則繼續按照步驟 2 的規則從右往左截取,最終表示根據 "AccountInfo.user.address.zip" 的值進行查詢。

可能會存在一種特殊情況,比如 AccountInfo 包含一個 user 的屬性,也有一個 userAddress 屬性,此時會存在混淆。讀者可以明确在屬性之間加上 "_" 以顯式表達意圖,比如 "findByUser_AddressZip()" 或者 "findByUserAddress_Zip()"。

在查詢時,通常需要同時根據多個屬性進行查詢,且查詢的條件也格式各樣(大于某個值、在某個範圍等等),Spring Data JPA 為此提供了一些表達條件查詢的關鍵字,大緻如下:

And --- 等價于 SQL 中的 and 關鍵字,比如 findByUsernameAndPassword(String user, Striang pwd);

Or --- 等價于 SQL 中的 or 關鍵字,比如 findByUsernameOrAddress(String user, String addr);

Between --- 等價于 SQL 中的 between 關鍵字,比如 findBySalaryBetween(int max, int min);

LessThan --- 等價于 SQL 中的 "<",比如 findBySalaryLessThan(int max);

GreaterThan --- 等價于 SQL 中的">",比如 findBySalaryGreaterThan(int min);

IsNull --- 等價于 SQL 中的 "is null",比如 findByUsernameIsNull();

IsNotNull --- 等價于 SQL 中的 "is not null",比如 findByUsernameIsNotNull();

NotNull --- 與 IsNotNull 等價;

Like --- 等價于 SQL 中的 "like",比如 findByUsernameLike(String user);

NotLike --- 等價于 SQL 中的 "not like",比如 findByUsernameNotLike(String user);

OrderBy --- 等價于 SQL 中的 "order by",比如 findByUsernameOrderBySalaryAsc(String user);

Not --- 等價于 SQL 中的 "! =",比如 findByUsernameNot(String user);

In --- 等價于 SQL 中的 "in",比如 findByUsernameIn(Collection<String> userList) ,方法的參數可以是 Collection 類型,也可以是數組或者不定長參數;

NotIn --- 等價于 SQL 中的 "not in",比如 findByUsernameNotIn(Collection<String> userList) ,方法的參數可以是 Collection 類型,也可以是數組或者不定長參數;

使用 @Query 建立查詢

@Query 注解的使用非常簡單,隻需在聲明的方法上面标注該注解,同時提供一個 JP QL 查詢語句即可,如下所示:

清單 16. 使用 @Query 提供自定義查詢語句示例

很多開發者在建立 JP QL 時喜歡使用命名參數來代替位置編号,@Query 也對此提供了支援。JP QL 語句中通過": 變量"的格式來指定參數,同時在方法的參數前面使用 @Param 将方法參數與 JP QL 中的命名參數對應,示例如下:

清單 17. @Query 支援命名參數示例

此外,開發者也可以通過使用 @Query 來執行一個更新操作,為此,我們需要在使用 @Query 的同時,用 @Modifying 來将該操作辨別為修改查詢,這樣架構最終會生成一個更新的操作,而非查詢。如下所示:

清單 18. 使用 @Modifying 将查詢辨別為修改查詢

通過調用 JPA 命名查詢語句建立查詢

命名查詢是 JPA 提供的一種将查詢語句從方法體中獨立出來,以供多個方法共用的功能。Spring Data JPA 對命名查詢也提供了很好的支援。使用者隻需要按照 JPA 規範在 orm.xml 檔案或者在代碼中使用 @NamedQuery(或 @NamedNativeQuery)定義好查詢語句,唯一要做的就是為該語句命名時,需要滿足”DomainClass.methodName()”的命名規則。假設定義了如下接口:

清單 19. 使用 JPA 命名查詢時,聲明接口及方法時不需要什麼特殊處理

如果希望為 findTop5() 建立命名查詢,并與之關聯,我們隻需要在适當的位置定義命名查詢語句,并将其命名為 "AccountInfo.findTop5",架構在建立代理類的過程中,解析到該方法時,優先查找名為 "AccountInfo.findTop5" 的命名查詢定義,如果沒有找到,則嘗試解析方法名,根據方法名字建立查詢。

建立查詢的順序

Spring Data JPA 在為接口建立代理對象時,如果發現同時存在多種上述情況可用,它該優先采用哪種政策呢?為此,<jpa:repositories> 提供了 query-lookup-strategy 屬性,用以指定查找的順序。它有如下三個取值:

create --- 通過解析方法名字來建立查詢。即使有符合的命名查詢,或者方法通過 @Query 指定的查詢語句,都将會被忽略。

create-if-not-found --- 如果方法通過 @Query 指定了查詢語句,則使用該語句實作查詢;如果沒有,則查找是否定義了符合條件的命名查詢,如果找到,則使用該命名查詢;如果兩者都沒有找到,則通過解析方法名字來建立查詢。這是 query-lookup-strategy 屬性的預設值。

use-declared-query --- 如果方法通過 @Query 指定了查詢語句,則使用該語句實作查詢;如果沒有,則查找是否定義了符合條件的命名查詢,如果找到,則使用該命名查詢;如果兩者都沒有找到,則抛出異常。

Spring Data JPA 對事務的支援

預設情況下,Spring Data JPA 實作的方法都是使用事務的。針對查詢類型的方法,其等價于 @Transactional(readOnly=true);增删改類型的方法,等價于 @Transactional。可以看出,除了将查詢的方法設為隻讀事務外,其他事務屬性均采用預設值。

如果使用者覺得有必要,可以在接口方法上使用 @Transactional 顯式指定事務屬性,該值覆寫 Spring Data JPA 提供的預設值。同時,開發者也可以在業務層方法上使用 @Transactional 指定事務屬性,這主要針對一個業務層方法多次調用持久層方法的情況。持久層的事務會根據設定的事務傳播行為來決定是挂起業務層事務還是加入業務層的事務。具體 @Transactional 的使用,請參考Spring的參考文檔。

為接口中的部分方法提供自定義實作

有些時候,開發者可能需要在某些方法中做一些特殊的處理,此時自動生成的代理對象不能完全滿足要求。為了享受 Spring Data JPA 帶給我們的便利,同時又能夠為部分方法提供自定義實作,我們可以采用如下的方法:

将需要開發者手動實作的方法從持久層接口(假設為 AccountDao )中抽取出來,獨立成一個新的接口(假設為 AccountDaoPlus ),并讓 AccountDao 繼承 AccountDaoPlus;

為 AccountDaoPlus 提供自定義實作(假設為 AccountDaoPlusImpl );

将 AccountDaoPlusImpl 配置為 Spring Bean;

在 <jpa:repositories> 中按清單 19 的方式進行配置。

清單 20. 指定自定義實作類

此外,<jpa:repositories > 提供了一個 repository-impl-postfix 屬性,用以指定實作類的字尾。假設做了如下配置:

清單 21. 設定自動查找時預設的自定義實作類命名規則

則在架構掃描到 AccountDao 接口時,它将嘗試在相同的包目錄下查找 AccountDaoImpl.java,如果找到,便将其中的實作方法作為最終生成的代理類中相應方法的實作。

結束語

本文主要介紹了 Spring Data JPA 的使用,以及它與 Spring 架構的無縫內建。Spring Data JPA 其實并不依賴于 Spring 架構,有興趣的讀者可以參考本文最後的"參考資源"進一步學習。

下載下傳

描述

名字

大小

下載下傳方法

樣例代碼

sample-code.rar

5KB

HTTP