跳到主要内容

持久化实体

DeepSeek V3 中英对照 Persisting Entities

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

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

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): 将对象插入到默认表中。

表名可以通过使用 Fluent 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
  • 使用 Personselect(…) 方法将表格结果映射到 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
  • 按名称从表中选择将使用给定的域类型返回行结果。

  • 发出的查询在 firstnamelastname 列上声明了一个 WHERE 条件以过滤结果。

  • 结果可以按单个列名排序,从而生成一个 ORDER BY 子句。

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

提示

你可以通过 select(Class<?>) 提供目标类型,直接将Projections应用到结果上。

你可以通过以下终止方法在检索单个实体和检索多个实体之间进行切换:

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

  • one(): 精确消费一行,返回一个 Mono。如果查询没有返回结果,返回的 Mono 会在不发出任何对象的情况下完成。如果查询返回多行,Mono 会异常完成并发出 IncorrectResultSizeDataAccessException

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

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

  • exists(): 通过返回 Mono<Boolean> 来判断查询是否返回了任何行。

你可以使用 select() 入口点来表达你的 SELECT 查询。生成的 SELECT 查询支持常用的子句(如 WHEREORDER 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 运算符基于集合创建一个条件。你可以将 CriteriaSELECTUPDATEDELETE 查询一起使用。

插入数据

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

考虑以下简单的类型化插入操作:

Mono<Person> insert = template.insert(Person.class)	1
.using(new Person("John", "Doe")); 2
java
  • 使用 Personinto(…) 方法基于映射元数据设置 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 并返回受影响的行数。

删除数据

你可以使用 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 子句。

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

使用 Repository 时,可以通过 ReactiveCrudRepository.save(…) 方法来保存实体。如果该实体是新的,这将导致对该实体执行插入操作。

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

备注

这种方法有一些明显的缺点。如果只有少数被引用的实体实际上发生了变化,删除和插入操作就是浪费的。虽然这个过程可能会并且很可能会被改进,但 Spring Data R2DBC 能提供的功能有一定的限制。它不知道聚合的先前状态。因此,任何更新过程都必须处理数据库中现有的内容,并确保将其转换为传递给 save 方法的实体状态。

ID 生成

Spring Data 使用标识符属性来识别实体。实体的 ID 必须使用 Spring Data 的 @Id 注解进行标注。

当你的数据库中有一个用于 ID 列的自增列时,插入数据库后,生成的值会被设置在实体中。

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

实体状态检测 详细解释了检测实体是否为新的策略,或者它是否预期存在于你的数据库中。

一个重要的约束是,在保存实体之后,实体不能再是新的。需要注意的是,实体是否新的是实体状态的一部分。对于自增列,这种情况会自动发生,因为 Spring Data 会使用 ID 列中的值来设置 ID。

乐观锁

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

  • 对于聚合根的更新语句将包含一个 where 子句,用于检查数据库中存储的版本是否确实未更改。

  • 如果情况并非如此,将抛出 OptimisticLockingFailureException 异常。

此外,version 属性在实体和数据库中都会递增,因此并发操作会注意到这一变化,并在适用时抛出 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>
java
  • 初始插入行。version 设置为 0

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

  • 使用 version = 0 更新该行。设置 lastname 并将 version 增加到 1

  • 尝试更新之前加载的行,该行仍然具有 version = 0。由于当前 version1,操作失败并抛出 OptimisticLockingFailureException