持久化實體

R2dbcEntityTemplate 是 Spring Data R2DBC 的核心入口。它提供直接面向實體的方法,以及一個更簡潔流暢的介面,用於處理典型的臨時用例,例如查詢、插入、更新和刪除資料。

入口點(insert()select()update() 等)遵循基於要執行的操作的自然命名方案。從入口點繼續,API 設計為僅提供依賴於上下文的方法,這些方法會引導到一個終止方法,該方法建立並執行 SQL 語句。Spring Data R2DBC 使用 R2dbcDialect 抽象來確定繫結標記、分頁支援以及底層驅動程式原生支援的資料型別。

所有終止方法都始終返回表示所需操作的 Publisher 型別。實際的語句在訂閱時傳送到資料庫。

插入和更新實體的方法

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

使用 save 操作的簡單情況是儲存一個 POJO。在這種情況下,表名由類的名稱(非完全限定名)決定。您也可以呼叫 save 操作並指定集合名。您可以使用對映元資料來覆蓋儲存物件的集合。

插入或儲存時,如果未設定 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(…) 方法用於從表中選擇資料。這兩個方法都接受一個定義欄位投影、WHERE 子句、ORDER BY 子句以及限制/偏移分頁的 Query 物件。無論底層資料庫如何,限制/偏移功能對應用程式都是透明的。此功能由 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 or (String column):將一個帶有指定 property 的鏈式 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 運算子建立條件。您可以在 SELECTUPDATEDELETE 查詢中使用 Criteria

插入資料

您可以使用 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 應用刪除操作並返回受影響的行數。

使用 Repositories,可以透過 ReactiveCrudRepository.save(…) 方法來儲存實體。如果實體是新的,這將導致對實體進行插入操作。

如果實體不是新的,它將被更新。注意,一個例項是否為新的,是該例項狀態的一部分。

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

ID 生成

Spring Data 使用識別符號屬性來標識實體。實體的 ID 必須使用 Spring Data 的 @Id 註解進行標註。

當您的資料庫對 ID 列具有自增列時,生成的 ID 值會在實體插入資料庫後設置到實體中。

當實體是新的且識別符號值預設為其初始值時,Spring Data 不會嘗試插入識別符號列的值。對於原始型別,初始值為 0;如果識別符號屬性使用數字包裝型別(如 Long),則初始值為 null

實體狀態檢測詳細解釋了檢測實體是新的還是預期已存在於資料庫中的策略。

一個重要的約束是,實體儲存後,它必須不再是新的。注意,實體是否為新的,是實體狀態的一部分。對於自增列,這會自動發生,因為 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 的行。操作因當前 version1 而失敗,並丟擲 OptimisticLockingFailureException 異常。