Spring Data Moore 有哪些新功能?

工程 | Christoph Strobl | 2019 年 10 月 08 日 | ...

Spring Data Moore 包含 16 個模組和超過 700 個已完成的工單。它包括整個產品組合的許多改進和新功能,並且主要關注三個主題:反應式、Kotlin 和效能。此版本新增了諸如宣告式反應式交易和 Coroutines/Flow 支援等功能,並且 finder 方法的速度最多提高 60%*

讓我們先來看看 Moore 的一些反應式功能。

宣告式反應式交易

Lovelace 版本 引入了對反應式交易的早期支援,其風格類似於閉包,留下了一些改進空間。以下列表顯示了該風格

Lovelace 中的反應式交易 (使用 MongoDB)

public Mono<Process> doSomething(Long id) {

  return template.inTransaction().execute(txTemplate -> {

    return txTemplate.findById(id)
      .flatMap(it -> start(txTemplate, it))
      .flatMap(it -> verify(it))
      .flatMap(it -> finish(txTemplate, it));

  }).next();
}

在前面的程式碼片段中,必須透過在使用交易感知範本的閉包內顯式呼叫 inTransaction() 來啟動交易,並在結尾呼叫 next() 以將傳回的 Flux 轉換為 Mono 以滿足方法簽章,即使 findById(…) 已經只發出單個元素。

顯然,這不是執行反應式交易最直觀的方式。因此,讓我們看看使用宣告式反應式交易支援的相同流程。與 Spring 的 交易支援一樣,您需要一個元件來為您處理交易。對於反應式交易,目前由 MongoDB 和 R2DBC 模組提供 ReactiveTransactionManager。以下列表顯示了這樣一個元件

@EnableTransactionManagement
class Config extends AbstractReactiveMongoConfiguration {

  // …

  @Bean
  ReactiveTransactionManager mgr(ReactiveMongoDatabaseFactory f) {
    return new ReactiveMongoTransactionManager(f);
  }
}

從那裡,您可以使用 @Transactional 註釋方法,並依靠基礎結構來啟動、提交和回滾交易流程,以透過 Reactor Context 處理生命週期。這讓您可以將 Lovelace 中的程式碼轉換為以下列表,從而無需使用範圍範本的閉包和多餘的 FluxMono 轉換

Moore 中的宣告式反應式交易 (使用 MongoDB)

@Transactional
public Mono<Process> doSomething(Long id) {

  return template.findById(id)
    .flatMap(it -> start(template, it))
    .flatMap(it -> verify(it))
    .flatMap(it -> finish(template, it));
}

反應式 Elasticsearch 儲存庫

反應式系列的另一個值得注意的補充可以在社群模組之一中找到,Spring Data Elasticsearch 現在提供基於完全反應式的 Elasticsearch REST 用戶端的反應式範本和儲存庫支援,而該用戶端又基於 Spring 的 WebClient

該用戶端透過公開一個熟悉的 API (接近 *Java High-Level REST Client*),為日常搜尋操作提供一流的支援,並在需要時進行必要的削減。範本和儲存庫 API 的組合讓您可以在需要時無縫轉換到反應式,而不會迷失方向。以下列表顯示了如何配置 Elasticsearch 以使用反應式用戶端

反應式 Elasticsearch

class Config extends AbstractReactiveElasticsearchConfiguration {

  // …

  @Bean
  public ReactiveElasticsearchClient reactiveClient() {
    return ReactiveRestClients.create(localhost());
  }
}

@Autowired
ReactiveElasticsearchTemplate template;

//…

Criteria criteria = new Criteria("topics").contains("spring")
    .and("date").greaterThanEqual(today())

Flux<Conference> result = template.find(new CriteriaQuery(criteria), Conference.class);

反應式 Querydsl

說到在轉換中迷失方向:Querydsl (← plain HTTP / 無 HTTPS) 提供了一種卓越的方式,可以為多個資料儲存定義型別安全的查詢,並且已經支援非反應式資料存取相當長的時間。為了在反應式情境中支援它,我們新增了一個反應式執行層,讓您可以執行由 Predicate 支援的查詢。當新增到儲存庫介面時,ReactiveQuerydslPredicateExecutor 提供所有入口點,如以下範例所示

反應式 Querydsl

interface SampleRepository extends …, ReactiveQuerydslPredicateExecutor<…> {
  // …
}

@Autowired
SampleRepository repository;

