持久化实体

R2dbcEntityTemplate 是 Spring Data R2DBC 的核心入口点。它提供直接面向实体的方法,以及更窄、更流畅的接口,用于典型的临时用例,例如查询、插入、更新和删除数据。

入口点 (insert()select()update() 等) 遵循基于要运行的操作的自然命名方案。从入口点开始,API 被设计为仅提供上下文相关的 method,这些 method 会导致一个终止 method,该 method 创建并运行 SQL 语句。Spring Data R2DBC 使用 R2dbcDialect 抽象来确定绑定标记、分页支持以及底层驱动程序原生支持的数据类型。

所有终端 method 始终返回一个 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): 在当前 Criteria 中添加一个带有指定 属性 的链式 Criteria,并返回新创建的 Criteria

  • Criteria or (String column): 在当前 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 应用删除操作并返回受影响的行数。

使用存储库,可以使用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 版本表示新实例,之后的增加的实例将实例标记为不再是新的,这与在对象构造期间生成 ID 的情况非常吻合,例如当使用 UUID 时。

在删除期间,版本检查也适用,但不会增加任何版本。

@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