jpa:repositories 做了什麼?還可以做什麼?
最重要的一件事情就是自動產生 impl,另外附送的是將底層的 Exception 轉成 Spring 專屬的 DataAccessException 家族。其他的功能可以在 http://www.springframework.org/schema/data/jpa/spring-jpa.xsd 和 http://www.springframework.org/schema/data/repository/spring-repository.xsd 找到一些線索,這裡大概可以看到 Spring Data JPA 的全貌了。
<jpa:repositories
base-package="idv.neil.dao"
entity-manager-factory-ref="emf"
transaction-manager-ref="transactionManager"
repository-impl-postfix="Impl"
query-lookup-strategy="create-if-not-found"
named-queries-location="classpath:META-INF/jpa-named-queries.properties"
factory-class="" />
- base-package:一定要有,用來告訴 Spring Data JPA 要到哪找 org.springframework.data.repository.Repository 的子界面,並自動產生 impl。
- entity-manager-factory-ref:用來設定 JPA EntityManager,ApplicationContext 裡只有一個 EntityManager 的話,就可以不用設定,一般狀況下應該都只有一個。
- transaction-manager-ref:用來設定 TransactionManager,ApplicationContext 裡只有一個 TransactionManager 的話,就可以不用設定,一般狀況下應該都只有一個。
- repository-impl-postfix:當智慧型的名稱定義查詢無法滿足需求時,還是可以回到最初的方式,真的寫一個 Impl 來實作想要的功能,其他稍後解釋。
- query-lookup-strategy:用來設定 Spring Data JPA 如何找到 query method,廣義的來說,query method 有兩個來源,第一個就是神奇的自動產生實作機制,也就是透過 interface 的 method name 去做名稱定義查詢,第二個則是沒那麼神奇但總是有需要的自行實作查詢,至於實作的方式則有許多種,其他稍後解釋。
- create:只用名稱定義查詢
- use_declared_only:只用自行實作查詢,如果沒找到會丟出錯誤。
- create_if_not_found:先嘗試用名稱定義查詢,若沒有符合,則改用自行實作查詢,為預設用法。
- named-queries-location:自行實作查詢方式有兩個位置可以放,一個是在程式裡,另一個是在 properties 檔裡,這裡就是在設定 properties 檔的位置。
Repository 架構
前面提過只要繼承 Spring Data JPA 提供的 interface,就可以自動產生 impl,但除了 Repository.java,Spring Data JPA 還內建一些好用的 interface 可以繼承。
從名稱大概就可以知道有哪些內建功能,JPA 開發是繼承 JpaRepository,有支援分頁與排序功能。
SaveOrUpdate?
看過 Repository 與 JpaRepository 之後會有一個疑問,只有 save(),沒有 update()?為了方便使用,Spring Data JPA 引用 Hibernate 的 saveOrUpdate() 概念,update 也是呼叫 save(),save 或 update 判斷的機制如下。
- id 為 null 表示新增,不為 null 則是更新,預設用法,所以 id 欄位得用物件形式(Integer 或 Long),不可以用 primitive type(int 或 long)。
- 實作 org.springframework.data.domain.Persistable 的 isNew()。
- 實作 org.springframework.data.repository.core.EntityInformation 的 isNew()。
一切都是為了查詢!
簡單的分類,Spring Data JPA 查詢可以分成兩類:- 名稱定義查詢:Spring Data JPA 存在的目的,透過 interface method name 的定義來表示查詢的內容。
- 自行實作查詢:為補足名稱定義查詢的不足,可以「自訂查詢語法」或者提供「真正的實作」。
名稱定義查詢
目前支援的基本關鍵字如下。- Is / Equal / ''
- (Is)Not
- And
- Or
- (Is)Between - 日期或數值
- (Is)LessThan - 數值
- (Is)LessThanEqual - 數值
- (Is)GreaterThan - 數值
- (Is)GreaterThanEqual - 數值
- (Is)After - 日期
- (Is)Before - 日期
- (Is)Null
- (Is)NotNull
- (Is)Like - 自行加 % 於關鍵字前或後或均加
- (Is)NotLike - 自行加 % 於關鍵字前或後或均加
- (Is)StartingWith / StartsWith - 系統自動加 % 於關鍵字後
- (Is)EndingWith / EndsWith - 系統自動加 % 於關鍵字前
- (Is)Containing / Contains - 系統自動加 % 於關鍵字前後
- (Is)In
- (Is)NotIn
- (Is)True
- (Is)False
- OrderBy - 自行加 Asc 或 Desc
- Exists - Spring Data JPA 不支援
- (Is)Near - Spring Data JPA 不支援
- (Is)Within - Spring Data JPA 不支援
- Regex / MatchesRegex / Matches - Spring Data JPA 不支援
循慣例,直接看 code。
public interface UserDao extends JpaRepository<User, Integer> { /** Equal */ User findByAccount(String account); /** Not */ List<User> findByAccountNot(String account); /** And */ User findByNameAndEmail(String name, String email); /** Or */ List<User> findByNameOrEmail(String name, String email); /** Between */ List<User> findByCreateTimeBetween(Date start, Date end); /** LessThan */ List<User> findByUidLessThan(int uid); /** LessThanEqual */ List<User> findByUidLessThanEqual(int uid); /** GreaterThan */ List<User> findByUidGreaterThan(int uid); /** GreaterThanEqual */ List<User> findByUidGreaterThanEqual(int uid); /** After */ List<User> findByCreateTimeAfter(Date date); /** Before */ List<User> findByCreateTimeBefore(Date date); /** IsNull */ List<User> findByEmailIsNull(); /** (Is)NotNull */ List<User> findByEmailNotNull(); /** Like */ List<User> findByEmailLike(String email); /** NotLike */ List<User> findByEmailNotLike(String email); /** StartingWith */ List<User> findByEmailStartingWith(String email); /** EndingWith */ List<User> findByEmailEndingWith(String email); /** Containing */ List<User> findByEmailContaining(String email); /** In */ List<User> findByAccountIn(Collection<String> accounts); /** NotIn */ List<User> findByAccountNotIn(Collection<String> accounts); /** True */ List<User> findByEnableTrue(); /** False */ List<User> findByEnableFalse(); /** OrderBy */ List<User> findByEnableTrueOrderByAccountDesc(); }
其實除了 find,也可以用 read、query 或 get,對 Spring Data JPA 來說都沒有差別。
進階用法如下。
public interface UserDao extends JpaRepository{ /** And */ List findByAccountAndName(String account, String name); /** Or */ List findByAccountOrName(String account, String name); /** Distinct Object */ List findDistinctUserByNameLike(String name); /** Distinct Object */ List findUserDistinctByNameLike(String name); /** Distinct Object */ List findDistinctNameByNameLike(String name); /** IgnoreCase */ List findByAccountIgnoreCase(String account); /** AllIgnoreCase */ List findByAccountAndNameAllIgnoreCase(String account, String name); }
Nested Properties
Spring Data JPA 甚至支援 Nested properties,例如 User 有個 Address 物件的屬性 address,然後 Address 物件有個字串型別的 zipCode 屬性,那可以 findByAddressZipCode 這樣下條件。但有時候屬性是複合字,例如 zipCode,那如果使用者真正想要的不是 user.address.zipCode,而是 user.addressZip.code 呢?Spring Data JPA 解析機制如下:
- 先用 user.addressZipCode 去找,有則用,沒有就往下。
- 再用 user.addressZip.code 去找,有則用,沒有就往下。
- 再用 user.address.zipCode 去找,有則用,沒有就往下。
也就是說,先用全字串去找,然後再逐字從最右邊拆過來。
但是有時候真的遇到多種情況成立時,可以用底線來強制斷字,例如 findByAddress_ZipCode。
特殊參數
Spring Data JPA 除了可以傳入查詢使用的參數(不限字串)外,另外支援兩個特別的參數。- org.springframework.data.domain.Sort.java
- org.springframework.data.domain.Pageable.java
this.userDao.findAll(new Sort(new Order(Direction.DESC, "account"))); this.userDao.findAll(new PageRequest(1, 100)); this.userDao.findAll(new PageRequest(1, 100, new Sort(new Order(Direction.DESC, "account"))));
自行實作查詢
若要使用自行實作查詢,記得前面提到的 query_lookup_strategy 要用 create_if_not_found 或者 use_declared_only。- NamedQuery
- Query
- Real Implementation
自行實作查詢 - NamedQuery
NamedQuery 有兩種用法,一種是放在 JPA 設定檔裡的<named-query name="User.findByKeyword"> <query>select u from User u where u.account like ?1 or u.name like ?1</query> </named-query>然後在 UserDAO.java 裡加上同樣的名稱宣告。
public interface UserDao extends JpaRepository<User, Integer> { List<User> findByKeyword(String keyword); }這樣就可以使用了。
至於 @NamedQuery 是要加在 Model 物件上。
@NamedQuery(name = "User.findByKeyword", query = "select u from User u where u.account like ?1 or u.name like ?1") public class User extends BaseModel { // ... }然後一樣在 UserDAO.java 裡加上同樣的名稱宣告,就可以使用了。
注意,以上用的語法是 JPA Query Language,如果想要用 SQL,那麼只要改用 <named-native-query />
自行實作查詢 - Query
NamedQuery 有一個小小的缺點,就是 Model 物件被 Spring Data JPA 玷污了,而且除了 Model 物件,還要另外在 interface 上加一個 method,難以知道該 method 不是名稱定義查詢,也不容易知道查詢定義在哪,甚至會有不同步的情況發生。所以可以用 @Query 直接加在 interface 裡,這樣上面的問題就都排除了,而且如果 @Query 與 @NamedQuery 有重複定義時,@Query 勝出。
public interface UserDao extends JpaRepository<User, Integer> { @Query("select u from User u where u.account like ?1 or u.name like ?1") List<User> findByKeyword(String keyword); }另外, @Query 有一個好處,就是可以直接在查詢語法裡加上 %,@NamedQuery 就不行,當然 % 可以不用放在語法裡,而是透過參數傳進來。
public interface UserDao extends JpaRepository<User, Integer> { @Query("select u from User u where u.account like ?1% or u.name like ?1%") List<User> findByKeyword(String keyword); }@Query 也支援 Native SQL,只是用法不一樣,且不支援分頁與排序。
public interface UserDao extends JpaRepository<User, Integer> { @Query(value = "select u.* from User u where u.account like ?1 or u.name like ?1", nativeQuery = true) List<User> findByKeyword(String keyword); }最後,@Query 支援 Named parameter,可以解決改變參數順序造成的問題。
public interface UserDao extends JpaRepository<User, Integer> { @Query(value = "select u from User u where u.account like :accountKeyword or u.name like :nameKeyword") List<User> findByKeyword(@Param("nameKeyword") String nameKeyword, @Param("accountKeyword") String accountKeyword); }因為查詢語法是寫死的,,很容易因為更名造成 Spring Data JPA 莫名的死去,這裡可以使用 SpEL 稍稍減輕這樣的痛苦。
public interface UserDao extends JpaRepository<User, Integer> { @Query(value = "select u from #{#entityName} u where u.account like :keyword or u.name like :keyword") List<User> findByKeyword(@Param("keyword") String keyword); }以 #{#entityName} 取代原本寫死的 Model name。
@Query 除了用來查詢資料,還可以用來修改資料,如果用內建的 save() 會更新整筆資料,但有時候為了效能,只想更新幾個欄位的話,就可以用 @Modifying 達到這樣的目的。
public interface UserDao extends JpaRepository<User, Integer> { @Modifying(clearAutomatically = true) @Query("update #{#entityName} set name = ?1 where account = ?2") int updateNameByAccount(String name, String account); }有一點要特別注意,做這樣的局部更新並不會影響到 EntityManager 已經 cache 的資料,換句話說,如果被更新的資料已經載入 EntityManager 的話,那該筆資料並不會同步更新,而保留在舊的狀態,如果這時再呼叫 save() 的話,就可能發生舊蓋新的問題,因此建議在 @Modifying 加上 clearAutomatically = true 以促使 EntityManager 立即拋棄已經 cache 的資料,並重新讀入新資料。
當然,@Modifying 也可以用於刪除資料上。
自行實作查詢 - Real Implementation
當名稱定義查詢或者使用 @Query 的自行定義查詢無法滿足需求時,可以回到最初的作法,自行實作一個 implementation,這種方式有兩種作法:- 建立一組獨立的 Spring interface 與 impl。
- 建立一組與 Spring Data JPA 結合的 Spring interface 與 impl。
第二種作法較複雜,但好處是 service 還是維持一個 DAO,不會有找不到 method 的情況發生。
第一種作法就不用舉例了,直接看第二種作法:
- 建立一個 interface
- 建立一個 implementation
- 將 Spring Data JPA 的 interface 繼承第一點的 interface
public interface UserDaoCustom { List<User> findByQueryVO(UserQueryVO v); }Interface 命名不重要,只要符合 Java 規定即可。
建立 implementation
@Repository("userDaoCustom") public class UserDaoImpl implements UserDaoCustom { private EntityManagerFactory entityManagerFactory; @Inject public UserDaoImpl(EntityManagerFactory entityManagerFactory) { this.entityManagerFactory = entityManagerFactory; } private EntityManager getEntityManager() { return this.entityManagerFactory.createEntityManager(); } @Override @SuppressWarnings("unchecked") public List<User> findByQueryVO(UserQueryVO v) { if (v == null) { return new ArrayList<User>(); } StringBuilder sql = new StringBuilder(); sql.append("select * from user u "); sql.append("where 1 = 1 "); if (StringUtils.isNotBlank(v.getKeyword())) { sql.append("and (u.account like '%" + v.getKeyword() + "%' or u.name like '%" + v.getKeyword() + "%') "); } return this.getEntityManager().createNativeQuery(sql.toString(), User.class).getResultList(); } }Implementation 命名非常重要,錯了就無法啟動!
那要怎樣命名呢?還記得最前面提過一個叫做 repository-impl-postfix 的設定,意思是說真正的實作要加上這個設定值,預設是 Impl,那就要加上 Impl,這樣 Spring Data JPA 就會去找有沒有這樣的實作存在,有就用,沒有就去找名稱定義查詢,那是誰要加上 Impl 呢?這才是重點!
一開始我一直以為是自行實作的 interface 名稱加上 Impl 作為 implementation 的名稱,但在得到 N 個啟動錯誤之後,終於領悟了,是用 Spring Data JPA 的 interface 名稱加上 Impl 作為 implementation 的名稱。
以上面的範例說明,implementation 要命名為 UserDaoImpl,而不是 UserDaoCustomImpl。
Spring Data JPA 的 interface 繼承第一點的 interface
public interface UserDao extends CrudRepository<User, Integer>, UserDaoCustom { // ... }繼承的目的只有一個,為了可以透過 UserDao 呼叫 UserDaoCustom 的 API,但這也是造成我出錯的原因,因為 Spring Data JPA 也會試著去解析繼承而來的 API,會先去 UserDaoImpl 或 @Query 裡找,最後才是用名稱定義查詢 。
以下做了一些測試來了解 Spring Data JPA 的作法:
- 只要有 UserDaoImpl 存在,不管 UserDao 有沒有繼承 UserDaoCustom,都會去執行 UserDaoImpl 裡的實作版本。
- UserDaoCustom 是可以不存在的,只要將 API 搬到 UserDao 裡,並提供實作這些 API 的 UserDaoImpl 即可,此時,UserDao 與 UserDao 是沒有繼承或實作關係的。
- 但為了實作的一致性,建議還是使用官方的作法。
Transaction
可以在 Repository 的子界面或其 method 加上 @Transaction,但是我習慣將 @Transaction 加在 Service 層,而非 DAO 層。不過有另一個收穫,可以將 @Transaction 加在 class 上,並設為 Read only,然後在增修刪的 method 上關閉 Read only。
@Transactional(readOnly = true) public class UserServiceImpl implements UserService { public List<User> findByKeyword(String keyword) { // ... } @Transactional public void deleteUsers(int uid) { // ... } }
Spring Data
最後要說的是,Spring Data JPA 只是 Spring Data 的一個成員,Spring 將 Spring Data 這樣的概念應用在數個主流的儲存技術上,例如 MongoDB 與 Hadoop,詳情請看 Spring Data 官網。---
寫得很好!受益良多,感謝!!!
回覆刪除謝謝,歡迎討論。
刪除請教您有使用到 Spring data JPA 1.7嗎?
回覆刪除目前我是使用 Spring3.0
因為想要使用
//In addition to query methods, query derivation for both count and delete queries, is available.
Long deleteByLastname(String lastname);
List removeByLastname(String lastname);
不好意思,由於 Spring Data 只能用難用的要命的 JPA,不能用熟悉的 Hibernate,以及在跟 Connection Pool 整合上有些效能問題,Spring Data 已經被我從工具箱裡移除了,並沒有繼續研究。
刪除