閑魚網(wǎng)站如何賺錢上海十大營(yíng)銷策劃公司
Spring Data JPA 中文文檔
1. 前言
Spring Data JPA 為 Jakarta Persistence API(JPA)提供 repository 支持。它簡(jiǎn)化了需要訪問(wèn)JPA數(shù)據(jù)源的應(yīng)用程序的開發(fā)。
1.1. 項(xiàng)目元數(shù)據(jù)
- 版本控制: https://github.com/spring-projects/spring-data-jpa
- Bug跟蹤: https://github.com/spring-projects/spring-data-jpa/issues
- Release repository: https://repo.spring.io/libs-release
- Milestone repository: https://repo.spring.io/libs-milestone
- Snapshot repository: https://repo.spring.io/libs-snapshot
2. Spring Data 升級(jí)
關(guān)于如何從Spring Data的早期版本升級(jí)的說(shuō)明在項(xiàng)目 wiki上提供。按照 發(fā)布說(shuō)明部分 的鏈接,找到你要升級(jí)的版本。
升級(jí)說(shuō)明總是在發(fā)行說(shuō)明中的第一項(xiàng)。如果你落后一個(gè)以上的版本,請(qǐng)確保你也查看你跳過(guò)的版本的發(fā)行說(shuō)明。
3. 依賴
由于各個(gè)Spring Data模塊的起始日期不同,它們中的大多數(shù)都有不同的主要和次要版本號(hào)。找到兼容的模塊最簡(jiǎn)單的方法是依靠Spring Data Release Train BOM,我們?cè)诎l(fā)行時(shí)定義了兼容版本。在Maven項(xiàng)目中,你可以在POM的 `` 部分聲明這一依賴,如下所示。
Example 1. Using the Spring Data release train BOM
<dependencyManagement><dependencies><dependency><groupId>org.springframework.data</groupId><artifactId>spring-data-bom</artifactId><version>2023.0.0-SNAPSHOT</version><scope>import</scope><type>pom</type></dependency></dependencies>
</dependencyManagement>
當(dāng)前發(fā)布的 train version 是 2023.0.0-SNAPSHOT
。train version 使用模式為 YYY.MINOR.MICRO
的 calver。對(duì)于GA版本和服務(wù)版本,版本名稱遵循 ${calver}
,對(duì)于所有其他版本,版本名稱遵循以下模式:${calver}-${modifier}
,其中 modifier
可以是以下之一。
SNAPSHOT
: 當(dāng)前快照M1
,M2
, 以此類推: 里程碑RC1
,RC2
, 以此類推: 候選發(fā)布
你可以在我們的 Spring Data示例庫(kù) 中找到一個(gè)使用BOM的工作實(shí)例。有了這些,你可以在 `` 塊中聲明你想使用的沒有版本的Spring Data模塊,如下所示。
Example 2. Declaring a dependency to a Spring Data module
<dependencies><dependency><groupId>org.springframework.data</groupId><artifactId>spring-data-jpa</artifactId></dependency>
<dependencies>
3.1. 使用Spring Boot的依賴管理
Spring Boot會(huì)為你選擇一個(gè)最新版本的Spring Data模塊。如果你仍然想升級(jí)到較新的版本,將 spring-data-releasetrain.version
屬性設(shè)置為你想使用的train version 和 iteration。
3.2. Spring Framework
當(dāng)前版本的Spring Data模塊需要Spring框架 6.0.4 或更高的版本。這些模塊也可能在該次要版本的較早的錯(cuò)誤修復(fù)版本中工作。但是,強(qiáng)烈建議使用那一代中的最新版本。
4. 與 Spring Data Repository 一起工作
Spring Data Repository 抽象的目標(biāo)是大大減少為各種持久性store實(shí)現(xiàn)數(shù)據(jù)訪問(wèn)層所需的模板代碼量。
4.1. 核心概念
Spring Data repository 抽象的中心接口是 Repository
。它把要管理的 domain 類以及 domain 類的ID類型作為泛型參數(shù)。這個(gè)接口主要是作為一個(gè)標(biāo)記接口,用來(lái)捕捉工作中的類型,并幫助你發(fā)現(xiàn)擴(kuò)展這個(gè)接口的接口。 CrudRepository
和 ListCrudRepository
接口為被管理的實(shí)體類提供復(fù)雜的CRUD功能。
Example 3. CrudRepository
接口
public interface CrudRepository<T, ID> extends Repository<T, ID> {<S extends T> S save(S entity); Optional<T> findById(ID primaryKey); Iterable<T> findAll(); long count(); void delete(T entity); boolean existsById(ID primaryKey); // … more functionality omitted.
}
ListCrudRepository
提供了同等的方法,但它們返回 List
,而 CrudRepository
的方法返回 Iterable
。
除了 CrudRepository
之外,還有一個(gè) PagingAndSortingRepository
的抽象,它增加了額外的分頁(yè),排序方法。
Example 4. PagingAndSortingRepository
接口
public interface PagingAndSortingRepository<T, ID> {Iterable<T> findAll(Sort sort);Page<T> findAll(Pageable pageable);
}
例如,訪問(wèn)第2頁(yè)的 User
,每頁(yè)20條數(shù)據(jù),你可以這樣:
PagingAndSortingRepository<User, Long> repository = // … get access to a bean
Page<User> users = repository.findAll(PageRequest.of(1, 20));
除了 query 方法外,count 和 delete 查詢的查詢派生也是可用的。下面的列表顯示了派生的 count 查詢的接口定義。
Example 5. Derived Count Query
interface UserRepository extends CrudRepository<User, Long> {long countByLastname(String lastname);
}
下面的列表顯示了一個(gè)派生的 delete 查詢的接口定義。
Example 6. Derived Delete Query
interface UserRepository extends CrudRepository<User, Long> {long deleteByLastname(String lastname);List<User> removeByLastname(String lastname);
}
4.2. Query 方法
標(biāo)準(zhǔn)的CRUD Repository 通常有對(duì)底層數(shù)據(jù)store的查詢。使用Spring Data,聲明這些查詢成為一個(gè)四步過(guò)程。
-
聲明一個(gè)擴(kuò)展
Repository
或其子接口之一的接口,并將其泛型指定為它應(yīng)該處理的domain類和ID類型,如以下例子所示。interface PersonRepository extends Repository<Person, Long> { … }
-
在接口中聲明query方法。
interface PersonRepository extends Repository<Person, Long> {List<Person> findByLastname(String lastname); }
-
設(shè)置Spring為這些接口創(chuàng)建代理實(shí)例,可以用JavaConfig或XML 配置。
Java
XML
@EnableJpaRepositories class Config { … }
本例中使用的是JPA namespace。如果你對(duì)任何其他store使用 Repository 抽象,你需要把它改為你的store模塊的適當(dāng) namespace 聲明。換句話說(shuō),你應(yīng)該把
jpa
換成,例如mongodb
。請(qǐng)注意,JavaConfig 并沒有明確地配置
package
,因?yàn)楸蛔⒔獾念惖?package
是默認(rèn)使用的。要自定義要掃描的包,請(qǐng)使用數(shù)據(jù)store特定庫(kù)的@EnableJpaRepositories
注解的basePackage…
屬性之一。class SomeClient {private final PersonRepository repository;SomeClient(PersonRepository repository) {this.repository = repository;}void doSomething() {List<Person> persons = repository.findByLastname("Matthews");} }
下面的章節(jié)將詳細(xì)解釋每一個(gè)步驟。
- 定義 Repository 接口
- 定義 Query 方法
- 創(chuàng)建 Repository 實(shí)例
- 自定義 Spring Data Repository 的實(shí)現(xiàn)
4.3. 定義 Repository 接口
要定義一個(gè) repository 接口,你首先需要定義一個(gè)domain類專用的 repository 接口。該接口必須繼承 Repository
,并將其泛型設(shè)置為domain類和ID類。如果你想為該domain類公開CRUD方法,你可以繼承 CrudRepository
,或其變體,而不是 Repository
。
4.3.1. 稍微修改 Repository 的定義
有幾種變體可以讓你開始使用你的 repository 接口。
典型的方法是繼承 CrudRepository
,它為你提供了 CRUD 功能的方法。CRUD是指創(chuàng)建、讀取、更新、刪除。在3.0版本中,我們還引入了 ListCrudRepository
,它與 CrudRepository
非常相似,但對(duì)于那些返回多個(gè)實(shí)體的方法,它返回一個(gè) List
而不是一個(gè) Iterable
,你可能會(huì)發(fā)現(xiàn)它更容易使用。
如果你使用的是響應(yīng)式store,你可以選擇 ReactiveCrudRepository
,或者 RxJava3CrudRepository
,這取決于你使用的是哪種響應(yīng)式框架。
如果你使用的是Kotlin,你可以選擇 CoroutineCrudRepository
,它利用了Kotlin的 coroutine
(協(xié)程)。
額外的你可以擴(kuò)展 PagingAndSortingRepository
、ReactiveSortingRepository
、RxJava3SortingRepository
或 CoroutineSortingRepository
,如果你需要允許指定一個(gè) Sort
抽象的方法,或者在第一種情況下是 Pageable
抽象。請(qǐng)注意,各種排序 repository 不再像Spring Data 3.0之前的版本那樣擴(kuò)展各自的CRUD庫(kù)。因此,如果你想獲得這兩個(gè)接口的功能,你需要擴(kuò)展這兩個(gè)接口。
如果你不想擴(kuò)展Spring Data接口,你也可以用 @RepositoryDefinition
來(lái)注解你的 repository 接口。擴(kuò)展CRUD repository 接口之一會(huì)暴露出一套完整的方法來(lái)操作你的實(shí)體。如果你想對(duì)暴露的方法有所選擇,可以從CRUD repository 復(fù)制你想暴露的方法到你的 domain repository。這樣做時(shí),你可以改變方法的返回類型。如果可能的話,Spring Data會(huì)尊重返回類型。例如,對(duì)于返回多個(gè)實(shí)體的方法,你可以選擇 Iterable
、List
、Collection
或 VAVR list
如果你的應(yīng)用程序中的許多 repository 應(yīng)該有相同的方法集,你可以定義你自己的基礎(chǔ)接口來(lái)繼承。這樣的接口必須用 @NoRepositoryBean
來(lái)注釋。這可以防止Spring Data試圖直接創(chuàng)建它的實(shí)例而導(dǎo)致異常,因?yàn)樗匀话粋€(gè)泛型變量,Spring data 無(wú)法確定該 repository 的實(shí)體。
下面的例子展示了如何有選擇地公開CRUD方法(本例中為 findById
和 save
)。
Example 7. Selectively exposing CRUD methods
@NoRepositoryBean
interface MyBaseRepository<T, ID> extends Repository<T, ID> {Optional<T> findById(ID id);<S extends T> S save(S entity);
}interface UserRepository extends MyBaseRepository<User, Long> {User findByEmailAddress(EmailAddress emailAddress);
}
在前面的例子中,你為所有的 domain repository 定義了一個(gè)通用的基礎(chǔ)接口,并暴露了 findById(…)
以及 save(…)
。這些方法被路由到Spring Data提供的你所選擇的store的基礎(chǔ) repository 實(shí)現(xiàn)(例如,如果你使用JPA,實(shí)現(xiàn)是 SimpleJpaRepository
),因?yàn)樗鼈兣c CrudRepository
中的方法簽名一致。 所以 UserRepository
現(xiàn)在可以保存用戶,通過(guò)ID查找單個(gè)用戶,通過(guò)電子郵件地址查找 User
。
4.3.2. 在多個(gè)Spring數(shù)據(jù)模塊中使用 Repository
在你的應(yīng)用程序中使用一個(gè)獨(dú)特的Spring Data模塊使事情變得簡(jiǎn)單,因?yàn)槎x范圍內(nèi)的所有 repository 接口都綁定到Spring Data模塊。有時(shí),應(yīng)用程序需要使用一個(gè)以上的Spring Data模塊。在這種情況下,repository 定義必須區(qū)分持久化技術(shù)。當(dāng)它檢測(cè)到類路徑上有多個(gè) repository 工廠時(shí),Spring Data會(huì)進(jìn)入嚴(yán)格的 repository 配置模式。嚴(yán)格的配置使用 repository 或domain類的細(xì)節(jié)來(lái)決定 repository 定義的Spring Data模塊綁定。
- 如果 repository 定義 繼承了特定模塊的 repository,那么它就是特定Spring Data模塊的有效候選 repository。
- 如果 domain 類被注解了 模塊特定的類型注解,它就是特定Spring Data模塊的有效候選者。Spring Data模塊接受第三方注解(如JPA的
@Entity
)或提供自己的注解(如Spring Data MongoDB和Spring Data Elasticsearch的@Document
)。
下面的例子顯示了一個(gè)使用特定模塊接口的 repository(本例中為JPA)。
Example 8. 使用模塊特定接口的 Repository 定義
interface MyRepository extends JpaRepository<User, Long> { }@NoRepositoryBean
interface MyBaseRepository<T, ID> extends JpaRepository<T, ID> { … }interface UserRepository extends MyBaseRepository<User, Long> { … }
MyRepository
和 UserRepository
在其類型層次上擴(kuò)展了 JpaRepository
。它們是Spring Data JPA 模塊的有效候選者。
下面的例子顯示了一個(gè)使用通用(泛型)接口的 repository。
Example 9. 使用泛型接口的 repository 定義
interface AmbiguousRepository extends Repository<User, Long> { … }@NoRepositoryBean
interface MyBaseRepository<T, ID> extends CrudRepository<T, ID> { … }interface AmbiguousUserRepository extends MyBaseRepository<User, Long> { … }
AmbiguousRepository
和 AmbiguousUserRepository
在其類型層次結(jié)構(gòu)中只繼承了 Repository
和 CrudRepository
。雖然在使用唯一的Spring Data模塊時(shí)這很好,但多個(gè)模塊無(wú)法區(qū)分這些 repository 應(yīng)該被綁定到哪個(gè)特定的Spring Data。
下面的例子顯示了一個(gè)使用帶注解的domain類的repository。
Example 10. 使用帶注解的 domain 類的Repository 定義
interface PersonRepository extends Repository<Person, Long> { … }@Entity
class Person { … }interface UserRepository extends Repository<User, Long> { … }@Document
class User { … }
PersonRepository
引用了 Person
,它被 JPA 的 @Entity
注解所注解,所以這個(gè) repository 顯然屬于Spring Data JPA。UserRepository
引用了 User
,它被Spring Data MongoDB 的 @Document
注解所注解。
下面的壞例子顯示了一個(gè)使用混合注解的 domain 類的 Repository。
Example 11. 使用具有混合注解的 domain 類的 repository 定義
interface JpaPersonRepository extends Repository<Person, Long> { … }interface MongoDBPersonRepository extends Repository<Person, Long> { … }@Entity
@Document
class Person { … }
這個(gè)例子展示了一個(gè)同時(shí)使用JPA和Spring Data MongoDB注解的 domain 類。它定義了兩個(gè)repository:JpaPersonRepository
和 MongoDBPersonRepository
。一個(gè)用于JPA,另一個(gè)用于MongoDB的使用。Spring Data不再能夠區(qū)分這些repository,這導(dǎo)致了未定義的行為。
Repository 類型細(xì)節(jié)和區(qū)分domain類注解用于嚴(yán)格的repository庫(kù)配置,以確定特定Spring Data模塊的repository候選者。在同一domain類型上使用多個(gè)持久化技術(shù)的特定注解是可能的,并且能夠在多個(gè)持久化技術(shù)中重復(fù)使用domain類型。然而,Spring Data就不能再確定一個(gè)唯一的模塊來(lái)綁定repository了。
區(qū)分 repository 的最后一個(gè)方法是通過(guò)對(duì) repository base package的掃描。base package 定義了掃描 repository 接口定義的起點(diǎn),這意味著將 repository 的定義放在適當(dāng)?shù)陌?。默認(rèn)情況下,注解驅(qū)動(dòng)的配置使用配置類所在的base package?;赬ML的配置中的base package,需要手動(dòng)強(qiáng)制配置。
下面的例子顯示了注解驅(qū)動(dòng)的 base package 的配置。
Example 12. 注解驅(qū)動(dòng)的 base package 的配置
@EnableJpaRepositories(basePackages = "com.acme.repositories.jpa")
@EnableMongoRepositories(basePackages = "com.acme.repositories.mongo")
class Configuration { … }
4.4. 定義 Query 方法
repository 代理有兩種方法可以從方法名中推導(dǎo)出 repository 特定的查詢。
- 通過(guò)直接從方法名派生出查詢。
- 通過(guò)使用手動(dòng)定義的查詢。
可用的選項(xiàng)取決于實(shí)際的store。然而,必須有一個(gè)策略來(lái)決定創(chuàng)建什么樣的實(shí)際查詢。下一節(jié)將介紹可用的選項(xiàng)。
4.4.1. Query 的查詢策略
下列策略可用于 repository 基礎(chǔ)設(shè)施解析查詢。 對(duì)于 XML 配置,你可以通過(guò) query-lookup-strategy
屬性在命名空間配置策略。 對(duì)于 Java 配置,你可以使用 EnableJpaRepositories
注解的 queryLookupStrategy
屬性。有些策略可能不支持特定的datastore。
CREATE
試圖從查詢方法名稱中構(gòu)建一個(gè)特定的存儲(chǔ)查詢。一般的做法是從方法名中刪除一組已知的前綴,然后解析方法的其余部分。你可以在 “Query 創(chuàng)建” 中關(guān)于查詢構(gòu)建的信息。USE_DECLARED_QUERY
試圖找到一個(gè)已聲明的查詢,如果找不到就會(huì)拋出一個(gè)異常。查詢可以由某處的注解來(lái)定義,也可以通過(guò)其他方式來(lái)聲明。請(qǐng)參閱特定store的文檔以找到該store的可用選項(xiàng)。如果 repository 基礎(chǔ)設(shè)施在啟動(dòng)時(shí)沒有為該方法找到一個(gè)已聲明的查詢,則會(huì)失敗。CREATE_IF_NOT_FOUND
(默認(rèn)) 結(jié)合了CREATE
和USE_DECLARED_QUERY
。它首先查找一個(gè)已聲明的查詢, 如果沒有找到已聲明的查詢, 它將創(chuàng)建一個(gè)基于方法名的自定義查詢。這是默認(rèn)的查詢策略,因此,如果你沒有明確地配置任何東西,就會(huì)使用這種策略。它允許通過(guò)方法名快速定義查詢,但也可以根據(jù)需要通過(guò)引入已聲明的查詢對(duì)這些查詢進(jìn)行自定義調(diào)整。
4.4.2. Query 創(chuàng)建
內(nèi)置在Spring Data repository 基礎(chǔ)架構(gòu)中的查詢 builder 機(jī)制對(duì)于在資源庫(kù)的實(shí)體上建立約束性查詢非常有用。
下面的例子展示了如何創(chuàng)建一些查詢。
Example 13. Query creation from method names
interface PersonRepository extends Repository<Person, Long> {List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);// Enables the distinct flag for the queryList<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);// Enabling ignoring case for an individual propertyList<Person> findByLastnameIgnoreCase(String lastname);// Enabling ignoring case for all suitable propertiesList<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);// Enabling static ORDER BY for a queryList<Person> findByLastnameOrderByFirstnameAsc(String lastname);List<Person> findByLastnameOrderByFirstnameDesc(String lastname);
}
解析查詢方法名稱分為主語(yǔ)和謂語(yǔ)。第一部分(find…By
, exists…By
)定義了查詢的主語(yǔ),第二部分形成謂語(yǔ)。引入句(主語(yǔ))可以包含進(jìn)一步的表達(dá)。在 find
(或其他引入關(guān)鍵詞)和 By
之間的任何文本都被認(rèn)為是描述性的,除非使用一個(gè)限制結(jié)果的關(guān)鍵詞,如 Distinct
在要?jiǎng)?chuàng)建的查詢上設(shè)置一個(gè)不同的標(biāo)志,或 Top
/ First
來(lái)限制查詢結(jié)果。
附錄中包含了 查詢方法主語(yǔ)關(guān)鍵詞 和 查詢方法謂語(yǔ)關(guān)鍵詞的完整列表,包括排序和字母修飾語(yǔ)。然而,第一個(gè) By
作為分界符,表示實(shí)際條件謂詞的開始。在一個(gè)非常基本的層面上,你可以在實(shí)體屬性上定義條件,并用 And
和 Or
來(lái)連接它們。
解析方法的實(shí)際結(jié)果取決于你為之創(chuàng)建查詢的持久性store。然而,有一些東西需要注意。
- 表達(dá)式通常是屬性遍歷與可以串聯(lián)的運(yùn)算符的組合。你可以用
AND
和OR
來(lái)組合屬性表達(dá)式。你還可以得到對(duì)屬性表達(dá)式的運(yùn)算符的支持,如Between
,LessThan
,GreaterThan
, 和Like
。支持的運(yùn)算符可能因 datastore 的不同而不同,所以請(qǐng)查閱參考文檔的適當(dāng)部分。 - 方法解析器支持為單個(gè)屬性(例如,
findByLastnameIgnoreCase(…)
)或支持忽略大小寫的類型的所有屬性(通常是字符串實(shí)例—例如,findByLastnameAndFirstnameAllIgnoreCase
(…))設(shè)置忽略大小寫標(biāo)志。是否支持忽略大小寫可能因store而異,所以請(qǐng)查閱參考文檔中的相關(guān)章節(jié),了解特定store的查詢方法。 - 你可以通過(guò)在引用屬性的查詢方法中附加一個(gè)
OrderBy
子句,并提供一個(gè)排序方向(Asc
或Desc
)來(lái)應(yīng)用靜態(tài)排序。要?jiǎng)?chuàng)建一個(gè)支持動(dòng)態(tài)排序的查詢方法,請(qǐng)參閱 “特殊參數(shù)處理”。
4.4.3. 屬性表達(dá)式
屬性表達(dá)式只能引用被管理實(shí)體的一個(gè)直接屬性,如前面的例子所示。在查詢創(chuàng)建時(shí),你已經(jīng)確保解析的屬性是被管理的domian類的一個(gè)屬性。然而,你也可以通過(guò)遍歷嵌套屬性來(lái)定義約束??紤]一下下面的方法簽名。
List<Person> findByAddressZipCode(ZipCode zipCode);
假設(shè) Person
有一個(gè)帶有 ZipCode
的 Address
。在這種情況下,該方法創(chuàng)建 x.address.zipCode
屬性遍歷。解析算法首先將整個(gè)部分(AddressZipCode
)解釋為屬性,并檢查domain類中是否有該名稱的屬性(未加首字母)。如果算法成功,它就使用該屬性。如果沒有,該算法將源頭的駝峰字母部分從右側(cè)分割成一個(gè)頭和一個(gè)尾,并試圖找到相應(yīng)的屬性—在我們的例子中,是 AddressZip
和 Code
。如果該算法找到了具有該頭部的屬性,它就取其尾部,并從那里繼續(xù)向下構(gòu)建樹,以剛才描述的方式將尾部分割開來(lái)。如果第一次分割不匹配,該算法將分割點(diǎn)移到左邊(Address
, ZipCode
)并繼續(xù)。
雖然這在大多數(shù)情況下應(yīng)該是有效的,但該算法有可能選擇錯(cuò)誤的屬性。假設(shè) Person
類也有一個(gè) addressZip
屬性。該算法將在第一輪分割中已經(jīng)匹配,選擇錯(cuò)誤的屬性,并且失敗(因?yàn)?addressZip
的類型可能沒有 code
屬性)。
為了解決這個(gè)模糊的問(wèn)題,你可以在你的方法名里面使用 _
來(lái)手動(dòng)定義遍歷點(diǎn)。因此,我們的方法名稱將如下。
List<Person> findByAddress_ZipCode(ZipCode zipCode);
因?yàn)槲覀儼严聞澗€字符當(dāng)作一個(gè)保留字符,所以我們強(qiáng)烈建議遵循標(biāo)準(zhǔn)的Java命名慣例(也就是說(shuō),不要在屬性名中使用下劃線,而要使用駝峰大寫)。
4.4.4. 特殊參數(shù)處理
為了處理你的查詢中的參數(shù),定義方法參數(shù),正如在前面的例子中已經(jīng)看到的。除此之外,基礎(chǔ)設(shè)施還能識(shí)別某些特定的類型,如 Pageable
和 Sort
,以動(dòng)態(tài)地將分頁(yè)和排序應(yīng)用于你的查詢。下面的例子演示了這些功能。
Example 14. 在查詢方法中使用 Pageable
、Slice
和 Sort
。
Page<User> findByLastname(String lastname, Pageable pageable);Slice<User> findByLastname(String lastname, Pageable pageable);List<User> findByLastname(String lastname, Sort sort);List<User> findByLastname(String lastname, Pageable pageable);
第一個(gè)方法讓你把 org.springframework.data.domain.Pageable
實(shí)例傳遞給 query 方法,以動(dòng)態(tài)地將分頁(yè)添加到你靜態(tài)定義的查詢中。一個(gè) Page
知道可用的元素和頁(yè)面的總數(shù)。它是通過(guò)基礎(chǔ)設(shè)施觸發(fā)一個(gè) count 查詢來(lái)計(jì)算總數(shù)量。由于這可能是昂貴的(取決于使用的store),你可以返回一個(gè) Slice
。一個(gè) Slice
只知道下一個(gè) Slice
是否可用,當(dāng)遍歷一個(gè)較大的結(jié)果集時(shí),這可能就足夠了。
排序選項(xiàng)也是通過(guò) Pageable
實(shí)例處理的。如果你只需要排序,在你的方法中加入 org.springframework.data.domain.Sort
參數(shù)。正如你所看到的,返回一個(gè) List
也是可能的。在這種情況下,構(gòu)建實(shí)際的 Page
實(shí)例所需的額外元數(shù)據(jù)并沒有被創(chuàng)建(這反過(guò)來(lái)意味著不需要發(fā)出額外的 count 查詢)。相反,它限制了查詢,只查詢給定范圍的實(shí)體。
分頁(yè)和排序
你可以通過(guò)使用屬性名稱來(lái)定義簡(jiǎn)單的排序表達(dá)式。你可以將表達(dá)式連接起來(lái),將多個(gè) criteria 收集到一個(gè)表達(dá)式中。
Example 15. Defining sort expressions
Sort sort = Sort.by("firstname").ascending().and(Sort.by("lastname").descending());
對(duì)于定義排序表達(dá)式的更加類型安全的方式,從定義排序表達(dá)式的類型開始,使用方法引用來(lái)定義排序的屬性。
Example 16. 通過(guò)使用類型安全的API來(lái)定義排序表達(dá)式
TypedSort<Person> person = Sort.sort(Person.class);Sort sort = person.by(Person::getFirstname).ascending().and(person.by(Person::getLastname).descending());
如果你的 store 實(shí)現(xiàn)支持 Querydsl,你也可以使用生成的 metamodel 類型來(lái)定義排序表達(dá)式。
Example 17. 通過(guò)使用Querydsl API定義排序表達(dá)式
QSort sort = QSort.by(QPerson.firstname.asc()).and(QSort.by(QPerson.lastname.desc()));
4.4.5. 限制查詢結(jié)果
你可以通過(guò)使用 first
或 top
關(guān)鍵字來(lái)限制查詢方法的結(jié)果,這兩個(gè)關(guān)鍵字可以互換使用。你可以在 top
或 first
后面附加一個(gè)可選的數(shù)值,以指定要返回的最大結(jié)果大小。如果不加數(shù)字,就會(huì)假定結(jié)果大小為 1
。下面的例子顯示了如何限制查詢的大小。
Example 18. 使用 Top
和 First
限制查詢結(jié)果集
User findFirstByOrderByLastnameAsc();User findTopByOrderByAgeDesc();Page<User> queryFirst10ByLastname(String lastname, Pageable pageable);Slice<User> findTop3ByLastname(String lastname, Pageable pageable);List<User> findFirst10ByLastname(String lastname, Sort sort);List<User> findTop10ByLastname(String lastname, Pageable pageable);
對(duì)于支持不同查詢的數(shù)據(jù)集,限制表達(dá)式也支持 Distinct
關(guān)鍵字。另外,對(duì)于將結(jié)果集限制在一個(gè)實(shí)例的查詢,支持用 Optional
關(guān)鍵字將結(jié)果包入。
如果分頁(yè)或 slice 應(yīng)用于 limit 查詢的分頁(yè)(以及可用頁(yè)數(shù)的計(jì)算),則會(huì)在 limit 結(jié)果中應(yīng)用。
4.4.6. Repository 方法返回 Collection 或 Iterable
返回多個(gè)結(jié)果的查詢方法可以使用標(biāo)準(zhǔn)的Java Iterable
、List
和 Set
。除此之外,我們還支持返回Spring Data的 Streamable
,這是 Iterable
的一個(gè)自定義擴(kuò)展,以及 Vavr 提供的 collection 類型。請(qǐng)參考附錄中對(duì)所有可能的 查詢方法返回類型的解釋。
使用 Streamable 作為 Query 方法的返回類型
你可以用 Streamable
來(lái)替代 Iterable
或任何 collection 類型。它提供了方便的方法來(lái)訪問(wèn)一個(gè)非并行的 Stream
(Iterable
所沒有的),并且能夠在元素上直接 …filter(…)
和 …map(…)
,并將 Streamable
與其他元素連接起來(lái)。
Example 19. 使用 Streamable 來(lái)組合 query 方法的結(jié)果
interface PersonRepository extends Repository<Person, Long> {Streamable<Person> findByFirstnameContaining(String firstname);Streamable<Person> findByLastnameContaining(String lastname);
}Streamable<Person> result = repository.findByFirstnameContaining("av").and(repository.findByLastnameContaining("ea"));
返回自定義的 Streamable Wrapper 類型
為集合提供專用的 wrapper 類型是一種常用的模式,為返回多個(gè)元素的查詢結(jié)果提供API。通常,這些類型的使用是通過(guò)調(diào)用返回類似集合類型的 repository 方法,并手動(dòng)創(chuàng)建一個(gè) wrapper 類型的實(shí)例。你可以避免這個(gè)額外的步驟,因?yàn)镾pring Data允許你使用這些 wrapper 類型作為查詢方法的返回類型,如果它們滿足以下條件。
- 該類型實(shí)現(xiàn)了
Streamable
. - 該類型暴露了一個(gè)構(gòu)造函數(shù)或一個(gè)名為
of(…)
或valueOf(…)
的靜態(tài)工廠方法,該方法將Streamable
作為一個(gè)參數(shù)。
下面列出了一個(gè)例子。
class Product { MonetaryAmount getPrice() { … }
}@RequiredArgsConstructor(staticName = "of")
class Products implements Streamable<Product> { private final Streamable<Product> streamable;public MonetaryAmount getTotal() { return streamable.stream().map(Priced::getPrice).reduce(Money.of(0), MonetaryAmount::add);}@Overridepublic Iterator<Product> iterator() { return streamable.iterator();}
}interface ProductRepository implements Repository<Product, Long> {Products findAllByDescriptionContaining(String text);
}
支持 Vavr 集合
Vavr 是一個(gè)擁抱Java中函數(shù)式編程概念的庫(kù)。它帶有一組自定義的集合類型,你可以將其作為查詢方法的返回類型,如下表所示。
Vavr collection 類型 | 使用的Vavr實(shí)現(xiàn)類型 | 有效的Java原類型 |
---|---|---|
io.vavr.collection.Seq | io.vavr.collection.List | java.util.Iterable |
io.vavr.collection.Set | io.vavr.collection.LinkedHashSet | java.util.Iterable |
io.vavr.collection.Map | io.vavr.collection.LinkedHashMap | java.util.Map |
你可以使用第一列中的類型(或其子類型)作為查詢方法的返回類型,并根據(jù)實(shí)際查詢結(jié)果的Java類型(第三列),獲得第二列中的類型作為實(shí)現(xiàn)類型使用。或者,你可以聲明 Traversable
(相當(dāng)于Vavr Iterable
),然后我們從實(shí)際返回值中派生出實(shí)現(xiàn)類。也就是說(shuō),java.util.List
會(huì)變成 Vavr List
或 Seq
,java.util.Set
會(huì)變成 Vavr LinkedHashSet
Set
,以此類推。
4.4.7. Repository 方法的 Null 處理
從Spring Data 2.0開始,返回單個(gè)聚合實(shí)例的 repository CRUD方法使用Java 8的 Optional
來(lái)表示可能沒有的值。除此之外,Spring Data還支持在查詢方法上返回以下 wrapper 類型。
com.google.common.base.Optional
scala.Option
io.vavr.control.Option
另外,查詢方法可以選擇完全不使用 wrapper 類型。沒有查詢結(jié)果的話會(huì)通過(guò)返回 null
來(lái)表示。Repository 方法返回集合、集合替代物、wrapper和流時(shí),保證不會(huì)返回 null
,而是返回相應(yīng)的空(Empty)表示。詳見 “Repository 查詢返回類型”。
null約束注解
你可以通過(guò)使用 Spring Framework的nullability注解 來(lái)表達(dá) repository 方法的 nullability 約束。它們提供了一種友好的方法,并在運(yùn)行時(shí)選擇加入 null
值檢查,如下所示。
@NonNullApi
: 在 package 的層面上用于聲明參數(shù)和返回值的默認(rèn)行為,分別是既不接受也不產(chǎn)生null
值。@NonNull
: 用于不得為null
的參數(shù)或返回值(不需要在參數(shù)和返回值中使用@NonNullApi
)。@Nullable
: 用于可以是null
的參數(shù)或返回值。
Spring注解是用 JSR 305 注解(一個(gè)休眠狀態(tài)但廣泛使用的JSR)進(jìn)行元注解的。JSR 305元注解讓工具供應(yīng)商(如 IDEA、 Eclipse 和 Kotlin)以通用的方式提供 null-safety 支持,而不需要對(duì)Spring注解進(jìn)行硬編碼支持。為了在運(yùn)行時(shí)檢查查詢方法的無(wú)效性約束,你需要通過(guò)在 package-info.java
中使用 Spring 的 @NonNullApi
,在包級(jí)別上激活null約束,如下例所示。
Example 20. Declaring Non-nullability in package-info.java
@org.springframework.lang.NonNullApi
package com.acme;
一旦定義了非null約束,repository 的查詢方法調(diào)用就會(huì)在運(yùn)行時(shí)被驗(yàn)證是否有nul約束。如果查詢結(jié)果違反了定義的約束條件,就會(huì)拋出一個(gè)異常。這種情況發(fā)生在方法會(huì)返回 null
,但被聲明為non-nullable(在 repository 所在的包上定義注解的默認(rèn)值)。如果你想再次選擇加入允許結(jié)果為null,可以有選擇地在個(gè)別方法上使用 @Nullable
。使用本節(jié)開頭提到的結(jié)果wrapper類型繼續(xù)按預(yù)期工作:一個(gè)空的結(jié)果被翻譯成代表不存在的值。
下面的例子顯示了剛才描述的一些技術(shù)。
Example 21. Using different nullability constraints
package com.acme; interface UserRepository extends Repository<User, Long> {User getByEmailAddress(EmailAddress emailAddress); @NullableUser findByEmailAddress(@Nullable EmailAddress emailAdress); Optional<User> findOptionalByEmailAddress(EmailAddress emailAddress);
}
基于Kotlin的 Repository 中的 Nullability
Kotlin在語(yǔ)言中加入了 對(duì)無(wú)效性約束的定義。Kotlin代碼編譯為字節(jié)碼,它不通過(guò)方法簽名來(lái)表達(dá)無(wú)效性約束,而是通過(guò)編譯后的元數(shù)據(jù)。請(qǐng)確保在你的項(xiàng)目中包含 kotlin-reflect
JAR,以實(shí)現(xiàn)對(duì)Kotlin的nullability約束的內(nèi)省。Spring Data Repository 使用語(yǔ)言機(jī)制來(lái)定義這些約束,以應(yīng)用相同的運(yùn)行時(shí)檢查,如下所示。
Example 22. Using nullability constraints on Kotlin repositories
interface UserRepository : Repository<User, String> {fun findByUsername(username: String): User fun findByFirstname(firstname: String?): User?
}
4.4.8. 流式(Stream)查詢結(jié)果
你可以通過(guò)使用Java 8 Stream
作為返回類型來(lái)增量地處理查詢方法的結(jié)果。如下面的例子所示,不把查詢結(jié)果包裹在 Stream
中,而是使用 data store 的特定方法來(lái)執(zhí)行流式處理。
Example 23. Stream the result of a query with Java 8 Stream
@Query("select u from User u")
Stream<User> findAllByCustomQueryAndStream();Stream<User> readAllByFirstnameNotNull();@Query("select u from User u")
Stream<User> streamAllPaged(Pageable pageable);
Example 24. Working with a Stream
result in a try-with-resources
block
try (Stream<User> stream = repository.findAllByCustomQueryAndStream()) {stream.forEach(…);
}
4.4.9. 異步(Asynchronous)查詢結(jié)果
你可以通過(guò)使用 Spring的異步方法運(yùn)行能力 來(lái)異步運(yùn)行 repository 查詢。這意味著該方法在調(diào)用后立即返回,而實(shí)際的查詢發(fā)生在一個(gè)已經(jīng)提交給Spring TaskExecutor
的任務(wù)中。異步查詢與響應(yīng)式查詢不同,不應(yīng)混合使用。關(guān)于響應(yīng)式支持的更多細(xì)節(jié),請(qǐng)參見store的特定文檔。下面的例子顯示了一些異步查詢的案例。
@Async
Future<User> findByFirstname(String firstname); @Async
CompletableFuture<User> findOneByFirstname(String firstname);
4.5. 創(chuàng)建 Repository 實(shí)例
本節(jié)介紹了如何為定義的 repository 接口創(chuàng)建實(shí)例和Bean定義。
4.5.1. Java 配置
在Java配置類上使用store特有的 @EnableJpaRepositories
注解來(lái)定義 repository 激活的配置。關(guān)于基于Java的Spring容器配置的介紹,請(qǐng)參見 Spring參考文檔中的JavaConfig。
啟用 Spring Data Repository 的示例配置類似于以下內(nèi)容。
Example 25. 基于注解的 repository 配置示例
@Configuration
@EnableJpaRepositories("com.acme.repositories")
class ApplicationConfiguration {@BeanEntityManagerFactory entityManagerFactory() {// …}
}
4.5.2. XML 配置
每個(gè)Spring Data模塊都包括一個(gè) repositories
元素,讓你定義一個(gè)Spring為你掃描的 base package,如下例所示。
Example 26. 通過(guò)XML啟用Spring Data Repository
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns:beans="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns="http://www.springframework.org/schema/data/jpa"xsi:schemaLocation="http://www.springframework.org/schema/beanshttps://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/data/jpahttps://www.springframework.org/schema/data/jpa/spring-jpa.xsd"><jpa:repositories base-package="com.acme.repositories" /></beans:beans>
在前面的例子中,Spring被指示掃描 com.acme.repositories
及其所有子包,以尋找擴(kuò)展 Repository
或其子接口之一的接口。對(duì)于找到的每個(gè)接口,基礎(chǔ)設(shè)施都會(huì)注冊(cè)持久化技術(shù)專用的 FactoryBean
,以創(chuàng)建適當(dāng)?shù)拇?#xff0c;處理查詢方法的調(diào)用。每個(gè)Bean都被注冊(cè)在一個(gè)從接口名稱衍生出來(lái)的Bean名稱下,所以 UserRepository
的接口將被注冊(cè)在 userRepository
下。嵌套的存儲(chǔ)庫(kù)接口的Bean名是以其包裹的類型名稱為前綴。base package 屬性允許通配符,這樣你就可以定義一個(gè)掃描包的模式。
4.5.3. 使用 Filter
默認(rèn)情況下,基礎(chǔ)架構(gòu)會(huì)抓取每個(gè)擴(kuò)展了位于配置的 base package 下的持久化技術(shù)特定的 Repository
子接口的接口,并為其創(chuàng)建一個(gè)Bean實(shí)例。然而,你可能想要更精細(xì)地控制哪些接口為其創(chuàng)建Bean實(shí)例。要做到這一點(diǎn),可以在 Repository 聲明中使用 filter 元素。其語(yǔ)義與Spring的組件過(guò)濾器中的元素完全等同。詳情請(qǐng)見 Spring參考文檔 中的這些元素。
例如,為了排除某些接口作為 Repository Bean 的實(shí)例化,你可以使用以下配置。
Example 27. 使用 Filter
Java
XML
@Configuration
@EnableJpaRepositories(basePackages = "com.acme.repositories",includeFilters = { @Filter(type = FilterType.REGEX, pattern = ".*SomeRepository") },excludeFilters = { @Filter(type = FilterType.REGEX, pattern = ".*SomeOtherRepository") })
class ApplicationConfiguration {@BeanEntityManagerFactory entityManagerFactory() {// …}
}
前面的例子排除了所有以 SomeRepository
結(jié)尾的接口被實(shí)例化,包括以 SomeOtherRepository
結(jié)尾的接口。
4.5.4. 單獨(dú)使用
你也可以在Spring容器之外使用資源庫(kù)基礎(chǔ)設(shè)施—例如,在CDI環(huán)境中。你仍然需要在你的classpath中使用一些Spring庫(kù),但是,一般來(lái)說(shuō),你也可以通過(guò)編程來(lái)設(shè)置 Repository。Repository 支持的Spring Data模塊都有一個(gè)特定于持久化技術(shù)的 RepositoryFactory
,你可以使用,如下所示。
Example 28. repository factory 的獨(dú)立使用
RepositoryFactorySupport factory = … // Instantiate factory here
UserRepository repository = factory.getRepository(UserRepository.class);
4.6. 自定義 Spring Data Repository 的實(shí)現(xiàn)
Spring Data提供了各種選項(xiàng)來(lái)創(chuàng)建查詢方法,只需少量編碼。但當(dāng)這些選項(xiàng)不符合你的需求時(shí),你也可以為 repository 方法提供你自己的自定義實(shí)現(xiàn)。本節(jié)介紹了如何做到這一點(diǎn)。
4.6.1. 自定義個(gè)別 Repository
要用自定義的功能來(lái)豐富 repository,你必須首先定義一個(gè)片段(fragment)接口和自定義功能的實(shí)現(xiàn),如下所示。
Example 29. 定制 repository 功能的接口
interface CustomizedUserRepository {void someCustomMethod(User user);
}
Example 30. 實(shí)現(xiàn)自定義 repository 的功能
class CustomizedUserRepositoryImpl implements CustomizedUserRepository {public void someCustomMethod(User user) {// Your custom implementation}
}
實(shí)現(xiàn)本身并不依賴于Spring Data,它可以是一個(gè)普通的Spring Bean。因此,你可以使用標(biāo)準(zhǔn)的依賴注入行為來(lái)注入對(duì)其他Bean(如 JdbcTemplate
)的引用,參與到各個(gè)切面,等等。
然后你可以讓你的 repository 接口繼承片段接口,如下所示。
Example 31. 改變你的 repository 接口
interface UserRepository extends CrudRepository<User, Long>, CustomizedUserRepository {// Declare query methods here
}
用你的存儲(chǔ)庫(kù)接口繼承片段接口,結(jié)合了CRUD和自定義功能,并使其對(duì)客戶端可用。
Spring Data Repository 是通過(guò)使用形成 repository 組合的片段來(lái)實(shí)現(xiàn)的。片段是基礎(chǔ)repository、功能方面(如 QueryDsl),以及自定義接口和它們的實(shí)現(xiàn)。每當(dāng)你為你的repository接口添加一個(gè)接口,你就通過(guò)添加一個(gè)片段來(lái)增強(qiáng)組合?;A(chǔ)repository和repository方面的實(shí)現(xiàn)是由每個(gè)Spring Data模塊提供的。
下面的例子顯示了自定義接口和它們的實(shí)現(xiàn)。
Example 32. 片段及其實(shí)現(xiàn)
interface HumanRepository {void someHumanMethod(User user);
}class HumanRepositoryImpl implements HumanRepository {public void someHumanMethod(User user) {// Your custom implementation}
}interface ContactRepository {void someContactMethod(User user);User anotherContactMethod(User user);
}class ContactRepositoryImpl implements ContactRepository {public void someContactMethod(User user) {// Your custom implementation}public User anotherContactMethod(User user) {// Your custom implementation}
}
下面的例子顯示了一個(gè)擴(kuò)展了 CrudRepository
的自定義 repository
的接口。
Example 33. 修改你的 repository 接口
interface UserRepository extends CrudRepository<User, Long>, HumanRepository, ContactRepository {// Declare query methods here
}
Repository可以由多個(gè)自定義實(shí)現(xiàn)組成,這些自定義實(shí)現(xiàn)按其聲明的順序被導(dǎo)入。自定義實(shí)現(xiàn)的優(yōu)先級(jí)高于基礎(chǔ)實(shí)現(xiàn)和Repository 切面。這種排序可以讓你覆蓋基礎(chǔ)Repository和切面的方法,并在兩個(gè)片段貢獻(xiàn)了相同的方法簽名時(shí)解決歧義。Repository片段不限于在單一存儲(chǔ)庫(kù)接口中使用。多個(gè)Repository可以使用一個(gè)片段接口,讓你在不同的 Repository 中重復(fù)使用自定義的內(nèi)容。
下面的例子顯示了一個(gè) Repository 片段及其實(shí)現(xiàn)。
Example 34. Fragments overriding save(…)
interface CustomizedSave<T> {<S extends T> S save(S entity);
}class CustomizedSaveImpl<T> implements CustomizedSave<T> {public <S extends T> S save(S entity) {// Your custom implementation}
}
下面的例子顯示了一個(gè)使用前述 repository 片段的 repository。
Example 35. 自定義 repository 接口
interface UserRepository extends CrudRepository<User, Long>, CustomizedSave<User> {
}interface PersonRepository extends CrudRepository<Person, Long>, CustomizedSave<Person> {
}
配置
repository基礎(chǔ)設(shè)施試圖通過(guò)掃描發(fā)現(xiàn)repository的包下面的類來(lái)自動(dòng)檢測(cè)自定義實(shí)現(xiàn)片段。這些類需要遵循后綴默認(rèn)為 Impl
的命名慣例。
下面的例子顯示了一個(gè)使用默認(rèn)后綴的 repository 和一個(gè)為后綴設(shè)置了自定義值的 repository。
Example 36. 配置示例
Java
XML
@EnableJpaRepositories(repositoryImplementationPostfix = "MyPostfix")
class Configuration { … }
前面例子中的第一個(gè)配置試圖查找一個(gè)叫做 com.acme.repository.CustomizedUserRepositoryImpl
的類,作為一個(gè)自定義的 repository 實(shí)現(xiàn)。第二個(gè)例子試圖查找 com.acme.repository.CustomizedUserRepositoryMyPostfix
。
消除歧義
如果在不同的包中發(fā)現(xiàn)了具有匹配類名的多個(gè)實(shí)現(xiàn),Spring Data會(huì)使用Bean名稱來(lái)確定使用哪一個(gè)。
考慮到前面顯示的 CustomizedUserRepository
的以下兩個(gè)自定義實(shí)現(xiàn),第一個(gè)實(shí)現(xiàn)被使用。它的Bean名是 customedUserRepositoryImpl
,與片段接口(CustomizedUserRepository
)加后綴 Impl
的名字一致。
Example 37. 解決模棱兩可的實(shí)現(xiàn)
package com.acme.impl.one;class CustomizedUserRepositoryImpl implements CustomizedUserRepository {// Your custom implementation
}
package com.acme.impl.two;@Component("specialCustomImpl")
class CustomizedUserRepositoryImpl implements CustomizedUserRepository {// Your custom implementation
}
如果你用 @Component("specialCustom")
來(lái)注解 UserRepository
接口,那么 Bean 的名字加上 Impl
就與 com.acme.impl.two
中為 repository 實(shí)現(xiàn)定義的名字相匹配,并被用來(lái)代替第一個(gè)接口。
手動(dòng)注入
如果你的自定義實(shí)現(xiàn)只使用基于注解的配置和自動(dòng)注入,前面所示的方法很好用,因?yàn)樗划?dāng)作任何其他Spring Bean。如果你的實(shí)現(xiàn)片段Bean需要特殊的注入,你可以根據(jù)前文所述的約定來(lái)聲明Bean并為其命名。然后,基礎(chǔ)設(shè)施通過(guò)名稱來(lái)引用手動(dòng)定義的Bean定義,而不是自己創(chuàng)建一個(gè)。下面的例子展示了如何手動(dòng)注入一個(gè)自定義的實(shí)現(xiàn)。
Example 38. 手動(dòng)注入一個(gè)自定義實(shí)現(xiàn)
Java
XML
class MyClass {MyClass(@Qualifier("userRepositoryImpl") UserRepository userRepository) {…}
}
4.6.2. 自定義 Base Repository
當(dāng)你想定制基礎(chǔ) repository 的行為時(shí),上一節(jié)描述的方法需要定制每個(gè) repository 的接口,以便所有的 repository 都受到影響。為了改變所有 repository 的行為,你可以創(chuàng)建一個(gè)擴(kuò)展持久化技術(shù)特定 repository 基類的實(shí)現(xiàn)。然后這個(gè)類作為 repository 代理的自定義基類,如下面的例子所示。
Example 39. 自定義 repository base 類
class MyRepositoryImpl<T, ID>extends SimpleJpaRepository<T, ID> {private final EntityManager entityManager;MyRepositoryImpl(JpaEntityInformation entityInformation,EntityManager entityManager) {super(entityInformation, entityManager);// Keep the EntityManager around to used from the newly introduced methods.this.entityManager = entityManager;}@Transactionalpublic <S extends T> S save(S entity) {// implementation goes here}
}
最后一步是讓Spring Data基礎(chǔ)設(shè)施意識(shí)到定制的 repository base 類。在配置中,你可以通過(guò)使用 repositoryBaseClass
來(lái)做到這一點(diǎn),如下面的例子所示。
Example 40. 配置一個(gè)自定義 repository base 類
Java
XML
@Configuration
@EnableJpaRepositories(repositoryBaseClass = MyRepositoryImpl.class)
class ApplicationConfiguration { … }
4.7. 從 Aggregate Root (聚合ROOT)中發(fā)布事件
由 Repository 管理的實(shí)體是 aggregate root。在領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)應(yīng)用程序中,這些aggregate root通常會(huì)發(fā)布 domain 事件。Spring Data提供了一個(gè)名為 @DomainEvents
的注解,你可以在 aggregate root 的一個(gè)方法上使用該注解,以使這種發(fā)布盡可能地簡(jiǎn)單,如下例所示。
Example 41. 從aggregate root中暴露domain 事件
class AnAggregateRoot {@DomainEvents Collection<Object> domainEvents() {// … return events you want to get published here}@AfterDomainEventPublication void callbackMethod() {// … potentially clean up domain events list}
}
每次調(diào)用Spring Data Repository的 save(…)
、saveAll(…)
、delete(…)
或 deleteAll(…)
方法時(shí)都會(huì)調(diào)用這些方法。
4.8. Spring Data 擴(kuò)展
本節(jié)記錄了一組Spring Data擴(kuò)展,這些擴(kuò)展使Spring Data能夠在各種情況下使用。目前,大部分的集成都是針對(duì)Spring MVC的。
4.8.1. Querydsl 擴(kuò)展
Querydsl 是一個(gè)框架,可以通過(guò)其 fluent API構(gòu)建靜態(tài)類型的類似SQL的查詢。
一些Spring Data模塊通過(guò) QuerydslPredicateExecutor
提供與 Querydsl 的集成,正如下面的例子所示。
Example 42. QuerydslPredicateExecutor interface
public interface QuerydslPredicateExecutor<T> {Optional<T> findById(Predicate predicate); Iterable<T> findAll(Predicate predicate); long count(Predicate predicate); boolean exists(Predicate predicate); // … more functionality omitted.
}
為了使用 Querydsl 支持,在你的版本庫(kù)接口上擴(kuò)展 QuerydslPredicateExecutor
,如下面的例子所示。
Example 43. Repository 上的 Querydsl 整合
interface UserRepository extends CrudRepository<User, Long>, QuerydslPredicateExecutor<User> {
}
前面的例子讓你通過(guò)使用 Querydsl Predicate
實(shí)例來(lái)編寫類型安全的查詢,如下圖所示。
Predicate predicate = user.firstname.equalsIgnoreCase("dave").and(user.lastname.startsWithIgnoreCase("mathews"));userRepository.findAll(predicate);
4.8.2. Web 的支持
支持 repository 編程模型的Spring Data模塊帶有各種Web支持。web相關(guān)的組件需要添加 Spring MVC 到項(xiàng)目。其中一些甚至提供了與 Spring HATEOAS的整合。一般來(lái)說(shuō),集成支持是通過(guò)在你的 JavaConfig
配置類中使用 @EnableSpringDataWebSupport
注解來(lái)啟用的,如下面例子所示。
Example 44. 啟用 Spring Data web 支持
Java
XML
@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
class WebConfiguration {}
@EnableSpringDataWebSupport
注解注冊(cè)了一些組件。我們將在本節(jié)后面討論這些組件。它還會(huì)檢測(cè)classpath上的Spring HATEOAS,并為其注冊(cè)整合組件(如果存在)。
基本的 Web 支持
在XML中啟用Spring Data Web支持
上一節(jié)所示的配置注冊(cè)了一些基本組件。
- 使用
DomainClassConverter
類 讓Spring MVC從請(qǐng)求參數(shù)或路徑變量中解析 Repository 管理的 domain 類實(shí)例。 HandlerMethodArgumentResolver
的實(shí)現(xiàn),讓Spring MVC從請(qǐng)求參數(shù)中解析Pageable
和Sort
實(shí)例。- Jackson模塊 對(duì)
Point
和Distance
等類型進(jìn)行序列化/反序列化,或存儲(chǔ)特定的類型,這取決于使用的Spring數(shù)據(jù)模塊。
使用 DomainClassConverter
類
DomainClassConverter
類讓你在Spring MVC Controller 方法簽名中直接使用 domain 類型,這樣你就不需要通過(guò) repository 手動(dòng)查找實(shí)例了,如下例所示。
Example 45. 一個(gè)在方法簽名中使用 domain 類型的Spring MVC controller
@Controller
@RequestMapping("/users")
class UserController {@RequestMapping("/{id}")String showUserForm(@PathVariable("id") User user, Model model) {model.addAttribute("user", user);return "userForm";}
}
該方法直接接收一個(gè) User
實(shí)例,而不需要進(jìn)一步的查找。該實(shí)例可以通過(guò)讓Spring MVC先將路徑變量轉(zhuǎn)換為domain類的 id
類型來(lái)解決,最終通過(guò)調(diào)用為domain類注冊(cè)的資源庫(kù)實(shí)例 findById(…)
來(lái)訪問(wèn)該實(shí)例。
使用 HandlerMethodArgumentResolvers 解析 Pageable 和 Sort
上一節(jié) 中的配置片段還注冊(cè)了一個(gè) PageableHandlerMethodArgumentResolver
以及一個(gè) SortHandlerMethodArgumentResolver
的實(shí)例。注冊(cè)后,Pageable
和 Sort
可以作為有效的controller方法參數(shù),如下圖所示。
Example 46. 使用 Pageable 作為 controller 方法參數(shù)
@Controller
@RequestMapping("/users")
class UserController {private final UserRepository repository;UserController(UserRepository repository) {this.repository = repository;}@RequestMappingString showUsers(Model model, Pageable pageable) {model.addAttribute("users", repository.findAll(pageable));return "users";}
}
前面的方法簽名使Spring MVC嘗試通過(guò)使用以下默認(rèn)配置從請(qǐng)求參數(shù)中派生出一個(gè) Pageable
實(shí)例。
page | 你想檢索的頁(yè)。索引從0開始,默認(rèn)為0。 |
---|---|
size | 你想檢索的每頁(yè)數(shù)據(jù)大小。默認(rèn)為20。 |
sort | 應(yīng)該按格式 `property,property(,ASC |
要自定義這種行為,請(qǐng)注冊(cè)一個(gè)分別實(shí)現(xiàn) PageableHandlerMethodArgumentResolverCustomizer
接口或 SortHandlerMethodArgumentResolverCustomizer
接口的bean。它的 customize()
方法會(huì)被調(diào)用,讓你改變?cè)O(shè)置,正如下面的例子所示。
@Bean SortHandlerMethodArgumentResolverCustomizer sortCustomizer() {return s -> s.setPropertyDelimiter("<-->");
}
如果設(shè)置現(xiàn)有 MethodArgumentResolver
的屬性不足以滿足你的目的,可以擴(kuò)展 SpringDataWebConfiguration
或啟用HATEOAS的等價(jià)物,覆蓋 pageableResolver()
或 sortResolver()
方法,并導(dǎo)入你的自定義的配置文件,而不是使用 @Enable
注解。
如果你需要從請(qǐng)求中解析多個(gè) Pageable
或 Sort
實(shí)例(例如多個(gè)表),你可以使用 Spring 的 @Qualifier
注解來(lái)區(qū)分一個(gè)和另一個(gè)。然后請(qǐng)求參數(shù)必須以 ${qualifier}_
為前綴。下面的例子顯示了由此產(chǎn)生的方法簽名。
String showUsers(Model model,@Qualifier("thing1") Pageable first,@Qualifier("thing2") Pageable second) { … }
你必須填充 thing1_page
、thing2_page
,以此類推。
傳入該方法的默認(rèn) Pageable
相當(dāng)于一個(gè) PageRequest.of(0, 20)
,但你可以通過(guò)在 Pageable
參數(shù)上使用 @PageableDefault
注解來(lái)定制它。
對(duì) Pageable 的 Hypermedia 支持
Spring HATEOAS提供了一個(gè)表示 model 類(PagedResources
),它允許用必要的頁(yè)面元數(shù)據(jù)以及鏈接來(lái)豐富 Page
實(shí)例的內(nèi)容,讓客戶輕松地瀏覽頁(yè)面。Page
到 PagedResources
的轉(zhuǎn)換是由Spring HATEOAS ResourceAssembler
接口的實(shí)現(xiàn)完成的,這個(gè)接口被稱為 PagedResourcesAssembler
。下面的例子展示了如何使用 PagedResourcesAssembler
作為 controller 方法的參數(shù)。
Example 47. 使用 PagedResourcesAssembler 作為 controller 方法參數(shù)
@Controller
class PersonController {@Autowired PersonRepository repository;@RequestMapping(value = "/persons", method = RequestMethod.GET)HttpEntity<PagedResources<Person>> persons(Pageable pageable,PagedResourcesAssembler assembler) {Page<Person> persons = repository.findAll(pageable);return new ResponseEntity<>(assembler.toResources(persons), HttpStatus.OK);}
}
啟用配置,如前面的例子所示,讓 PagedResourcesAssembler
被用作控制器方法的參數(shù)。對(duì)它調(diào)用 toResources(…)
有以下效果。
Page
的內(nèi)容成為PagedResources
實(shí)例的內(nèi)容。PagedResources
對(duì)象被附加了一個(gè)PageMetadata
實(shí)例,它被填充了來(lái)自Page
和基礎(chǔ)PageRequest
的信息。PagedResources
可能會(huì)有一個(gè)prev
和一個(gè)next
鏈接,這取決于頁(yè)面的狀態(tài)。這些鏈接指向該方法所映射的URI。添加到方法中的分頁(yè)參數(shù)與PageableHandlerMethodArgumentResolver
的設(shè)置相匹配,以確保鏈接可以在稍后被解析。
假設(shè)我們?cè)跀?shù)據(jù)庫(kù)中有30個(gè)的 Person
實(shí)例?,F(xiàn)在你可以觸發(fā)一個(gè)請(qǐng)求(GET http://localhost:8080/persons
),看到類似以下的輸出。
{ "links" : [ { "rel" : "next","href" : "http://localhost:8080/persons?page=1&size=20" }],"content" : [… // 20 Person instances rendered here],"pageMetadata" : {"size" : 20,"totalElements" : 30,"totalPages" : 2,"number" : 0}
}
assembler 產(chǎn)生了正確的URI,并且還拾取了默認(rèn)的配置,以便為即將到來(lái)的請(qǐng)求將參數(shù)解析為一個(gè) Pageable
。這意味著,如果你改變了配置,鏈接會(huì)自動(dòng)遵守這一變化。默認(rèn)情況下,assembler 會(huì)指向它被調(diào)用的controller方法,但你可以通過(guò)傳遞一個(gè)自定義的 Link
來(lái)定制,作為建立分頁(yè)鏈接的基礎(chǔ),它重載了 PagedResourcesAssembler.toResource(..)
方法。
Spring Data Jackson 模塊
核心模塊和一些特定的存儲(chǔ)模塊與一組Jackson模塊一起發(fā)布,用于Spring Data domain 域使用的類型,如 org.springframework.data.geo.Distance
和 org.springframework.data.geo.Point
。 一旦啟用 web支持 和 com.fasterxml.jackson.databind.ObjectMapper
可用,就會(huì)導(dǎo)入這些模塊。
在初始化過(guò)程中,SpringDataJacksonModules
和 SpringDataJacksonConfiguration
一樣,被基礎(chǔ)設(shè)施所接收,這樣,聲明的 com.fasterxml.jackson.databind.Module
就被提供給Jackson ObjectMapper
。
以下domain類型的 Data binding mixins 由公共基礎(chǔ)設(shè)施注冊(cè)。
org.springframework.data.geo.Distance
org.springframework.data.geo.Point
org.springframework.data.geo.Box
org.springframework.data.geo.Circle
org.springframework.data.geo.Polygon
Web Databinding 的支持
你可以通過(guò)使用 JSONPath 表達(dá)式(需要 Jayway JsonPath)或 XPath 表達(dá)式(需要 XmlBeam)來(lái)使用 Spring Data 投影(在 投影 中描述)來(lái)綁定傳入的請(qǐng)求的payload,如下例所示。
Example 48. 使用JSONPath 或 XPath 表達(dá)式來(lái)綁定HTTP payload
@ProjectedPayload
public interface UserPayload {@XBRead("//firstname")@JsonPath("$..firstname")String getFirstname();@XBRead("/lastname")@JsonPath({ "$.lastname", "$.user.lastname" })String getLastname();
}
你可以將前面的例子中顯示的類型作為Spring MVC controller 的方法參數(shù),或者通過(guò)在 RestTemplate
的某個(gè)方法中使用 ParameterizedTypeReference
。前面的方法聲明將嘗試在給定 document 中的任何地方找到 firstname
。lastname
的XML查找是在傳入 document 的頂層進(jìn)行的。JSON的變體首先嘗試頂層的 lastname
,但是如果前者沒有返回一個(gè)值,也會(huì)嘗試嵌套在 user
子 document 中的 lastname
。這樣,源 document 結(jié)構(gòu)的變化可以很容易地被減輕,而不需要客戶端調(diào)用暴露的方法(通常是基于類的 payload 綁定的一個(gè)缺點(diǎn))。
如 投影 中所述,支持嵌套投影。如果該方法返回一個(gè)復(fù)雜的、非接口類型,則使用Jackson ObjectMapper
來(lái)映射最終值。
對(duì)于Spring MVC,一旦 @EnableSpringDataWebSupport
被激活,并且classpath上有必要的依賴,必要的 converter 就會(huì)被自動(dòng)注冊(cè)。對(duì)于 RestTemplate
的使用,需要手動(dòng)注冊(cè)一個(gè) ProjectingJackson2HttpMessageConverter
(JSON)或 XmlBeamHttpMessageConverter
。
欲了解更多信息,請(qǐng)參見 Spring Data 示例庫(kù) 中的 web投影示例。
Querydsl 的 Web 支持
對(duì)于那些有 QueryDSL 集成的 store,你可以從 Request
查詢字符串中包含的屬性導(dǎo)出查詢。
考慮下面這個(gè)查詢字符串:
?firstname=Dave&lastname=Matthews
給出前面例子中的 User
對(duì)象,你可以通過(guò)使用 QuerydslPredicateArgumentResolver
將一個(gè)查詢字符串解析為以下值,如下所示。
QUser.user.firstname.eq("Dave").and(QUser.user.lastname.eq("Matthews"))
在方法簽名中添加 @QuerydslPredicate
提供了一個(gè)隨時(shí)可用的 Predicate
,你可以通過(guò)使用 QuerydslPredicateExecutor
來(lái)運(yùn)行它。
下面的例子顯示了如何在方法簽名中使用 @QuerydslPredicate
。
@Controller
class UserController {@Autowired UserRepository repository;@RequestMapping(value = "/", method = RequestMethod.GET)String index(Model model, @QuerydslPredicate(root = User.class) Predicate predicate, Pageable pageable, @RequestParam MultiValueMap<String, String> parameters) {model.addAttribute("users", repository.findAll(predicate, pageable));return "index";}
}
默認(rèn)的綁定方式如下。
Object
在簡(jiǎn)單的屬性上作為eq
。Object
在集合上的屬性與contains
一樣。Collection
上的簡(jiǎn)單屬性為in
。
你可以通過(guò) @QuerydslPredicate
的 bindings
屬性或者利用Java 8的 default methods
來(lái)定制這些綁定,并將 QuerydslBinderCustomizer
方法添加到 repository 接口,如下所示。
interface UserRepository extends CrudRepository<User, String>,QuerydslPredicateExecutor<User>, QuerydslBinderCustomizer<QUser> { @Overridedefault void customize(QuerydslBindings bindings, QUser user) {bindings.bind(user.username).first((path, value) -> path.contains(value)) bindings.bind(String.class).first((StringPath path, String value) -> path.containsIgnoreCase(value)); bindings.excluding(user.password); }
}
QuerydslPredicateExecutor 提供了對(duì) Predicate 的特定查找方法的訪問(wèn)。 | |
---|---|
repository 接口上定義的 QuerydslBinderCustomizer 被自動(dòng)拾取,并成為 @QuerydslPredicate(bindings=…) 的快捷方式。 | |
定義 username 屬性的綁定是一個(gè)簡(jiǎn)單的 contains 綁定。 | |
定義 String 屬性的默認(rèn)綁定為不區(qū)分大小寫的 contains 匹配。 | |
將 password 屬性排除在 Predicate 解析之外。 |
4.8.3. Repository 填充
如果你使用Spring JDBC模塊,你可能很熟悉對(duì)用SQL腳本填充 DataSource
的支持。在 repository 層面也有類似的抽象,盡管它不使用SQL作為數(shù)據(jù)定義語(yǔ)言,因?yàn)樗仨毷仟?dú)立于store的。因此,填充器支持XML(通過(guò)Spring的OXM抽象)和JSON(通過(guò)Jackson)來(lái)定義數(shù)據(jù),用它來(lái)填充repository。
假設(shè)你有一個(gè)名為 data.json
的文件,內(nèi)容如下。
Example 49. 在JSON中定義的數(shù)據(jù)
[ { "_class" : "com.acme.Person","firstname" : "Dave","lastname" : "Matthews" },{ "_class" : "com.acme.Person","firstname" : "Carter","lastname" : "Beauford" } ]
你可以通過(guò)使用Spring Data Commons中提供的 Repository 命名空間的populator元素來(lái)填充你的Repository。為了將前面的數(shù)據(jù)填充到你的 PersonRepository
中,聲明一個(gè)類似于下面的 populator。
Example 50. 聲明一個(gè) Jackson repository populator
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:repository="http://www.springframework.org/schema/data/repository"xsi:schemaLocation="http://www.springframework.org/schema/beanshttps://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/data/repositoryhttps://www.springframework.org/schema/data/repository/spring-repository.xsd"><repository:jackson2-populator locations="classpath:data.json" /></beans>
前面的聲明導(dǎo)致 data.json
文件被 Jackson ObjectMapper
讀取和反序列化。
JSON對(duì)象被反序列化的類型是通過(guò)檢查JSON文檔的 _class
屬性決定的?;A(chǔ)設(shè)施最終會(huì)選擇適當(dāng)?shù)?repository 來(lái)處理被反序列化的對(duì)象。
為了使用XML來(lái)定義 repository 應(yīng)該填充的數(shù)據(jù),你可以使用 unmarshaller-populator
元素。你把它配置為使用Spring OXM中的一個(gè) XML marshaller 選項(xiàng)。詳情請(qǐng)參見 Spring參考文檔。下面的例子展示了如何用JAXB來(lái) unmarshall 對(duì) repository 填充器的 marshall。
下面的例子顯示了如何用 JAXB 來(lái) unmarshall
一個(gè) repository 填充器(populator)。
Example 51. 聲明一個(gè) unmarshalling
repository populator(使用JAXB)。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:repository="http://www.springframework.org/schema/data/repository"xmlns:oxm="http://www.springframework.org/schema/oxm"xsi:schemaLocation="http://www.springframework.org/schema/beanshttps://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/data/repositoryhttps://www.springframework.org/schema/data/repository/spring-repository.xsdhttp://www.springframework.org/schema/oxmhttps://www.springframework.org/schema/oxm/spring-oxm.xsd"><repository:unmarshaller-populator locations="classpath:data.json"unmarshaller-ref="unmarshaller" /><oxm:jaxb2-marshaller contextPath="com.acme" /></beans>
5. 參考文檔
5.1. JPA Repository
本章指出了 JPA 的 repository 支持的專業(yè)性。這建立在 “與 Spring Data Repository 一起工作” 中解釋的核心 repository 支持的基礎(chǔ)上。請(qǐng)確保你對(duì)那里解釋的基本概念有良好的理解。
5.1.1. 簡(jiǎn)介
本節(jié)介紹了通過(guò)以下兩種方式配置Spring Data JPA的基礎(chǔ)知識(shí)。
- “Spring 命名空間” (XML 配置)
- “基于注解的配置” (Java 配置)
基于注解的配置
Spring Data JPA Repository 的支持可以通過(guò) JavaConfig 以及自定義XML命名空間來(lái)激活,如下例所示。
Example 52. Spring Data JPA repositories using JavaConfig
@Configuration
@EnableJpaRepositories
@EnableTransactionManagement
class ApplicationConfig {@Beanpublic DataSource dataSource() {EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();return builder.setType(EmbeddedDatabaseType.HSQL).build();}@Beanpublic LocalContainerEntityManagerFactoryBean entityManagerFactory() {HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();vendorAdapter.setGenerateDdl(true);LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();factory.setJpaVendorAdapter(vendorAdapter);factory.setPackagesToScan("com.acme.domain");factory.setDataSource(dataSource());return factory;}@Beanpublic PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {JpaTransactionManager txManager = new JpaTransactionManager();txManager.setEntityManagerFactory(entityManagerFactory);return txManager;}
}
前面的配置類通過(guò)使用 spring-jdbc
的 EmbeddedDatabaseBuilder
API設(shè)置了一個(gè)嵌入式HSQL數(shù)據(jù)庫(kù)。然后,Spring Data設(shè)置了一個(gè) EntityManagerFactory
,并使用Hibernate作為樣本持久化提供者。這里聲明的最后一個(gè)基礎(chǔ)設(shè)施組件是 JpaTransactionManager
。最后,該示例通過(guò)使用 @EnableJpaRepositories
注解來(lái)激活Spring Data JPA Repository,該注解本質(zhì)上與XML命名空間攜帶相同的屬性。如果沒有配置 base package,它就使用配置類所在的包。
Spring 命名空間
Spring Data 的 JPA 模塊包含一個(gè)自定義命名空間,允許定義 repository bean。它還包含JPA的某些特殊功能和元素屬性。一般來(lái)說(shuō),JPA repository 可以通過(guò)使用 repositories
元素來(lái)設(shè)置,如下面的例子所示。
Example 53. 通過(guò)使用命名空間設(shè)置JPA repository
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:jpa="http://www.springframework.org/schema/data/jpa"xsi:schemaLocation="http://www.springframework.org/schema/beanshttps://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/data/jpahttps://www.springframework.org/schema/data/jpa/spring-jpa.xsd"><jpa:repositories base-package="com.acme.repositories" /></beans>
正如 “創(chuàng)建 Repository 實(shí)例” 中所述,使用 repositories
元素可以查找Spring Data Repository 。除此之外,它還為所有用 @Repository
注解的Bean激活了持久化異常轉(zhuǎn)換,以便讓JPA持久化提供者拋出的異常被轉(zhuǎn)換為Spring的 DataAccessException
層次結(jié)構(gòu)。
自定義命名空間屬性
除了 repositories
元素的默認(rèn)屬性,JPA命名空間還提供了額外的屬性,讓你對(duì) repository 的設(shè)置獲得更詳細(xì)的控制。
entity-manager-factory-ref | 明確地將 EntityManagerFactory 與被 repositories 元素檢測(cè)到的 Repository 一起使用。通常在應(yīng)用程序中使用多個(gè) EntityManagerFactory Bean時(shí)使用。如果沒有配置,Spring Data會(huì)自動(dòng)在 ApplicationContext 中查找名稱為 entityManagerFactory 的 EntityManagerFactory bean。 |
---|---|
transaction-manager-ref | 明確地將 PlatformTransactionManager 與被 repositories 元素檢測(cè)到的 Repository 連接起來(lái)。通常只有在配置了多個(gè)事務(wù)管理器或 EntityManagerFactory Bean 時(shí)才需要。默認(rèn)為當(dāng)前 ApplicationContext 內(nèi)的單個(gè)定義的 PlatformTransactionManager 。 |
Bootstrap Mode
默認(rèn)情況下,Spring Data JPA Repository 是默認(rèn)的Spring Bean。它們是單例和立即初始化的。在啟動(dòng)過(guò)程中,它們已經(jīng)與JPA EntityManager
交互,以達(dá)到驗(yàn)證和元數(shù)據(jù)分析的目的。Spring Framework支持在后臺(tái)線程中初始化JPA EntityManagerFactory
,因?yàn)檫@個(gè)過(guò)程通常會(huì)在Spring應(yīng)用程序中占用大量的啟動(dòng)時(shí)間。為了有效地利用該后臺(tái)初始化,我們需要確保JPA Repository 盡可能晚地被初始化。
從 Spring Data JPA 2.1 開始,你現(xiàn)在可以配置一個(gè) BootstrapMode
(通過(guò) @EnableJpaRepositories
注解或XML命名空間),它可以取以下值。
DEFAULT
(默認(rèn))?—?除非明確地用@Lazy
注解,否則 repository 是立即實(shí)例化的。懶加載只在沒有客戶Bean需要 repository 的實(shí)例時(shí)才有效,因?yàn)檫@需要初始化 repository Bean。LAZY
?—?隱式聲明所有的 Repository Bean 都是懶加載的,也會(huì)導(dǎo)致懶加載的初始化代理被創(chuàng)建,注入到客戶端Bean中。這意味著,如果客戶Bean只是將實(shí)例存儲(chǔ)在一個(gè)字段中,而不是在初始化過(guò)程中使用Repository,那么Repository將不會(huì)被實(shí)例化。Repository實(shí)例將在與Repository的第一次交互時(shí)被初始化和驗(yàn)證。DEFERRED
?—?基本上與LAZY的操作模式相同,但在響應(yīng)ContextRefreshedEvent
時(shí)觸發(fā) Repository 的初始化,因此在應(yīng)用程序完全啟動(dòng)之前就對(duì) Repository 進(jìn)行驗(yàn)證。
建議
如果你不使用異步 JPA bootstrap,請(qǐng)堅(jiān)持使用默認(rèn)的 bootstrap mode。
如果你以異步方式引導(dǎo)(bootstrap)JPA,DEFERRED
是一個(gè)合理的默認(rèn)值,因?yàn)樗鼘⒋_保 Spring Data JPA 引導(dǎo)只等待 EntityManagerFactory
的設(shè)置,如果這本身比初始化所有其他應(yīng)用程序組件所需的時(shí)間更長(zhǎng)。盡管如此,它仍能確保在應(yīng)用程序發(fā)出啟動(dòng)信號(hào)之前,repository 被正確地初始化和驗(yàn)證。
對(duì)于測(cè)試場(chǎng)景和本地開發(fā)來(lái)說(shuō),LAZY
是一個(gè)不錯(cuò)的選擇。一旦你確信 repository 可以正常啟動(dòng),或者你正在測(cè)試應(yīng)用程序的其他部分,為所有 repository 運(yùn)行驗(yàn)證可能會(huì)不必要地增加啟動(dòng)時(shí)間。這同樣適用于本地開發(fā),在本地開發(fā)中,你只需要訪問(wèn)可能需要初始化一個(gè) repository 的應(yīng)用程序的部分。
5.1.2. 持久化實(shí)體
本節(jié)介紹了如何用Spring Data JPA持久化(保存)實(shí)體。
保存實(shí)體
保存一個(gè)實(shí)體可以通過(guò) CrudRepository.save(…)
方法進(jìn)行。它通過(guò)使用底層的JPA EntityManager
來(lái)持久化或合并給定的實(shí)體。如果實(shí)體還沒有被持久化,Spring Data JPA會(huì)通過(guò)調(diào)用 entityManager.persist(…)
方法來(lái)保存實(shí)體。否則,它會(huì)調(diào)用 entityManager.merge(…)
方法。
實(shí)體狀態(tài)檢測(cè)策略
Spring Data JPA提供了以下策略來(lái)檢測(cè)一個(gè)實(shí)體是否是新的。
- Version 屬性和Id 屬性檢查(默認(rèn))。默認(rèn)情況下,Spring Data JPA首先檢查是否有一個(gè)非原始類型的Version屬性。如果有的話,如果該屬性的值是
null
,那么該實(shí)體就被認(rèn)為是新的。如果沒有這樣的Version屬性,Spring Data JPA會(huì)檢查給定實(shí)體的Id屬性。如果Id屬性是null
,那么該實(shí)體被認(rèn)為是新的。否則,它就被認(rèn)為是不新的。 - 實(shí)現(xiàn)
Persistable
。如果一個(gè)實(shí)體實(shí)現(xiàn)了Persistable
,Spring Data JPA 會(huì)將“是否是新的”的檢測(cè)委托給實(shí)體的isNew(..)
方法。詳情請(qǐng)參見 JavaDoc。 - 實(shí)現(xiàn)
EntityInformation
。你可以通過(guò)創(chuàng)建JpaRepositoryFactory
的子類并重寫getEntityInformation(…)
方法來(lái)定制SimpleJpaRepository
實(shí)現(xiàn)中使用的EntityInformation
抽象。然后你必須將JpaRepositoryFactory
的自定義實(shí)現(xiàn)注冊(cè)為Spring Bean。請(qǐng)注意,這應(yīng)該是很少需要的。詳見 JavaDoc。
對(duì)于那些使用手動(dòng)分配id和沒有version屬性的實(shí)體來(lái)說(shuō),選項(xiàng)1不是一個(gè)選項(xiàng),因?yàn)閷?duì)于那些實(shí)體來(lái)說(shuō),id總是非 null
的。在這種情況下,一個(gè)常見的模式是使用一個(gè)普通的基類,它有一個(gè)默認(rèn)為新實(shí)例的 transient 標(biāo)志,并使用JPA生命周期回調(diào)來(lái)翻轉(zhuǎn)(flip)持久化操作的標(biāo)志。
Example 54. 一個(gè)用于手動(dòng)分配id的實(shí)體的基類
@MappedSuperclass
public abstract class AbstractEntity<ID> implements Persistable<ID> {@Transientprivate boolean isNew = true; @Overridepublic boolean isNew() {return isNew; }@PrePersist @PostLoadvoid markNotNew() {this.isNew = false;}// More code…
}
聲明一個(gè)標(biāo)志來(lái)保持新的狀態(tài)。暫時(shí)性的(Transient),這樣它就不會(huì)持久化到數(shù)據(jù)庫(kù)中。 | |
---|---|
在 Persistable.isNew() 的實(shí)現(xiàn)中返回該標(biāo)志,以便Spring Data Repository 知道是調(diào)用 EntityManager.persist() 還是 ….merge() 。 | |
使用JPA實(shí)體回調(diào)聲明一個(gè)方法,以便在 repository 調(diào)用 save(…) 或 persistence provider 創(chuàng)建實(shí)例后,標(biāo)志被切換為表示現(xiàn)有實(shí)體。 |
5.1.3. 查詢(Query)方法
本節(jié)介紹了用Spring Data JPA創(chuàng)建查詢的各種方法。
查詢(Query)的查找策略
JPA模塊支持將查詢手動(dòng)定義為一個(gè)字符串,或者讓它從方法名稱中衍生出來(lái)。
帶有謂詞 IsStartingWith
, StartingWith
, StartsWith
, IsEndingWith
, EndingWith
, EndsWith
, IsNotContaining
, NotContaining
, NotContains
, IsContaining
, Containing
, Contains
的衍生查詢,這些查詢的各自參數(shù)將被“消毒”處理。這意味著如果參數(shù)實(shí)際上包含 LIKE
識(shí)別為通配符的字符,這些參數(shù)將被轉(zhuǎn)義,所以它們只作為字面意義上的字符匹配。使用的轉(zhuǎn)義字符可以通過(guò)設(shè)置 @EnableJpaRepositories
注解的 escapeCharacter
來(lái)進(jìn)行配置。與 使用 SpEL 表達(dá)式 比較。
聲明查詢
盡管從方法名中獲取查詢是非常方便的,但是我們可能會(huì)面臨這樣的情況:要么方法名解析器不支持我們想要使用的關(guān)鍵字,要么方法名會(huì)變得不必要的丑陋。因此,你可以通過(guò)命名慣例使用JPA命名的查詢(更多信息請(qǐng)參見使用JPA的命名查詢 ),或者用 @Query
注解你的查詢方法(詳見使用 @Query
)。
查詢創(chuàng)建
一般來(lái)說(shuō),JPA的查詢創(chuàng)建機(jī)制如 “Query 方法” 中描述的那樣工作。下面的例子顯示了JPA的查詢方法轉(zhuǎn)化為什么。
Example 55. 從方法名創(chuàng)建查詢
public interface UserRepository extends Repository<User, Long> {List<User> findByEmailAddressAndLastname(String emailAddress, String lastname);
}
我們使用JPA標(biāo)準(zhǔn)API從中創(chuàng)建一個(gè)查詢,但基本上,這轉(zhuǎn)化為以下查詢:select u from User u where u.emailAddress = ?1 and u.lastname = ?2
。 Spring Data JPA做了一個(gè)屬性檢查并遍歷嵌套屬性,如 “屬性表達(dá)式” 中描述。
下表描述了JPA支持的關(guān)鍵字,以及包含該關(guān)鍵字的方法所轉(zhuǎn)換的內(nèi)容。
關(guān)鍵字 | 示例 | JPQL片段 |
---|---|---|
Distinct | findDistinctByLastnameAndFirstname | select distinct … where x.lastname = ?1 and x.firstname = ?2 |
And | findByLastnameAndFirstname | … where x.lastname = ?1 and x.firstname = ?2 |
Or | findByLastnameOrFirstname | … where x.lastname = ?1 or x.firstname = ?2 |
Is , Equals | findByFirstname ,findByFirstnameIs ,findByFirstnameEquals | … where x.firstname = ?1 |
Between | findByStartDateBetween | … where x.startDate between ?1 and ?2 |
LessThan | findByAgeLessThan | … where x.age < ?1 |
LessThanEqual | findByAgeLessThanEqual | … where x.age <= ?1 |
GreaterThan | findByAgeGreaterThan | … where x.age > ?1 |
GreaterThanEqual | findByAgeGreaterThanEqual | … where x.age >= ?1 |
After | findByStartDateAfter | … where x.startDate > ?1 |
Before | findByStartDateBefore | … where x.startDate < ?1 |
IsNull , Null | findByAge(Is)Null | … where x.age is null |
IsNotNull , NotNull | findByAge(Is)NotNull | … where x.age not null |
Like | findByFirstnameLike | … where x.firstname like ?1 |
NotLike | findByFirstnameNotLike | … where x.firstname not like ?1 |
StartingWith | findByFirstnameStartingWith | … where x.firstname like ?1 (用 % 綁定參數(shù)) |
EndingWith | findByFirstnameEndingWith | … where x.firstname like ?1 (用 % 綁定參數(shù)) |
Containing | findByFirstnameContaining | … where x.firstname like ?1 (用 % 包裹參數(shù)) |
OrderBy | findByAgeOrderByLastnameDesc | … where x.age = ?1 order by x.lastname desc |
Not | findByLastnameNot | … where x.lastname <> ?1 |
In | findByAgeIn(Collection ages) | … where x.age in ?1 |
NotIn | findByAgeNotIn(Collection ages) | … where x.age not in ?1 |
True | findByActiveTrue() | … where x.active = true |
False | findByActiveFalse() | … where x.active = false |
IgnoreCase | findByFirstnameIgnoreCase | … where UPPER(x.firstname) = UPPER(?1) |
基于注解的配置
基于注解的配置的優(yōu)點(diǎn)是不需要編輯另一個(gè)配置文件,降低了維護(hù)工作量。但是,你需要為每一個(gè)新的命名查詢重新編譯你的 domain 類。
Example 56. 基于注解的命名查詢配置
@Entity
@NamedQuery(name = "User.findByEmailAddress",query = "select u from User u where u.emailAddress = ?1")
public class User {}
使用JPA的命名查詢
XML命名的查詢定義
要使用XML配置,將必要的 `` 元素添加到位于classpath的 META-INF
文件夾中的 orm.xml
JPA配置文件中。通過(guò)使用一些定義好的命名慣例來(lái)啟用命名查詢的自動(dòng)調(diào)用。更多細(xì)節(jié),見下文。
Example 57. XML命名查詢配置
<named-query name="User.findByLastname"><query>select u from User u where u.lastname = ?1</query>
</named-query>
查詢需要指定一個(gè)名字,用于在運(yùn)行時(shí)解析它。
聲明接口
要允許這些命名的查詢,請(qǐng)按如下方式指定 UserRepositoryWithRewriter
。
Example 58. 在UserRepository中的查詢方法聲明
public interface UserRepository extends JpaRepository<User, Long> {List<User> findByLastname(String lastname);User findByEmailAddress(String emailAddress);
}
Spring Data 試圖將對(duì)這些方法的調(diào)用解析為一個(gè)命名的查詢,以配置的 domain 類的簡(jiǎn)單名稱開始,后面是用點(diǎn)分隔的方法名稱。所以前面的例子會(huì)使用前面定義的命名查詢,而不是試圖從方法名中創(chuàng)建一個(gè)查詢。
使用 @Query
使用命名的查詢來(lái)聲明對(duì)實(shí)體的查詢是一種有效的方法,對(duì)于少量的查詢來(lái)說(shuō)效果也不錯(cuò)。由于查詢本身與運(yùn)行它們的Java方法相聯(lián)系,你實(shí)際上可以通過(guò)使用Spring Data JPA @Query
注解來(lái)直接綁定它們,而不是將它們注解到 domain 類中。這就把 domain 類從持久化的特定信息中解放出來(lái),并把查詢與 repository 接口放在一起。
對(duì)查詢方法進(jìn)行注解的查詢優(yōu)先于使用 @NamedQuery
定義的查詢或在 orm.xml
中聲明的命名查詢。
下面的例子顯示了一個(gè)用 @Query
注解創(chuàng)建的查詢。
Example 59. 在查詢方法中使用 @Query
聲明查詢。
public interface UserRepository extends JpaRepository<User, Long> {@Query("select u from User u where u.emailAddress = ?1")User findByEmailAddress(String emailAddress);
}
使用 QueryRewriter
有時(shí),無(wú)論你嘗試應(yīng)用多少功能,似乎都不可能讓Spring Data JPA在查詢被發(fā)送到 EntityManager
之前將你想要的東西都應(yīng)用到其中。
你有能力在查詢被發(fā)送到 EntityManager
之拿到它,并 “重寫” 它。也就是說(shuō),你可以在最后時(shí)刻做任何改動(dòng)。
Example 60. 使用 @Query
聲明一個(gè)QueryRewriter
public interface MyRepository extends JpaRepository<User, Long> {@Query(value = "select original_user_alias.* from SD_USER original_user_alias",nativeQuery = true,queryRewriter = MyQueryRewriter.class)List<User> findByNativeQuery(String param);@Query(value = "select original_user_alias from User original_user_alias",queryRewriter = MyQueryRewriter.class)List<User> findByNonNativeQuery(String param);
}
這個(gè)例子既展示了本地(純SQL)重寫器(rewriter),也展示了JPQL查詢,兩者都利用了同一個(gè) QueryRewriter
。在這種情況下,Spring Data JPA將尋找在 application context 中注冊(cè)的相應(yīng)類型的bean。
你可以這樣寫一個(gè)查詢重寫器。
Example 61. QueryRewriter
示例
public class MyQueryRewriter implements QueryRewriter {@Overridepublic String rewrite(String query, Sort sort) {return query.replaceAll("original_user_alias", "rewritten_user_alias");}
}
你必須確保你的 QueryRewriter
在應(yīng)用上下文中被注冊(cè),無(wú)論是通過(guò)應(yīng)用Spring Framework的 @Component
系列注解之一,還是將其作為 @Configuration
類中的 @Bean
方法的一部分。
另一個(gè)選擇是讓 repository 本身實(shí)現(xiàn)該接口。
Example 62. 提供 QueryRewriter
的 Repository
public interface MyRepository extends JpaRepository<User, Long>, QueryRewriter {@Query(value = "select original_user_alias.* from SD_USER original_user_alias",nativeQuery = true,queryRewriter = MyRepository.class)List<User> findByNativeQuery(String param);@Query(value = "select original_user_alias from User original_user_alias",queryRewriter = MyRepository.class)List<User> findByNonNativeQuery(String param);@Overridedefault String rewrite(String query, Sort sort) {return query.replaceAll("original_user_alias", "rewritten_user_alias");}
}
根據(jù)你對(duì)你的 QueryRewriter
所做的工作,最好是有多個(gè),每個(gè)都在 application context 中注冊(cè)。
使用高級(jí)的 LIKE
表達(dá)式
用 @Query
創(chuàng)建的手動(dòng)定義查詢的查詢運(yùn)行機(jī)制允許在查詢定義內(nèi)定義高級(jí) LIKE
表達(dá)式,如下面的例子所示。
Example 63. 在 @Query
中 like
的高級(jí)表達(dá)式
public interface UserRepository extends JpaRepository<User, Long> {@Query("select u from User u where u.firstname like %?1")List<User> findByFirstnameEndsWith(String firstname);
}
在前面的例子中,LIKE
分隔符(%
)被識(shí)別,查詢被轉(zhuǎn)化為有效的 JPQL 查詢(去除 %
)。在運(yùn)行查詢時(shí),傳遞給方法調(diào)用的參數(shù)被增加了先前識(shí)別的 LIKE
模式。
原生查詢
@Query
注解允許通過(guò)設(shè)置 nativeQuery
標(biāo)志為 true
來(lái)運(yùn)行原生查詢,如下例所示。
Example 64. 使用 @Query
在查詢方法中聲明一個(gè)原生查詢
public interface UserRepository extends JpaRepository<User, Long> {@Query(value = "SELECT * FROM USERS WHERE EMAIL_ADDRESS = ?1", nativeQuery = true)User findByEmailAddress(String emailAddress);
}
Example 65. 通過(guò)使用 @Query
在查詢方法中聲明用于分頁(yè)的原生 count 查詢
public interface UserRepository extends JpaRepository<User, Long> {@Query(value = "SELECT * FROM USERS WHERE LASTNAME = ?1",countQuery = "SELECT count(*) FROM USERS WHERE LASTNAME = ?1",nativeQuery = true)Page<User> findByLastname(String lastname, Pageable pageable);
}
類似的方法也適用于原生的命名查詢(named native query),方法是將 .count
后綴添加到查詢的副本中。不過(guò),你可能需要為你的 count 查詢注冊(cè)一個(gè)結(jié)果集映射。
使用 Sort(排序)
排序可以通過(guò)提供一個(gè) PageRequest
或者直接使用 Sort
來(lái)完成。在 Sort
的 Order
實(shí)例中實(shí)際使用的屬性需要與你的 domain model 相匹配,這意味著它們需要解析到查詢中使用的一個(gè)屬性或一個(gè)別名。JPQL 將其定義為一個(gè)狀態(tài)字段路徑表達(dá)式。
然而,與 @Query
一起使用 Sort
可以讓你在 ORDER BY
子句中潛入含有函數(shù)的非路徑檢查的 Order
實(shí)例。這是有可能的,因?yàn)?Order
被附加到了給定的查詢字符串中。默認(rèn)情況下,Spring Data JPA拒絕任何包含函數(shù)調(diào)用的 Order
實(shí)例,但你可以使用 JpaSort.unsafe
來(lái)添加潛在的不安全排序。
下面的例子使用了 Sort
和 JpaSort
,包括 JpaSort
上的一個(gè) unsafe 選項(xiàng)。
Example 66. 使用 Sort
和 JpaSort
public interface UserRepository extends JpaRepository<User, Long> {@Query("select u from User u where u.lastname like ?1%")List<User> findByAndSort(String lastname, Sort sort);@Query("select u.id, LENGTH(u.firstname) as fn_len from User u where u.lastname like ?1%")List<Object[]> findByAsArrayAndSort(String lastname, Sort sort);
}repo.findByAndSort("lannister", Sort.by("firstname"));
repo.findByAndSort("stark", Sort.by("LENGTH(firstname)"));
repo.findByAndSort("targaryen", JpaSort.unsafe("LENGTH(firstname)"));
repo.findByAsArrayAndSort("bolton", Sort.by("fn_len"));
使用命名參數(shù)
默認(rèn)情況下,Spring Data JPA使用基于位置的參數(shù)綁定,正如前面所有例子中所描述的那樣。這使得查詢方法在關(guān)于參數(shù)位置的重構(gòu)中有點(diǎn)容易出錯(cuò)。為了解決這個(gè)問(wèn)題,你可以使用 @Param
注解來(lái)給方法參數(shù)一個(gè)具體的名字,并在查詢中綁定這個(gè)名字,如下面的例子所示。
Example 67. 使用命名參數(shù)
public interface UserRepository extends JpaRepository<User, Long> {@Query("select u from User u where u.firstname = :firstname or u.lastname = :lastname")User findByLastnameOrFirstname(@Param("lastname") String lastname,@Param("firstname") String firstname);
}
使用 SpEL 表達(dá)式
從Spring Data JPA 1.4版本開始,我們支持在用 @Query
定義的手動(dòng)查詢中使用受限的SpEL模板表達(dá)式。在查詢運(yùn)行時(shí),這些表達(dá)式會(huì)針對(duì)一組預(yù)定義的變量進(jìn)行評(píng)估。Spring Data JPA支持一個(gè)名為 entityName
的變量。它的用法是 select x from #{#entityName} x
。它插入了與給定 repository 相關(guān)的 domain 類型的 entityName
。entityName
的解析方法如下。如果 domain 類已經(jīng)在 @Entity
注解上設(shè)置了名稱屬性,它就會(huì)被使用。否則,將使用 doamin 類的簡(jiǎn)單類名。
下面的例子演示了查詢字符串中 #{#entityName}
表達(dá)式的一個(gè)用例,你想用查詢方法和手動(dòng)定義的查詢來(lái)定義一個(gè) repository 接口。
Example 68. 在 repository 查詢方法中使用SpEL表達(dá)式 - entityName
@Entity
public class User {@Id@GeneratedValueLong id;String lastname;
}public interface UserRepository extends JpaRepository<User,Long> {@Query("select u from #{#entityName} u where u.lastname = ?1")List<User> findByLastname(String lastname);
}
為了避免在 @Query
注解的查詢字符串中說(shuō)明實(shí)際的 entity name,你可以使用 #{#entityName}
變量。
當(dāng)然,你可以直接在查詢聲明中使用 User
,但這也需要你改變查詢的方式。對(duì) #entityName
的引用可以捕捉到未來(lái)潛在的 User
類對(duì)不同實(shí)體名稱的重新映射(例如,通過(guò)使用 @Entity(name = "MyUser")
)。
查詢字符串中 #{#entityName}
表達(dá)式的另一個(gè)用例是,如果你想為一個(gè)具體的domain類型定義一個(gè)通用的 repository 接口與專門的 repository 接口。為了不重復(fù)定義具體接口上的自定義查詢方法,你可以在通用 repository 接口的 @Query
注解的查詢字符串中使用實(shí)體名稱(entity name)表達(dá)式,如下例所示。
Example 69. 在 repository 查詢方法中使用SpEL表達(dá)式 - 具有繼承性的 entityName
@MappedSuperclass
public abstract class AbstractMappedType {…String attribute
}@Entity
public class ConcreteType extends AbstractMappedType { … }@NoRepositoryBean
public interface MappedTypeRepository<T extends AbstractMappedType>extends Repository<T, Long> {@Query("select t from #{#entityName} t where t.attribute = ?1")List<T> findAllByAttribute(String attribute);
}public interface ConcreteRepositoryextends MappedTypeRepository<ConcreteType> { … }
在前面的例子中,MappedTypeRepository
接口是一些擴(kuò)展 AbstractMappedType
的 domain 類型的共同父接口。它還定義了通用的 findAllByAttribute(…)
方法,它可以在專門的 repository 接口實(shí)例上使用。如果你現(xiàn)在在 ConcreteRepository
上調(diào)用 findByAllAttribute(…)
,查詢會(huì)變成 select t from ConcreteType t where t.attribute = ?1
。
處理參數(shù)的SpEL表達(dá)式也可以用來(lái)處理方法參數(shù)。在這些SpEL表達(dá)式中,實(shí)體名稱(entity name)是不可用的,但是參數(shù)是可用的。它們可以通過(guò)名稱或索引來(lái)訪問(wèn),正如下面的例子所展示的那樣。
Example 70. 在 repository 查詢方法中使用SpEL表達(dá)式—訪問(wèn)參數(shù)。
@Query("select u from User u where u.firstname = ?1 and u.firstname=?#{[0]} and u.emailAddress = ?#{principal.emailAddress}")
List<User> findByFirstnameAndCurrentUserWithCustomQuery(String firstname);
對(duì)于 like
條件,人們經(jīng)常希望在一個(gè)字符串值的參數(shù)的開頭或結(jié)尾處添加 %
。這可以通過(guò)在綁定參數(shù)標(biāo)記或SpEL表達(dá)式中附加或前綴 %
來(lái)實(shí)現(xiàn)。下面的例子再次證明了這一點(diǎn)。
Example 71. 在 repository 查詢方法中使用SpEL表達(dá)式—通配符快捷方式。
@Query("select u from User u where u.lastname like %:#{[0]}% and u.lastname like %:lastname%")
List<User> findByLastnameWithSpelExpression(@Param("lastname") String lastname);
當(dāng)使用 like
條件的值來(lái)自不安全的來(lái)源時(shí),這些值應(yīng)該被“凈化”,所以它們不能包含任何通配符,從而使攻擊者能夠選擇更多的數(shù)據(jù)。為了這個(gè)目的,escape(String)
方法在SpEL上下文中可用。它將第一個(gè)參數(shù)中所有 _
和 %
的實(shí)例用第二個(gè)參數(shù)中的單個(gè)字符進(jìn)行前綴。與JPQL和標(biāo)準(zhǔn)SQL中的 like
表達(dá)式的 escape
結(jié)合起來(lái),可以輕松地清理綁定的參數(shù)。
Example 72. 在 repository 查詢方法中使用SpEL表達(dá)式—對(duì)輸入值進(jìn)行消毒
@Query("select u from User u where u.firstname like %?#{escape([0])}% escape ?#{escapeCharacter()}")
List<User> findContainingEscaped(String namePart);
在 repository 接口中給出這個(gè)方法聲明,findContainingEscaped("Peter_")
會(huì)找到 Peter_Parker
,而不是 Peter Parker
。使用的轉(zhuǎn)義字符可以通過(guò)設(shè)置 @EnableJpaRepositories
注解的 escapeCharacter
來(lái)配置。請(qǐng)注意,在SpEL上下文中可用的方法 escape(String)
將只轉(zhuǎn)義SQL和JPQL標(biāo)準(zhǔn)通配符 _
和 %
。如果底層數(shù)據(jù)庫(kù)或JPA實(shí)現(xiàn)支持額外的通配符,這些通配符將不會(huì)被轉(zhuǎn)義。
修改查詢
前面所有的章節(jié)都描述了如何聲明查詢來(lái)訪問(wèn)一個(gè)給定的實(shí)體或?qū)嶓w集合。你可以通過(guò)使用 “自定義 Spring Data Repository 的實(shí)現(xiàn)” 中描述的自定義方法設(shè)施添加自定義修改行為。由于這種方法對(duì)于全面的自定義功能是可行的,你可以通過(guò)用 @Modifying
來(lái)注解查詢方法來(lái)修改只需要參數(shù)綁定的查詢,如下面的例子所示。
Example 73. Declaring manipulating queries
@Modifying
@Query("update User u set u.firstname = ?1 where u.lastname = ?2")
int setFixedFirstnameFor(String firstname, String lastname);
這樣做會(huì)觸發(fā)被注解到該方法的查詢,作為一個(gè)update查詢而不是select查詢。由于在執(zhí)行修改查詢后, EntityManager
可能包含過(guò)時(shí)的實(shí)體,我們不會(huì)自動(dòng)清除它(詳見 EntityManager.clear()
的 JavaDoc),因?yàn)檫@將有效地丟棄 EntityManager
中仍然待定的所有非刷新變化。如果你希望 EntityManager
被自動(dòng)清除,你可以將 @Modifying
注解的 clearAutomatically
屬性設(shè)置為 true
。
@Modifying
注解只與 @Query
注解結(jié)合使用。派生的查詢方法或自定義方法不需要這個(gè)注解。
派生的刪除查詢
Spring Data JPA還支持派生的刪除查詢,讓你不必明確地聲明JPQL查詢,如下面的例子所示。
Example 74. 使用派生的刪除查詢
interface UserRepository extends Repository<User, Long> {void deleteByRoleId(long roleId);@Modifying@Query("delete from User u where u.role.id = ?1")void deleteInBulkByRoleId(long roleId);
}
盡管 deleteByRoleId(…)
方法看起來(lái)基本上與 deleteInBulkByRoleId(…)
產(chǎn)生了相同的結(jié)果,但這兩個(gè)方法聲明在運(yùn)行方式上有一個(gè)重要區(qū)別。顧名思義,后一個(gè)方法針對(duì)數(shù)據(jù)庫(kù)發(fā)出一個(gè)單一的JPQL查詢(注解中定義的查詢)。這意味著即使是當(dāng)前加載的 User
實(shí)例也不會(huì)看到生命周期的回調(diào)被調(diào)用。
為了確保生命周期查詢被實(shí)際調(diào)用,對(duì) deleteByRoleId(…)
的調(diào)用會(huì)運(yùn)行一個(gè)查詢,然后逐一刪除返回的實(shí)例,這樣持久化提供者就可以在這些實(shí)體上實(shí)際調(diào)用 @PreRemove
的回調(diào)。
事實(shí)上,派生的刪除查詢是運(yùn)行查詢的捷徑,然后對(duì)結(jié)果調(diào)用 CrudRepository.delete(Iterable users)
,并與 CrudRepository
中其他 delete(…)
方法的實(shí)現(xiàn)保持行為的同步性。
應(yīng)用查詢提示 (Query Hints)
為了將JPA查詢提示應(yīng)用到你的 repository 接口中聲明的查詢,你可以使用 @QueryHints
注解。它需要一個(gè)JPA @QueryHint
注解的數(shù)組,加上一個(gè)布爾標(biāo)志,以潛在地禁用應(yīng)用分頁(yè)時(shí)觸發(fā)的額外count查詢的hints,如下面的例子所示。
Example 75. 在repository 方法中使用QueryHints
public interface UserRepository extends Repository<User, Long> {@QueryHints(value = { @QueryHint(name = "name", value = "value")},forCounting = false)Page<User> findByLastname(String lastname, Pageable pageable);
}
前面的聲明將為該實(shí)際查詢應(yīng)用配置的 @QueryHint
,但省略了應(yīng)用于為計(jì)算總頁(yè)數(shù)而觸發(fā)的count查詢。
在查詢中添加注釋
有時(shí),你需要根據(jù)數(shù)據(jù)庫(kù)性能來(lái)調(diào)試一個(gè)查詢。你的數(shù)據(jù)庫(kù)管理員向你展示的查詢可能與你使用 @Query
編寫的查詢有很大的不同,或者它可能與你推測(cè)的Spring Data JPA生成的關(guān)于自定義查找器的查詢完全不同,或者如果你使用 example 查詢。
為了使這個(gè)過(guò)程更容易,你可以在幾乎所有的JPA操作中插入自定義注釋,無(wú)論是查詢還是其他操作,都可以通過(guò)應(yīng)用 @Meta
注釋來(lái)實(shí)現(xiàn)。
Example 76. 將 @Meta
注解應(yīng)用于 repository 操作中
public interface RoleRepository extends JpaRepository<Role, Integer> {@Meta(comment = "find roles by name")List<Role> findByName(String name);@Override@Meta(comment = "find roles using QBE")<S extends Role> List<S> findAll(Example<S> example);@Meta(comment = "count roles for a given name")long countByName(String name);@Override@Meta(comment = "exists based on QBE")<S extends Role> boolean exists(Example<S> example);
}
這個(gè)樣本 repository 混合了自定義的查找器,以及覆蓋了 JpaRepository
的繼承操作。無(wú)論哪種方式, @Meta
注解都可以讓你添加一個(gè) comment
,這個(gè)注釋將在查詢被發(fā)送到數(shù)據(jù)庫(kù)之前被插入。
同樣重要的是要注意,這個(gè)功能并不僅僅局限于查詢。它延伸到 count
和 exists
操作。雖然沒有顯示,但它也延伸到了某些 delete
操作。
JPQL日志和SQL日志都不是JPA的標(biāo)準(zhǔn),所以每個(gè)提供者都需要自定義配置,如下所示。
激活 Hibernate 注釋
要在 Hibernate 中激活查詢注釋,必須將 hibernate.use_sql_comments
設(shè)置為 true
。
如果你使用的是基于Java的配置設(shè)置,可以這樣做。
Example 77. 基于Java的JPA配置
@Bean
public Properties jpaProperties() {Properties properties = new Properties();properties.setProperty("hibernate.use_sql_comments", "true");return properties;
}
如果你有一個(gè) persistence.xml
文件,你可以在那里應(yīng)用它。
Example 78. 基于 persistence.xml
的配置
<persistence-unit name="my-persistence-unit">...registered classes...<properties><property name="hibernate.use_sql_comments" value="true" /></properties>
</persistence-unit>
最后,如果你使用的是Spring Boot,那么你可以在你的 application.properties
文件中進(jìn)行設(shè)置。
Example 79. 基于Spring Boot屬性的配置
spring.jpa.properties.hibernate.use_sql_comments=true
激活EclipseLink注釋
要激活 EclipseLink 中的查詢注釋,你必須將 eclipselink.logging.level.sql
設(shè)置為 FINE
。
如果你使用的是基于Java的配置設(shè)置,可以這樣做。
Example 80. 基于Java的JPA配置
@Bean
public Properties jpaProperties() {Properties properties = new Properties();properties.setProperty("eclipselink.logging.level.sql", "FINE");return properties;
}
如果你有一個(gè) persistence.xml
文件,你可以在那里應(yīng)用它。
Example 81. 基于 persistence.xml
的配置
<persistence-unit name="my-persistence-unit">...registered classes...<properties><property name="eclipselink.logging.level.sql" value="FINE" /></properties>
</persistence-unit>
最后,如果你使用的是Spring Boot,那么你可以在你的 application.properties
文件中進(jìn)行設(shè)置。
Example 82. 基于Spring Boot屬性的配置
spring.jpa.properties.eclipselink.logging.level.sql=FINE
配置 Fetch- 和 LoadGraphs
JPA 2.1規(guī)范引入了對(duì)指定 Fetch- 和 LoadGraphs 的支持,我們也用 @EntityGraph
注解來(lái)支持,它允許你引用 @NamedEntityGraph
定義。你可以在實(shí)體上使用該注解來(lái)配置結(jié)果查詢的獲取計(jì)劃。獲取的類型(Fetch
或 Load
)可以通過(guò)使用 @EntityGraph
注解的 type
屬性來(lái)配置。請(qǐng)參閱JPA 2.1 Spec 3.7.4以獲得進(jìn)一步的參考。
下面的例子顯示了如何在一個(gè)實(shí)體上定義一個(gè)命名的實(shí)體圖(named entity graph)。
Example 83. 在一個(gè)實(shí)體上定義一個(gè)命名實(shí)體圖。
@Entity
@NamedEntityGraph(name = "GroupInfo.detail",attributeNodes = @NamedAttributeNode("members"))
public class GroupInfo {// default fetch mode is lazy.@ManyToManyList<GroupMember> members = new ArrayList<GroupMember>();…
}
下面的例子顯示了如何在 repository 的查詢方法上引用一個(gè)命名的實(shí)體圖。
Example 84. 在 repository 的查詢方法上引用命名實(shí)體圖的定義。
public interface GroupRepository extends CrudRepository<GroupInfo, String> {@EntityGraph(value = "GroupInfo.detail", type = EntityGraphType.LOAD)GroupInfo getByGroupName(String name);}
也可以通過(guò)使用 @EntityGraph
來(lái)定義特殊的實(shí)體圖。提供的 attributePaths
會(huì)被翻譯成相應(yīng)的 EntityGraph
,而不需要明確地將 @NamedEntityGraph
添加到你的 domain 類型中,如以下例子所示。
Example 85. 在 repository 查詢方法上使用AD-HOC實(shí)體圖定義。
public interface GroupRepository extends CrudRepository<GroupInfo, String> {@EntityGraph(attributePaths = { "members" })GroupInfo getByGroupName(String name);}
投影
Spring Data的查詢方法通常會(huì)返回由 repository 管理的聚合根(aggregate root)的一個(gè)或多個(gè)實(shí)例。 然而,有時(shí)可能需要根據(jù)這些類型的某些屬性來(lái)創(chuàng)建投影。 Spring Data允許建模專用的返回類型,以更有選擇地檢索管理的聚合體(aggregate)的部分視圖。
想象一下,一個(gè)repository和聚合根類型,如下面的例子。
Example 86. aggregate 和 repository 的實(shí)例
class Person {@Id UUID id;String firstname, lastname;Address address;static class Address {String zipCode, city, street;}
}interface PersonRepository extends Repository<Person, UUID> {Collection<Person> findByLastname(String lastname);
}
現(xiàn)在想象一下,我們只想檢索這個(gè)人的 name 屬性。Spring Data提供了什么手段來(lái)實(shí)現(xiàn)這一點(diǎn)?本章的其余部分將回答這個(gè)問(wèn)題。
基于接口的投影
將查詢結(jié)果限制在只有名稱屬性的最簡(jiǎn)單的方法是聲明一個(gè)接口,為要讀取的屬性公開 accessor 方法,如下面的例子中所示。
Example 87. 檢索一個(gè)屬性子集的投影接口
interface NamesOnly {String getFirstname();String getLastname();
}
這里重要的一點(diǎn)是,這里定義的屬性與聚合根(aggregate root)中的屬性完全匹配。這樣做可以添加一個(gè)查詢方法,如下所示。
Example 88. 一個(gè)使用基于接口的投影與查詢方法的 repository
interface PersonRepository extends Repository<Person, UUID> {Collection<NamesOnly> findByLastname(String lastname);
}
查詢執(zhí)行引擎在運(yùn)行時(shí)為每個(gè)返回的元素創(chuàng)建該接口的代理實(shí)例,并將對(duì)公開方法的調(diào)用轉(zhuǎn)發(fā)給目標(biāo)對(duì)象。
投影可以被遞歸使用。如果你想同時(shí)包括一些 Address
信息,為其創(chuàng)建一個(gè)投影接口,并從 getAddress()
的聲明中返回該接口,如以下例子所示。
Example 89. 檢索一個(gè)屬性子集的投影接口
interface PersonSummary {String getFirstname();String getLastname();AddressSummary getAddress();interface AddressSummary {String getCity();}
}
在方法調(diào)用時(shí),目標(biāo)實(shí)例的 address
屬性被獲取,并依次封裝成一個(gè)投影代理。
封閉的投影
一個(gè)投影接口,其訪問(wèn)器(accessor)方法都與目標(biāo)集合的屬性相匹配,被認(rèn)為是一個(gè)封閉的投影。下面的例子(我們?cè)诒菊虑懊嬉灿眠^(guò))就是一個(gè)封閉投影。
Example 90. 一個(gè)封閉投影
interface NamesOnly {String getFirstname();String getLastname();
}
如果你使用一個(gè)封閉的投影,Spring Data可以優(yōu)化查詢的執(zhí)行,因?yàn)槲覀冎浪行枰С滞队按淼膶傩?。關(guān)于這一點(diǎn)的更多細(xì)節(jié),請(qǐng)參見參考文檔中的特定模塊部分。
開放的投影
投影接口中的訪問(wèn)器方法也可以通過(guò)使用 @Value
注解來(lái)計(jì)算新的值,如下面的例子中所示。
Example 91. 開放的投影
interface NamesOnly {@Value("#{target.firstname + ' ' + target.lastname}")String getFullName();…
}
支持投影的聚合根(aggregate root)在 target
變量中可用。使用 @Value
的投影接口是一個(gè)開放的投影。在這種情況下,Spring Data不能應(yīng)用查詢執(zhí)行優(yōu)化,因?yàn)镾pEL表達(dá)式可以使用聚合根的任何屬性。
在 @Value
中使用的表達(dá)式不應(yīng)過(guò)于復(fù)雜—你要避免在 String
變量中編程。對(duì)于非常簡(jiǎn)單的表達(dá)式,一種選擇可能是求助于默認(rèn)方法(在Java 8中引入),如下面的例子所示。
Example 92. 一個(gè)使用默認(rèn)方法的自定義邏輯的投影接口
interface NamesOnly {String getFirstname();String getLastname();default String getFullName() {return getFirstname().concat(" ").concat(getLastname());}
}
這種方法要求你能夠純粹地根據(jù)投影接口上暴露的其他訪問(wèn)器方法來(lái)實(shí)現(xiàn)邏輯。第二個(gè)更靈活的選擇是在Spring Bean中實(shí)現(xiàn)自定義邏輯,然后從SpEL表達(dá)式中調(diào)用該邏輯,如下面的例子所示。
Example 93. Sample Person object
@Component
class MyBean {String getFullName(Person person) {…}
}interface NamesOnly {@Value("#{@myBean.getFullName(target)}")String getFullName();…
}
請(qǐng)注意SpEL表達(dá)式是如何引用 myBean
并調(diào)用 getFullName(…)
方法和轉(zhuǎn)發(fā)投影目標(biāo)作為方法參數(shù)的。由SpEL表達(dá)式評(píng)估支持的方法也可以使用方法參數(shù),然后可以從表達(dá)式中引用這些參數(shù)。方法參數(shù)可以通過(guò)一個(gè)名為 args
的 Object
數(shù)組獲得。下面的例子顯示了如何從 args
數(shù)組中獲取方法參數(shù)。
Example 94. Sample Person object
interface NamesOnly {@Value("#{args[0] + ' ' + target.firstname + '!'}")String getSalutation(String prefix);
}
同樣,對(duì)于更復(fù)雜的表達(dá)式,你應(yīng)該使用Spring Bean并讓表達(dá)式調(diào)用一個(gè)方法,如 前所述。
Nullable Wrapper
投影接口中的Getter可以使用nullable的wrapper,以提高null的安全性。目前支持的wrapper類型有。
java.util.Optional
com.google.common.base.Optional
scala.Option
io.vavr.control.Option
Example 95. 使用 nullable wrapper 的投影接口
interface NamesOnly {Optional<String> getFirstname();
}
如果底層投影值不是 null
的,那么將使用 wrapper 類型的當(dāng)前表示法返回值。如果持有的值是 null
的,那么 getter 方法會(huì)返回所使用的 wrapper 類型的 null
表示。
基于類的投影(DTO)
另一種定義投影的方法是使用value類型的DTO(數(shù)據(jù)傳輸對(duì)象),它持有應(yīng)該被檢索的字段的屬性。這些DTO類型的使用方式與投影接口的使用方式完全相同,只是沒有代理發(fā)生,也不能應(yīng)用嵌套投影。
如果store通過(guò)限制要加載的字段來(lái)優(yōu)化查詢的執(zhí)行,要加載的字段是由暴露出來(lái)的構(gòu)造函數(shù)的參數(shù)名決定的。
下面的例子顯示了一個(gè)投影的DTO。
Example 96. 投影 DTO
class NamesOnly {private final String firstname, lastname;NamesOnly(String firstname, String lastname) {this.firstname = firstname;this.lastname = lastname;}String getFirstname() {return this.firstname;}String getLastname() {return this.lastname;}// equals(…) and hashCode() implementations
}
Unresolved directive in …/…/…/…/spring-data-commons-doc/src/main/asciidoc/repository-projections.adoc - include::…/…/…/…/spring-data-jpa/src/main/asciidoc/repository-projections-dto-limitations.adoc[]
動(dòng)態(tài)投影
到目前為止,我們已經(jīng)使用投影類型作為集合的返回類型或元素類型。然而,你可能想在調(diào)用時(shí)選擇要使用的類型(這使它成為動(dòng)態(tài)的)。為了應(yīng)用動(dòng)態(tài)投影,請(qǐng)使用一個(gè)查詢方法,如下面的例子中所示。
Example 97. 使用動(dòng)態(tài)投影參數(shù)的repository
interface PersonRepository extends Repository<Person, UUID> {<T> Collection<T> findByLastname(String lastname, Class<T> type);
}
這樣,該方法可用于獲得原樣或應(yīng)用投影的聚合體(aggregate),如以下例子所示。
Example 98. 使用具有動(dòng)態(tài)投影的repository
void someMethod(PersonRepository people) {Collection<Person> aggregates =people.findByLastname("Matthews", Person.class);Collection<NamesOnly> aggregates =people.findByLastname("Matthews", NamesOnly.class);
}
5.1.4. 存儲(chǔ)過(guò)程
JPA 2.1規(guī)范引入了對(duì)通過(guò)使用JPA標(biāo)準(zhǔn)查詢API調(diào)用存儲(chǔ)過(guò)程的支持。我們引入了 @Procedure
注解,用于在 repository 方法上聲明存儲(chǔ)過(guò)程元數(shù)據(jù)。
下面的例子使用以下存儲(chǔ)過(guò)程。
Example 99. HSQL DB中 plus1inout
過(guò)程的定義。
/;
DROP procedure IF EXISTS plus1inout
/;
CREATE procedure plus1inout (IN arg int, OUT res int)
BEGIN ATOMICset res = arg + 1;
END
/;
存儲(chǔ)過(guò)程的元數(shù)據(jù)可以通過(guò)在實(shí)體類型上使用 NamedStoredProcedureQuery
注解來(lái)配置。
Example 100. 一個(gè)實(shí)體上的 StoredProcedure 元數(shù)據(jù)定義。
@Entity
@NamedStoredProcedureQuery(name = "User.plus1", procedureName = "plus1inout", parameters = {@StoredProcedureParameter(mode = ParameterMode.IN, name = "arg", type = Integer.class),@StoredProcedureParameter(mode = ParameterMode.OUT, name = "res", type = Integer.class) })
public class User {}
請(qǐng)注意,@NamedStoredProcedureQuery
為存儲(chǔ)過(guò)程提供了兩個(gè)不同的名稱。name
是JPA使用的名稱。 procedureName
是存儲(chǔ)過(guò)程在數(shù)據(jù)庫(kù)中的名稱。
你可以通過(guò)多種方式從 repository 方法中引用存儲(chǔ)過(guò)程。要調(diào)用的存儲(chǔ)過(guò)程可以通過(guò)使用 @Procedure
注解的 value
或 procedureName
屬性直接定義。這直接指的是數(shù)據(jù)庫(kù)中的存儲(chǔ)過(guò)程,而忽略了通過(guò) @NamedStoredProcedureQuery
的任何配置。
或者,你可以指定 @NamedStoredProcedureQuery.name
屬性作為 @Procedure.name
屬性。如果既沒有配置 value
、procedureName
,也沒有配置 name
,那么 repository 方法的名稱將被用作 name
屬性。
下面的例子顯示了如何引用一個(gè)明確映射的存儲(chǔ)過(guò)程。
Example 101. 引用數(shù)據(jù)庫(kù)中名稱為 “plus1inout” 的存儲(chǔ)過(guò)程。
@Procedure("plus1inout")
Integer explicitlyNamedPlus1inout(Integer arg);
下面的例子與前面的例子相當(dāng),但使用了 procedureName
的別名。
通過(guò) procedureName
別名引用數(shù)據(jù)庫(kù)中名稱為 “plus1inout” 的隱式存儲(chǔ)過(guò)程。
@Procedure(procedureName = "plus1inout")
Integer callPlus1InOut(Integer arg);
下面的內(nèi)容同樣等同于前兩個(gè),但使用了方法名而不是一個(gè)明確的注解屬性。
Example 102. 在 EntityManager
中通過(guò)使用方法名稱引用隱式映射的命名存儲(chǔ)過(guò)程 “User.plus1”。
@Procedure
Integer plus1inout(@Param("arg") Integer arg);
下面的例子顯示了如何通過(guò)引用 @NamedStoredProcedureQuery.name
屬性來(lái)引用一個(gè)存儲(chǔ)過(guò)程。
Example 103. 在 EntityManager
中引用了明確映射的命名存儲(chǔ)過(guò)程 “User.plus1IO”。
@Procedure(name = "User.plus1IO")
Integer entityAnnotatedCustomNamedProcedurePlus1IO(@Param("arg") Integer arg);
如果被調(diào)用的存儲(chǔ)過(guò)程有一個(gè)單一的輸出參數(shù),該參數(shù)可以作為方法的返回值返回。如果在 @NamedStoredProcedureQuery
注解中指定了多個(gè)輸出參數(shù),這些參數(shù)可以作為 Map
返回,其關(guān)鍵是 @NamedStoredProcedureQuery
注解中給出的參數(shù)名稱。
5.1.5. Specification
JPA 2引入了一個(gè) criteria API,你可以用它來(lái)以編程方式建立查詢。通過(guò)編寫一個(gè) criteria
,你定義了一個(gè) domain 類的查詢的 where 子句。再退一步說(shuō),這些 criteria 可以被看作是對(duì) JPA criteria API約束所描述的實(shí)體的謂詞。
Spring Data JPA從Eric Evans的《領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)》一書中提取了 specification 的概念,遵循相同的語(yǔ)義,并提供了一個(gè)API來(lái)用 JPA criteria API 定義這種 specification。為了支持 specification,你可以用 JpaSpecificationExecutor
接口擴(kuò)展你的 repository 接口,如下所示。
public interface CustomerRepository extends CrudRepository<Customer, Long>, JpaSpecificationExecutor<Customer> {…
}
附加接口有一些方法,讓你以各種方式運(yùn)行 specification。例如,findAll
方法返回所有符合 specification 的實(shí)體,如下面的例子中所示。
List<T> findAll(Specification<T> spec);
Specification
接口的定義如下。
public interface Specification<T> {Predicate toPredicate(Root<T> root, CriteriaQuery<?> query,CriteriaBuilder builder);
}
Specification 可以很容易地用于在實(shí)體之上建立一個(gè)可擴(kuò)展的謂語(yǔ)(predicate)集,然后可以與 JpaRepository
組合使用,而不需要為每一個(gè)需要的組合聲明一個(gè)查詢(方法),如以下例子所示。
Example 104. Customer 的 Specification
public class CustomerSpecs {public static Specification<Customer> isLongTermCustomer() {return (root, query, builder) -> {LocalDate date = LocalDate.now().minusYears(2);return builder.lessThan(root.get(Customer_.createdAt), date);};}public static Specification<Customer> hasSalesOfMoreThan(MonetaryAmount value) {return (root, query, builder) -> {// build query here};}
}
Customer_
類型是使用JPA元模型生成器生成的元模型類型(見 Hibernate實(shí)現(xiàn)文檔中的例子)。所以表達(dá)式,Customer_.createdAt
,假設(shè) Customer
有一個(gè) Date
類型的 createdAt
屬性。除此之外,我們?cè)跇I(yè)務(wù)需求的抽象層次上表達(dá)了一些 criteria,并創(chuàng)建了可執(zhí)行的 Specifications
。所以客戶端可能會(huì)使用如下的 Specification
。
Example 105. 使用一個(gè)簡(jiǎn)單的 Specification
List<Customer> customers = customerRepository.findAll(isLongTermCustomer());
為什么不為這種數(shù)據(jù)訪問(wèn)創(chuàng)建一個(gè)查詢?與普通的查詢聲明相比,使用單一的 Specification
并不能獲得很多好處。當(dāng)你把它們組合起來(lái)創(chuàng)建新的 Specification
對(duì)象時(shí),Specification
的力量才真正發(fā)揮出來(lái)。你可以通過(guò)我們提供的 Specification
的默認(rèn)方法來(lái)實(shí)現(xiàn)這一點(diǎn),以建立類似于下面的表達(dá)式。
Example 106. 組合 Specification
MonetaryAmount amount = new MonetaryAmount(200.0, Currencies.DOLLAR);
List<Customer> customers = customerRepository.findAll(isLongTermCustomer().or(hasSalesOfMoreThan(amount)));
Specification
提供了一些 “膠水代碼” 的默認(rèn)方法來(lái)連鎖和組合 Specification
實(shí)例。這些方法讓你通過(guò)創(chuàng)建新的 Specification
實(shí)現(xiàn)并將它們與已有的實(shí)現(xiàn)結(jié)合起來(lái)來(lái)擴(kuò)展你的數(shù)據(jù)訪問(wèn)層。
而在JPA 2.1中,CriteriaBuilder
API引入了 CriteriaDelete
。這是由 JpaSpecificationExecutor
的 delete(Specification)
API提供的。
Example 107. 使用 Specification
來(lái)刪除條目。
Specification<User> ageLessThan18 = (root, query, cb) -> cb.lessThan(root.get("age").as(Integer.class), 18)userRepository.delete(ageLessThan18);
該 Specification
建立了一個(gè) criteria,其中 age
字段(強(qiáng)制轉(zhuǎn)換為integer)小于 18
。傳遞給 userRepository
,它將使用 JPA 的 CriteriaDelete
功能來(lái)生成正確的 DELETE
操作。然后它返回被刪除的實(shí)體的數(shù)量。
5.1.6. Example 查詢
介紹
本章介紹了 “Example 查詢” 并解釋了如何使用它。
Example 查詢(QBE)是一種用戶友好的查詢技術(shù),接口簡(jiǎn)單。它允許動(dòng)態(tài)查詢創(chuàng)建,不要求你寫包含字段名的查詢。事實(shí)上,“Example 查詢” 根本不要求你通過(guò)使用store特定的查詢語(yǔ)言來(lái)編寫查詢。
使用方式
Example 查詢API由四部分組成。
Probe
: 帶有填充字段的domain對(duì)象的實(shí)際例子。ExampleMatcher
承載了如何匹配特定字段的細(xì)節(jié)。它可以在多個(gè)實(shí)例中重復(fù)使用。Example
: 一個(gè)Example
由 probe 和ExampleMatcher
組成。它被用來(lái)創(chuàng)建查詢。FetchableFluentQuery
:FetchableFluentQuery
提供了一個(gè) fluent API,它允許進(jìn)一步定制從Example
衍生的查詢。使用fluent API,你可以為你的查詢指定排序投影和結(jié)果處理。
Example 查詢很適合幾種使用情況。
- 用一組靜態(tài)或動(dòng)態(tài)約束來(lái)查詢你的data store。
- 頻繁地重構(gòu)domain對(duì)象而不用擔(dān)心破壞現(xiàn)有的查詢。
- 獨(dú)立于底層 data store API 工作。
Example 查詢也有一些限制。
- 不支持嵌套或分組的屬性約束,如
firstname = ?0 or (firstname = ?1 and lastname = ?2)
.。 - 對(duì)于字符串只支持 開始/包含/結(jié)束/regex 匹配,對(duì)于其他屬性類型支持精確匹配。
在開始使用Example 查詢之前,你需要有一個(gè)domain對(duì)象。為了開始,為你的 repository 創(chuàng)建一個(gè)接口,如下面的例子所示。
Example 108. Sample Person object
public class Person {@Idprivate String id;private String firstname;private String lastname;private Address address;// … getters and setters omitted
}
前面的例子顯示了一個(gè)簡(jiǎn)單的domain對(duì)象。你可以用它來(lái)創(chuàng)建一個(gè) Example
。默認(rèn)情況下,具有 null
值的字段會(huì)被忽略,而字符串則通過(guò)使用store特定的默認(rèn)值進(jìn)行匹配。
例子可以通過(guò)使用工廠方法或通過(guò)使用 ExampleMatcher
來(lái)構(gòu)建。Example
是不可改變的。下面的列表顯示了一個(gè)簡(jiǎn)單的例子。
Example 109. Simple Example
Person person = new Person();
person.setFirstname("Dave"); Example<Person> example = Example.of(person);
你可以通過(guò)使用 repository 來(lái)運(yùn)行示例查詢。要做到這一點(diǎn),讓你的 repository 接口擴(kuò)展 QueryByExampleExecutor
。下面的列表顯示了 QueryByExampleExecutor
接口的一個(gè)節(jié)選。
Example 110. QueryByExampleExecutor
public interface QueryByExampleExecutor<T> {<S extends T> S findOne(Example<S> example);<S extends T> Iterable<S> findAll(Example<S> example);// … more functionality omitted.
}
Example Matcher
示例不限于默認(rèn)設(shè)置。你可以通過(guò)使用 ExampleMatcher
為字符串匹配、null處理和特定屬性設(shè)置指定你自己的默認(rèn)值,如下面的例子所示。
Example 111. Example matcher with customized matching
Person person = new Person();
person.setFirstname("Dave"); ExampleMatcher matcher = ExampleMatcher.matching() .withIgnorePaths("lastname") .withIncludeNullValues() .withStringMatcher(StringMatcher.ENDING); Example<Person> example = Example.of(person, matcher);
默認(rèn)情況下,ExampleMatcher
希望 probe 上設(shè)置的所有值都能匹配。如果你想得到與任何隱式定義的謂詞(predicate)相匹配的結(jié)果,請(qǐng)使用 ExampleMatcher.matchingAny()
。
你可以為單個(gè)屬性(如 “firstname” 和 “l(fā)astname”,或者對(duì)于嵌套屬性,“address.city”)指定行為。你可以用匹配選項(xiàng)和大小寫敏感性來(lái)調(diào)整它,如下面的例子所示。
Example 112. 配置 matcher 屬性
ExampleMatcher matcher = ExampleMatcher.matching().withMatcher("firstname", endsWith()).withMatcher("lastname", startsWith().ignoreCase());
}
配置 matcher 選項(xiàng)的另一種方法是使用 lambda
(在Java 8中引入)。這種方法創(chuàng)建一個(gè)回調(diào),要求實(shí)現(xiàn)者修改matcher。你不需要返回matcher,因?yàn)榕渲眠x項(xiàng)被保存在matcher實(shí)例中。下面的例子顯示了一個(gè)使用lambda的matcher。
Example 113. 使用 lambda 配置 matcher option
ExampleMatcher matcher = ExampleMatcher.matching().withMatcher("firstname", match -> match.endsWith()).withMatcher("firstname", match -> match.startsWith());
}
Example
創(chuàng)建的查詢使用的是配置的合并視圖。默認(rèn)的匹配設(shè)置可以在 ExampleMatcher
層面上設(shè)置,而個(gè)別設(shè)置可以應(yīng)用于特定的屬性路徑。在 ExampleMatcher
上設(shè)置的設(shè)置會(huì)被屬性路徑設(shè)置所繼承,除非它們被明確定義。屬性補(bǔ)丁(patch)上的設(shè)置比默認(rèn)設(shè)置有更高的優(yōu)先權(quán)。下表描述了各種 ExampleMatcher
設(shè)置的范圍。
Setting | Scope |
---|---|
Null-handling | ExampleMatcher |
String matching | ExampleMatcher and property path |
Ignoring properties | Property path |
Case sensitivity | ExampleMatcher and property path |
Value transformation | Property path |
Fluent API
QueryByExampleExecutor
還提供了一個(gè)方法,我們到目前為止還沒有提到。 R findBy(Example example, Function, R> queryFunction)
。和其他方法一樣,它執(zhí)行一個(gè)從 Example
派生的查詢。然而,通過(guò)第二個(gè)參數(shù),你可以控制該執(zhí)行的各個(gè)方面,否則你無(wú)法動(dòng)態(tài)地控制。你可以通過(guò)調(diào)用第二個(gè)參數(shù)中的 FetchableFluentQuery
的各種方法來(lái)做到這一點(diǎn)。 sortBy
讓你為你的結(jié)果指定一個(gè)排序。as
讓你指定你希望結(jié)果被轉(zhuǎn)換的類型。project
限制了被查詢的屬性。first
, firstValue
, one
, oneValue
, all
, page
, stream
, count
, 和 exists
定義了你得到什么樣的結(jié)果以及當(dāng)超過(guò)預(yù)期結(jié)果數(shù)量時(shí)查詢的行為方式。
Example 114. 使fluent API來(lái)獲得可能是許多結(jié)果中的最后一個(gè),按lastname排序。
Optional<Person> match = repository.findBy(example,q -> q.sortBy(Sort.by("lastname").descending()).first()
);
運(yùn)行 Example
在Spring Data JPA中,你可以使用 Repository 的Query by Example,如以下例子所示。
Example 115. 使用 Repository 的 Example 查詢
public interface PersonRepository extends JpaRepository<Person, String> { … }public class PersonService {@Autowired PersonRepository personRepository;public List<Person> findPeople(Person probe) {return personRepository.findAll(Example.of(probe));}
}
屬性指定器(property specifier)接受屬性名稱(如 firstname
和 lastname
)。你可以通過(guò)將屬性用點(diǎn)連接起來(lái)進(jìn)行導(dǎo)航(address.city
)。你還可以用匹配選項(xiàng)和大小寫敏感性來(lái)調(diào)整它。
下表顯示了你可以使用的各種 StringMatcher
選項(xiàng)以及在一個(gè)名為 firstname
的字段上使用這些選項(xiàng)的結(jié)果。
Matching | Logical result |
---|---|
DEFAULT (區(qū)分大小寫) | firstname = ?0 |
DEFAULT (不區(qū)分大小寫) | LOWER(firstname) = LOWER(?0) |
EXACT (區(qū)分大小寫) | firstname = ?0 |
EXACT (不區(qū)分大小寫) | LOWER(firstname) = LOWER(?0) |
STARTING (區(qū)分大小寫) | firstname like ?0 + '%' |
STARTING (不區(qū)分大小寫) | LOWER(firstname) like LOWER(?0) + '%' |
ENDING (區(qū)分大小寫) | firstname like '%' + ?0 |
ENDING (不區(qū)分大小寫) | LOWER(firstname) like '%' + LOWER(?0) |
CONTAINING (區(qū)分大小寫) | firstname like '%' + ?0 + '%' |
CONTAINING (不區(qū)分大小寫) | LOWER(firstname) like '%' + LOWER(?0) + '%' |
5.1.7. 事務(wù)
默認(rèn)情況下,從 SimpleJpaRepository
繼承的 repository 實(shí)例上的 CRUD 方法是事務(wù)性的。對(duì)于讀操作,事務(wù)配置的 readOnly
標(biāo)志被設(shè)置為 true
。所有其他的都被配置為普通的 @Transactional
,所以默認(rèn)的事務(wù)配置也適用。由事務(wù)性 repository 片段支持的 repository 方法繼承了實(shí)際片段方法的事務(wù)性屬性。
如果你需要調(diào)整 repository 中聲明的某個(gè)方法的事務(wù)配置,可以在你的 repository 接口中重新聲明該方法,如下所示。
Example 116. 用于CRUD的自定義事務(wù)配置
public interface UserRepository extends CrudRepository<User, Long> {@Override@Transactional(timeout = 10)public List<User> findAll();// Further query method declarations
}
這樣做會(huì)導(dǎo)致 findAll()
方法以10秒的超時(shí)運(yùn)行,并且沒有 readOnly
標(biāo)志。
另一種改變事務(wù)行為的方式是使用一個(gè) facade 或 service 實(shí)現(xiàn),(通常)涵蓋一個(gè)以上的 repository。它的目的是為非 CRUD 操作定義事務(wù)性的邊界。下面的例子展示了如何為一個(gè)以上的 repository 使用這樣的 facade。
Example 117. 使用facade來(lái)定義多個(gè)repository調(diào)用的事務(wù)
@Service
public class UserManagementImpl implements UserManagement {private final UserRepository userRepository;private final RoleRepository roleRepository;public UserManagementImpl(UserRepository userRepository,RoleRepository roleRepository) {this.userRepository = userRepository;this.roleRepository = roleRepository;}@Transactionalpublic void addRoleToAllUsers(String roleName) {Role role = roleRepository.findByName(roleName);for (User user : userRepository.findAll()) {user.addRole(role);userRepository.save(user);}}
}
這個(gè)例子導(dǎo)致對(duì) addRoleToAllUsers(…)
的調(diào)用在一個(gè)事務(wù)中運(yùn)行(參與到一個(gè)現(xiàn)有的事務(wù)中,或者在沒有事務(wù)的情況下創(chuàng)建一個(gè)新事務(wù))。repository 的事務(wù)配置就被忽略了,因?yàn)橥獠康氖聞?wù)配置決定了實(shí)際使用的配置。請(qǐng)注意,你必須激活 `` 或明確使用 @EnableTransactionManagement
,才能讓基于注解的外設(shè)配置發(fā)揮作用。這個(gè)例子假設(shè)你使用組件掃描。
請(qǐng)注意,從JPA的角度來(lái)看,對(duì) save
的調(diào)用并不是嚴(yán)格必要的,但為了與Spring Data提供的 repository 抽象保持一致,還是應(yīng)該存在的。
事務(wù)性的查詢方法
要讓你的查詢方法是事務(wù)性的,在你定義的 repository 接口處使用 @Transactional
,如下例所示。
Example 118. 在查詢方法中使用 @Transactional
@Transactional(readOnly = true)
interface UserRepository extends JpaRepository<User, Long> {List<User> findByLastname(String lastname);@Modifying@Transactional@Query("delete from User u where u.active = false")void deleteInactiveUsers();
}
通常情況下,你希望 readOnly
標(biāo)志被設(shè)置為 true
,因?yàn)榇蠖鄶?shù)的查詢方法只讀數(shù)據(jù)。與此相反, deleteInactiveUsers()
使用了 @Modifying
注解并覆蓋了事務(wù)配置。因此,該方法在運(yùn)行時(shí),readOnly
標(biāo)志被設(shè)置為 false
。
5.1.8. 鎖
為了指定要使用的鎖模式,你可以在查詢方法上使用 @Lock
注解,如下例所示。
Example 119. 在查詢方法上定義鎖的元數(shù)據(jù)
interface UserRepository extends Repository<User, Long> {// Plain query method@Lock(LockModeType.READ)List<User> findByLastname(String lastname);
}
這個(gè)方法聲明導(dǎo)致被觸發(fā)的查詢配備了 READ
的 LockModeType
。你也可以通過(guò)在你的 repository 接口中重新聲明 CRUD 方法并添加 @Lock
注解來(lái)定義鎖,如下面例子所示。
Example 120. 在CRUD方法上定義鎖元數(shù)據(jù)
interface UserRepository extends Repository<User, Long> {// Redeclaration of a CRUD method@Lock(LockModeType.READ)List<User> findAll();
}
5.1.9. 審計(jì)
基礎(chǔ)
Spring Data提供了復(fù)雜的支持,可以透明地跟蹤誰(shuí)創(chuàng)建或更改了實(shí)體以及更改發(fā)生的時(shí)間。為了從該功能中獲益,你必須為你的實(shí)體類配備審計(jì)元數(shù)據(jù),這些元數(shù)據(jù)可以使用注解或?qū)崿F(xiàn)接口來(lái)定義。此外,審計(jì)必須通過(guò)注解配置或XML配置來(lái)啟用,以注冊(cè)所需的基礎(chǔ)設(shè)施組件。關(guān)于配置樣本,請(qǐng)參考特定store部分。
基于注解的審計(jì)元數(shù)據(jù)
我們提供 @CreatedBy
和 @LastModifiedBy
來(lái)捕獲創(chuàng)建或修改實(shí)體的用戶,以及 @CreatedDate
和 @LastModifiedDate
來(lái)捕獲變化發(fā)生的時(shí)間。
Example 121. An audited entity
class Customer {@CreatedByprivate User user;@CreatedDateprivate Instant createdDate;// … further properties omitted
}
正如你所看到的,注解可以有選擇地應(yīng)用,這取決于你想捕獲哪些信息。這些注解,表示捕捉變化的時(shí)間,可以用在JDK8 date和time類型、long
、Long
以及傳統(tǒng)的Java Date
和 Calendar
的屬性上。
審計(jì)元數(shù)據(jù)不一定需要存在于根級(jí)實(shí)體中,但可以添加到一個(gè)嵌入式實(shí)體中(取決于實(shí)際使用的store),如下面的片段所示。
Example 122. 嵌入實(shí)體中的審計(jì)元數(shù)據(jù)
class Customer {private AuditMetadata auditingMetadata;// … further properties omitted
}class AuditMetadata {@CreatedByprivate User user;@CreatedDateprivate Instant createdDate;}
基于接口的審計(jì)元數(shù)據(jù)
如果你不想使用注解來(lái)定義審計(jì)元數(shù)據(jù),你可以讓你的domain類實(shí)現(xiàn) Auditable
接口。它為所有的審計(jì)屬性暴露了 setter
方法。
AuditorAware
如果你使用 @CreatedBy
或 @LastModifiedBy
,審計(jì)基礎(chǔ)設(shè)施需要以某種方式知道當(dāng)前的principal。為此,我們提供了一個(gè) AuditorAware
SPI接口,你必須實(shí)現(xiàn)這個(gè)接口來(lái)告訴基礎(chǔ)設(shè)施誰(shuí)是與應(yīng)用程序交互的當(dāng)前用戶或系統(tǒng)。泛型 T
定義了用 @CreatedBy
或 @LastModifiedBy
注解的屬性必須是什么類型。
下面的例子顯示了一個(gè)使用Spring Security的 Authentication
對(duì)象的接口實(shí)現(xiàn)。
Example 123. 基于 Spring Security 的 AuditorAware
的實(shí)現(xiàn)
class SpringSecurityAuditorAware implements AuditorAware<User> {@Overridepublic Optional<User> getCurrentAuditor() {return Optional.ofNullable(SecurityContextHolder.getContext()).map(SecurityContext::getAuthentication).filter(Authentication::isAuthenticated).map(Authentication::getPrincipal).map(User.class::cast);}
}
該實(shí)現(xiàn)訪問(wèn)由Spring Security提供的 Authentication
對(duì)象,并查找你在 UserDetailsService
實(shí)現(xiàn)中創(chuàng)建的自定義 UserDetails
實(shí)例。我們?cè)谶@里假設(shè)你是通過(guò) UserDetails
實(shí)現(xiàn)來(lái)暴露domain用戶的,但根據(jù)找到的 Authentication
,你也可以從任何地方查到它。
ReactiveAuditorAware
當(dāng)使用響應(yīng)式基礎(chǔ)設(shè)施時(shí),你可能想利用上下文(Context)信息來(lái)提供 @CreatedBy
或 @LastModifiedBy
信息。我們提供了一個(gè) ReactiveAuditorAware
SPI接口,你必須實(shí)現(xiàn)這個(gè)接口來(lái)告訴基礎(chǔ)設(shè)施誰(shuí)是當(dāng)前與應(yīng)用程序交互的用戶或系統(tǒng)。泛型 T
定義了用 @CreatedBy
或 @LastModifiedBy
注釋的屬性必須是什么類型。
下面的例子顯示了一個(gè)接口的實(shí)現(xiàn),它使用了Spring Security的 Authentication
對(duì)象。
Example 124. 基于 Spring Security 的 ReactiveAuditorAware
的實(shí)現(xiàn)
class SpringSecurityAuditorAware implements ReactiveAuditorAware<User> {@Overridepublic Mono<User> getCurrentAuditor() {return ReactiveSecurityContextHolder.getContext().map(SecurityContext::getAuthentication).filter(Authentication::isAuthenticated).map(Authentication::getPrincipal).map(User.class::cast);}
}
該實(shí)現(xiàn)訪問(wèn)由Spring Security提供的 Authentication
對(duì)象,并查找你在 UserDetailsService
實(shí)現(xiàn)中創(chuàng)建的自定義 UserDetails
實(shí)例。我們?cè)谶@里假設(shè)你是通過(guò) UserDetails
實(shí)現(xiàn)來(lái)暴露domain用戶的,但根據(jù)找到的 Authentication
,你也可以從任何地方查到它。
還有一個(gè)方便的基類,AbstractAuditable
,你可以擴(kuò)展它以避免手動(dòng)實(shí)現(xiàn)接口方法。這樣做會(huì)增加你的domain類與Spring Data的耦合度,這可能是你想避免的事情。通常情況下,基于注解的定義審計(jì)元數(shù)據(jù)的方式更受歡迎,因?yàn)樗那秩胄愿?#xff0c;也更靈活。
5.1.10. JPA 審計(jì)
一般的審計(jì)配置
Spring Data JPA 提供了一個(gè)實(shí)體監(jiān)聽器(entity listener),可以用來(lái)觸發(fā)審計(jì)信息的捕獲。首先,你必須注冊(cè) AuditingEntityListener
,以便在你的 orm.xml
文件中為持久化上下文中的所有實(shí)體使用,如下面的例子所示。
Example 125. 審計(jì)配置 orm.xml
<persistence-unit-metadata><persistence-unit-defaults><entity-listeners><entity-listener class="….data.jpa.domain.support.AuditingEntityListener" /></entity-listeners></persistence-unit-defaults>
</persistence-unit-metadata>
你也可以通過(guò)使用 @EntityListeners
注解在每個(gè)實(shí)體的基礎(chǔ)上啟用 AuditingEntityListener
,如下所示。
@Entity
@EntityListeners(AuditingEntityListener.class)
public class MyEntity {}
在對(duì) orm.xml
進(jìn)行適當(dāng)修改,并在classpath上添加 spring-aspects.jar
后,激活審計(jì)功能只需在配置中添加Spring Data JPA auditing
命名空間元素,如下所示。
Example 126. 使用XML配置激活審計(jì)
<jpa:auditing auditor-aware-ref="yourAuditorAwareBean" />
從Spring Data JPA 1.5開始,你可以通過(guò)給配置類加上 @EnableJpaAuditing
注解來(lái)啟用審計(jì)。你仍然必須修改 orm.xml
文件,并且在 classpath 上有 spring-aspects.jar
。下面的例子展示了如何使用 @EnableJpaAuditing
注解。
Example 127. 用Java配置激活審計(jì)
@Configuration
@EnableJpaAuditing
class Config {@Beanpublic AuditorAware<AuditableUser> auditorProvider() {return new AuditorAwareImpl();}
}
如果你將 AuditorAware
類型的Bean暴露給 ApplicationContext
,審計(jì)基礎(chǔ)設(shè)施會(huì)自動(dòng)拾取它并使用它來(lái)確定要在 domain 類型上設(shè)置的當(dāng)前用戶。如果你在 ApplicationContext
中注冊(cè)了多個(gè)實(shí)現(xiàn),你可以通過(guò)明確設(shè)置 @EnableJpaAuditing
的 auditorAwareRef
屬性來(lái)選擇要使用的一個(gè)。
5.2. 其他考慮因素
5.2.1. 在自定義實(shí)現(xiàn)中使用 JpaContext
當(dāng)使用多個(gè) EntityManager
實(shí)例和 自定義 repository 實(shí)現(xiàn) 時(shí),你需要將正確的 EntityManager
接入 repository 實(shí)現(xiàn)類。你可以通過(guò)在 @PersistenceContext
注解中明確命名 EntityManager
來(lái)做到這一點(diǎn),如果 EntityManager
是 @Autowired
,則可以使用 @Qualifier
。
從Spring Data JPA 1.9開始,Spring Data JPA包括一個(gè)名為 JpaContext
的類,它可以讓你通過(guò)管理domain類獲得 EntityManager
,假設(shè)它只由應(yīng)用程序中的一個(gè) EntityManager
實(shí)例管理。下面的例子展示了如何在一個(gè)自定義的 repository 中使用 JpaContext
。
Example 128. 在自定義存儲(chǔ)庫(kù)的實(shí)現(xiàn)中使用 `JpaContext’。
class UserRepositoryImpl implements UserRepositoryCustom {private final EntityManager em;@Autowiredpublic UserRepositoryImpl(JpaContext context) {this.em = context.getEntityManagerByManagedType(User.class);}…
}
這種方法的優(yōu)點(diǎn)是,如果 domain 類型被分配給不同的持久化單元(persistence unit),repository 不必被觸動(dòng)以改變對(duì)持久化單元的引用。
5.2.2. 合并持久化單元(persistence unit)
Spring支持擁有多個(gè)持久化單元。然而,有時(shí)你可能想將你的應(yīng)用程序模塊化,但仍要確保所有這些模塊在一個(gè)持久化單元內(nèi)運(yùn)行。為了實(shí)現(xiàn)這種行為,Spring Data JPA提供了一個(gè) PersistenceUnitManager
實(shí)現(xiàn),它可以根據(jù)名字自動(dòng)合并持久化單元,如下面的例子所示。
Example 129. 使用 MergingPersistenceUnitmanager
<bean class="….LocalContainerEntityManagerFactoryBean"><property name="persistenceUnitManager"><bean class="….MergingPersistenceUnitManager" /></property>
</bean>
對(duì) @Entity 類和JPA映射文件進(jìn)行類路徑掃描
一個(gè)普通的JPA設(shè)置要求所有注解映射的實(shí)體類被列在 orm.xml
中。這同樣適用于XML映射文件。Spring Data JPA提供了一個(gè) ClasspathScanningPersistenceUnitPostProcessor
,它獲得了一個(gè)配置好的 base package,并可選擇接受一個(gè)映射文件名模式。然后,它在給定的包中掃描帶有 @Entity
或 @MappedSuperclass
注解的類,加載符合文件名模式的配置文件,并將它們交給JPA配置。后處理程序必須按以下方式配置。
Example 130. 使用 ClasspathScanningPersistenceUnitPostProcessor
<bean class="….LocalContainerEntityManagerFactoryBean"><property name="persistenceUnitPostProcessors"><list><bean class="org.springframework.data.jpa.support.ClasspathScanningPersistenceUnitPostProcessor"><constructor-arg value="com.acme.domain" /><property name="mappingFileNamePattern" value="**/*Mapping.xml" /></bean></list></property>
</bean>
5.2.3. CDI 整合
repository 接口的實(shí)例通常由一個(gè)容器來(lái)創(chuàng)建,在使用Spring Data時(shí),Spring是最自然的選擇。Spring為創(chuàng)建Bean實(shí)例提供了復(fù)雜的支持,如 創(chuàng)建 Repository 實(shí)例 中所記載的。從1.1.0版本開始,Spring Data JPA帶有一個(gè)自定義的CDI擴(kuò)展,允許在CDI環(huán)境中使用 repository 抽象。該擴(kuò)展是JAR的一部分。要激活它,請(qǐng)?jiān)谀愕腸lasspath上包含 Spring Data JPA JAR。
現(xiàn)在你可以通過(guò)為 EntityManagerFactory
和 EntityManager
實(shí)現(xiàn)一個(gè) CDI Producer 來(lái)設(shè)置基礎(chǔ)設(shè)施,如下面的例子所示
class EntityManagerFactoryProducer {@Produces@ApplicationScopedpublic EntityManagerFactory createEntityManagerFactory() {return Persistence.createEntityManagerFactory("my-persistence-unit");}public void close(@Disposes EntityManagerFactory entityManagerFactory) {entityManagerFactory.close();}@Produces@RequestScopedpublic EntityManager createEntityManager(EntityManagerFactory entityManagerFactory) {return entityManagerFactory.createEntityManager();}public void close(@Disposes EntityManager entityManager) {entityManager.close();}
}
必要的設(shè)置會(huì)根據(jù) JavaEE 環(huán)境的不同而不同。你可能只需要把一個(gè) EntityManager
重新聲明為CDI Bean,如下所示。
class CdiConfig {@Produces@RequestScoped@PersistenceContextpublic EntityManager entityManager;
}
在前面的例子中,容器必須有能力自己創(chuàng)建JPA EntityManagers
。配置所做的就是把JPA EntityManager
作為CDI Bean重新輸出。
Spring Data JPA CDI擴(kuò)展將所有可用的 EntityManager
實(shí)例作為 CDI Bean,并在容器請(qǐng)求某個(gè) repository 類型的 bean 時(shí)為 Spring Data Repository 創(chuàng)建一個(gè)代理。因此,獲取Spring Data Repository 的實(shí)例只需聲明一個(gè) @Injected
屬性,如下面的例子所示。
class RepositoryClient {@InjectPersonRepository repository;public void businessMethod() {List<Person> people = repository.findAll();}
}
5.3. Spring Data Envers
5.3.1. Spring Data Envers 是什么?
Spring Data Envers 使典型的 Envers 查詢?cè)赟pring Data JPA的 repository 中可用。它與其他Spring Data 模塊的不同之處在于,它總是與另一個(gè) Spring Data 模塊結(jié)合使用:Spring Data JPA。
5.3.2. Envers 是什么?
Envers是一個(gè) Hibernate模塊,為JPA實(shí)體增加了審計(jì)功能。本文檔假設(shè)你熟悉 Envers,就像 Spring Data Envers 依賴于 Envers 的正確配置。
5.3.3. 配置
作為使用Spring Data Envers的起點(diǎn),你需要一個(gè)在 classpath 上有Spring Data JPA的項(xiàng)目和一個(gè)額外的 spring-data-envers
依賴。
<dependencies><!-- other dependency elements omitted --><dependency><groupId>org.springframework.data</groupId><artifactId>spring-data-envers</artifactId><version>3.1.0-SNAPSHOT</version></dependency></dependencies>
這也將 hibernate-envers
作為一個(gè)臨時(shí)(transient)的依賴項(xiàng)帶入項(xiàng)目。
為了啟用Spring Data Envers和Spring Data JPA,我們需要配置兩個(gè)bean和一個(gè)特殊的 repositoryFactoryBeanClass
。
@Configuration
@EnableEnversRepositories
@EnableTransactionManagement
public class EnversDemoConfiguration {@Beanpublic DataSource dataSource() {EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();return builder.setType(EmbeddedDatabaseType.HSQL).build();}@Beanpublic LocalContainerEntityManagerFactoryBean entityManagerFactory() {HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();vendorAdapter.setGenerateDdl(true);LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();factory.setJpaVendorAdapter(vendorAdapter);factory.setPackagesToScan("example.springdata.jpa.envers");factory.setDataSource(dataSource());return factory;}@Beanpublic PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {JpaTransactionManager txManager = new JpaTransactionManager();txManager.setEntityManagerFactory(entityManagerFactory);return txManager;}
}
要實(shí)際使用 Spring Data Envers,要把一個(gè)或多個(gè) Repository 作為擴(kuò)展接口添加到 {spring-data-commons-javadoc-base}/org/springframework/data/repository/history/RevisionRepository.html[RevisionRepository
] 中。
interface PersonRepository {extends CrudRepository<Person, Long>,RevisionRepository<Person, Long, Long>
該 repository 的實(shí)體必須是啟用了 Envers 審計(jì)的實(shí)體(也就是說(shuō),它必須有一個(gè) @Audited
注解)。
@Entity
@Audited
class Person {@Id @GeneratedValueLong id;String name;@Version Long version;
}
5.3.4. 用法
現(xiàn)在你可以使用 RevisionRepository
的方法來(lái)查詢實(shí)體的 revision,如下測(cè)試案例所示。
@ExtendWith(SpringExtension.class)
@Import(EnversDemoConfiguration.class)
class EnversIntegrationTests {final PersonRepository repository;final TransactionTemplate tx;EnversIntegrationTests(@Autowired PersonRepository repository, @Autowired PlatformTransactionManager tm) {this.repository = repository;this.tx = new TransactionTemplate(tm);}@Testvoid testRepository() {Person updated = preparePersonHistory();Revisions<Long, Person> revisions = repository.findRevisions(updated.id);Iterator<Revision<Long, Person>> revisionIterator = revisions.iterator();checkNextRevision(revisionIterator, "John", RevisionType.INSERT);checkNextRevision(revisionIterator, "Jonny", RevisionType.UPDATE);checkNextRevision(revisionIterator, null, RevisionType.DELETE);assertThat(revisionIterator.hasNext()).isFalse();}/*** Checks that the next element in the iterator is a Revision entry referencing a Person* with the given name after whatever change brought that Revision into existence.* <p>* As a side effect the Iterator gets advanced by one element.** @param revisionIterator the iterator to be tested.* @param name the expected name of the Person referenced by the Revision.* @param revisionType the type of the revision denoting if it represents an insert, update or delete.*/private void checkNextRevision(Iterator<Revision<Long, Person>> revisionIterator, String name,RevisionType revisionType) {assertThat(revisionIterator.hasNext()).isTrue();Revision<Long, Person> revision = revisionIterator.next();assertThat(revision.getEntity().name).isEqualTo(name);assertThat(revision.getMetadata().getRevisionType()).isEqualTo(revisionType);}/*** Creates a Person with a couple of changes so it has a non-trivial revision history.* @return the created Person.*/private Person preparePersonHistory() {Person john = new Person();john.setName("John");// createPerson saved = tx.execute(__ -> repository.save(john));assertThat(saved).isNotNull();saved.setName("Jonny");// updatePerson updated = tx.execute(__ -> repository.save(saved));assertThat(updated).isNotNull();// deletetx.executeWithoutResult(__ -> repository.delete(updated));return updated;}
}
5.3.5. 其他資源
你可以在 Spring Data實(shí)例庫(kù) 中下載Spring Data Envers的例子,并進(jìn)行玩耍以了解該庫(kù)的工作原理。
你也應(yīng)該看看 {spring-data-commons-javadoc-base}/org/springframework/data/repository/history/RevisionRepository.html[RevisionRepository
和相關(guān)類的 Javadoc]。
你可以使用 spring-data-envers
標(biāo)簽在 Stackoverflow 提問(wèn)。
Spring Data Envers 的源代碼和 issue tracker 托管在 GitHub。
https://springdoc.cn/docs/)