Persisting Entities

R2dbcEntityTemplate 是 Spring Data R2DBC 的中央入口点。它提供了直接面向实体的方法以及更窄的、流畅的界面,用于典型的临时用例,例如查询、插入、更新和删除数据。

R2dbcEntityTemplate is the central entrypoint for Spring Data R2DBC. It provides direct entity-oriented methods and a more narrow, fluent interface for typical ad-hoc use-cases, such as querying, inserting, updating, and deleting data.

入口点(insert()select()update() 和其他)遵循基于要运行的操作的自然命名模式。从入口点开始,API 被设计为仅提供上下文相关的方法,这些方法导致创建和运行 SQL 语句的终止方法。Spring Data R2DBC 使用 R2dbcDialect 抽象来确定绑定标记、分页支持和底层驱动程序本机支持的数据类型。

The entry points (insert(), select(), update(), and others) follow a natural naming schema based on the operation to be run. Moving on from the entry point, the API is designed to offer only context-dependent methods that lead to a terminating method that creates and runs a SQL statement. Spring Data R2DBC uses a R2dbcDialect abstraction to determine bind markers, pagination support and the data types natively supported by the underlying driver.

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

All terminal methods return always a Publisher type that represents the desired operation. The actual statements are sent to the database upon subscription.

Methods for Inserting and Updating Entities

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

There are several convenient methods on R2dbcEntityTemplate for saving and inserting your objects. To have more fine-grained control over the conversion process, you can register Spring converters with R2dbcCustomConversions — for example Converter<Person, OutboundRow> and Converter<Row, Person>.

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

The simple case of using the save operation is to save a POJO. In this case, the table name is determined by name (not fully qualified) of the class. You may also call the save operation with a specific collection name. You can use mapping metadata to override the collection in which to store the object.

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

When inserting or saving, if the Id property is not set, the assumption is that its value will be auto-generated by the database. Consequently, for auto-generation the type of the Id property or field in your class must be a Long, or Integer.

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

The following example shows how to insert a row and retrieving its contents:

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

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

The following insert and update operations are available:

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

A similar set of insert operations is also available:

  • Mono<T> insert (T objectToSave): Insert the object to the default table.

  • Mono<T> update (T objectToSave): Insert the object to the default table.

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

Table names can be customized by using the fluent API.

Selecting Data

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

The select(…) and selectOne(…) methods on R2dbcEntityTemplate are used to select data from a table. Both methods take a <<`Query`,r2dbc.datbaseclient.fluent-api.criteria>> object that defines the field projection, the WHERE clause, the ORDER BY clause and limit/offset pagination. Limit/offset functionality is transparent to the application regardless of the underlying database. This functionality is supported by the R2dbcDialect abstraction to cater for differences between the individual SQL flavors.

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

Fluent API

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

This section explains the fluent API usage. Consider the following simple query:

Unresolved include directive in modules/ROOT/pages/r2dbc/entity-persistence.adoc - include::example$r2dbc/R2dbcEntityTemplateSnippets.java[]
1 Using Person with the select(…) method maps tabular results on Person result objects.
2 Fetching all() rows returns a Flux<Person> without limiting results.

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

The following example declares a more complex query that specifies the table name by name, a WHERE condition, and an ORDER BY clause:

Unresolved include directive in modules/ROOT/pages/r2dbc/entity-persistence.adoc - include::example$r2dbc/R2dbcEntityTemplateSnippets.java[]
1 Selecting from a table by name returns row results using the given domain type.
2 The issued query declares a WHERE condition on firstname and lastname columns to filter results.
3 Results can be ordered by individual column names, resulting in an ORDER BY clause.
4 Selecting the one result fetches only a single row. This way of consuming rows expects the query to return exactly a single result. Mono emits a IncorrectResultSizeDataAccessException if the query yields more than a single result.

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

You can directly apply Projections to results by providing the target type via select(Class<?>).

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

You can switch between retrieving a single entity and retrieving multiple entities through the following terminating methods:

  • first(): Consume only the first row, returning a Mono. The returned Mono completes without emitting an object if the query returns no results.

  • one(): Consume exactly one row, returning a Mono. The returned Mono completes without emitting an object if the query returns no results. If the query returns more than one row, Mono completes exceptionally emitting IncorrectResultSizeDataAccessException.

  • all(): Consume all returned rows returning a Flux.

  • count(): Apply a count projection returning Mono<Long>.

  • exists(): Return whether the query yields any rows by returning Mono<Boolean>.

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

You can use the select() entry point to express your SELECT queries. The resulting SELECT queries support the commonly used clauses (WHERE and ORDER BY) and support pagination. The fluent API style let you chain together multiple methods while having easy-to-understand code. To improve readability, you can use static imports that let you avoid using the 'new' keyword for creating Criteria instances.

Methods for the Criteria Class

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

The Criteria class provides the following methods, all of which correspond to SQL operators:

  • Criteria and (String column): Adds a chained Criteria with the specified property to the current Criteria and returns the newly created one.

  • Criteria or (String column): Adds a chained Criteria with the specified property to the current Criteria and returns the newly created one.

  • Criteria greaterThan (Object o): Creates a criterion by using the > operator.

  • Criteria greaterThanOrEquals (Object o): Creates a criterion by using the >= operator.

  • Criteria in (Object…​ o): Creates a criterion by using the IN operator for a varargs argument.

  • Criteria in (Collection<?> collection): Creates a criterion by using the IN operator using a collection.

  • Criteria is (Object o): Creates a criterion by using column matching (property = value).

  • Criteria isNull (): Creates a criterion by using the IS NULL operator.

  • Criteria isNotNull (): Creates a criterion by using the IS NOT NULL operator.

  • Criteria lessThan (Object o): Creates a criterion by using the < operator.

  • Criteria lessThanOrEquals (Object o): Creates a criterion by using the operator.

  • Criteria like (Object o): Creates a criterion by using the LIKE operator without escape character processing.

  • Criteria not (Object o): Creates a criterion by using the != operator.

  • Criteria notIn (Object…​ o): Creates a criterion by using the NOT IN operator for a varargs argument.

  • Criteria notIn (Collection<?> collection): Creates a criterion by using the NOT IN operator using a collection.

