自 JPA 伴隨 Java EE 5 釋出以來,受到了各大廠商及開源社群的追捧,各種商用的和開源的 JPA 框架如雨後春筍般出現,為開發者提供了豐富的選擇。它一改之前 EJB 2.x 中實體 Bean 笨重且難以使用的形象,充分吸收了在開源社群已經相對成熟的 ORM 思想。另外,它並不依賴於 EJB 容器,可以作為一個獨立的持久層技術而存在。目前比較成熟的 JPA 框架主要包括 Jboss 的 Hibernate EntityManager、Oracle 捐獻給 Eclipse 社群的 EclipseLink、Apache 的 OpenJPA 等。
Java持久化規範,是從EJB2.x以前的實體Bean(Entity bean)分離出來的,EJB3以後不再有實體bean,而是將實體bean放到JPA中實現。JPA是sun提出的一個物件持久化規範,各JavaEE應用伺服器自主選擇具體實現,JPA的設計者是Hibernate框架的作者,因此Hibernate作為Jboss伺服器中JPA的預設實現,Oracle的Weblogic使用EclipseLink(以前叫TopLink)作為預設的JPA實現,IBM的Websphere和Sun的Glassfish預設使用OpenJPA(Apache的一個開源專案)作為其預設的JPA實現。
JPA的底層實現是一些流行的開源ORM(物件關係對映)框架,因此JPA其實也就是java實體物件和關係型資料庫建立起對映關係,通過物件導向程式設計的思想操作關係型資料庫的規範。
Spring 框架對 JPA 提供的支援主要體現在如下幾個方面:
-
首先,它使得 JPA 配置變得更加靈活。JPA 規範要求,配置檔案必須命名為 persistence.xml,並存在於類路徑下的 META-INF 目錄中。該檔案通常包含了初始化 JPA 引擎所需的全部資訊。Spring 提供的 LocalContainerEntityManagerFactoryBean 提供了非常靈活的配置,persistence.xml 中的資訊都可以在此以屬性注入的方式提供。
- 其次,Spring 實現了部分在 EJB 容器環境下才具有的功能,比如對 @PersistenceContext、@PersistenceUnit 的容器注入支援。
-
第三,也是最具意義的,Spring 將 EntityManager 的建立與銷燬、事務管理等程式碼抽取出來,並由其統一管理,開發者不需要關心這些,業務方法中只剩下操作領域物件的程式碼,事務管理和 EntityManager 建立、銷燬的程式碼都不再需要開發者關心了。
Spring Data JPA 框架,主要針對的就是 Spring 唯一沒有簡化到的業務邏輯程式碼,至此,開發者連僅剩的實現持久層業務邏輯的工作都省了,唯一要做的,就只是宣告持久層的介面,其他都交給 Spring Data JPA 來幫你完成!
下面就來了解Spring Data JPA。
1.下載需要的包。
需要先 下載Spring Data JPA 的釋出包(需要同時下載 Spring Data Commons 和 Spring Data JPA 兩個釋出包,Commons 是 Spring Data 的公共基礎包),並把相關的依賴 JAR 檔案加入到 CLASSPATH 中。
2.讓持久層介面 Dao(以UserDao) 繼承 Repository 介面。
該介面使用了泛型,需要為其提供兩個型別:第一個為該介面處理的域物件型別,第二個為該域物件的主鍵型別。 如下:
Spring Data JPA 風格的持久層介面:
public interface UserDao extends Repository<AccountInfo, Long> { public AccountInfo save(AccountInfo accountInfo); }
不需要UserDao的實現類,框架會為我們完成業務邏輯。
3.在 Spring 配置檔案中啟用掃描並自動建立代理的功能。
<-- 需要在 <beans> 標籤中增加對 jpa 名稱空間的引用 --> <jpa:repositories base-package="footmark.springdata.jpa.dao" entity-manager-factory-ref="entityManagerFactory" transaction-manager-ref="transactionManager"/>
4.測試程式碼。
public interface UserDao extends Repository<AccountInfo, Long> { public AccountInfo save(AccountInfo accountInfo); // 你需要做的,僅僅是新增如下一行方法宣告 public AccountInfo findByAccountId(Long accountId); }
5.總結
使用 Spring Data JPA 進行持久層開發大致需要的三個步驟:
1.宣告持久層的介面,該介面繼承 Repository,Repository 是一個標記型介面,它不包含任何方法,當然如果有需要,Spring Data 也提供了若干 Repository 子介面,其中定義了一些常用的增刪改查,以及分頁相關的方法。
2.在介面中宣告需要的業務方法。Spring Data 將根據給定的策略來為其生成實現程式碼。
3.在 Spring 配置檔案中增加一行宣告,讓 Spring 為宣告的介面建立代理物件。配置了 <jpa:repositories> 後,Spring 初始化容器時將會掃描 base-package 指定的包目錄及其子目錄,為繼承 Repository 或其子介面的介面建立代理物件,並將代理物件註冊為 Spring Bean,業務層便可以通過 Spring 自動封裝的特性來直接使用該物件。
此外,<jpa:repository> 還提供了一些屬性和子標籤,便於做更細粒度的控制。可以在 <jpa:repository> 內部使用 <context:include-filter>、<context:exclude-filter> 來過濾掉一些不希望被掃描到的介面。
持久層介面繼承 Repository 並不是唯一選擇。Repository 介面是 Spring Data 的一個核心介面,它不提供任何方法,開發者需要在自己定義的介面中宣告需要的方法。與繼承 Repository 等價的一種方式,就是在持久層介面上使用 @RepositoryDefinition 註解,併為其指定 domainClass 和 idClass 屬性。如下兩種方式是完全等價的:
兩種等價的繼承介面方式示例:
public interface UserDao extends Repository<AccountInfo, Long> { …… } @RepositoryDefinition(domainClass = AccountInfo.class, idClass = Long.class) public interface UserDao { …… }
1.如果持久層介面較多,且每一個介面都需要宣告相似的增刪改查方法,直接繼承 Repository 就顯得有些囉嗦,這時可以繼承 CrudRepository,它會自動為域物件建立增刪改查方法,供業務層直接使用。開發者只是多寫了 "Crud" 四個字母,即刻便為域物件提供了開箱即用的十個增刪改查方法。
2.使用 CrudRepository 也有副作用,它可能暴露了你不希望暴露給業務層的方法。比如某些介面你只希望提供增加的操作而不希望提供刪除的方法。針對這種情況,開發者只能退回到 Repository 介面,然後到 CrudRepository 中把希望保留的方法宣告覆制到自定義的介面中即可.
3.分頁查詢和排序是持久層常用的功能,Spring Data 為此提供了 PagingAndSortingRepository 介面,它繼承自 CrudRepository 介面,在 CrudRepository 基礎上新增了兩個與分頁有關的方法。但是,我們很少會將自定義的持久層介面直接繼承自 PagingAndSortingRepository,而是在繼承 Repository 或 CrudRepository 的基礎上,在自己宣告的方法引數列表最後增加一個 Pageable 或 Sort 型別的引數,用於指定分頁或排序資訊即可,這比直接使用 PagingAndSortingRepository 提供了更大的靈活性。
4.JpaRepository 是繼承自 PagingAndSortingRepository 的針對 JPA 技術提供的介面,它在父介面的基礎上,提供了其他一些方法,比如 flush(),saveAndFlush(),deleteInBatch() 等。如果有這樣的需求,則可以繼承該介面。
1.通過解析方法名建立查詢
框架在進行方法名解析時,會先把方法名多餘的字首擷取掉,比如 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" 的值進行查詢。
在查詢時,通常需要同時根據多個屬性進行查詢,且查詢的條件也格式各樣(大於某個值、在某個範圍等等),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 型別,也可以是陣列或者不定長引數;
2.使用 @Query 建立查詢
@Query 註解的使用非常簡單,只需在宣告的方法上面標註該註解,同時提供一個 JP QL 查詢語句即可,如下所示:
public interface UserDao extends Repository<AccountInfo, Long> { @Query("select a from AccountInfo a where a.accountId = ?1") public AccountInfo findByAccountId(Long accountId); @Query("select a from AccountInfo a where a.balance > ?1") public Page<AccountInfo> findByBalanceGreaterThan( Integer balance,Pageable pageable); }
很多開發者在建立 JP QL 時喜歡使用命名引數來代替位置編號,@Query 也對此提供了支援。JP QL 語句中通過": 變數"的格式來指定引數,同時在方法的引數前面使用 @Param 將方法引數與 JP QL 中的命名引數對應,示例如下:
public interface UserDao extends Repository<AccountInfo, Long> { public AccountInfo save(AccountInfo accountInfo); @Query("from AccountInfo a where a.accountId = :id") public AccountInfo findByAccountId(@Param("id")Long accountId); @Query("from AccountInfo a where a.balance > :balance") public Page<AccountInfo> findByBalanceGreaterThan( @Param("balance")Integer balance,Pageable pageable); }
此外,開發者也可以通過使用 @Query 來執行一個更新操作,為此,我們需要在使用 @Query 的同時,用 @Modifying 來將該操作標識為修改查詢,這樣框架最終會生成一個更新的操作,而非查詢。如下所示:
@Modifying @Query("update AccountInfo a set a.salary = ?1 where a.salary < ?2") public int increaseSalary(int after, int before);
3.通過呼叫 JPA 命名查詢語句建立查詢
命名查詢是 JPA 提供的一種將查詢語句從方法體中獨立出來,以供多個方法共用的功能。Spring Data JPA 對命名查詢也提供了很好的支援。使用者只需要按照 JPA 規範在 orm.xml 檔案或者在程式碼中使用 @NamedQuery(或 @NamedNativeQuery)定義好查詢語句,唯一要做的就是為該語句命名時,需要滿足”DomainClass.methodName()”的命名規則。假設定義瞭如下介面:
public interface UserDao extends Repository<AccountInfo, Long> { ...... public List<AccountInfo> findTop5(); }
如果希望為 findTop5() 建立命名查詢,並與之關聯,我們只需要在適當的位置定義命名查詢語句,並將其命名為 "AccountInfo.findTop5",框架在建立代理類的過程中,解析到該方法時,優先查詢名為 "AccountInfo.findTop5" 的命名查詢定義,如果沒有找到,則嘗試解析方法名,根據方法名字建立查詢。
預設情況下,Spring Data JPA 實現的方法都是使用事務的。針對查詢型別的方法,其等價於 @Transactional(readOnly=true);增刪改型別的方法,等價於 @Transactional。可以看出,除了將查詢的方法設為只讀事務外,其他事務屬性均採用預設值。
如果使用者覺得有必要,可以在介面方法上使用 @Transactional 顯式指定事務屬性,該值覆蓋 Spring Data JPA 提供的預設值。同時,開發者也可以在業務層方法上使用 @Transactional 指定事務屬性,這主要針對一個業務層方法多次呼叫持久層方法的情況。持久層的事務會根據設定的事務傳播行為來決定是掛起業務層事務還是加入業務層的事務。具體 @Transactional 的使用可以參考Spring的參考文件。