對于不是使用sPRing管理的項目,我們就自己創建對象使用:大概的思路就是①創建dao接口②實現該接口,并且編寫邏輯:
Dao:
public interface StudentDao {public List<Student> findAll();}
因為是測試,所以我們就只定義了一個查詢所有記錄的方法,下面是實現:
public class StudentDaoImpl implements StudentDao{@Override@SuppressWarnings("unchecked")public List<Student> findAll() {JPAConnection jc=new JPAConnection("jpa-1");EntityManager em=jc.getEntityManager();String sql="select s from Student s";Query query=em.createQuery(sql);List<Student> students=query.getResultList();if(students==null){students=Lists.newArrayList();}jc.destory();return students;} }
,因為創建EntityManager會重復很多的代碼:所以我就將這些東西給抽出來了
public class JPAConnection {private EntityManagerFactory entityManagerFactory;private EntityManager entityManager;private EntityTransaction entityTransaction;public JPAConnection(){}public JPAConnection(String unitName) {this.entityManagerFactory=Persistence.createEntityManagerFactory(unitName);this.entityManager=entityManagerFactory.createEntityManager();this.entityTransaction=entityManager.getTransaction();entityTransaction.begin();}/**Getter And Setter**/public void destory(){if(this.entityTransaction.isActive()){this.entityTransaction.commit();}if(this.entityManager.isOpen()){this.entityManager.close();}if(this.entityManagerFactory.isOpen()){this.entityManagerFactory.close();}}}
上面雖然是簡單的實現了查詢的功能,但是對于真正實際的項目還是遠遠不夠的,對于真是項目中,還要考慮事務等等,這里的代碼只是簡單的給個jpa實現的思路。
Spring管理的項目:
對于上面的代碼,覺得最多余的就是創建EntityManager這部分了,所以當然會使用spring的特性減少這些創建的代碼了,同時操作事務也變得很簡單了,只要添加一些注解就可以了:
下面是spring的配置文件:
<context:component-scan base-package="com.hotusm.common"/><tx:annotation-driven transaction-manager="transactionManager"/><bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager"><property name="entityManagerFactory" ref="entityManager"/></bean><bean id="entityManager" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"/>
之后代碼就變成這樣了:
@Repository("userDao")public class UserDaoImpl implements UserDao{@PersistenceContextprivate EntityManager em;@Transactional@Override@SuppressWarnings("unchecked")public List<User> findAll() {String sql="select o from User o";Query query=em.createQuery(sql);List<User> users=query.getResultList();if(users==null){users=Lists.newArrayList();}return users;}}
是不是超級爽快?,如果是web項目,在使用的時候,在service中直接注入進去就可以直接使用,同時使用注解@Transactional即可進行事務操作了。
下面總結一下使用 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> 來過濾掉一些不希望被掃描到的接口。具體的使用方法見 Spring參考文檔。
應該繼承哪個接口?
前面提到,持久層接口繼承 Repository 并不是唯一選擇。Repository 接口是 Spring Data 的一個核心接口,它不提供任何方法,開發者需要在自己定義的接口中聲明需要的方法。與繼承 Repository 等價的一種方式,就是在持久層接口上使用 @RepositoryDefinition 注解,并為其指定 domainClass 和 idClass 屬性。如下兩種方式是完全等價的:
清單 15. 兩種等價的繼承接口方式示例
public interface UserDao extends Repository<AccountInfo, Long> { …… }@RepositoryDefinition(domainClass = AccountInfo.class, idClass = Long.class) public interface UserDao { …… }
如果持久層接口較多,且每一個接口都需要聲明相似的增刪改查方法,直接繼承 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 ()??蚣茉诮馕鲈摲椒〞r,首先剔除 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 提供自定義查詢語句示例
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 中的命名參數對應,示例如下:
清單 17. @Query 支持命名參數示例
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 來將該操作標識為修改查詢,這樣框架最終會生成一個更新的操作,而非查詢。如下所示:
清單 18. 使用 @Modifying 將查詢標識為修改查詢
@Modifying @Query("update AccountInfo a set a.salary = ?1 where a.salary < ?2") public int increaseSalary(int after, int before);
通過調用 JPA 命名查詢語句創建查詢
命名查詢是 JPA 提供的一種將查詢語句從方法體中獨立出來,以供多個方法共用的功能。Spring Data JPA 對命名查詢也提供了很好的支持。用戶只需要按照 JPA 規范在 orm.xml 文件或者在代碼中使用 @NamedQuery(或 @NamedNativeQuery)定義好查詢語句,唯一要做的就是為該語句命名時,需要滿足”DomainClass.methodName()”的命名規則。假設定義了如下接口:
清單 19. 使用 JPA 命名查詢時,聲明接口及方法時不需要什么特殊處理
public interface UserDao extends Repository<AccountInfo, Long> {...... public List<AccountInfo> findTop5(); }
如果希望為 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 base-package="footmark.springdata.jpa.dao"> <jpa:repository id="accountDao" repository-impl-ref=" accountDaoPlus " /> </jpa:repositories><bean id="accountDaoPlus" class="......."/>
此外,<jpa:repositories > 提供了一個 repository-impl-postfix 屬性,用以指定實現類的后綴。假設做了如下配置:
清單 21. 設置自動查找時默認的自定義實現類命名規則
<jpa:repositories base-package="footmark.springdata.jpa.dao"
repository-impl-postfix="Impl"/>
則在框架掃描到 AccountDao 接口時,它將嘗試在相同的包目錄下查找 AccountDaoImpl.java,如果找到,便將其中的實現方法作為最終生成的代理類中相應方法的實現。
新聞熱點
疑難解答