2013-12-09

More in Spring Data JPA 1.5.0 M1

前一篇「從 Spring + Hibernate 到 Spring Data JPA」只有介紹怎樣從 Spring + Hibernate 轉換到 Spring Data JPA,這篇要好好的來看一下 Spring Data JPA 可以怎麼用。

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 設定檔裡的 ,另一種是在放程式裡的 @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 /> 或者 @NamedNativeQuery 就可以了。

自行實作查詢 - 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。
第一種作法就是 Spring Data JPA 之前的作法,當作這個系統裡沒有 Spring Data JPA 一樣,自行建立一組 Spring interface 與 impl,缺點就是 service 裡會多塞入一個 DAOImpl,然後使用時得記得哪個 method 放在哪個 DAO 裡。

第二種作法較複雜,但好處是 service 還是維持一個 DAO,不會有找不到 method 的情況發生。

第一種作法就不用舉例了,直接看第二種作法:
  • 建立一個 interface
  • 建立一個 implementation
  • 將 Spring Data JPA 的 interface 繼承第一點的 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 官網
---

4 則留言:

  1. 寫得很好!受益良多,感謝!!!

    回覆刪除
  2. 請教您有使用到 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);

    回覆刪除
    回覆
    1. 不好意思,由於 Spring Data 只能用難用的要命的 JPA,不能用熟悉的 Hibernate,以及在跟 Connection Pool 整合上有些效能問題,Spring Data 已經被我從工具箱裡移除了,並沒有繼續研究。

      刪除