// …
Predicate predicate = QCustomer.customer.lastname.eq("Matthews");
Flux<Customer> result = repository.findAll(predicate);

對 Kotlin 協同程式和 MongoDB criteria API DSL 的支援

沿著 Moore 中增強的反應式支援的路線,我們繼續了我們已經從 Lovelace 版本開始的 Kotlin 故事。特別是,我們為 Kotlin 協同程式和 Flows 提供了幾個擴充功能,例如提供諸如 awaitSingle()asFlow() 等方法。以下方法使用 awaitSingle() 方法

Kotlin 協同程式支援

val result = runBlocking {
  operations.query<Person>()
   .matching(query(where("lastname").isEqualTo("Matthews")))
   .awaitSingle()
}

另一個使用 Kotlin 語言特性的重大增強功能是由社群貢獻的,它為 Spring Data MongoDB criteria API 新增了一個型別安全的查詢 DSL。這讓您可以將諸如 query(where("lastname").isEqualTo("Matthews")) 等程式碼轉換為以下表示法

Kotlin 型別安全查詢

val people = operations.query<Person>()
  .matching(query(Person::lastname isEqualTo "Matthews"))
  .all()

效能改進

在製作所有這些新功能的同時,我們還花了一些時間來調查目前實作的潛在瓶頸,並找到了一些可以改進的領域。這包括擺脫 Optional,捕獲許多地方的 lambda 和串流執行,新增快取,以及避免不必要的查閱操作。最後,基準測試顯示,JPA 單一屬性 finder 方法 (例如 findByTitle(…)) 的輸送量提高了近 60%。

這很棒,而且值得花費的時間!但是,我想明確說明的是,所有基準測試都使用乾淨的房間情境,避免任何形式的額外負荷。如果您將它們移到更真實的情境中 (例如,透過將記憶體中的 H2 資料庫替換為實際可投入生產的資料庫),結果看起來會大不相同,因為效能瓶頸會轉移到網路互動、查詢執行和結果傳輸。改進仍然可見,但通常會降至個位數百分比。基準測試可以在這個 GitHub 儲存庫 中找到。

新的實體回呼 API

我們還改進了現有的掛鉤,透過從目前基於 ApplicationEvent 的方法轉移到更直接的互動模型,來攔截實體在持續性操作期間的生命週期。EntityCallback API 引入了對不可變型別的更好支援,提供了執行時間保證,並且還無縫整合到反應式流程中。當然,我們仍然支援並發布 ApplicationEvents,但我們強烈建議在應該對已處理的實體進行變更時切換到 EntityCallbacks

在以下範例中,BeforeConvertCallback 透過使用將 id 指派給實體副本的 wither 方法來修改給定的不可變實體,然後傳回該副本,並在下一步中將其轉換為儲存區特定的表示法

EntityCallback API

@Bean
BeforeConvertCallback<Person> beforeConvert() {

  return (entity, collection) -> {
    return entity.withId(…);
  }
}

ApplicationEvents (可以使用 AsyncTaskExecutor 進行配置,幾乎可以隨時執行該動作) 不同,EntityCallback API 保證在觸發實際事件之前立即被呼叫。即使在反應式串流中也是如此。以下列表顯示了它的運作方式

反應式 EntityCallback API

@Bean
ReactiveBeforeConvertCallback<Person> beforeConvert() {

  return (entity, collection) -> {
    return Mono.just(entity.withId(…));
  }
}

對 Redis Streams 的支援

說到串流,Spring Data Redis 現在支援 Redis Streams,它與反應式串流幾乎沒有關係,但它是一種新的 Redis 僅附加資料結構,用於模擬日誌,其中每個條目都包含一個 id (通常是時間戳記加上一個序列號) 和多個鍵/值對。除了常見的嫌疑人 (例如新增到日誌和從日誌讀取) 之外,Spring Data Redis 還提供了容器,允許無限偵聽和處理新增到日誌的條目。它的工作方式類似於 tail -f,但適用於 Redis Stream。以下範例顯示了 Redis 串流接聽器

Redis Streams 接聽器

@Autowired
RedisConnectionFactory factory;

StreamListener<String, MapRecord<…>> listener =
  (msg) -> {

    // … msg.getId()
    // … msg.getStream()
    // … msg.getValue()
  };

StreamMessageListenerContainer container = StreamMessageListenerContainer.create(factory));

container.receive(StreamOffset.fromStart("my-stream"), listener);

