Persisting Entities

R2dbcEntityTemplate 是 Spring Data R2DBC 的中央入口点。它提供了直接面向实体的方法以及更窄的、流畅的界面,用于典型的临时用例,例如查询、插入、更新和删除数据。 入口点(insert()select()update() 和其他)遵循基于要运行的操作的自然命名模式。从入口点开始,API 被设计为仅提供上下文相关的方法,这些方法导致创建和运行 SQL 语句的终止方法。Spring Data R2DBC 使用 R2dbcDialect 抽象来确定绑定标记、分页支持和底层驱动程序本机支持的数据类型。

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

Methods for Inserting and Updating Entities

R2dbcEntityTemplate 上有几种方便的方法,用于保存和插入你的对象。为了对转换过程进行更细粒度的控制,你可以使用 R2dbcCustomConversions 注册 Spring 转换器,例如 Converter<Person, OutboundRow>Converter<Row, Person>

使用保存操作的简单情况是保存一个 POJO。在这种情况下,表名由类的名称(不完全限定)确定。你还可以使用特定集合名称调用保存操作。你可以使用映射元数据覆盖存储对象的集合。

插入或保存时,如果未设置 Id 属性,则假设其值将由数据库自动生成。因此,对于自动生成,类中 Id 属性或字段的类型必须为 LongInteger

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

Inserting and retrieving entities using the R2dbcEntityTemplate
Unresolved include directive in modules/ROOT/pages/r2dbc/entity-persistence.adoc - include::example$r2dbc/R2dbcEntityTemplateSnippets.java[]

以下插入和更新操作可用:

也有一组类似的插入操作可用:

  • Mono&lt;T&gt; insert (T objectToSave):将对象插入默认表。

  • Mono&lt;T&gt; update (T objectToSave):插入对象到默认表中。

表名可以通过使用流畅的 API 来自定义。

Selecting Data

R2dbcEntityTemplate 上的 select(…)selectOne(…) 方法用于从表中选择数据。这两种方法都会采用一个 <<`Query`,r2dbc.datbaseclient.fluent-api.criteria>> 对象,此对象定义字段投影、WHERE 子句、ORDER BY 子句以及限制/偏移分页。限制/偏移功能对于应用程序是透明的,无论底层数据库如何。xref:r2dbc/getting-started.adoc#r2dbc.dialects[R2dbcDialect 抽象支持此功能,以满足不同 SQL 风味之间的差异。

Selecting entities using the R2dbcEntityTemplate
Unresolved include directive in modules/ROOT/pages/r2dbc/entity-persistence.adoc - include::example$r2dbc/R2dbcEntityTemplateSnippets.java[]

Fluent API

本节介绍流畅 API 的用法。请考虑以下简单的查询:

Unresolved include directive in modules/ROOT/pages/r2dbc/entity-persistence.adoc - include::example$r2dbc/R2dbcEntityTemplateSnippets.java[]
1 使用 Personselect(…) 方法将表格结果映射到 Person 结果对象中。
2 获取 all() 行会返回一个 Flux&lt;Person&gt;,且不限制结果。

以下示例声明了一个更复杂的查询,它按名称指定表名、WHERE 条件和 ORDER BY 子句:

Unresolved include directive in modules/ROOT/pages/r2dbc/entity-persistence.adoc - include::example$r2dbc/R2dbcEntityTemplateSnippets.java[]
1 根据名称选择表格会使用给定的域名类型返回行结果。
2 发出的查询在 firstnamelastname 列中声明了一个 WHERE 条件来过滤结果。
3 可以使用各个列名对结果进行排序,从而生成一个 ORDER BY 子句。
4 选择一个结果只能获取一个单一的行。这种消费行的方式预期查询只返回单个结果。Mono 在查询产生多于单个结果时会发出一个 IncorrectResultSizeDataAccessException

您可以通过提供目标类型(通过“ select(Class<?>)”)直接对结果应用“ Projections”。

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

  • first():只消费第一行,返回一个 Mono。如果查询没有返回结果,则返回的 Mono 完成时不会发出对象。

  • one():只消费一行,返回一个 Mono。如果查询没有返回结果,则返回的 Mono 完成时不会发出对象。如果查询返回多于一行,则 Mono 完成时会发出一个 IncorrectResultSizeDataAccessException 异常。

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

  • count():应用一个计数投影,返回 Mono&lt;Long&gt;

  • exists():通过返回 Mono&lt;Boolean&gt; 来返回查询是否产生任何行。

