持久化实体

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 异常。