持久化实体
R2dbcEntityTemplate
是 Spring Data R2DBC 的核心入口点。它提供直接面向实体的方法和更窄、更流畅的接口,用于典型的临时用例,例如查询、插入、更新和删除数据。
入口点 (insert()
、select()
、update()
等) 遵循基于要运行的操作的自然命名方案。从入口点开始,API 的设计只提供上下文相关的方法,这些方法会导致创建和运行 SQL 语句的终止方法。Spring Data R2DBC 使用 R2dbcDialect
抽象来确定绑定标记、分页支持以及底层驱动程序原生支持的数据类型。
所有终止方法始终返回表示所需操作的 Publisher 类型。实际语句在订阅时发送到数据库。 |
插入和更新实体的方法
R2dbcEntityTemplate
上有几个方便的方法可以保存和插入对象。为了更精细地控制转换过程,您可以使用 R2dbcCustomConversions
注册 Spring 转换器,例如 Converter<Person, OutboundRow>
和 Converter<Row, Person>
。
使用保存操作的简单情况是保存 POJO。在这种情况下,表名由类的名称 (非全限定名) 确定。您也可以使用特定的集合名称调用保存操作。您可以使用映射元数据来覆盖存储对象的集合。
插入或保存时,如果未设置 Id
属性,则假设其值将由数据库自动生成。因此,对于自动生成,类中 Id
属性或字段的类型必须是 Long
或 Integer
。
以下示例显示如何插入一行并检索其内容
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);
可以使用以下插入和更新操作
还可以使用类似的插入操作集
-
Mono<T>
insert(T objectToSave)
:将对象插入默认表。 -
Mono<T>
update(T objectToSave)
:将对象插入默认表。
可以使用流畅的 API 自定义表名。
选择数据
R2dbcEntityTemplate
上的 select(…)
和 selectOne(…)
方法用于从表中选择数据。这两种方法都采用 Query
对象,该对象定义字段投影、WHERE
子句、ORDER BY
子句和 limit/offset 分页。无论底层数据库如何,limit/offset 功能对应用程序都是透明的。此功能由 R2dbcDialect
抽象 支持,以适应各个 SQL 方言之间的差异。
R2dbcEntityTemplate
选择实体Flux<Person> loaded = template.select(query(where("firstname").is("John")),
Person.class);
流畅的 API
本节解释了流畅的 API 用法。考虑以下简单的查询
Flux<Person> people = template.select(Person.class) (1)
.all(); (2)
1 | 使用 Person 和 select(…) 方法将表格结果映射到 Person 结果对象。 |
2 | 获取 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)
1 | 按名称从表中选择将使用给定的域类型返回行结果。 |
2 | 发出的查询在 firstname 和 lastname 列上声明一个 WHERE 条件来筛选结果。 |
3 | 结果可以按各个列名排序,从而产生 ORDER BY 子句。 |
4 | 选择一个结果只会获取一行。这种使用行的方法期望查询返回恰好一个结果。如果查询产生多个结果,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 风格允许您将多个方法链接在一起,同时拥有易于理解的代码。为了提高可读性,您可以使用静态导入,让您避免使用“new”关键字来创建 Criteria
实例。
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)
1 | 使用Person 和into(…) 方法设置INTO 表,基于映射元数据。它还准备插入语句以接受Person 对象进行插入。 |
2 | 提供一个标量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)
1 | 更新Person 对象并根据映射元数据应用映射。 |
2 | 通过调用inTable(…) 方法设置不同的表名。 |
3 | 指定转换为WHERE 子句的查询。 |
4 | 应用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)
1 | 删除Person 对象并根据映射元数据应用映射。 |
2 | 通过调用from(…) 方法设置不同的表名。 |
3 | 指定转换为WHERE 子句的查询。 |
4 | 应用删除操作并返回受影响的行数。 |
使用存储库,可以使用ReactiveCrudRepository.save(…)
方法保存实体。如果实体是新的,则会导致插入实体。
如果实体不是新的,则会更新它。请注意,实例是否为新实例是实例状态的一部分。
这种方法有一些明显的缺点。如果只有少数被引用实体发生了实际更改,则删除和插入是浪费的。虽然这个过程可以而且可能会改进,但是Spring Data R2DBC所能提供的功能有一定的限制。它不知道聚合体的先前状态。因此,任何更新过程都必须获取数据库中找到的内容,并确保将其转换为传递给save方法的实体的状态。 |
ID生成
Spring Data使用标识符属性来标识实体。实体的ID必须用Spring Data的@Id
注解进行注解。
当您的数据库为ID列具有自动递增列时,生成的会在将其插入数据库后设置在实体中。
当实体是新的并且标识符值默认为其初始值时,Spring Data不会尝试插入标识符列的值。对于原始类型,它是0
;如果标识符属性使用数字包装器类型(例如Long
),则为null
。
实体状态检测详细解释了检测实体是新的还是预期存在于数据库中的策略。
一个重要的约束是,保存实体后,实体不能再是新的了。请注意,实体是否为新实体是实体状态的一部分。对于自动递增列,这是自动发生的,因为ID由Spring Data使用ID列中的值进行设置。
乐观锁
Spring Data通过使用用@Version
注解的聚合根上的数值属性来支持乐观锁。每当Spring Data保存具有此类版本属性的聚合体时,就会发生两件事
-
聚合根的更新语句将包含一个where子句,用于检查数据库中存储的版本实际上是否未更改。
-
如果不是这种情况,则会抛出
OptimisticLockingFailureException
。
此外,版本属性会在实体和数据库中都增加,因此并发操作会注意到更改,如果适用,则会抛出OptimisticLockingFailureException
,如上所述。
此过程也适用于插入新的聚合体,其中null
或0
版本表示新实例,而后续增加的实例将实例标记为不再是新的,这与例如使用UUID时在对象构造期间生成id的情况非常有效。
在删除期间,版本检查也适用,但不会增加版本。
@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 。 |