持久化实体
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);
可用的插入和更新操作如下:
-
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);
流式 API
本节介绍流式 API 的用法。请看以下简单查询:
Flux<Person> people = template.select(Person.class) 1
.all(); 2
将
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
按名称从表中查询会使用给定的领域类型返回行结果。
发出的查询在
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 运算符:
-
Criteriaand(String column): 将带有指定property的链式Criteria添加到当前Criteria,并返回新创建的Criteria。 -
Criteriaor(String column): 将带有指定property的链式Criteria添加到当前Criteria,并返回新创建的Criteria。 -
CriteriagreaterThan(Object o): 使用>运算符创建一个条件。 -
CriteriagreaterThanOrEquals(Object o): 使用>=运算符创建一个条件。 -
Criteriain(Object… o): 对可变参数使用IN运算符创建一个条件。 -
Criteriain(Collection<?> collection): 使用集合,通过IN运算符创建一个条件。 -
Criteriais(Object o): 使用列匹配(property = value)创建一个条件。 -
CriteriaisNull(): 使用IS NULL运算符创建一个条件。 -
CriteriaisNotNull(): 使用IS NOT NULL运算符创建一个条件。 -
CriterialessThan(Object o): 使用<运算符创建一个条件。 -
CriterialessThanOrEquals(Object o): 使用⇐运算符创建一个条件。 -
Criterialike(Object o): 使用LIKE运算符创建一个条件,不进行转义字符处理。 -
Criterianot(Object o): 使用!=运算符创建一个条件。 -
CriterianotIn(Object… o): 对可变参数使用NOT IN运算符创建一个条件。 -
CriterianotIn(Collection<?> collection): 使用集合,通过NOT IN运算符创建一个条件。您可以将Criteria用于SELECT、UPDATE和DELETE查询。
插入数据
您可以使用 insert() 入口来插入数据。
请看以下简单的带类型插入操作:
Mono<Person> insert = template.insert(Person.class) 1
.using(new Person("John", "Doe")); 2
将
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
更新
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);
更新
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
删除
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>
初始插入一行。
version被设置为0。加载刚插入的行。
version仍为0。更新
version = 0的行。设置lastname并将version提升为1。尝试更新先前加载的、仍为
version = 0的行。由于当前version为1,该操作以OptimisticLockingFailureException失败。