https://docs.spring.io/spring-framework/reference/data-access/r2dbc.html
R2DBC(“Reactive Relational Database Connectivity”)是一项由社区推动的规范制定工作,旨在使用响应式模式标准化对SQL数据库的访问。
包层次结构(Package Hierarchy)
Spring框架的R2DBC抽象框架由两个不同的包组成:
core
:org.springframework.r2dbc.core
包包含DatabaseClient
类以及各种相关的类。connection
:org.springframework.r2dbc.connection
包包含了一个用于简化ConnectionFactory
访问的工具类,以及多种简单的ConnectionFactory
实现,你可以使用它们进行测试和运行未经修改的R2DBC。
使用R2DBC核心类来控制基本的R2DBC处理和错误处理
使用DatabaseClient
DatabaseClient
是R2DBC核心包中的核心类。它负责资源的创建和释放,有助于避免常见错误,例如忘记关闭连接。它执行R2DBC工作流程的基本任务(如语句的创建和执行),而将提供SQL和提取结果的工作留给应用程序代码。DatabaseClient
类具有以下功能:
- 执行SQL查询
- 更新语句和存储过程调用
- 遍历
Result
实例 - 捕获R2DBC异常,并将其转换为
org.springframework.dao
包中定义的通用、更具信息性的异常层次结构。
客户端具有一个功能性的、流畅的API,它使用响应式类型来进行声明式组合。
当你在自己的代码中使用DatabaseClient
时,只需要实现java.util.function
接口,并为它们提供一个明确定义的接口。给定由DatabaseClient
类提供的Connection
,一个Function
回调函数会创建一个Publisher
。同样地,用于提取Row
结果的映射函数也是如此。
可以通过直接实例化并引用ConnectionFactory
在DAO实现中使用DatabaseClient
,或者可以在Spring IoC容器中配置它,并将其作为bean引用传递给DAO。
创建DatabaseClient
对象的最简单方法是通过静态工厂方法,如下所示:
DatabaseClient client = DatabaseClient.create(connectionFactory);
ConnectionFactory
应该始终在Spring IoC容器中配置为bean。
上述方法会创建一个具有默认设置的DatabaseClient
。
还可以通过DatabaseClient.builder()
获取一个Builder
实例。通过调用以下方法来定制客户端:
....bindMarkers(…)
: 提供一个特定的BindMarkersFactory
来配置命名参数到数据库绑定标记的翻译。....executeFunction(…)
: 设置ExecuteFunction
来决定如何运行Statement
对象。....namedParameters(false):
禁用命名参数扩展。默认情况下是启用的。
方言(Dialects )是通过BindMarkersFactoryResolver
从ConnectionFactory
解析的,通常是通过检查ConnectionFactoryMetadata
来完成的。
可以通过在META-INF/spring.factories
中注册一个实现org.springframework.r2dbc.core.binding.BindMarkersFactoryResolver$BindMarkerFactoryProvider
的类,让Spring自动发现你的BindMarkersFactory
。BindMarkersFactoryResolver
使用Spring的SpringFactoriesLoader
从类路径中发现绑定标记提供者实现。
目前支持的数据库有:
- H2
- MariaDB
- Microsoft SQL Server
- MySQL
- Postgres
这个类发出的所有SQL语句都会以DEBUG
级别记录在与客户端实例的完全限定类名(通常是DefaultDatabaseClient
)对应的类别下。此外,每次执行都会在响应式序列中注册一个检查点,以辅助调试。
以下部分提供了一些DatabaseClient
的使用示例。这些示例并没有穷尽DatabaseClient
所暴露的所有功能。
执行语句
DatabaseClient
提供了执行语句的基本功能。下面的示例展示了创建新表所需的最简单但功能完备的代码应包括哪些内容:
Mono<Void> completion = client.sql("CREATE TABLE person (id VARCHAR(255) PRIMARY KEY, name VARCHAR(255), age INTEGER);")
.then();
DatabaseClient
被设计用于方便、流畅的使用。它在执行规范的每个阶段都暴露了中间方法、延续方法和终止方法。上述示例使用了then()
方法来返回一个完成的Publisher
,该Publisher
会在查询(如果SQL查询包含多个语句,则为多个查询)完成后立即完成。
execute(…)
方法可以接受SQL查询字符串或一个查询的Supplier<String>
,以便在执行时推迟实际的查询创建。
查询(SELECT)
SQL查询可以通过Row
对象或受影响的行数返回值。DatabaseClient
可以根据发出的查询返回更新的行数或行本身。
以下查询从表中获取id
和name
列:
Mono<Map<String, Object>> first = client.sql("SELECT id, name FROM person")
.fetch().first();
以下查询使用了绑定变量:
Mono<Map<String, Object>> first = client.sql("SELECT id, name FROM person WHERE first_name = :fn")
.bind("fn", "Joe")
.fetch().first();
你可能已经注意到上面的示例中使用了fetch()
方法。fetch()
是一个延续操作符,它允许你指定要消费多少数据。
调用first()
方法会返回结果集中的第一行数据,并丢弃剩余的行。你可以使用以下操作符来消费数据:
first()
返回整个结果集的第一行数据。其Kotlin协程(轻量级线程)的变种被命名为awaitSingle()
,用于返回非空值;如果值是可选的,则使用awaitSingleOrNull()
。
one()
方法返回恰好一个结果,如果结果包含多行则会失败。在使用 Kotlin 协程时,可以使用awaitOne()
来获取恰好一个值,如果值可能为null
,则使用awaitOneOrNull()
。all()
方法返回结果集中的所有行。当使用 Kotlin 协程时,可以使用flow()
来获取一个数据流。rowsUpdated()
返回受影响的行数(插入/更新/删除计数)。其在Kotlin协程中的变体名为awaitRowsUpdated()
。
如果不指定进一步的映射细节,查询将返回表格形式的结果作为Map
,其键是不区分大小写的列名,这些列名映射到它们的列值。
你可以通过提供一个Function<Row, T>
来控制结果映射,该函数会被每一行调用,因此它可以返回任意值(单一值、集合和映射,以及对象)。
以下示例提取了 name
列并返回其值:
Flux<String> names = client.sql("SELECT name FROM person")
.map(row -> row.get("name", String.class))
.all();
或者,映射到单个值有一个快捷方式:
Flux names = client.sql(“SELECT name FROM person”)
.mapValue(String.class)
.all();
或者,可以映射到一个带有bean属性或记录组件的结果对象:
// assuming a name property on Person
Flux<Person> persons = client.sql("SELECT name FROM person")
.mapProperties(Person.class)
.all();
对于null怎么办?
关系型数据库的结果可能包含null
值。响应式流规范禁止发出null
值。这一要求规定了提取函数必须正确处理null
值。虽然你可以从Row
获取null
值,但绝不能发出一个null
值。必须将任何null
值包装在一个对象中(例如,对于单个值使用Optional
),以确保你的提取函数永远不会直接返回null
值。
使用DatabaseClient进行更新(插入、更新和删除)
修改语句的唯一区别是,这些语句通常不返回表格数据,因此你使用rowsUpdated()
来处理结果。
以下示例显示了一个返回更新行数的UPDATE
语句:
Mono<Integer> affectedRows = client.sql("UPDATE person SET first_name = :fn")
.bind("fn", "Joe")
.fetch().rowsUpdated();
绑定值到查询
典型的应用程序需要参数化的SQL语句来根据某些输入选择或更新行。这些通常是由WHERE
子句约束的SELECT
语句,或者接受输入参数的INSERT
和UPDATE
语句。如果参数没有被正确转义,参数化语句存在SQL注入的风险。DatabaseClient
利用R2DBC的绑定API来消除查询参数的SQL注入风险。你可以使用execute(…)
操作符提供一个参数化的SQL语句,并将参数绑定到实际的Statement
。然后,你的R2DBC驱动程序通过使用预处理语句和参数替换来运行该语句。
参数绑定支持两种绑定策略:
- 按索引,使用基于零的参数索引。
- 按名称,使用占位符名称。
以下示例显示了查询的参数绑定:
db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
.bind("id", "joe")
.bind("name", "Joe")
.bind("age", 34);
或者,可以传入一个名称和值的映射:
Map<String, Object> params = new LinkedHashMap<>();
params.put("id", "joe");
params.put("name", "Joe");
params.put("age", 34);
db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
.bindValues(params);
或者,可以传入一个带有bean属性或记录组件的参数对象:
// assuming id, name, age properties on Person
db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
.bindProperties(new Person("joe", "Joe", 34);
R2DBC原生绑定标记(R2DBC Native Bind Markers)
R2DBC使用依赖于实际数据库供应商的数据库原生绑定标记。例如,Postgres使用索引标记,如$1
、$2
、$n
。另一个例子是SQL Server,它使用以@
为前缀的命名绑定标记。
这与JDBC不同,JDBC要求使用?
作为绑定标记。在JDBC中,实际的驱动程序将?
绑定标记转换为数据库原生标记,作为它们语句执行的一部分。
Spring框架的R2DBC支持允许你使用原生绑定标记或带有:name
语法的命名绑定标记。
命名参数支持利用BindMarkersFactory
实例在查询执行时将命名参数扩展为原生绑定标记,这为你提供了在不同数据库供应商之间一定程度的查询可移植性。
查询预处理器将命名的Collection
参数展开为一系列绑定标记,以消除基于参数数量动态创建查询的需要。嵌套的对象数组被展开,以便使用(例如)选择列表。
请考虑以下查询:
SELECT id, name, state FROM table WHERE (name, age) IN (('John', 35), ('Ann', 50))
前面的查询可以参数化并按如下方式运行:
List<Object[]> tuples = new ArrayList<>();
tuples.add(new Object[] {"John", 35});
tuples.add(new Object[] {"Ann", 50});
client.sql("SELECT id, name, state FROM table WHERE (name, age) IN (:tuples)")
.bind("tuples", tuples);
使用选择列表依赖于供应商。
以下示例显示了使用IN
谓词的更简单变体:
client.sql("SELECT id, name, state FROM table WHERE age IN (:ages)")
.bind("ages", Arrays.asList(35, 50));
R2DBC本身不支持类似集合的值。然而,在上述示例中展开给定的List
对于Spring的R2DBC支持中的命名参数是有效的,例如用于如上所示的IN
子句中。但是,插入或更新数组类型的列(例如在Postgres中)需要由底层R2DBC驱动程序支持的数组类型:通常是Java数组,例如String[]
用于更新text[]
列。不要将Collection
或类似内容作为数组参数传递。
语句过滤器(Statement Filters)
有时你需要在语句实际运行之前对其进行微调。为此,可以向DatabaseClient
注册一个语句过滤器(StatementFilterFunction
),以在其执行过程中拦截和修改语句,如下例所示:
client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
.filter((s, next) -> next.execute(s.returnGeneratedValues("id")))
.bind("name", …)
.bind("state", …);
DatabaseClient
还公开了一个简化的filter(…)
重载,它接受一个Function<Statement, Statement>
:
client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
.filter(statement -> s.returnGeneratedValues("id"));
client.sql("SELECT id, name, state FROM table")
.filter(statement -> s.fetchSize(25));
StatementFilterFunction
实现允许过滤Statement
和Result
对象。
DatabaseClient 最佳实践
一旦配置完成,DatabaseClient
类的实例就是线程安全的。这一点很重要,因为这意味着你可以配置一个DatabaseClient
的单个实例,然后安全地将这个共享引用注入到多个DAO(或仓库)中。DatabaseClient
是有状态的,因为它维护着对ConnectionFactory
的引用,但这种状态不是会话状态。
使用DatabaseClient
类的一个常见做法是在Spring配置文件中配置一个ConnectionFactory
,然后将这个共享的ConnectionFactory
bean依赖注入到你的DAO类中。DatabaseClient
在ConnectionFactory
的setter中创建。这导致了类似以下的DAO:
public class R2dbcCorporateEventDao implements CorporateEventDao {
private DatabaseClient databaseClient;
public void setConnectionFactory(ConnectionFactory connectionFactory) {
this.databaseClient = DatabaseClient.create(connectionFactory);
}
// R2DBC-backed implementations of the methods on the CorporateEventDao follow...
}
显式配置的一种替代方法是使用组件扫描和注解支持进行依赖注入。在这种情况下,可以使用@Component
注解类(使其成为组件扫描的候选),并使用@Autowired
注解ConnectionFactory
setter方法。以下示例显示了如何执行此操作:
@Component
public class R2dbcCorporateEventDao implements CorporateEventDao {
private DatabaseClient databaseClient;
@Autowired
public void setConnectionFactory(ConnectionFactory connectionFactory) {
this.databaseClient = DatabaseClient.create(connectionFactory);
}
// R2DBC-backed implementations of the methods on the CorporateEventDao follow...
}
无论选择使用上述哪种模板初始化样式(或不使用),每次要运行SQL时都很少需要创建DatabaseClient
类的新实例。一旦配置完成,DatabaseClient
实例就是线程安全的。如果你的应用程序访问多个数据库,可能需要多个DatabaseClient
实例,这需要多个ConnectionFactory
,随后需要多个不同配置的DatabaseClient
实例。
检索自动生成的键
将行插入定义了自增或标识列的表中时,INSERT
语句可能会生成键。要完全控制要生成的列名,只需注册一个请求所需列的生成键的StatementFilterFunction
。
Mono<Integer> generatedId = client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
.filter(statement -> s.returnGeneratedValues("id"))
.map(row -> row.get("id", Integer.class))
.first();
// generatedId emits the generated key once the INSERT statement has finished
控制数据库连接
使用ConnectionFactory
Spring通过ConnectionFactory
获得到数据库的R2DBC连接。ConnectionFactory
是R2DBC规范的一部分,也是驱动程序的共同入口点。它允许容器或框架将连接池和事务管理问题隐藏在应用程序代码之外。
当你使用Spring的R2DBC层时,可以使用第三方提供的连接池实现来配置自己的连接池。一个流行的实现是R2DBC Pool(r2dbc-pool
)。Spring发行版中的实现仅供测试用途,不提供连接池功能。
要配置ConnectionFactory
:
- 像通常获取R2DBC
ConnectionFactory
一样,使用ConnectionFactory
获取连接。 - 提供一个R2DBC URL。
以下示例显示了如何配置ConnectionFactory
:
ConnectionFactory factory = ConnectionFactories.get("r2dbc:h2:mem:///test?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE");
使用ConnectionFactoryUtils
ConnectionFactoryUtils
类是一个方便而强大的辅助类,它提供了从ConnectionFactory
获取连接和关闭连接(如果需要)的静态方法。
它支持订阅者上下文绑定连接,例如R2dbcTransactionManager
。
使用SingleConnectionFactory
SingleConnectionFactory
类是DelegatingConnectionFactory
接口的一个实现,它封装了一个在每次使用后不会关闭的单个连接。
如果任何客户端代码在假设有连接池的情况下调用close
(例如在使用持久化工具时),你应该将suppressClose
属性设置为true
。此设置返回一个包装物理连接的关闭抑制代理。请注意,不再能将其转换为原生Connection
或类似对象。
SingleConnectionFactory
主要是一个测试类,如果R2DBC驱动程序允许,例如管道操作等特定需求,也可以使用它。与连接池的ConnectionFactory
相比,它一直重用相同的连接,避免了过多创建物理连接。
使用TransactionAwareConnectionFactoryProxy
TransactionAwareConnectionFactoryProxy
是目标ConnectionFactory
的代理。该代理包装目标ConnectionFactory
,以添加对Spring管理的事务的感知。
如果使用的R2DBC客户端没有与Spring的R2DBC支持集成,那么使用这个类是必需的。在这种情况下,仍然可以使用这个客户端,并同时让这个客户端参与Spring管理的事务。通常更可取的是将R2DBC客户端与适当访问ConnectionFactoryUtils
进行资源管理集成。
使用R2dbcTransactionManager
R2dbcTransactionManager
类是针对单个R2DBC ConnectionFactory
的ReactiveTransactionManager
实现。它将指定的ConnectionFactory
的R2DBC连接绑定到订阅者上下文,可能允许每个ConnectionFactory
有一个订阅者连接。
应用程序代码需要通过ConnectionFactoryUtils.getConnection(ConnectionFactory)
检索R2DBC连接,而不是使用R2DBC的标准ConnectionFactory.create()
。所有框架类(如DatabaseClient
)都隐式使用这种策略。如果不与事务管理器一起使用,查找策略的行为与ConnectionFactory.create()
完全相同,因此可以在任何情况下使用。