Spring Data 擴展

本節說明一組 Spring Data 擴展,這些擴展使 Spring Data 能夠在各種情境中使用。目前,大多數整合都針對 Spring MVC。

Querydsl 擴展

Querydsl 是一個框架,它透過其流暢的 API 實現靜態類型 SQL 類型的查詢建構。

許多 Spring Data 模組透過 QuerydslPredicateExecutor 提供與 Querydsl 的整合,如下例所示

QuerydslPredicateExecutor 介面
public interface QuerydslPredicateExecutor<T> {

  Optional<T> findById(Predicate predicate);  (1)

  Iterable<T> findAll(Predicate predicate);   (2)

  long count(Predicate predicate);            (3)

  boolean exists(Predicate predicate);        (4)

  // … more functionality omitted.
}
1 尋找並傳回符合 Predicate 的單一實體。
2 尋找並傳回所有符合 Predicate 的實體。
3 傳回符合 Predicate 的實體數量。
4 傳回是否存在符合 Predicate 的實體。

要使用 Querydsl 支援,請在您的 repository 介面上擴展 QuerydslPredicateExecutor,如下例所示

在 repositories 上整合 Querydsl
interface UserRepository extends CrudRepository<User, Long>, QuerydslPredicateExecutor<User> {
}

前面的範例讓您可以使用 Querydsl Predicate 實例編寫類型安全的查詢,如下例所示

Predicate predicate = user.firstname.equalsIgnoreCase("dave")
	.and(user.lastname.startsWithIgnoreCase("mathews"));

userRepository.findAll(predicate);

Web 支援

支援 repository 程式設計模型的 Spring Data 模組附帶各種 Web 支援。Web 相關組件需要 Spring MVC JAR 位於類別路徑中。它們中的一些甚至提供與 Spring HATEOAS 的整合。一般來說,整合支援透過在您的 JavaConfig 組態類別中使用 `@EnableSpringDataWebSupport` 註解來啟用,如下例所示

啟用 Spring Data Web 支援
  • Java

  • XML

@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
class WebConfiguration {}
<bean class="org.springframework.data.web.config.SpringDataWebConfiguration" />

<!-- If you use Spring HATEOAS, register this one *instead* of the former -->
<bean class="org.springframework.data.web.config.HateoasAwareSpringDataWebConfiguration" />

`@EnableSpringDataWebSupport` 註解註冊了一些組件。我們稍後在本節中討論這些組件。它還偵測類別路徑上的 Spring HATEOAS,並為其註冊整合組件(如果存在)。

基本 Web 支援

在 XML 中啟用 Spring Data Web 支援

前一節中顯示的組態註冊了一些基本組件

  • 使用 DomainClassConverter 類別,讓 Spring MVC 從請求參數或路徑變數解析 repository 管理的網域類別的實例。

  • HandlerMethodArgumentResolver 實作,讓 Spring MVC 從請求參數解析 PageableSort 實例。

  • Jackson 模組,用於反序列化/序列化 PointDistance 等類型,或儲存特定的類型,具體取決於使用的 Spring Data 模組。

使用 DomainClassConverter 類別

`DomainClassConverter` 類別讓您直接在 Spring MVC 控制器方法簽名中使用網域類型,這樣您就不需要透過 repository 手動查找實例,如下例所示

在方法簽名中使用網域類型的 Spring MVC 控制器
@Controller
@RequestMapping("/users")
class UserController {

  @RequestMapping("/{id}")
  String showUserForm(@PathVariable("id") User user, Model model) {

    model.addAttribute("user", user);
    return "userForm";
  }
}

