持久化實體

R2dbcEntityTemplate 是 Spring Data R2DBC 的核心進入點。它提供直接面向實體的方法,以及更精簡、流暢的介面,適用於典型的臨時使用案例,例如查詢、插入、更新和刪除資料。

進入點 (insert()select()update() 和其他) 遵循基於要執行的操作的自然命名模式。從進入點開始,API 設計為僅提供上下文相關的方法,這些方法會導向一個終止方法,該方法會建立並執行 SQL 陳述式。Spring Data R2DBC 使用 R2dbcDialect 抽象化來判斷綁定標記、分頁支援以及底層驅動程式原生支援的資料類型。

所有終止方法都始終回傳代表所需操作的 Publisher 類型。實際的陳述式在訂閱時傳送到資料庫。

用於插入和更新實體的方法

R2dbcEntityTemplate 上有多種方便的方法可用於儲存和插入物件。為了更精細地控制轉換過程,您可以向 R2dbcCustomConversions 註冊 Spring 轉換器,例如 Converter<Person, OutboundRow>Converter<Row, Person>

使用儲存操作的簡單情況是儲存 POJO。在這種情況下,表格名稱由類別的名稱(非完整限定名稱)決定。您也可以使用特定的集合名稱呼叫儲存操作。您可以使用對應中繼資料來覆寫要儲存物件的集合。

在插入或儲存時,如果未設定 Id 屬性,則假設其值將由資料庫自動產生。因此,對於自動產生,類別中 Id 屬性或欄位的類型必須是 LongInteger

以下範例示範如何插入列並檢索其內容

使用 R2dbcEntityTemplate 插入和檢索實體
Person person = new Person("John", "Doe");

Mono<Person> saved = template.insert(person);
Mono<Person> loaded = template.selectOne(query(where("firstname").is("John")),
		Person.class);

以下插入和更新操作可用

也提供一組類似的插入操作

  • Mono<T> insert (T objectToSave):將物件插入預設表格。

  • Mono<T> update (T objectToSave):將物件插入預設表格。

表格名稱可以使用流暢 API 進行自訂。

選取資料

R2dbcEntityTemplate 上的 select(…)selectOne(…) 方法用於從表格中選取資料。這兩種方法都採用 Query 物件,該物件定義了欄位投影、WHERE 子句、ORDER BY 子句以及限制/偏移分頁。限制/偏移功能對應用程式是透明的,無論底層資料庫為何。此功能由 R2dbcDialect 抽象化支援,以應對個別 SQL 風格之間的差異。

使用 R2dbcEntityTemplate 選取實體
Flux<Person> loaded = template.select(query(where("firstname").is("John")),
		Person.class);

流暢 API

本節說明流暢 API 的用法。考慮以下簡單查詢

Flux<Person> people = template.select(Person.class) (1)
		.all(); (2)
1 Personselect(…) 方法搭配使用,會將表格結果對應到 Person 結果物件上。
2 擷取 all() 列會回傳 Flux<Person>,而不限制結果。

以下範例宣告了一個更複雜的查詢,該查詢依名稱指定表格名稱、WHERE 條件和 ORDER BY 子句

Mono<Person> first = template.select(Person.class)	(1)
	.from("other_person")
	.matching(query(where("firstname").is("John")			(2)
		.and("lastname").in("Doe", "White"))
	  .sort(by(desc("id"))))													(3)
	.one();																						(4)
1 依名稱從表格中選取會使用給定的網域類型回傳列結果。
2 發出的查詢在 firstnamelastname 欄位上宣告了 WHERE 條件,以篩選結果。
3 結果可以依個別欄位名稱排序,從而產生 ORDER BY 子句。
4 選取單一結果只會擷取單一列。這種使用列的方式期望查詢只回傳一個結果。如果查詢產生多個結果,Mono 會發出 IncorrectResultSizeDataAccessException
您可以透過 select(Class<?>) 提供目標類型,直接將 投影套用至結果。

