跳到主要内容

持久化实体

Doubao 中英对照 Persisting Entities

R2dbcEntityTemplate 是 Spring Data R2DBC 的核心入口。它既提供面向实体的直接方法,也为典型的即席用例(如查询、插入、更新和删除数据)提供更精简的流式接口。

各个入口(insert()、select()、update()、upsert() 等)遵循基于所执行操作的自然命名模式。从入口出发,该 API 仅提供依赖于上下文的方法,这些方法最终导向一个终结方法,由该方法创建并执行一条 SQL 语句。Spring Data R2DBC 使用 R2dbcDialect 抽象来确定绑定标记、分页支持以及底层驱动程序原生支持的数据类型。

备注

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

插入和更新实体的方法​

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

使用保存操作的简单情形是保存一个 POJO。此时,表名由类的名称(而非全限定名)决定。您也可以用指定的集合名称调用保存操作。您可以使用映射元数据来覆盖用于存储该对象的集合。

在插入或保存时,如果未设置 Id 属性,则假定其值由数据库自动生成。因此,要实现自动生成,您类中 Id 属性或字段的类型必须是数值类型,例如 Long 或 Integer。

以下示例展示如何插入一行并检索其内容:

Person person = new Person("John", "Doe");

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

可用的插入和更新操作如下:

  • Mono<T> insert (T objectToSave): 将对象插入默认表。

  • Mono<T> update (T objectToSave): 更新默认表中的对象。

  • Mono<T> upsert (T objectToSave): 将对象 upsert(合并)到默认表。

可以使用流式 API 自定义表名。

查询数据​

R2dbcEntityTemplate 上的 select(…) 和 selectOne(…) 方法用于从表中查询数据。这两个方法都接收一个 Query 对象,该对象定义字段投影、WHERE 子句、ORDER BY 子句以及 limit/offset 分页。无论底层数据库如何,limit/offset 功能对应用程序都是透明的。该功能由 R2dbcDialect 抽象 提供支持,以适配不同 SQL 方言之间的差异。

Flux<Person> loaded = template.select(query(where("firstname").is("John")),
Person.class);
java

流式 API​

本节介绍流式 API 的用法。请看以下简单查询:

Flux<Person> people = template.select(Person.class) 1
.all(); 2
java
  • 将 Person 与 select(…) 方法配合使用,会将表格结果映射为 Person 结果对象。

  • 使用 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
java
  • 按名称从表中查询会使用给定的领域类型返回行结果。

  • 发出的查询在 firstname 和 lastname 列上声明了 WHERE 条件以过滤结果。

  • 可以按各列名称对结果排序,从而生成 ORDER BY 子句。

  • 选取单个结果只会获取一行。这种消费行的方式期望查询恰好返回一个结果。如果查询产生多于一个结果,Mono 会发出 IncorrectResultSizeDataAccessException。

提示

您可以通过 select(Class<?>) 提供目标类型,从而将投影直接应用于结果。

您可以通过以下终结方法在检索单个实体与检索多个实体之间切换:

  • first(): 只消费第一行,返回 Mono。如果查询未返回结果,返回的 Mono 在不发出任何对象的情况下完成。

  • one(): 恰好消费一行,返回 Mono。如果查询未返回结果,返回的 Mono 在不发出任何对象的情况下完成。如果查询返回多于一行,Mono 会以异常完成并发出 IncorrectResultSizeDataAccessException。

  • all(): 消费所有返回的行,返回 Flux。

  • count(): 应用计数投影,返回 Mono<Long>。

  • exists(): 通过返回 Mono<Boolean> 表明查询是否产生了任何行。

您可以使用 select() 入口来表达 SELECT 查询。由此产生的 SELECT 查询支持常用子句(WHERE 和 ORDER BY)并支持分页。流式 API 风格让您可以将多个方法链式串联,同时代码易于理解。为提高可读性,您可以使用静态导入,从而在创建 Criteria 实例时避免使用 'new' 关键字。

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 运算符创建一个条件。您可以将 Criteria 用于 SELECT、UPDATE 和 DELETE 查询。

插入数据​

您可以使用 insert() 入口来插入数据。

请看以下简单的带类型插入操作:

Mono<Person> insert = template.insert(Person.class)	1
.using(new Person("John", "Doe")); 2
java
  • 将 Person 与 into(…) 方法配合使用,会基于映射元数据设置 INTO 表。它还会准备插入语句以接收待插入的 Person 对象。

  • 提供一个标量 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