可以使用 select() 入口点来表达 SELECT 查询。生成的 SELECT 查询支持常用的子句(WHEREORDER BY)并支持分页。流畅 API 样式允许你链接多个方法,同时具有易于理解的代码。为了提高可读性,可以使用静态导入,这样你无需使用 'new' 关键字来创建 Criteria 实例。

Methods for the Criteria Class

Criteria 类提供以下方法,所有方法都对应于 SQL 运算符:

  • Criteria and (String column):向当前 Criteria 添加一个链式的 Criteria,其中包含指定的 property,并返回新创建的对象。

  • Criteria or (String column):向当前 Criteria 添加一个链式的 Criteria,其中包含指定的 property,并返回新创建的对象。

  • Criteria greaterThan (Object o):使用 &gt; 操作符创建标准。

  • Criteria greaterThanOrEquals (Object o):使用 &gt;= 操作符创建标准。

  • Criteria in (Object&#8230;&#8203; o):使用 IN 操作符为一个可变参数创建标准。

  • Criteria in (Collection&lt;?&gt; collection):使用 IN 操作符使用一个集合创建标准。

  • Criteria is (Object o):使用列匹配(property = value)创建标准。

  • Criteria isNull ():使用 IS NULL 操作符创建标准。

  • Criteria isNotNull ():使用 IS NOT NULL 操作符创建标准。

  • Criteria lessThan (Object o):使用 &lt; 操作符创建标准。

  • Criteria lessThanOrEquals (Object o):使用 &#8656; 操作符创建标准。

  • Criteria like (Object o):使用 LIKE 操作符创建标准而不进行转义字符处理。

  • Criteria not (Object o):使用 != 操作符创建标准。

  • Criteria notIn (Object&#8230;&#8203; o):使用 NOT IN 操作符为一个可变参数创建标准。

  • Criteria notIn (Collection&lt;?&gt; collection):使用 NOT IN 操作符使用一个集合创建标准。

你可以对 SELECTUPDATEDELETE 查询使用 Criteria

Inserting Data

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

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

Unresolved include directive in modules/ROOT/pages/r2dbc/entity-persistence.adoc - include::example$r2dbc/R2dbcEntityTemplateSnippets.java[]
1 使用带有 into(…) 方法的 Person 基于映射元数据设置 INTO 表。它还准备插入语句以接受 Person 对象进行插入。
2 提供标量 Person 对象。或者,您可以提供 Publisher,以运行 INSERT 语句流。此方法会提取所有非 null 值并插入它们。

Updating Data

可以使用 update() 入口点来更新行。更新数据以通过接受指定要更新的表的 Update 指定赋值开始。它还接受 Query 来创建一个 WHERE 子句。

请考虑以下简单的类型化更新操作:

Person modified = …

Unresolved include directive in modules/ROOT/pages/r2dbc/entity-persistence.adoc - include::example$r2dbc/R2dbcEntityTemplateSnippets.java[]
1 更新 Person 对象并基于映射元数据应用映射。
2 通过调用 inTable(…) 方法设置不同的表名。
3 指定转换为 WHERE 子句的查询。
4 应用 Update 对象。在这种情况下,将 age 设置为 42,并返回受影响的行数。

Deleting Data

可以使用 delete() 入口点来删除行。删除数据以指定要删除的表开始,并可以接受 Criteria 以创建 WHERE 子句(如果需要)。

请考虑以下简单的插入操作:

Unresolved include directive in modules/ROOT/pages/r2dbc/entity-persistence.adoc - include::example$r2dbc/R2dbcEntityTemplateSnippets.java[]
1 删除 Person 对象并基于映射元数据应用映射。
2 通过调用 from(…) 方法设置不同的表名。
3 指定转换为 WHERE 子句的查询。
4 应用删除操作并返回受影响的行数。

使用存储库时,可以使用 ReactiveCrudRepository.save(…) 方法来保存实体。如果实体是新的,这会导致插入该实体。

如果实体不是新的,则对其进行更新。请注意,实例是否新是实例状态的一部分。

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

ID Generation

ID Generation

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

当你的数据库为 ID 列拥有自动增加列时,在将其插入数据库之后,生成的值会设置在实体中。

当实体是新的且标识符值默认为其初始值时,Spring Data 不会尝试插入标识符列的值。对于原始类型是 0,对于标识符属性使用 Long 等数字包装器类型则为 null

Entity State Detection 详细解释了检测实体是新实体还是它存在于数据库中的策略。

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

Optimistic Locking

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 的先前加载的行。由于当前 version1,因此此操作将失败,并出现 OptimisticLockingFailureException