您可以透過以下終止方法在擷取單一實體和擷取多個實體之間切換

  • first():僅使用第一列,回傳 Mono。如果查詢未回傳任何結果,則回傳的 Mono 完成時不會發出物件。

  • one():完全使用一列,回傳 Mono。如果查詢未回傳任何結果,則回傳的 Mono 完成時不會發出物件。如果查詢回傳多個列,則 Mono 會異常完成,並發出 IncorrectResultSizeDataAccessException

  • all():使用所有回傳的列,回傳 Flux

  • count():套用計數投影,回傳 Mono<Long>

  • exists():透過回傳 Mono<Boolean>,判斷查詢是否產生任何列。

您可以使用 select() 進入點來表達您的 SELECT 查詢。產生的 SELECT 查詢支援常用的子句 (WHEREORDER BY),並支援分頁。流暢的 API 風格可讓您將多個方法鏈結在一起,同時擁有易於理解的程式碼。為了提高可讀性,您可以使用靜態匯入,讓您避免使用 'new' 關鍵字來建立 Criteria 實例。

Criteria 類別的方法

Criteria 類別提供以下方法,所有方法都對應於 SQL 運算子

  • Criteria and (String column):將具有指定 property 的鏈結 Criteria 新增至目前的 Criteria,並回傳新建立的 Criteria

  • Criteria or (String column):將具有指定 property 的鏈結 Criteria 新增至目前的 Criteria,並回傳新建立的 Criteria

  • Criteria greaterThan (Object o):使用 > 運算子建立條件。

  • Criteria greaterThanOrEquals (Object o):使用 >= 運算子建立條件。

  • Criteria in (Object…​ o):使用 IN 運算子為可變參數引數建立條件。

  • Criteria in (Collection<?> collection):使用 IN 運算子,使用集合建立條件。

  • Criteria is (Object o):使用欄位比對 (property = value) 建立條件。

  • Criteria isNull ():使用 IS NULL 運算子建立條件。

  • Criteria isNotNull ():使用 IS NOT NULL 運算子建立條件。

  • Criteria lessThan (Object o):使用 < 運算子建立條件。

  • Criteria lessThanOrEquals (Object o):使用 運算子建立條件。

  • Criteria like (Object o):使用 LIKE 運算子建立條件,而不進行逸出字元處理。

  • Criteria not (Object o):使用 != 運算子建立條件。

  • Criteria notIn (Object…​ o):使用 NOT IN 運算子為可變參數引數建立條件。

  • Criteria notIn (Collection<?> collection):使用 NOT IN 運算子,使用集合建立條件。您可以將 CriteriaSELECTUPDATEDELETE 查詢搭配使用。

插入資料

您可以使用 insert() 進入點來插入資料。

考慮以下簡單的類型化插入操作

Mono<Person> insert = template.insert(Person.class)	(1)
		.using(new Person("John", "Doe")); (2)
1 Personinto(…) 方法搭配使用,會根據對應中繼資料設定 INTO 表格。它也會準備插入陳述式以接受 Person 物件進行插入。
2 提供純量 Person 物件。或者,您可以提供 Publisher 來執行 INSERT 陳述式的串流。此方法會擷取所有非 null 值並插入它們。

更新資料

您可以使用 update() 進入點來更新列。更新資料首先指定要更新的表格,方法是接受指定指派的 Update。它也接受 Query 來建立 WHERE 子句。

考慮以下簡單的類型化更新操作

Person modified = …

		Mono<Long> update = template.update(Person.class)	(1)
				.inTable("other_table")														(2)
				.matching(query(where("firstname").is("John")))		(3)
				.apply(update("age", 42));												(4)
1 更新 Person 物件,並根據對應中繼資料套用對應。
2 透過呼叫 inTable(…) 方法來設定不同的表格名稱。
3 指定轉換為 WHERE 子句的查詢。
4 套用 Update 物件。在本例中,將 age 設定為 42,並回傳受影響的列數。