你可以对 SELECTUPDATEDELETE 查询使用 Criteria

You can use Criteria with SELECT, UPDATE, and DELETE queries.

Inserting Data

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

You can use the insert() entry point to insert data.

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

Consider the following simple typed insert operation:

Unresolved include directive in modules/ROOT/pages/r2dbc/entity-persistence.adoc - include::example$r2dbc/R2dbcEntityTemplateSnippets.java[]
1 Using Person with the into(…) method sets the INTO table, based on mapping metadata. It also prepares the insert statement to accept Person objects for inserting.
2 Provide a scalar Person object. Alternatively, you can supply a Publisher to run a stream of INSERT statements. This method extracts all non-null values and inserts them.

Updating Data

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

You can use the update() entry point to update rows. Updating data starts by specifying the table to update by accepting Update specifying assignments. It also accepts Query to create a WHERE clause.

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

Consider the following simple typed update operation:

Person modified = …

Unresolved include directive in modules/ROOT/pages/r2dbc/entity-persistence.adoc - include::example$r2dbc/R2dbcEntityTemplateSnippets.java[]
1 Update Person objects and apply mapping based on mapping metadata.
2 Set a different table name by calling the inTable(…) method.
3 Specify a query that translates into a WHERE clause.
4 Apply the Update object. Set in this case age to 42 and return the number of affected rows.

Deleting Data

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

You can use the delete() entry point to delete rows. Removing data starts with a specification of the table to delete from and, optionally, accepts a Criteria to create a WHERE clause.

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

Consider the following simple insert operation:

Unresolved include directive in modules/ROOT/pages/r2dbc/entity-persistence.adoc - include::example$r2dbc/R2dbcEntityTemplateSnippets.java[]
1 Delete Person objects and apply mapping based on mapping metadata.
2 Set a different table name by calling the from(…) method.
3 Specify a query that translates into a WHERE clause.
4 Apply the delete operation and return the number of affected rows.

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

Using Repositories, saving an entity can be performed with the ReactiveCrudRepository.save(…) method. If the entity is new, this results in an insert for the entity.

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

If the entity is not new, it gets updated. Note that whether an instance is new is part of the instance’s state.

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

This approach has some obvious downsides. If only few of the referenced entities have been actually changed, the deletion and insertion is wasteful. While this process could and probably will be improved, there are certain limitations to what Spring Data R2DBC can offer. It does not know the previous state of an aggregate. So any update process always has to take whatever it finds in the database and make sure it converts it to whatever is the state of the entity passed to the save method.

ID Generation

ID Generation

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

Spring Data uses the identifer property to identify entities. The ID of an entity must be annotated with Spring Data’s @Id annotation.

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

When your database has an auto-increment column for the ID column, the generated value gets set in the entity after inserting it into the database.

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

Spring Data does not attempt to insert values of identifier columns when the entity is new and the identifier value defaults to its initial value. That is 0 for primitive types and null if the identifier property uses a numeric wrapper type such as Long.

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

Entity State Detection explains in detail the strategies to detect whether an entity is new or whether it is expected to exist in your database.

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

One important constraint is that, after saving an entity, the entity must not be new anymore. Note that whether an entity is new is part of the entity’s state. With auto-increment columns, this happens automatically, because the ID gets set by Spring Data with the value from the ID column.

Optimistic Locking

Spring Data 通过聚合根上使用 @Version注释的数字属性来支持乐观锁定。每当 Spring Data 保存具有此类版本属性的聚合时,都会发生两件事:

Spring Data supports optimistic locking by means of a numeric attribute that is annotated with @Version on the aggregate root. Whenever Spring Data saves an aggregate with such a version attribute two things happen:

  • The update statement for the aggregate root will contain a where clause checking that the version stored in the database is actually unchanged.

  • If this isn’t the case an OptimisticLockingFailureException will be thrown.

另外,版本属性在实体和数据库中都会增加,因此,并发操作会注意到更改,并在适用的情况下抛出`OptimisticLockingFailureException` ,如上所述。

Also, the version attribute gets increased both in the entity and in the database so a concurrent action will notice the change and throw an OptimisticLockingFailureException if applicable as described above.

此过程也适用于插入新聚合,其中 null0 版本指示一个新实例,而增加的实例随后将实例标记为不再是新的,这在对象构造期间生成 id 的情况下可以很好地发挥作用,例如在使用 UUID 时。

This process also applies to inserting new aggregates, where a null or 0 version indicates a new instance and the increased instance afterwards marks the instance as not new anymore, making this work rather nicely with cases where the id is generated during object construction for example when UUIDs are used.

在删除期间,版本检查也会适用,但不会增加版本。

During deletes the version check also applies but no version is increased.

@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 Initially insert row. version is set to 0.
2 Load the just inserted row. version is still 0.
3 Update the row with version = 0.Set the lastname and bump version to 1.
4 Try to update the previously loaded row that still has version = 0.The operation fails with an OptimisticLockingFailureException, as the current version is 1.