該方法直接接收 User 實例,無需進一步查找。實例可以透過讓 Spring MVC 首先將路徑變數轉換為網域類別的 id 類型,然後最終透過在為網域類型註冊的 repository 實例上呼叫 findById(…)` 來存取實例來解析。

目前,repository 必須實作 CrudRepository 才有資格被發現以進行轉換。

Pageable 和 Sort 的 HandlerMethodArgumentResolvers

前一節中顯示的組態程式碼片段也註冊了 PageableHandlerMethodArgumentResolver 以及 SortHandlerMethodArgumentResolver 的實例。註冊啟用了 PageableSort 作為有效的控制器方法引數,如下例所示

使用 Pageable 作為控制器方法引數
@Controller
@RequestMapping("/users")
class UserController {

  private final UserRepository repository;

  UserController(UserRepository repository) {
    this.repository = repository;
  }

  @RequestMapping
  String showUsers(Model model, Pageable pageable) {

    model.addAttribute("users", repository.findAll(pageable));
    return "users";
  }
}

前面的方法簽名導致 Spring MVC 嘗試透過使用以下預設組態從請求參數中派生 Pageable 實例

表 1. 為 Pageable 實例評估的請求參數

頁碼

您要檢索的頁面。從 0 開始索引,預設為 0。

大小

您要檢索的頁面大小。預設為 20。

排序

應排序的屬性,格式為 `property,property(,ASC|DESC)(,IgnoreCase)`。預設排序方向為區分大小寫的升序。如果要切換方向或大小寫敏感度,請使用多個 `sort` 參數 — 例如,`?sort=firstname&sort=lastname,asc&sort=city,ignorecase`。

要自訂此行為,請註冊一個 bean,該 bean 分別實作 `PageableHandlerMethodArgumentResolverCustomizer` 介面或 `SortHandlerMethodArgumentResolverCustomizer` 介面。將呼叫其 customize() 方法,讓您變更設定,如下例所示

@Bean SortHandlerMethodArgumentResolverCustomizer sortCustomizer() {
    return s -> s.setPropertyDelimiter("<-->");
}

如果設定現有 `MethodArgumentResolver` 的屬性不足以滿足您的目的,請擴展 `SpringDataWebConfiguration` 或啟用 HATEOAS 的等效項,覆寫 `pageableResolver()` 或 `sortResolver()` 方法,並匯入您的自訂組態檔案,而不是使用 `@Enable` 註解。

如果您需要從請求中解析多個 `Pageable` 或 `Sort` 實例(例如,對於多個表格),您可以使用 Spring 的 `@Qualifier` 註解來區分它們。然後,請求參數必須以 `${qualifier}_` 作為前綴。以下範例顯示了結果方法簽名

String showUsers(Model model,
      @Qualifier("thing1") Pageable first,
      @Qualifier("thing2") Pageable second) { … }

您必須填入 `thing1_page`、`thing2_page` 等。

傳遞到方法中的預設 `Pageable` 等同於 `PageRequest.of(0, 20)`,但您可以使用 `Pageable` 參數上的 `@PageableDefault` 註解來自訂它。

Page 建立 JSON 表示

Spring MVC 控制器通常會嘗試最終向客戶端呈現 Spring Data 頁面的表示。雖然可以簡單地從處理程序方法傳回 `Page` 實例,以讓 Jackson 按原樣呈現它們,但我們強烈建議不要這樣做,因為底層實作類別 `PageImpl` 是一個網域類型。這表示我們可能想要或必須出於不相關的原因而變更其 API,而此類變更可能會以破壞性的方式改變產生的 JSON 表示。

在 Spring Data 3.1 中,我們開始透過發出警告日誌來暗示問題。我們仍然最終建議利用與 Spring HATEOAS 的整合,以獲得完全穩定且啟用超媒體的方式來呈現頁面,以便客戶端輕鬆瀏覽它們。但從 Spring Data 3.3 版本開始,提供了一種方便使用但不需要包含 Spring HATEOAS 的頁面呈現機制。

使用 Spring Data 的 PagedModel

在其核心,該支援包含 Spring HATEOAS 的 `PagedModel` 的簡化版本(Spring Data 版本位於 `org.springframework.data.web` 套件中)。它可用於包裝 `Page` 實例,並產生簡化的表示,該表示反映了 Spring HATEOAS 建立的結構,但省略了導航連結。

import org.springframework.data.web.PagedModel;

@Controller
class MyController {

  private final MyRepository repository;

  // Constructor ommitted

  @GetMapping("/page")
  PagedModel<?> page(Pageable pageable) {
    return new PagedModel<>(repository.findAll(pageable)); (1)
  }
}
1 Page 實例包裝到 PagedModel 中。

這將產生如下所示的 JSON 結構

{
  "content" : [
     … // Page content rendered here
  ],
  "page" : {
    "size" : 20,
    "totalElements" : 30,
    "totalPages" : 2,
    "number" : 0
  }
}

請注意文件如何包含一個 `page` 欄位,用於公開必要的分頁元數據。

全域啟用簡化的 Page 呈現

如果您不想變更所有現有的控制器以新增對應步驟來傳回 `PagedModel` 而不是 `Page`,您可以透過如下調整 `@EnableSpringDataWebSupport` 來啟用將 `PageImpl` 實例自動轉換為 `PagedModel`

@EnableSpringDataWebSupport(pageSerializationMode = VIA_DTO)
class MyConfiguration { }

這將允許您的控制器仍然傳回 `Page` 實例,並且它們將自動呈現為簡化的表示

@Controller
class MyController {

  private final MyRepository repository;

  // Constructor ommitted

  @GetMapping("/page")
  Page<?> page(Pageable pageable) {
    return repository.findAll(pageable);
  }
}

PageSlice 的超媒體支援

Spring HATEOAS 附帶一個表示模型類別 (`PagedModel`/`SlicedModel`),它允許使用必要的 `Page`/`Slice` 元數據以及連結來豐富 `Page` 或 `Slice` 實例的內容,以便客戶端輕鬆導航頁面。`Page` 到 `PagedModel` 的轉換是透過 Spring HATEOAS `RepresentationModelAssembler` 介面的實作來完成的,稱為 `PagedResourcesAssembler`。同樣,`Slice` 實例可以使用 `SlicedResourcesAssembler` 轉換為 `SlicedModel`。以下範例顯示如何將 `PagedResourcesAssembler` 用作控制器方法引數,因為 `SlicedResourcesAssembler` 的工作方式完全相同

使用 PagedResourcesAssembler 作為控制器方法引數
@Controller
class PersonController {

  private final PersonRepository repository;

  // Constructor omitted

  @GetMapping("/people")
  HttpEntity<PagedModel<Person>> people(Pageable pageable,
    PagedResourcesAssembler assembler) {

    Page<Person> people = repository.findAll(pageable);
    return ResponseEntity.ok(assembler.toModel(people));
  }
}

啟用組態,如前面的範例所示,允許將 `PagedResourcesAssembler` 用作控制器方法引數。在其上呼叫 `toModel(…)` 具有以下效果

  • `Page` 的內容成為 `PagedModel` 實例的內容。

  • `PagedModel` 物件附加了一個 `PageMetadata` 實例,並且它使用來自 `Page` 和底層 `Pageable` 的資訊進行填充。

  • `PagedModel` 可能會附加 `prev` 和 `next` 連結,具體取決於頁面的狀態。這些連結指向該方法對應的 URI。新增至方法的頁碼參數與 `PageableHandlerMethodArgumentResolver` 的設定相符,以確保稍後可以解析連結。

假設我們在資料庫中有 30 個 `Person` 實例。您現在可以觸發請求 (GET localhost:8080/people) 並查看類似於以下的輸出

{ "links" : [
    { "rel" : "next", "href" : "https://127.0.0.1:8080/persons?page=1&size=20" }
  ],
  "content" : [
     … // 20 Person instances rendered here
  ],
  "page" : {
    "size" : 20,
    "totalElements" : 30,
    "totalPages" : 2,
    "number" : 0
  }
}
此處顯示的 JSON 封裝格式不遵循任何正式指定的結構,並且不保證穩定,我們可能會隨時變更它。強烈建議啟用呈現為超媒體啟用的官方媒體類型,Spring HATEOAS 支援,例如 HAL。這些可以透過使用其 `@EnableHypermediaSupport` 註解來啟用。在 Spring HATEOAS 參考文件中找到更多資訊。

組譯器產生了正確的 URI,並且還拾取了預設組態,以將參數解析為即將到來的請求的 `Pageable`。這表示,如果您變更該組態,連結會自動遵循變更。預設情況下,組譯器指向調用它的控制器方法,但您可以透過傳遞自訂 `Link` 作為基礎來建構分頁連結來自訂它,這會重載 `PagedResourcesAssembler.toModel(…)` 方法。

Spring Data Jackson 模組

核心模組以及某些商店特定的模組附帶一組 Jackson 模組,用於 Spring Data 網域使用的類型,例如 `org.springframework.data.geo.Distance` 和 `org.springframework.data.geo.Point`。
一旦啟用 Web 支援並且 `com.fasterxml.jackson.databind.ObjectMapper` 可用,就會匯入這些模組。

在初始化期間,`SpringDataJacksonModules`(例如 `SpringDataJacksonConfiguration`)會被基礎架構拾取,以便將宣告的 `com.fasterxml.jackson.databind.Module` 提供給 Jackson `ObjectMapper`。

通用基礎架構註冊了以下網域類型的資料繫結 mixins。

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

個別模組可能會提供額外的 `SpringDataJacksonModules`。
有關更多詳細資訊,請參閱商店特定章節。

Web 資料繫結支援

您可以使用 Spring Data 投影(在投影中描述)來繫結傳入的請求酬載,方法是使用 JSONPath 表達式(需要 Jayway JsonPath)或 XPath 表達式(需要 XmlBeam),如下例所示

使用 JSONPath 或 XPath 表達式的 HTTP 酬載繫結
@ProjectedPayload
public interface UserPayload {

  @XBRead("//firstname")
  @JsonPath("$..firstname")
  String getFirstname();

  @XBRead("/lastname")
  @JsonPath({ "$.lastname", "$.user.lastname" })
  String getLastname();
}

您可以使用前面範例中顯示的類型作為 Spring MVC 處理程序方法引數,或透過在 `RestTemplate` 的方法之一上使用 `ParameterizedTypeReference`。前面的方法宣告將嘗試在給定文件中任何位置尋找 `firstname`。`lastname` XML 查找是在傳入文件的頂層執行的。JSON 變體首先嘗試頂層 `lastname`,但也嘗試巢狀在 `user` 子文件中的 `lastname`(如果前者未傳回值)。這樣,可以輕鬆地減輕來源文件結構中的變更,而無需客戶端呼叫公開的方法(通常是基於類別的酬載繫結的缺點)。

投影中描述了巢狀投影。如果該方法傳回複雜的非介面類型,則使用 Jackson `ObjectMapper` 來映射最終值。

對於 Spring MVC,必要的轉換器會在 `@EnableSpringDataWebSupport` 啟用且所需的相依性在類別路徑上可用時自動註冊。對於與 `RestTemplate` 一起使用,請手動註冊 `ProjectingJackson2HttpMessageConverter` (JSON) 或 `XmlBeamHttpMessageConverter`。

有關更多資訊,請參閱標準 Spring Data 範例 repository 中的 Web 投影範例。

Querydsl Web 支援

對於那些具有 QueryDSL 整合的商店,您可以從 `Request` 查詢字串中包含的屬性派生查詢。

考慮以下查詢字串

?firstname=Dave&lastname=Matthews

給定先前範例中的 User 物件,您可以使用 QuerydslPredicateArgumentResolver 將查詢字串解析為以下值,如下所示

QUser.user.firstname.eq("Dave").and(QUser.user.lastname.eq("Matthews"))
當在類別路徑上找到 Querydsl 時,此功能會與 `@EnableSpringDataWebSupport` 一起自動啟用。

將 `@QuerydslPredicate` 新增至方法簽名會提供可立即使用的 Predicate,您可以使用 `QuerydslPredicateExecutor` 來執行它。

類型資訊通常從方法的傳回類型解析。由於該資訊不一定與網域類型相符,因此使用 `QuerydslPredicate` 的 `root` 屬性可能是個好主意。

以下範例顯示如何在方法簽名中使用 `@QuerydslPredicate`

@Controller
class UserController {

  @Autowired UserRepository repository;

  @RequestMapping(value = "/", method = RequestMethod.GET)
  String index(Model model, @QuerydslPredicate(root = User.class) Predicate predicate,    (1)
          Pageable pageable, @RequestParam MultiValueMap<String, String> parameters) {

    model.addAttribute("users", repository.findAll(predicate, pageable));

    return "index";
  }
}
1 將查詢字串引數解析為 User 的相符 Predicate

預設繫結如下

  • 簡單屬性上的 Object 作為 eq

  • 類似集合屬性上的 Object 作為 contains

  • 簡單屬性上的 Collection 作為 in

您可以透過 `@QuerydslPredicate` 的 `bindings` 屬性或透過使用 Java 8 預設方法 並將 `QuerydslBinderCustomizer` 方法新增至 repository 介面來自訂這些繫結,如下所示

interface UserRepository extends CrudRepository<User, String>,
                                 QuerydslPredicateExecutor<User>,                (1)
                                 QuerydslBinderCustomizer<QUser> {               (2)

  @Override
  default void customize(QuerydslBindings bindings, QUser user) {

    bindings.bind(user.username).first((path, value) -> path.contains(value))    (3)
    bindings.bind(String.class)
      .first((StringPath path, String value) -> path.containsIgnoreCase(value)); (4)
    bindings.excluding(user.password);                                           (5)
  }
}
1 `QuerydslPredicateExecutor` 提供對 Predicate 的特定尋找器方法的存取。
2 在 repository 介面上定義的 `QuerydslBinderCustomizer` 會自動拾取並捷徑 `@QuerydslPredicate(bindings=…​)`。
3 定義 `username` 屬性的繫結為簡單的 `contains` 繫結。
4 定義 `String` 屬性的預設繫結為不區分大小寫的 `contains` 比對。
5 從 `Predicate` 解析中排除 `password` 屬性。
您可以在從 repository 或 `@QuerydslPredicate` 套用特定繫結之前,註冊一個持有預設 Querydsl 繫結的 `QuerydslBinderCustomizerDefaults` bean。

Repository Populators

如果您使用 Spring JDBC 模組,您可能熟悉使用 SQL 指令碼填充 DataSource 的支援。repository 層級提供類似的抽象,儘管它不使用 SQL 作為資料定義語言,因為它必須與商店無關。因此,populators 支援 XML(透過 Spring 的 OXM 抽象)和 JSON(透過 Jackson)來定義用於填充 repository 的資料。

假設您有一個名為 data.json 的檔案,其內容如下

在 JSON 中定義的資料
[ { "_class" : "com.acme.Person",
 "firstname" : "Dave",
  "lastname" : "Matthews" },
  { "_class" : "com.acme.Person",
 "firstname" : "Carter",
  "lastname" : "Beauford" } ]

您可以使用 Spring Data Commons 中提供的 repository 命名空間的 populator 元素來填充您的 repository。要將前面的資料填充到您的 PersonRepository,請宣告類似於以下的 populator

宣告 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/beans
    https://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/data/repository
    https://www.springframework.org/schema/data/repository/spring-repository.xsd">

  <repository:jackson2-populator locations="classpath:data.json" />

</beans>

前面的宣告會導致 Jackson ObjectMapper 讀取和反序列化 `data.json` 檔案。

JSON 物件被解組的類型是透過檢查 JSON 文件的 `_class` 屬性來確定的。基礎架構最終選擇適當的 repository 來處理已反序列化的物件。

要改為使用 XML 來定義應該填充 repository 的資料,您可以使用 `unmarshaller-populator` 元素。您可以將其配置為使用 Spring OXM 中可用的 XML marshaller 選項之一。有關詳細資訊,請參閱 Spring 參考文件。以下範例顯示如何使用 JAXB 解組 repository populator

宣告解組 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/beans
    https://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/data/repository
    https://www.springframework.org/schema/data/repository/spring-repository.xsd
    http://www.springframework.org/schema/oxm
    https://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>