刪除資料

您可以使用 delete() 進入點來刪除列。移除資料首先指定要從哪個表格刪除,並選擇性地接受 Criteria 來建立 WHERE 子句。

考慮以下簡單的插入操作

		Mono<Long> delete = template.delete(Person.class)	(1)
				.from("other_table")															(2)
				.matching(query(where("firstname").is("John")))		(3)
				.all();																						(4)
1 刪除 Person 物件,並根據對應中繼資料套用對應。
2 透過呼叫 from(…) 方法來設定不同的表格名稱。
3 指定轉換為 WHERE 子句的查詢。
4 套用刪除操作並回傳受影響的列數。

使用 Repository,可以使用 ReactiveCrudRepository.save(…) 方法執行實體的儲存。如果實體是新的,則會導致實體的插入。

如果實體不是新的,則會更新它。請注意,實例是否為新的實例是實例狀態的一部分。

這種方法有一些明顯的缺點。如果只有少數參考實體實際已變更,則刪除和插入是浪費的。雖然這個過程可以而且可能會改進,但 Spring Data R2DBC 可以提供的功能存在某些限制。它不知道聚合的先前狀態。因此,任何更新過程都必須始終取得在資料庫中找到的任何內容,並確保將其轉換為傳遞至儲存方法的實體的狀態。

ID 產生

Spring Data 使用識別碼屬性來識別實體。實體的 ID 必須使用 Spring Data 的 @Id 註釋進行註釋。

當您的資料庫具有 ID 欄位的自動遞增欄位時,產生的值會在將其插入資料庫後設定在實體中。

當實體是新的且識別碼值預設為其初始值時,Spring Data 不會嘗試插入識別碼欄位的值。對於基本類型,它是 0,如果識別碼屬性使用數值包裝類型(例如 Long),則它是 null

實體狀態偵測 詳細說明了偵測實體是新的還是預期存在於資料庫中的策略。

一個重要的限制是,在儲存實體之後,實體不得再是新的。請注意,實體是否為新的實例是實體狀態的一部分。使用自動遞增欄位,這會自動發生,因為 ID 會由 Spring Data 設定為來自 ID 欄位的值。

樂觀鎖定

Spring Data 透過在聚合根上使用以 @Version 註釋的數值屬性來支援樂觀鎖定。每當 Spring Data 儲存具有此版本屬性的聚合時,都會發生兩件事

  • 聚合根的更新陳述式將包含一個 where 子句,檢查資料庫中儲存的版本是否實際未變更。

  • 如果不是這種情況,則會擲回 OptimisticLockingFailureException

此外,版本屬性會在實體和資料庫中都增加,因此並行動作將注意到變更,並在適用的情況下擲回 OptimisticLockingFailureException,如上所述。

此過程也適用於插入新的聚合,其中 null0 版本表示新實例,而之後增加的實例會將實例標記為不再是新的,這使得它在例如使用 UUID 時在物件建構期間產生 id 的情況下相當順利地運作。

在刪除期間,版本檢查也適用,但不會增加版本。

@Table
class Person {

  @Id Long id;
  String firstname;
  String lastname;
  @Version Long version;
}

R2dbcEntityTemplate template = …;

Mono<Person> daenerys = template.insert(new Person("Daenerys"));                      (1)

Person other = template.select(Person.class)
                 .matching(query(where("id").is(daenerys.getId())))
                 .first().block();                                                    (2)

daenerys.setLastname("Targaryen");
template.update(daenerys);                                                            (3)

template.update(other).subscribe(); // emits OptimisticLockingFailureException        (4)
1 初始插入列。version 設定為 0
2 載入剛插入的列。version 仍然是 0
3 使用 version = 0 更新列。設定 lastname 並將 version 遞增為 1
4 嘗試更新先前載入的仍然具有 version = 0 的列。操作失敗,並出現 OptimisticLockingFailureException,因為目前的 version1