Spring Framework:使用R2DBC进行数据访问

https://docs.spring.io/spring-framework/reference/data-access/r2dbc.html

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

包层次结构(Package Hierarchy)

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

  • coreorg.springframework.r2dbc.core包包含DatabaseClient类以及各种相关的类。
  • connectionorg.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 )是通过BindMarkersFactoryResolverConnectionFactory解析的,通常是通过检查ConnectionFactoryMetadata来完成的。
可以通过在META-INF/spring.factories中注册一个实现org.springframework.r2dbc.core.binding.BindMarkersFactoryResolver$BindMarkerFactoryProvider的类,让Spring自动发现你的BindMarkersFactoryBindMarkersFactoryResolver使用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可以根据发出的查询返回更新的行数或行本身。

以下查询从表中获取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() 返回整个结果集的第一行数据。其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语句,或者接受输入参数的INSERTUPDATE语句。如果参数没有被正确转义,参数化语句存在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实现允许过滤StatementResult对象。

DatabaseClient 最佳实践

一旦配置完成,DatabaseClient类的实例就是线程安全的。这一点很重要,因为这意味着你可以配置一个DatabaseClient的单个实例,然后安全地将这个共享引用注入到多个DAO(或仓库)中。DatabaseClient是有状态的,因为它维护着对ConnectionFactory的引用,但这种状态不是会话状态。

使用DatabaseClient类的一个常见做法是在Spring配置文件中配置一个ConnectionFactory,然后将这个共享的ConnectionFactory bean依赖注入到你的DAO类中。DatabaseClientConnectionFactory的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 ConnectionFactoryReactiveTransactionManager实现。它将指定的ConnectionFactory的R2DBC连接绑定到订阅者上下文,可能允许每个ConnectionFactory有一个订阅者连接。

应用程序代码需要通过ConnectionFactoryUtils.getConnection(ConnectionFactory)检索R2DBC连接,而不是使用R2DBC的标准ConnectionFactory.create()。所有框架类(如DatabaseClient)都隐式使用这种策略。如果不与事务管理器一起使用,查找策略的行为与ConnectionFactory.create()完全相同,因此可以在任何情况下使用。

  • 9
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Data R2DBCSpring框架为响应式数据库操作提供的模块。R2DBC是Reactive Relational Database Connectivity的缩写,它是一种基于响应式编程模型的数据访问方式。相比传统的阻塞式JDBC,R2DBC允许在异步非阻塞的环境下进行数据库操作。 使用Spring Data R2DBC,你可以通过定义接口和方法来实现对响应式数据库的操作。它提供了一套统一的API,支持常见的关系型数据库(如MySQL、PostgreSQL等)。 以下是一个简单的示例,展示了如何使用Spring Data R2DBC进行数据库操作: ```java import org.springframework.data.annotation.Id; import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.Table; @Table("users") public class User { @Id private Long id; @Column("name") private String name; // Getters and setters } @Repository public interface UserRepository extends ReactiveCrudRepository<User, Long> { Flux<User> findByName(String name); } @Service public class UserService { private final UserRepository userRepository; public UserService(UserRepository userRepository) { this.userRepository = userRepository; } public Flux<User> findUsersByName(String name) { return userRepository.findByName(name); } } ``` 在上述示例中,我们定义了一个User实体类,并使用@Table和@Id注解进行映射。然后创建了一个UserRepository接口,继承自ReactiveCrudRepository,这个接口提供了一些基本的CRUD方法,并支持响应式的返回类型。最后,在UserService中使用UserRepository进行数据库操作。 通过Spring Data R2DBC,你可以使用像Flux和Mono这样的响应式类型来处理数据流,以实现异步非阻塞的数据库操作。这使得你可以更好地利用系统资源,提高系统的响应性能。 希望这个简单的示例能够帮助你理解Spring Data R2DBC的基本用法。如果需要更详细的信息,可以查阅Spring官方文档或参考其他资源。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值