本文主要講述 Spring Data JPA,可是爲了避免至於給 JPA 和 Spring 的初學者形成較大的學習曲線,咱們首先從 JPA 開始,簡單介紹一個 JPA 示例;接着重構該示例,並引入 Spring 框架,這兩部分不會涉及過多的篇幅,若是但願可以深刻學習 Spring 和 JPA,能夠根據本文最後提供的參考資料進一步學習。html
自 JPA 伴隨 Java EE 5 發佈以來,受到了各大廠商及開源社區的追捧,各類商用的和開源的 JPA 框架如雨後春筍般出現,爲開發者提供了豐富的選擇。它一改以前 EJB 2.x 中實體 Bean 笨重且難以使用的形象,充分吸取了在開源社區已經相對成熟的 ORM 思想。另外,它並不依賴於 EJB 容器,能夠做爲一個獨立的持久層技術而存在。目前比較成熟的 JPA 框架主要包括 Jboss 的 Hibernate EntityManager、Oracle 捐獻給 Eclipse 社區的 EclipseLink、Apache 的 OpenJPA 等。java
本文的示例代碼基於 Hibernate EntityManager 開發,可是讀者幾乎不用修改任何代碼,即可以很是容易地切換到其餘 JPA 框架,由於代碼中使用到的都是 JPA 規範提供的接口 / 類,並無使用到框架自己的私有特性。示例主要涉及七個文件,可是很清晰:業務層包含一個接口和一個實現;持久層包含一個接口、一個實現、一個實體類;另外加上一個 JPA 配置文件和一個測試類。相關類 / 接口代碼以下:mysql
清單 1. 實體類 AccountInfo.java
spring
@Entity @Table(name = "t_accountinfo") public class AccountInfo implements Serializable { private Long accountId; private Integer balance; // 此處省略 getter 和 setter 方法。 } |
清單 2. 業務層接口 UserService.java
sql
public interface UserService { public AccountInfo createNewAccount(String user, String pwd, Integer init); } |
清單 3. 業務層的實現類 UserServiceImpl.java
數據庫
public class UserServiceImpl implements UserService { private UserDao userDao = new UserDaoImpl(); public AccountInfo createNewAccount(String user, String pwd, Integer init){ // 封裝域對象 AccountInfo accountInfo = new AccountInfo(); UserInfo userInfo = new UserInfo(); userInfo.setUsername(username); userInfo.setPassword(password); accountInfo.setBalance(initBalance); accountInfo.setUserInfo(userInfo); // 調用持久層,完成數據的保存 return userDao.save(accountInfo); } } |
public interface UserDao { public AccountInfo save(AccountInfo accountInfo); } |
public class UserDaoImpl implements UserDao { public AccountInfo save(AccountInfo accountInfo) { EntityManagerFactory emf = Persistence.createEntityManagerFactory("SimplePU"); EntityManager em = emf.createEntityManager(); em.getTransaction().begin(); em.persist(accountInfo); em.getTransaction().commit(); emf.close(); return accountInfo; } } |
清單 6. JPA 標準配置文件 persistence.xml
dom
<?xml version="1.0" encoding="UTF-8"?> <persistence xmlns="http://java.sun.com/xml/ns/persistence" version="2.0"> <persistence-unit name="SimplePU" transaction-type="RESOURCE_LOCAL"> <provider>org.hibernate.ejb.HibernatePersistence</provider> <class>footmark.springdata.jpa.domain.UserInfo</class> <class>footmark.springdata.jpa.domain.AccountInfo</class> <properties> <property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver"/> <property name="hibernate.connection.url" value="jdbc:mysql://10.40.74.197:3306/zhangjp"/> <property name="hibernate.connection.username" value="root"/> <property name="hibernate.connection.password" value="root"/> <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5Dialect"/> <property name="hibernate.show_sql" value="true"/> <property name="hibernate.format_sql" value="true"/> <property name="hibernate.use_sql_comments" value="false"/> <property name="hibernate.hbm2ddl.auto" value="update"/> </properties> </persistence-unit> </persistence> |
清單 7. 本文使用以下的 main 方法進行開發者測試
ide
public class SimpleSpringJpaDemo { public static void main(String[] args) { new UserServiceImpl().createNewAccount("ZhangJianPing", "123456", 1); } } |
接下來咱們引入 Spring,以展現 Spring 框架對 JPA 的支持。業務層接口 UserService 保持不變,UserServiceImpl 中增長了三個註解,以讓 Spring 完成依賴注入,所以再也不須要使用 new 操做符建立 UserDaoImpl 對象了。同時咱們還使用了 Spring 的聲明式事務:
@Service("userService") public class UserServiceImpl implements UserService { @Autowired private UserDao userDao; @Transactional public AccountInfo createNewAccount( String name, String pwd, Integer init) { …… } } |
對於持久層,UserDao 接口也不須要修改,只需修改 UserDaoImpl 實現,修改後的代碼以下:
@Repository("userDao") public class UserDaoImpl implements UserDao { @PersistenceContext private EntityManager em; @Transactional public Long save(AccountInfo accountInfo) { em.persist(accountInfo); return accountInfo.getAccountId(); } } |
<?xml version="1.0" encoding="UTF-8"?> <beans...> <context:component-scan base-package="footmark.springdata.jpa"/> <tx:annotation-driven transaction-manager="transactionManager"/> <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager"> <property name="entityManagerFactory" ref="entityManagerFactory"/> </bean> <bean id="entityManagerFactory" class= "org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> </bean> </beans> |
public class SimpleSpringJpaDemo{ public static void main(String[] args){ ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("spring-demo-cfg.xml"); UserDao userDao = ctx.getBean("userDao", UserDao.class); userDao.createNewAccount("ZhangJianPing", "123456", 1); } } |
經過對比重構先後的代碼,能夠發現 Spring 對 JPA 的簡化已經很是出色了,咱們能夠大體總結一下 Spring 框架對 JPA 提供的支持主要體如今以下幾個方面:
首先,它使得 JPA 配置變得更加靈活。JPA 規範要求,配置文件必須命名爲 persistence.xml,並存在於類路徑下的 META-INF 目錄中。該文件一般包含了初始化 JPA 引擎所需的所有信息。Spring 提供的 LocalContainerEntityManagerFactoryBean 提供了很是靈活的配置,persistence.xml 中的信息均可以在此以屬性注入的方式提供。
其次,Spring 實現了部分在 EJB 容器環境下才具備的功能,好比對 @PersistenceContext、@PersistenceUnit 的容器注入支持。
第三,也是最具意義的,Spring 將 EntityManager 的建立與銷燬、事務管理等代碼抽取出來,並由其統一管理,開發者不須要關心這些,如前面的代碼所示,業務方法中只剩下操做領域對象的代碼,事務管理和 EntityManager 建立、銷燬的代碼都再也不須要開發者關心了。
經過前面的分析能夠看出,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 風格的持久層接口
public interface UserDao extends Repository<AccountInfo, Long> { public AccountInfo save(AccountInfo accountInfo); } |
而後刪除 UserDaoImpl 類,由於咱們前面說過,框架會爲咱們完成業務邏輯。最後,咱們須要在 Spring 配置文件中增長以下配置,以使 Spring 識別出須要爲其實現的持久層接口:
清單 13. 在 Spring 配置文件中啓用掃描並自動建立代理的功能
<-- 須要在 <beans> 標籤中增長對 jpa 命名空間的引用 --> <jpa:repositories base-package="footmark.springdata.jpa.dao" entity-manager-factory-ref="entityManagerFactory" transaction-manager-ref="transactionManager"/> |
至此便大功告成了!執行一下測試代碼,而後看一下數據庫,新的數據已經如咱們預期的添加到表中了。若是要再增長新的持久層業務,好比但願查詢出給 ID 的 AccountInfo 對象,該怎麼辦呢?很簡單,在 UserDao 接口中增長一行代碼便可:
public interface UserDao extends Repository<AccountInfo, Long> { public AccountInfo save(AccountInfo accountInfo); // 你須要作的,僅僅是新增以下一行方法聲明 public AccountInfo findByAccountId(Long accountId); } |
下面總結一下使用 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 屬性。以下兩種方式是徹底等價的:
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 ()。框架在解析該方法時,首先剔除 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 提供的一種將查詢語句從方法體中獨立出來,以供多個方法共用的功能。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 實現的方法都是使用事務的。針對查詢類型的方法,其等價於 @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 的方式進行配置。
<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 屬性,用以指定實現類的後綴。假設作了以下配置:
<jpa:repositories base-package="footmark.springdata.jpa.dao" repository-impl-postfix="Impl"/> |
則在框架掃描到 AccountDao 接口時,它將嘗試在相同的包目錄下查找 AccountDaoImpl.java,若是找到,便將其中的實現方法做爲最終生成的代理類中相應方法的實現。