java
  • 更新 Person 对象,并基于映射元数据应用映射。

  • 通过调用 inTable(…) 方法设置不同的表名。

  • 指定一个会转换为 WHERE 子句的查询。

  • 应用 Update 对象。本例中将 age 设置为 42,并返回受影响的行数。

Upsert 数据​

R2dbcEntityOperations 提供了 upsert(instance) 操作,适用于您需要在不预先进行存在性检查的情况下,通过一次数据库往返来持久化实体的情形。该操作执行数据库原生的 upsert:当关联主键不存在匹配行时插入该行,否则更新该行。

请注意,该操作要求实体实例具有标识符值。同时还需要数据库方言的支持:该功能适用于 PostgreSQL、H2、MySQL、MariaDB、SQL Server、Oracle 和 HSQLDB。

备注

upsert 目前不支持乐观锁。

upsert 数据时,先通过接收 objectToSave 指定要 upsert 的对象。请看以下 upsert 操作:

Person modified = …

Mono<Person> update = template.upsert(Person.class) 1
.inTable("other_table") 2
.one(modified);
java
  • 更新 Person 对象,并基于映射元数据应用映射。

  • 提供要 upsert 的对象。

删除数据​

您可以使用 delete() 入口来删除行。删除数据时,先指定要从中删除的表,并可选地接收一个 Criteria 以创建 WHERE 子句。

请看以下简单的删除操作:

Mono<Long> delete = template.delete(Person.class)	1
.from("other_table") 2
.matching(query(where("firstname").is("John"))) 3
.all(); 4
java
  • 删除 Person 对象,并基于映射元数据应用映射。

  • 通过调用 from(…) 方法设置不同的表名。

  • 指定一个会转换为 WHERE 子句的查询。

  • 执行删除操作并返回受影响的行数。

使用仓库时,可以通过 ReactiveCrudRepository.save(…) 方法来保存实体。如果实体是新的,则会对该实体执行插入。

如果实体不是新的,则会更新它。请注意,一个实例是否为新的,是该实例状态的一部分。

备注

这种方法有一些明显的缺点。如果被引用的实体中实际只有少数发生了更改,那么删除再插入就是浪费。虽然这个过程可以、而且很可能会被改进,但 Spring Data R2DBC 能提供的能力存在某些限制。它并不了解聚合先前的状态。因此,任何更新过程都必须取它在数据库中找到的内容,并确保将其转换为传递给保存方法的实体状态。

ID 生成​

Spring Data 使用标识符属性来识别实体,即据此查找实体或创建针对特定行的语句。实体的 ID 必须使用 Spring Data 的 @Id 注解进行标注。

当您的数据库为 ID 列设置了自增列时,生成的值会在插入数据库后设置到实体中。

如果您额外用 @Sequence 注解标识符属性,且底层 Dialect 支持序列,则会使用数据库序列来获取 id 的值。

否则,当实体为新的且标识符值默认为其初始值时,Spring Data 不会尝试插入标识符列的值。对于基本类型,该初始值为 0;如果标识符属性使用数值包装类型(例如 Long),则为 null。

实体状态检测 详细说明了用于检测实体是否为新的、或是否应已存在于您数据库中的各种策略。

一个重要的约束是:保存实体之后,该实体不得再被视为新的。请注意,一个实体是否为新的,是该实体状态的一部分。对于自增列,这会自动发生,因为 Spring Data 会用 ID 列中的值设置 ID。

乐观锁​

Spring Data 通过在聚合根上使用 @Version 注解标注的数值属性来支持乐观锁。每当 Spring Data 保存一个带有此版本属性的聚合时,会发生两件事:

  • 针对聚合根的更新语句会包含一个 where 子句,用于检查数据库中存储的版本确实未发生变化。

  • 如果情况并非如此,则会抛出 OptimisticLockingFailureException。

此外,版本属性会在实体和数据库中同时递增,因此并发操作会注意到该变化,并在适用时如上所述抛出 OptimisticLockingFailureException。

这个过程同样适用于插入新的聚合:此时 null 或 0 版本表示一个新实例,之后递增的版本会将该实例标记为不再是新的。这在 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>
java
  • 初始插入一行。version 被设置为 0。

  • 加载刚插入的行。version 仍为 0。

  • 更新 version = 0 的行。设置 lastname 并将 version 提升为 1。

  • 尝试更新先前加载的、仍为 version = 0 的行。由于当前 version 为 1,该操作以 OptimisticLockingFailureException 失败。