前面的範例中的 StreamMessageListenerContainer 讀取 my-stream 的所有現有條目,並收到有關新新增條目的通知。對於收到的每條訊息,都會呼叫 StreamListener。單個容器可以接收來自多個串流的訊息。

當然,串流式結構最適合由反應式基礎設施使用,如下列範例所示

StreamReceiver receiver = // …

receiver.receive(StreamOffset.fromStart("my-stream"))
  .doOnNext(msg -> {
      // …
  })
  .subscribe();

JPA 預存程序的 Multiple OUT 參數

在 JPA 方面,現在有一個小改進,讓您可以為預存程序提供多個 OUT 參數,這些參數會以 Map 的形式傳回。 以下範例展示了如何操作

具有 JPA 預存程序的 Out 參數

@NamedStoredProcedureQuery(name = "User.s1p", procedureName = "s1p",
  parameters = {
    @StoredProcedureParameter(mode = IN, name = "in_1", type = …),
    @StoredProcedureParameter(mode = OUT, name = "out_1", type = …),
    @StoredProcedureParameter(mode = OUT, name = "out_2", type = …)})
@Table(name = "SD_User")
class User { … }

interface UserRepository extends JpaRepository<…> {

  @Procedure(name = "User.s1p")
  Map<String, Integer> callS1P(@Param("in_1") Integer arg);
}

JPA 的 @StoredProcedureParameter 註解中宣告的所有 out 參數最終都可以在儲存庫查詢方法傳回的 Map 中使用。

在儲存庫方法上宣告式 MongoDB 聚合

對於 MongoDB,複雜的資料處理是使用聚合來完成的,Spring Data 為此提供了一個特定的(流暢)API,其中包含操作和表達式的抽象。 然而,Stackoverflow 告訴我們,人們傾向於在命令列上建立他們的聚合,然後再將它們轉換為 Java 程式碼。 事實證明,這種轉換是一個主要的痛點。
因此,我們藉此機會引入 @Aggregation,作為在儲存庫方法中執行聚合的直接方式。 以下範例展示了如何操作

宣告式 MongoDB 聚合

interface OrderRepository extends CrudRepository<Order, Long> {

  @Aggregation("{ $group : { _id : '$cust_id', total : { $sum : '$amount' }}}")
  List<TotalByCustomer> totalByCustomer(Sort sort);

  @Aggregation(pipeline = {
    "{ $match : { customerId : ?0 }}",
    "{ $count : total }"
  })
  Long totalOrdersForCustomer(String customerId);
}

與其親戚 @Query 註解一樣,@Aggregation 支援參數替換,如果查詢方法引數提供排序,則會將排序新增至聚合,如前面的範例所示。 我們甚至更進一步,提取用於傳回簡單類型的方法的單一屬性文件值,例如前面範例中的 *totalOrdersForCustomer* 方法。 在這種情況下,$count 階段傳回一個類似 {"total" : 101 } 的文件,通常需要對應到 plain org.bson.Document 或相應的網域類型。 但是,由於該方法宣告 Long 作為其傳回類型,因此我們會檢查結果文件並從那裡提取/轉換該值,從而消除了對專用類型的需求。

還有更多等您探索

為了現在總結一下,我想提及其他模組的一些其他功能。 如果您對所有這些功能都感興趣,請查看我們的版本 wiki 或參閱個別模組參考文件中「新功能」部分。 因此,事不宜遲,以下是此版本提供的更多改進

  • Gemfire/Apache Geode:改進的 SSL 支援和動態埠設定

  • JDBC:唯讀屬性、SQL 產生和可嵌入載入選項

  • REST:利用 HATEOAS 1.0 以及其中的所有酷炫功能!

  • MongoDB:反應式 GridFS、宣告式排序規則支援和 JSON 結構描述產生器

  • neo4j:空間類型和 exists 投影

  • Apache Cassandra:範圍查詢、樂觀鎖定和稽核支援

  • Redis:叢集快取和非阻塞連線方法

  • Elasticsearch:高階 REST 用戶端支援和非 Jackson 基於實體映射

如果您想了解更多資訊,請這裡是在德克薩斯州奧斯丁舉行的 SpringOne 2019 上錄製的 30 分鐘簡報。

取得 Spring 電子報

隨時關注 Spring 電子報

訂閱

取得領先

VMware 提供培訓和認證,以加速您的進度。

了解更多

取得支援

Tanzu Spring 在一個簡單的訂閱中提供對 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位檔。

了解更多

即將舉辦的活動

查看 Spring 社群中所有即將舉辦的活動。

查看全部