持久化实体

R2dbcEntityTemplate 是 Spring Data R2DBC 的核心入口点。 它提供了直接面向实体的方法,以及一个更精简、流畅的接口,用于典型的临时使用场景,例如查询、插入、更新和删除数据。spring-doc.cadn.net.cn

入口方法(insert()select()update() 等)采用基于待执行操作的自然命名方案。 从入口方法开始,该 API 的设计仅提供上下文相关的方法,这些方法最终导向一个终止方法,用于创建并执行 SQL 语句。 Spring Data R2DBC 使用 R2dbcDialect 抽象来确定绑定标记符、分页支持以及底层驱动程序原生支持的数据类型。spring-doc.cadn.net.cn

所有终端方法始终返回一个代表所需操作的 Publisher 类型。 实际的语句在订阅时才会发送到数据库。

插入和更新实体的方法

R2dbcEntityTemplate 提供了多种便捷方法用于保存和插入您的对象。 若要对转换过程进行更细粒度的控制,您可以向 R2dbcCustomConversions 注册 Spring 转换器 —— 例如 Converter<Person, OutboundRow>Converter<Row, Person>spring-doc.cadn.net.cn

使用 save 操作的简单情况是保存一个 POJO 对象。 在这种情况下,表名由该类的名称(非全限定名)决定。 你也可以在调用 save 操作时指定一个具体的集合名称。 你可以使用映射元数据来覆盖用于存储该对象的集合。spring-doc.cadn.net.cn

在执行插入或保存操作时,如果未设置 Id 属性,则假定其值将由数据库自动生成。 因此,若要启用自动生成,类中的 Id 属性或字段的类型必须为 LongIntegerspring-doc.cadn.net.cn

以下示例展示了如何插入一行并检索其内容:spring-doc.cadn.net.cn

使用 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);

以下插入和更新操作可用:spring-doc.cadn.net.cn

还提供了一组类似的插入操作:spring-doc.cadn.net.cn

可以通过使用流式 API 来自定义表名。spring-doc.cadn.net.cn

选择数据

select(…)selectOne(…)方法用于从表中选择数据。 这两个方法都接受一个Query对象,该对象定义了字段投影、WHERE子句、ORDER BY子句以及限制/偏移分页。 无论底层数据库如何,限制/偏移功能对应用程序都是透明的。 此功能由R2dbcDialect抽象支持,以适应不同 SQL 方言之间的差异。spring-doc.cadn.net.cn

使用 R2dbcEntityTemplate 选择实体
Flux<Person> loaded = template.select(query(where("firstname").is("John")),
		Person.class);

流畅 API

本节介绍流畅 API 的用法。 请考虑以下简单查询:spring-doc.cadn.net.cn

Flux<Person> people = template.select(Person.class) (1)
		.all(); (2)
1 使用 Person 类与 select(…) 方法可将表格结果映射到 Person 结果对象上。
2 调用 all() 方法获取所有行会返回一个 Flux<Person>,且不会限制结果数量。

以下示例声明了一个更复杂的查询,其中通过名称指定表名、一个WHERE条件和一个ORDER BY子句:spring-doc.cadn.net.cn

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 所执行的查询在 WHEREfirstname 列上声明了一个 lastname 条件,用于过滤结果。
3 结果可以按单个列名进行排序,从而生成一个 ORDER BY 子句。
4 选择单一结果仅获取一行数据。 这种处理行的方式要求查询必须恰好返回一个结果。 如果查询返回的结果多于一个,Mono 会抛出 IncorrectResultSizeDataAccessException 异常。
你可以通过 xref page 提供目标类型,直接将投影(Projections)应用到结果上。

你可以通过以下终止方法在检索单个实体和检索多个实体之间进行切换:spring-doc.cadn.net.cn

  • first():仅消费第一行,返回一个 Mono。 如果查询未返回任何结果,则返回的 Mono 将在不发出任何对象的情况下完成。spring-doc.cadn.net.cn

  • one():恰好消费一行,返回一个 Mono。 如果查询未返回任何结果,则返回的 Mono 在不发出任何对象的情况下完成。 如果查询返回多于一行的结果,则 Mono 会异常完成,并抛出 IncorrectResultSizeDataAccessExceptionspring-doc.cadn.net.cn

  • all():消费所有返回的行,并返回一个 Fluxspring-doc.cadn.net.cn

  • count():应用一个计数投影,返回 Mono<Long>spring-doc.cadn.net.cn

  • exists():通过返回 Mono<Boolean> 来判断查询是否返回任何行。spring-doc.cadn.net.cn

您可以使用 select() 入口点来编写您的 SELECT 查询。 生成的 SELECT 查询支持常用子句(WHEREORDER BY),并支持分页。 流畅的 API 风格允许您将多个方法链式调用,同时保持代码易于理解。 为了提高可读性,您可以使用静态导入,从而避免在创建 Criteria 实例时使用 'new' 关键字。spring-doc.cadn.net.cn

Criteria 类的方法

