持久化实体
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 运算符
-
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运算符的条件。您可以在SELECT、UPDATE和DELETE查询中使用Criteria。
插入数据
您可以使用 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 | 应用删除操作并返回受影响的行数。 |
使用 Repositories,保存实体可以通过 ReactiveCrudRepository.save(…) 方法执行。如果实体是新的,这将导致对实体进行插入。
如果实体不是新的,它将被更新。请注意,实例是否是新的是实例状态的一部分。
| 这种方法有一些明显的缺点。如果只有少数引用的实体实际发生了更改,那么删除和插入是浪费的。虽然这个过程可以并且可能会得到改进,但 Spring Data R2DBC 能提供的功能是有限的。它不知道聚合的先前状态。因此,任何更新过程都必须始终获取数据库中找到的任何内容,并确保将其转换为传递给 save 方法的实体状态。 |
ID 生成
Spring Data 使用标识符属性来识别实体。即,查找它们或创建针对特定行的语句。实体的 ID 必须使用 Spring Data 的 @Id 注解进行标注。
当您的数据库为 ID 列具有自增列时,在将实体插入数据库后,生成的值将设置在实体中。
如果您还使用 @Sequence 注解标识符属性,如果底层 Dialect 支持序列,将使用数据库序列获取 ID 的值。
否则,当实体是新实体且标识符值默认为其初始值时,Spring Data 不会尝试插入标识符列的值。即,对于原始类型为 0,如果标识符属性使用数字包装类型(如 Long),则为 null。
实体状态检测 详细解释了检测实体是新实体还是预期存在于数据库中的策略。
一个重要的限制是,保存实体后,实体不能再是新的。请注意,实体是否是新的是实体状态的一部分。对于自增列,这会自动发生,因为 ID 会被 Spring Data 用 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)
| 1 | 初始插入行。version 设置为 0。 |
| 2 | 加载刚刚插入的行。version 仍然是 0。 |
| 3 | 使用 version = 0 更新行。设置 lastname 并将 version 提升到 1。 |
| 4 | 尝试更新先前加载的仍然具有 version = 0 的行。操作因 OptimisticLockingFailureException 而失败,因为当前 version 是 1。 |