20220507 4. Data Access - Data Access with R2DBC

前言

文档地址

R2DBC(“Reactive Relational Database Connectivity”)是一项社区驱动的规范工作,旨在使用反应模式标准化对 SQL 数据库的访问。

相关依赖:

<!-- R2DBC -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>

包层次结构

Spring Framework 的 R2DBC 抽象框架由两个不同的包组成:

  • coreorg.springframework.r2dbc.core 包中包含 DatabaseClient 类以及很多相关类
  • connectionorg.springframework.r2dbc.connection 包中包含易于访问 ConnectionFactory 的实用程序类和可用于测试和运行未修改的 R2DBC 的各种简单 ConnectionFactory 实现。请参阅 控制数据库连接

使用 R2DBC 核心类来控制基本的 R2DBC 处理和错误处理

使用 DatabaseClient

DatabaseClient 是 R2DBC 核心包中的中心类。它处理资源的创建和释放,这有助于避免常见错误,例如忘记关闭连接。它执行核心 R2DBC 工作流的基本任务(例如语句创建和执行),让应用程序代码来提供 SQL 和提取结果。相应的,JdbcTemplate 是 JDBC 核心包中的中心类。

DatabaseClient 类提供以下功能:

  • 运行 SQL 查询
  • 更新语句和存储过程调用
  • Result 实例执行迭代
  • 捕获 R2DBC 异常并将它们转换为 org.springframework.dao 包中定义的通用的、信息更丰富的异常层次结构。(请参阅 一致的异常层次结构

客户端有一个功能性的、链式的 API,使用反应式类型进行声明性组合。

当你使用 DatabaseClient 时,你只需要实现 java.util.function 接口,给它们一个明确定义的契约。 DatabaseClient 提供的 ConnectionFunction 回调会创建一个 Publisher 。对于提取 Row 结果的映射函数也是如此。

您可以通过 ConnectionFactory 引用直接实例化在 DAO 实现中使用 DatabaseClient ,或者您可以在 Spring IoC 容器中配置它并将其作为 bean 引用提供给 DAO。

创建 DatabaseClient 对象最简单的方法是通过静态工厂方法,如下:

DatabaseClient client = DatabaseClient.create(connectionFactory);

ConnectionFactory 应始终被配置为 Spring IoC 容器的 bean 。

上述方法使用默认设置创建 DatabaseClient

您还可以从 DatabaseClient.builder() 获取 Builder 实例。您可以通过调用以下方法来自定义客户端:

  • ….bindMarkers(…) :提供特定的 BindMarkerFactory ,以将命名参数配置为数据库绑定标记转换
  • ….executeFunction(…) :设置 ExecuteFunctionStatement 对象如何运行
  • ….namedParameters(false) :禁用命名参数扩展。默认启用

方言由 BindMarkersFactoryResolverConnectionFactory 中解析,通常通过检查 ConnectionFactoryMetadata

您可以通过在 META-INF/spring.factories 注册一个实现 org.springframework.r2dbc.core.binding.BindMarkersFactoryResolver$BindMarkerFactoryProviderBindMarkersFactory 让 Spring 自动发现。 BindMarkersFactoryResolver 使用 Spring 的 SpringFactoriesLoader 从类路径中发现绑定标记提供程序实现。

目前支持的数据库有:

  • H2
  • MariaDB
  • Microsoft SQL Server
  • MySQL
  • Postgres

此类发出的所有 SQL 都被日志记录在与客户端实例的完全限定类名(通常为 DefaultDatabaseClient )对应的类别下的 DEBUG 级别 。此外,每次执行都会在反应序列中注册一个检查点以帮助调试。

以下部分提供了一些 DatabaseClient 使用示例

执行语句(Statements)

DatabaseClient 提供运行语句的基本功能。以下示例创建新表:

Mono<Void> completion = client.sql("CREATE TABLE person (id VARCHAR(255) PRIMARY KEY, name VARCHAR(255), age INTEGER);")
        .then();

DatabaseClient 专为方便、链式的使用而设计。它在执行规范的每个阶段公开中间方法、连续方法和终止方法。上面的示例 then() 用于返回在查询完成后立即返回完成的 Publisher

execute(…) 接受 SQL 查询字符串或查询 Supplier<String> 以将实际查询的创建推迟到执行

查询 ( SELECT )

SQL 查询可以通过 Row 对象或受影响的行数返回值。 DatabaseClient 可以返回更新的行数或行本身,具体取决于发出的查询。

以下查询从表中获取 idname 列:

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() 返回整个结果的第一行
  • one() 只返回一个结果,如果结果包含更多行,则失败
  • all() 返回结果的所有行
  • rowsUpdated() 返回受影响的行的数量 ( INSERT / UPDATE / DELETE

无需指定进一步的映射详细信息,查询将返回表格结果 Map ,其键是映射到其列值的不区分大小写的列名称。

您可以通过提供一个为每行 Row 调用的 Function<Row, T> 来控制结果映射,以便它可以返回任意值(单一值、集合和映射以及对象)。

以下示例提取 id 列并发出其值:

Flux<String> names = client.sql("SELECT name FROM person")
        .map(row -> row.get("id", String.class))
        .all();

关系数据库结果可以包含 null 值。Reactive Streams 规范禁止发出 null 值。要求在提取器函数中进行适当 null 处理。虽然您可以从 Row 中获取 null ,但您不能发出 null 。您必须将任何 null 值包装在对象中(例如,对于单一值使用 Optional ),以确保提取器函数永远不会直接返回 null 值。

使用 DatabaseClient 更新 ( INSERT , UPDATEDELETE )

修改语句的唯一区别是这些语句通常不返回表格数据,因此您可以使用 rowsUpdated() 获取结果。

以下示例显示了一个返回更新行数的 UPDATE 语句:

Mono<Integer> affectedRows = client.sql("UPDATE person SET first_name = :fn")
        .bind("fn", "Joe")
        .fetch().rowsUpdated();
将值绑定到查询

典型的应用程序需要参数化的 SQL 语句来根据某些输入选择或更新行。这些语句通常是受 WHERE 子句约束的 SELECT 语句或接受输入参数的 INSERTUPDATE 语句。如果参数没有正确转义,参数化语句将承担 SQL 注入的风险。DatabaseClient 利用 R2DBC 的 bind API 来消除查询参数的 SQL 注入风险。您可以提供带有 execute(…) 操作符的参数化 SQL 语句,并将参数绑定到实际的 Statement 。然后,您的 R2DBC 驱动程序使用准备好的语句和参数替换来运行该语句。

参数绑定支持两种绑定策略:

  • 按索引,使用从 0 开始的参数索引
  • 按名称,使用占位符名称

以下示例显示了查询的参数绑定:

db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
    .bind("id", "joe")
    .bind("name", "Joe")
    .bind("age", 34);

R2DBC 原生绑定标记

R2DBC 使用依赖于实际数据库供应商的数据库原生绑定标记。例如,Postgres 使用索引标记,例如 $1$2$n 。另一个例子是 SQL Server,它使用以 @ 为前缀的命名绑定标记。

这与需要 ? 绑定标记的 JDBC 不同。在 JDBC 中,实际驱动程序将 ? 绑定标记转换为数据库原生标记,作为其语句执行的一部分。

Spring Framework 的 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<String> 作为数组参数传递。

语句过滤器

有时,您需要在实际 Statement 运行之前微调选项。通过 DatabaseClient 注册 Statement 过滤器 ( 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 类中。

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 实例。

posted @ 2022-06-09 21:22  流星<。)#)))≦  阅读(198)  评论(0编辑  收藏  举报