Criteria 类提供了以下方法,所有这些方法都对应于 SQL 操作符:spring-doc.cadn.net.cn

  • Criteria (String column):向当前 Criteria 添加一个带有指定 property 的链式 Criteria,并返回新创建的 5spring-doc.cadn.net.cn

  • Criteria (String column):向当前 Criteria 添加一个带有指定 property 的链式 Criteria,并返回新创建的 5spring-doc.cadn.net.cn

  • Criteria greaterThan (Object o):使用 > 运算符创建一个条件。spring-doc.cadn.net.cn

  • Criteria greaterThanOrEquals (Object o):使用 >= 运算符创建一个条件。spring-doc.cadn.net.cn

  • Criteria in (Object…​ o):使用 IN 操作符为可变参数创建一个条件。spring-doc.cadn.net.cn

  • Criteria in (Collection<?> collection):使用集合创建一个采用 IN 运算符的条件。spring-doc.cadn.net.cn

  • Criteria is (Object o):通过使用列匹配(property = value)创建一个条件。spring-doc.cadn.net.cn

  • Criteria isNull ():使用 IS NULL 运算符创建一个条件。spring-doc.cadn.net.cn

  • Criteria isNotNull ():使用 IS NOT NULL 运算符创建一个条件。spring-doc.cadn.net.cn

  • Criteria lessThan (Object o):使用 < 运算符创建一个条件。spring-doc.cadn.net.cn

  • Criteria lessThanOrEquals (Object o):使用 运算符创建一个条件。spring-doc.cadn.net.cn

  • Criteria like (Object o):使用 LIKE 运算符创建一个条件,且不进行转义字符处理。spring-doc.cadn.net.cn

  • Criteria not (Object o):使用 != 运算符创建一个条件。spring-doc.cadn.net.cn

  • Criteria notIn (Object…​ o):使用 NOT IN 运算符为可变参数创建一个条件。spring-doc.cadn.net.cn

  • Criteria notIn (Collection<?> collection):使用集合创建一个采用 NOT IN 运算符的条件。 您可以在 CriteriaSELECTUPDATE 查询中使用 DELETEspring-doc.cadn.net.cn

插入数据

您可以使用 insert() 入口点来插入数据。spring-doc.cadn.net.cn

考虑以下简单的类型化插入操作:spring-doc.cadn.net.cn

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 子句。spring-doc.cadn.net.cn

考虑以下简单的类型化更新操作:spring-doc.cadn.net.cn

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 子句。spring-doc.cadn.net.cn

考虑以下简单的插入操作:spring-doc.cadn.net.cn

		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-doc.cadn.net.cn

如果实体不是新的,则会被更新。 请注意,一个实例是否为新实例是该实例状态的一部分。spring-doc.cadn.net.cn

这种方法存在一些明显的缺点。 如果被引用的实体中只有少数几个实际上发生了变更,那么删除和重新插入的操作就显得浪费资源。 尽管这一过程未来可能会(也极有可能会)得到改进,但 Spring Data R2DBC 的能力仍存在一定局限性。 它无法获知聚合对象之前的状态。 因此,任何更新操作都必须以数据库中当前存在的数据为基础,并确保将其转换为传递给 save 方法的实体所表示的状态。

ID 生成

Spring Data 使用标识符属性来识别实体。 也就是说,查找这些实体或创建针对特定行的语句。 实体的 ID 必须使用 Spring Data 的 @Id 注解进行标注。spring-doc.cadn.net.cn

当您的数据库的 ID 列是自增列时,在将实体插入数据库后,生成的值会被设置到该实体中。spring-doc.cadn.net.cn

如果你在标识符属性上额外添加 @Sequence 注解,并且底层的 Dialect 支持序列(sequences),则将使用数据库序列来获取 ID 的值。spring-doc.cadn.net.cn

否则,当实体为新实体且标识符值为其初始默认值时,Spring Data 不会尝试向标识符列插入值。 对于基本数据类型,该初始值为 0;如果标识符属性使用了如 null 这样的数值包装类型,则初始值为 Longspring-doc.cadn.net.cn

实体状态检测详细解释了用于判断一个实体是新实体,还是预期已在数据库中存在的策略。spring-doc.cadn.net.cn

一个重要的约束是,在保存实体之后,该实体不能再处于“新建”状态。 请注意,实体是否为新建状态是实体自身状态的一部分。 对于自增列,这一过程会自动完成,因为 Spring Data 会使用 ID 列的值来设置实体的 ID。spring-doc.cadn.net.cn

乐观锁

Spring Data 通过聚合根上标注了 @Version 的数值属性来支持乐观锁。 每当 Spring Data 保存具有此类版本属性的聚合时,会发生两件事:spring-doc.cadn.net.cn

  • 针对聚合根的更新语句将包含一个 WHERE 子句,用于检查数据库中存储的版本实际上未被更改。spring-doc.cadn.net.cn

  • 如果情况并非如此,则会抛出 OptimisticLockingFailureException 异常。spring-doc.cadn.net.cn

此外,version 属性在实体和数据库中都会递增,因此并发操作将察觉到这一变更,并在适用的情况下抛出 OptimisticLockingFailureException,如上文所述。spring-doc.cadn.net.cn

该过程同样适用于插入新的聚合对象,其中 null0 的版本号表示一个新实例,而随后递增的版本号则将该实例标记为不再是新的。这种方式在例如使用 UUID 作为 ID 并在对象构造期间生成 ID 的场景下,能够很好地发挥作用。spring-doc.cadn.net.cn

在执行删除操作时,版本检查同样适用,但版本号不会增加。spring-doc.cadn.net.cn

@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 的行。由于当前 version 已为 1,该操作会失败并抛出 4 异常。