R2DBC-揭秘-全-

R2DBC 揭秘(全)

原文:R2DBC Revealed

协议:CC BY-NC-SA 4.0

一、反应式编程的案例

在软件工程界,时髦的技术是再熟悉不过的了。多年来,一些最具创造力的头脑给了我们开发创新,彻底改变了我们解决方案的面貌。另一方面,从长远来看,一些趋势性技术给我们带来了更多的痛苦。所有的炒作可能很难评估。

在第一章中,我想介绍一下几年来越来越受欢迎的反应式编程的范例,并帮助为我们进入反应式关系数据库连接(R2DBC)的想法奠定基础。

既然你正在读这本书,我将假设你在这之前可能至少听过或读过“反应式”和“编程”这两个词。见鬼,我甚至可以大胆地猜测,你可能听说过或读到过,为了创造一个真正的反应式解决方案,你需要反应式地思考。但是所有这些意味着什么呢?好吧,让我们来看看!

传统的方法

引入新技术可能具有挑战性,但我发现它有助于识别我们作为开发人员遇到的常见的真实用例,并了解它如何适应该环境。

想象一个包含客户端应用和服务器应用之间的 web 请求工作流的基本解决方案,让我们从客户端向服务器发起异步请求的地方开始。

Note

当某件事情发生异步时,这意味着在同一个系统内相互作用的两个或多个事件/对象之间存在关系,但不是以预定的间隔发生,也不一定依赖于彼此的存在才能发挥作用。更简单的说,就是相互不协调,同时发生的事件。

收到请求后,服务器启动一个新线程进行处理(图 1-1 )。

img/504354_1_En_1_Fig1_HTML.png

图 1-1

从客户端到服务器执行简单的同步 web 请求

很简单,而且可以运行,所以发货吧,对吗?别这么快!请求很少会如此简单。可能是这样的情况,如图 1-2 所示,服务器线程需要访问数据库来完成客户端请求的工作。但是,在访问数据库时,服务器线程会等待,从而阻止执行更多的工作,直到数据库返回响应。

img/504354_1_En_1_Fig2_HTML.png

图 1-2

当数据库返回响应时,服务器线程被阻止执行工作

不幸的是,这可能不是最佳解决方案,因为没有办法知道数据库调用需要多长时间。这不太可能扩大规模。因此,为了优化客户端的时间并保持其工作,您可以添加更多的线程来并行处理多个请求,如图 1-3 所示。

img/504354_1_En_1_Fig3_HTML.png

图 1-3

后续传入的请求使用附加线程进行处理

现在我们在做饭!您可以继续添加线程来处理额外的处理,对吗?没那么快。就像生活中的大多数事情一样,如果有些事情看起来好得不像真的,那么它很可能就是真的。添加额外线程的代价是令人讨厌的问题,如更高的资源消耗,可能导致吞吐量降低,并且在许多情况下,由于线程上下文管理,增加了开发人员的复杂性。

Note

对于许多应用,使用多线程将是一个可行的解决方案。反应式编程不是银弹。然而,反应式解决方案有助于更有效地利用资源。请继续阅读,了解如何操作!

命令式编程与声明式编程

使用多线程来防止阻塞操作是命令式编程范例和语言中的一种常见方法。这是因为命令式方法是一个过程,它一步一步地描述了一个程序为了完成一个特定的目标应该处于的状态。最终,命令式进程依赖于对信息流的控制,这在某些情况下非常有用,但是正如我之前提到的那样,也会给内存和线程管理带来相当大的麻烦。

另一方面,声明式编程并不关注如何实现一个特定的目标,而是关注目标本身。但这是相当模糊的,所以让我们后退一点。

考虑以下类比:

  • 命令式编程就像上美术课,听老师一步一步地指导你如何画风景。

  • 声明式编程就像上艺术课,被告知要画一幅风景。老师不在乎你怎么做,只在乎你把它做完。

Tip

命令式和声明式编程范例都有优点和缺点。像任何任务一样,确保你选择了正确的工具!

对于这本书,我将在所有的例子中使用 Java 编程语言。现在,众所周知,Java 是一种命令式语言,因此它的重点是如何实现最终结果。也就是说,我们很容易想象如何使用 Java 编写命令式指令,但您可能不知道的是,您也可以编写声明式流。例如,考虑以下情况。

这里有一个从 1 到 10 逐步求和一系列数字的必要方法:

int sum = 0;
for (int i = 1; i <= 10; i++) {
     sum += i;
}
System.out.println(sum); // 55

或者,对从 1 到 10 的一系列数字求和的声明性方法涉及处理数据流,并在未来某个未知或更确切地说未指定的时间接收结果:

int sumByStream = IntStream.rangeClosed(0,10).sum();
System.out.println(sumByStream); // 55

Note

在 Java 8 中,引入流是为了提高语言的声明性编程能力。IntStream rangeClosed(int startInclusive, int endInclusive)startInclusive(含)到endInclusive(含)以 1 为增量步长返回一个IntStream

然而,就目前而言,理解一个Stream对象或底层的IntStream专门化并不重要。不,现在,把“流”的想法放在一边;我们会回来的。

这里真正的要点是这两种方法产生相同的结果,但是声明性方法仅仅是为操作的结果设置一个期望,而不是规定底层的实现步骤。从根本上来说,这就是反应式编程的工作方式。还是有点朦胧?让我们潜入更深的地方。

反应性思维

正如我前面提到的,反应式编程的核心是声明性的。它旨在通过消除因必须维护大量线程而导致的许多问题来规避阻塞状态。最终,这是通过管理客户机和服务器之间的期望来实现的。

事实上,追求一种更具反应性的方法,在收到来自客户机的请求时,服务器线程调用数据库进行处理,但不等待响应。这释放了服务器线程来继续处理传入的请求。然后,在不确定的时间量之后,服务器线程从数据库接收并响应事件形式的响应。

img/504354_1_En_1_Fig4_HTML.png

图 1-4

客户端不等待来自服务器的直接响应

图 1-4 中显示的非阻塞和事件驱动行为是反应式编程的基础。

反动宣言

我知道你在想什么。反应式编程背后的总体概念并不新鲜,那么为什么要大肆宣传呢?嗯,它开始于反应系统的形式化。

2013 年,Reactive Manifesto 作为一种解决当时应用开发中新需求的方式而创建,并为讨论复杂概念(如 reactive programming)提供了一个通用词汇。然后,在 2014 年,通过 2.0 版本对其进行了修订,以更准确地反映反应式设计的核心价值。

反应系统

创建反应宣言是为了明确定义反应系统的四个目标,逐字引用如下:

  • 响应迅速: 系统尽可能及时响应。响应性是可用性和实用性的基石,但不仅如此,响应性意味着问题可以被快速检测到并得到有效处理。响应式系统专注于提供快速一致的响应时间,建立可靠的上限,以便提供一致的服务质量。这种一致的行为反过来简化了错误处理,建立了最终用户的信心,并鼓励进一步的互动。

  • 有弹性: 面对失败,系统保持反应灵敏。这不仅适用于高可用性的任务关键型系统,任何不具备弹性的系统在出现故障后都将无法响应。复原力是通过复制、遏制、隔离和授权实现的。故障包含在每个组件中,将组件彼此隔离,从而确保系统的各个部分可以在不影响整个系统的情况下发生故障并恢复。每个组件的恢复被委托给另一个(外部)组件,并且在必要时通过复制来确保高可用性。组件的客户机没有处理其故障的负担。

  • 弹性: 系统在变化的工作负载下保持响应。反应式系统可以通过增加或减少分配给这些输入的 资源 来对输入速率的变化做出反应。这意味着没有争用点或中心瓶颈的设计,从而能够分割或复制组件并在它们之间分配输入。反应式系统通过提供相关的实时性能测量来支持预测以及反应式扩展算法。它们在商用硬件和软件平台上以经济高效的方式实现了灵活性。

  • Message Driven: Reactive Systems rely on asynchronous message-passing to establish a boundary between components that ensures loose coupling, isolation and location transparency. This boundary also provides the means to delegate failures as messages. Employing explicit message-passing enables load management, elasticity, and flow control by shaping and monitoring the message queues in the system and applying back-pressure when necessary. Location transparent messaging as a means of communication makes it possible for the management of failure to work with the same constructs and semantics across a cluster or within a single host. Non-blocking communication allows recipients to only consume resources while active, leading to less system overhead.

    img/504354_1_En_1_Fig5_HTML.jpg

    图 1-5

    反应宣言的四个原则

更简单地说,反应式系统是一种架构方法,它寻求将多个独立的解决方案组合成一个单一的、有凝聚力的单元,作为一个整体,它对周围的世界保持响应,或者说反应,同时这些解决方案保持相互了解。尽可能简单地说,反应式系统是指当系统中的一个单元遵循一套准则时,它对同一系统中的其他每个单元保持反应,而这些单元使用相同的准则,对外部系统集体反应。

反应系统!反应式编程

在这一点上,很容易混淆术语“反应式系统”和“反应式编程”,因为它们是可以互换的,但是需要注意的是,在解决方案中使用反应式编程并不意味着解决方案是反应式系统。

如前所述,Reactive Manifesto 在创建一年后进行了修订,其中一项更新是建立 Reactive 系统的核心租户之一,以使用异步消息传递。另一方面,反应式编程是事件驱动的

那么有什么区别呢?反应式系统依赖于消息的使用来为分布式系统创建弹性的解决方案(图 1-6 )。通常,消息都有一个目标。相比之下,事件在更小、更简洁的范围内使用,并且没有预期的目的地。

img/504354_1_En_1_Fig6_HTML.png

图 1-6

反应式系统与反应式编程

事件是一个组件在达到某个状态时发出的信号,任何连接的侦听器都可以观察到该信号。别担心。在接下来的章节中,我将深入探讨听众是如何观察事件的。

异步数据流

但是处理事件并不是一个新概念。事实上,用户界面事件,像按钮点击和各种其他控件交互,只不过是可以订阅、观察和响应的异步事件流。

数据流

类似地,反应式编程使用的数据流是按时间顺序排列的正在进行的事件序列(图 1-7 )。从数据流中可以观察到三种类型的事件:值、错误和完成信号。

img/504354_1_En_1_Fig7_HTML.png

图 1-7

数据流的剖析

流中发出的事件被异步观察,或者以间歇的时间间隔观察,这允许数据流订阅者依次响应,或者被动响应(图 1-8 )。

img/504354_1_En_1_Fig8_HTML.png

图 1-8

订阅观察器响应数据流中发出的事件

在这一点上,重要的是要注意数据流不仅限于发送用户界面事件。事实上,如果是这样的话,反应式编程作为一个整体就不会很有趣或者很有用。因此,顾名思义,如果它是数据,大多数东西都是数据,它可以在数据流中流动。这包括但不限于变量、用户输入值、属性、对象和数据结构。

如果这一切看起来有些熟悉,很可能是因为您在生活中的某个时刻已经了解了观察者设计模式。

Note

观察者设计模式被定义为对象之间的一对多关系,例如,如果一个对象被修改,它的依赖对象将被自动通知。

背压

数据流在发布者、发送数据者和订阅者之间的使用很容易理解。在一个完美的世界中,数据的流动将以同样的速度发生,其中元素、项目或基本上一些未知数量的数据以同样的速度发布和消费(图 1-9 )。

img/504354_1_En_1_Fig9_HTML.png

图 1-9

发布者和订阅者的理想状态

但是,因为我们并不是生活在一个完美的世界中,所以使用这种方法,数据的发送速率可能会高于订阅者能够处理的速率。如果发生这种情况,订户将需要创建一个待处理的工作积压(图 1-10 )。

img/504354_1_En_1_Fig10_HTML.png

图 1-10

如果发布者发出元素的速度比订阅者处理元素的速度快,就会产生未处理元素的积压

幸运的是,这个问题可以通过简单地允许订阅者与发布者进行沟通来解决,发布者已经准备好接收更多的元素。这种反馈过程被称为背压,它在促进有效的反应解决方案方面起着至关重要的作用。然而,这并不像只允许订阅者直接与发布者沟通那么简单,因为发布者可能无法以订阅者请求的速度发布数据,这可能会简单地将工作积压的问题转移给发布者(图 1-11 )。

img/504354_1_En_1_Fig11_HTML.png

图 1-11

背压的最简单实现

使用反压来有效地利用异步数据流在促进有效的反应式编程解决方案中起着关键作用。然而,这也不是一个要解决的小问题,但是,幸运的是,有多种方法可以实现背压。在下一章中,我将探索一个名为 Reactive Streams 的规范,R2DBC 用它来创建真正的反应式数据库通信。

摘要

在这一章中,你已经了解了什么是反应式编程,它什么时候有用,以及它是如何工作的。您已经对声明式编程如何帮助异步数据流创建非阻塞、反应式解决方案有了较高的理解。

在下一章中,我们将研究如何利用这些原则,通过使用反应式关系数据库连接(R2DBC)来促进与关系数据库的反应式交互。

二、R2DBC 简介

反应式编程已经成为应用开发的一个游戏变化,这一点也不奇怪。正如我在前一章所解释的,它对于创建非阻塞解决方案来帮助优化资源使用非常有用。但是为了让一个解决方案真正具有反应性,它必须无处不在,包括数据库交互。

毕竟,大多数应用都需要某种持久存储,许多应用使用关系数据库来完成这一任务。关系数据库已经存在了几十年,像 Java 数据库连接(Java Database Connectivity,JDBC)应用编程接口(Application Programming Interface,API)这样的用于连接和通信的技术也已经存在了很多年。正因为如此,在反应式解决方案日益流行之前创建的 JDBC API 在与数据库通信时使用了阻塞操作。

然而,正如我之前指出的,为了让一个解决方案真正具有反应性它需要如此普遍,即使是在处理数据库的时候。反应式编程的使用越来越多,而且大多数应用都使用关系数据库,这促使业界寻找一种解决方案来创建与关系数据库的反应式交互。

*## R2DBC 是什么?

创建反应式关系数据库连接(R2DBC)是为了在关系数据存储和使用反应式编程模型的系统之间架起一座桥梁。

新方法

在功能上,通过使用 R2DBC,用 Java 虚拟机(JVM)编程语言编写的应用可以运行结构化查询语言(SQL)语句,并从目标数据源检索结果,所有这些都是被动的。

这是可能的,因为 R2DBC 是一种新的开放规范,它提供了完全反应式编程 API 来连接关系数据存储并与之通信(图 2-1 )。

img/504354_1_En_2_Fig1_HTML.png

图 2-1

R2DBC 层次结构和工作流程

Note

一个有线协议 l 指的是一种从一点到另一点获取数据的方式,一种网络中一个或多个应用互操作的方式。它通常指高于物理层的协议。

最终,通过使用反应式编程范式的基本概念,R2DBC 消除了其关系数据库连接前辈的阻塞性质,如 JDBC(图 2-2 )。

img/504354_1_En_2_Fig2_HTML.png

图 2-2

JDBC 层次结构和工作流

超越 JDBC

但是为什么要采用全新的方法呢?您可能想知道,“难道不能做些什么来修改 JDBC API,使其反应性地工作吗?”答案是肯定的。这当然是可能的,但代价是什么?

创建新方法的决定可以追溯到 2017 年,当时 R2DBC 的创建者在 R2DBC 存在之前,渴望使用一种 Oracle 诞生的解决方案来被动处理关系数据库,称为异步数据库访问(ADBA),也称为“java.sql2”。理想情况下,该小组希望研究一种完全被动的 API,而不必承担与标准机构打交道的负担。然而,ADBA 的使用是短命的,因为在调查该方法时,该小组未能说服 Oracle 团队合并某些不可协商的架构更改。

Note

2017 年 9 月 18 日,在甲骨文 CodeOne 开发者大会上,甲骨文宣布他们将停止 ADBA(异步数据库访问)的工作。

考虑到他们在 ADBA 的经历,这个团队无法说服他们自己演进 JDBC 是正确的方法,而是更喜欢一个现代的、真正反应性的 API。结合快速创新周期和创建开放标准的优势,将 R2DBC 开发为独立的规范是最有意义的。新规范的创建也为实现者在技术方向和其中的依赖性上提供了更多的自由空间。

另外,如前所述,虽然 R2DBC 的主要焦点是关系数据库,但并不局限于此。相反,目的是将重点放在使用 SQL 或类似 SQL 的方言以表格格式表示数据的存储机制上。

R2DBC 实现

R2DBC 通过提供服务提供商接口(SPI)来工作。SPI 只是一组接口,通过定义用于被动处理关系数据库的基本元素,作为关系数据存储供应商实施的指南(图 2-3 )。

img/504354_1_En_2_Fig3_HTML.png

图 2-3

R2DBC SPI 定义的一些功能

在接下来的几章中,我将更深入地研究 SPI 中可用的特定接口,以及它们如何组合在一起使关系数据库交互真正具有反应性。

客户端库和应用可以使用 R2DBC 驱动程序实现,该驱动程序利用 SPI 来创建完全反应式解决方案(图 2-4 )。

img/504354_1_En_2_Fig4_HTML.png

图 2-4

R2DBC 驱动程序实现拓扑和工作流

拥抱反应式编程

最重要的是,R2DBC 规范的目标是提供一个 API,能够使用反应式编程模型促进与关系数据存储的集成。为了实现这一目标,该规范包含了反应式编程的关键属性,这些属性侧重于有效利用资源,包括以下内容:

  • 通过延迟和异步执行实现非阻塞 I/O

  • 使用背压来允许流量控制,推迟实际的执行,并且不会让消费者不知所措

  • 将应用控制视为一系列事件(数据、错误、完成)和面向流的数据消费

  • 不再承担对资源的控制,而是将资源调度留给运行时或平台

激发供应商的创造力

与 JDBC 不同,R2DBC API 旨在尽可能地轻量级,从而允许实现具有很大的灵活性。为了帮助实现这一目标,R2DBC 被构建为支持以 Java 为主要平台的反应式 JVM 平台,能够使用结构化查询语言(SQL)作为交互接口来访问数据。

虽然 SPI 还提供对许多不同供应商实施中常见功能的访问,但 R2DBC 对简单性的关注为供应商提供了很大的灵活性。因为每个数据库都有自己的特性,R2DBC 的目标是为常用的功能定义一个最低标准,并允许特定于供应商的偏差。

最终,R2DBC 的强大之处在于它能够在驱动程序中实现的特性和在客户端库中更好实现的特性之间提供平衡。

强制合规

R2DBC 驱动程序实现必须满足各种要求,并通过一系列测试,才能被官方认可。正如我前面提到的,R2DBC 的主要焦点是 SQL 与关系数据存储的使用;那只是冰山一角。

在众多实施要求中,SPI 必须实施非阻塞 I/O 层。

Stay Tuned

在第三章中,我将详细介绍 R2DBC 的合规性要求。

在下一节中,您将更好地理解 Reactive Streams API 如何支持无阻塞背压感知数据访问。

反应流

之前,我介绍了反应式编程的概念,以及在异步数据流的帮助下使用背压来帮助调节数据流。回想一下,背压的概念围绕着限制在传输管道的各个阶段之间传输的数据量,以便数据移动过程中的任何阶段都不会不堪重负。

Reactive Streams 是一项倡议,它提供了一个标准,通过许多接口定义,用于管理具有非阻塞背压的异步流处理。

另一个规格

最重要的是,要知道 Reactive Streams 标准化了异步数据流的使用,确保接收端不会被迫缓冲或积压任意数量的数据。事实上,关键目标是以异步方式强制使用背压信号,以确保反应流实现的完全异步、无阻塞行为。

反应流仅仅是一个规范,因此,它的目的是创建符合规范的实现。这意味着流操作的各种选项,比如转换、分割和合并,不是由规范本身来处理的。相反,Reactive Streams 只关心调节底层 API 组件之间的数据流或工作流。

与 R2DBC SPI(我将在下一章详细讨论)类似,Reactive Streams 规范提供了一个标准的测试套件——技术兼容性工具包(TCK ),用于测试实现的兼容性。虽然实现可以根据规范自由地实现额外的特性,但是它们必须符合所有的反应流 API 要求,并通过 TCK 中的所有测试。

API 基础

API 由以下组件组成,这些组件需要由 Reactive Streams 实现提供:

  1. 出版者

  2. 订户

  3. 签署

  4. 处理器

订阅者的角色是让发布者知道它已经准备好接受大量的项目,如果项目可用,发布者会推送尽可能多的项目,直到请求的最大值(图 2-5 )。

img/504354_1_En_2_Fig5_HTML.png

图 2-5

订阅者向发布者请求项目

这应该看起来很熟悉,因为正如我在上一章(图 1-11)中描述的,限制订户愿意接受的项目数量的过程,正如订户自己指出的,被称为背压

反应流 API 在订阅者和发布者之间建立双向连接作为订阅。订阅表示订阅发布者的订阅者的一对一生命周期(图 2-6 )。

img/504354_1_En_2_Fig6_HTML.png

图 2-6

发布者和订阅者之间的订阅

虽然一个发布者可以有多个订阅者,但是一个订阅者只能订阅一个发布者(图 2-7 )。

img/504354_1_En_2_Fig7_HTML.png

图 2-7

一个发布者可以有多个订阅者

订阅者订阅发布者后,发布者会通知订阅者已创建的订阅。那么订户可以自由请求 n 个项目。

一旦出版商有可用的项目,它最多只能向订阅者发送 n 个项目。如果在任何时候,出版者内部发生错误,它就发出了一个错误的信号。当发布者完成发送数据时,它会向订阅者发出信号,表示它完成了

*img/504354_1_En_2_Fig8_HTML.png

图 2-8

反应流订阅工作流

处理器

既作为发布者又作为订阅者存在的实体被称为处理器。处理器通常被用作发布者和订阅者之间的中介,以处理数据流上的转换,如数据过滤(图 2-9 )。

img/504354_1_En_2_Fig9_HTML.png

图 2-9

在发布者和订阅者之间使用的处理器

JVM 接口

如前一节所述,Reactive Streams 由四个主要实体组成,发布者订阅者订阅者处理器,它们作为接口存在,用于在 JVM 中创建实现库。

发布者接口只允许订阅者通过一个公开的名为subscribe的方法订阅发布者。通用类型 T 用于表示发行商生产的项目类型:

public interface Publisher<T> {
    public void subscribe(Subscriber<? super T> s);
}

用户界面需要四种交互方法:

  1. onSubscribe:用于通知用户订阅成功

  2. onNext:接受发布者推送的项目

  3. onError:接受来自发布者的错误通知

  4. onComplete:接受来自发布者的完成信号

    public interface Subscriber<T> {
        public void onSubscribe(Subscription s);
        public void onNext(T t);
        public void onError(Throwable t);
        public void onComplete();
    }
    
    

订阅需要两种交互方式:

  1. request:接受来自用户的项目请求

  2. cancel:接受用户的取消

public interface Subscription {
    public void request(long n);
    public void cancel();
}

处理器既是订阅者又是发布者。处理器可以生成与它所消费的项目类型不同的项目,因此,泛型(T,R)用于表示消费和生成的类型:

public interface Processor<T, R> extends Subscriber<T>, Publisher<R> {
}

履行

可以想象,已经创建了各种各样的反应流实现,它们作为第三方库提供,可以包含在 JVM 应用中。事实上,由于 Reactive Streams 标准及其在当时许多 Java 包中的使用非常流行,作为并发更新的一部分,该规范作为 Java 开发人员工具包(JDK) 9 版本的一部分被添加到 Java 标准库中作为增强。

包含 Reactive Streams 标准有助于减少由所有用法(仅由包名分隔)引起的重复和固有的兼容性问题。自 Java 9 发布以来,基本的反应流接口包含在流并发库中,允许 Java 应用依赖单个库来实现反应流接口,而不是决定特定的实现。

然而,重要的是要注意,虽然 JDK 中有流并发库,但是 R2DBC 规范直接从源使用反应流规范。例如,这意味着 R2DBC 规范使用直接来自org.reactivestreams.Publisher发布者,而不是java.util.concurrent.Flow中可用的接口。

摘要

在本章中,我向您介绍了反应式关系数据库连接(R2DBC ),它创建的原因,它旨在解决的问题,以及它如何使用反应式流 API 来完成这一切。

此外,我简要描述了实现需要遵守严格的遵从性级别,才能被视为合法的 R2DBC 客户机。在下一章中,我将详细介绍 SPI 接口,如何使用这些接口创建驱动程序实现,以及实现完全符合 R2DBC 标准的要求。**

三、实现之路

到目前为止,您已经了解了 R2DBC 规范的存在是为了提供一种方法来实现与关系数据存储的异步、非阻塞交互。展望未来,我将特别关注关系数据库解决方案,以及该规范是如何设计的,以便为实现提供灵活性,从而针对具有各种不同类型功能的各种关系数据库。

为了给开发人员提供最通用的解决方案,该规范的目标是在能够支持所有关系数据库之间共享的通用功能的同时,能够突出特定实现的独特功能。

数据库前景

正如我在上一章中所解释的,R2DBC 在最高层次上寻求提供一种异步的、非阻塞的方法来查询和管理

  1. 使用结构化查询语言(SQL)的关系数据库

  2. 用 Java 虚拟机(JVM)编程语言编写的应用

如果您以前使用过多种关系数据库解决方案,您可能会注意到它们之间存在某些共性和特征,例如,在能够执行查询之前需要建立一个连接。显然,情况是这样的,但是规范和 JDBC API 一样,需要为它创建实现需求。

当然,还有其他的共性,比如执行查询、管理事务的能力,等等,但是数据库解决方案之间也有许多不同之处。例如,考虑对特定数据类型的支持,如二进制大对象和字符大对象(BLOB,CLOB ),它们并不存在于每个数据库中。为了使用底层数据库,使用这些类型,驱动程序需要实现特定的支持。

我为什么要提这个?为什么这很重要?好吧,要点是有些功能是一些数据库不能支持或者选择不支持的。R2DBC 规范的创建是为了在所有关系数据库之间建立一个基线或标准,而不是过分固执己见或过于专注,从而增加一些驱动程序的复杂性(图 3-1 )。

img/504354_1_En_3_Fig1_HTML.png

图 3-1

R2DBC 为实现提供了使用共享和特定于驱动程序的功能的能力

Note

驱动程序在实现 R2DBC 规范时有很大的灵活性,包括使用不同的反应流 API 实现,如 Project Reactor、RxJava 等。

简约中的力量

R2DBC 驱动程序必须在非阻塞 I/O 层之上完全实现数据库有线协议,但是,除此之外,技术前景是开放的。回顾以前的连接标准,如 JDBC,R2DBC 试图保持对特定技术的不可知。如您所知,这是可能的,因为 R2DBC 仅仅是接口的集合,缺少任何实际的实现内容。

最终,驱动程序必须提供所有实现的内容,使用 R2DBC 作为一种蓝图。在这个蓝图中,使用了 Reactive Streams API 作为另一个也需要实现的蓝图。

例如,记住反应流为Publisher指定了一个接口:

public interface Publisher<T> {
    public void subscribe(Subscriber<? super T> s);
}

这很重要;因为,正如您将在以后的章节中了解到的,R2DBC 关注非阻塞行为,一些方法不直接返回值。

以 R2DBC 的ConnectionFactory接口为例:

package io.r2dbc.spi;
import org.reactivestreams.Publisher;
public interface ConnectionFactory {
    Publisher<? extends Connection> create();
    ConnectionFactoryMetadata getMetadata();
}

注意,创建新的Connection的过程并不直接返回新的连接对象。相反,create 方法返回一个Connection对象的承诺,它可以在未来某个未知的时间被接收时使用。这就是车手如何能够利用反压力的概念,把它整个圈!

Note

我将在第四章深入探讨更多关于连接规范、指南和接口的细节。

可以想象,这些类型的声明式工作流为底层实现创造了大量的机会。事实上,有多种选择可供选择。Reactive Streams 规范可以从头开始实现,或者有各种现有的实现(例如,Project Reactor、Reactor Netty、RxJava)可用。每个司机都有实施的自由(图 3-2 )。

img/504354_1_En_3_Fig2_HTML.png

图 3-2

可能的驱动技术变化

R2DBC 合规性

至此,我们已经大致描述了驱动程序实现 R2DBC 规范的过程,但是为了被认为真正符合规范,驱动程序必须满足特定的标准。

指导原则

根据 R2DBC 文档,所有驱动程序都必须满足一系列准则和要求,如下所示:

  • R2DBC SPI 应该实现 SQL 支持作为其主要接口。R2DBC 不依赖也不假定特定的 SQL 版本。SQL 和语句的各个方面可以完全在数据源中处理,也可以作为驱动程序的一部分。

  • 该规范由官方 R2DBC 规范文档和每个接口的 Javadoc 中记录的规范组成。Javadocs 在 http://r2dbc.io/spec/ 可用。

  • 支持参数化语句的驱动程序也必须支持绑定参数标记。

  • 支持参数化语句的驱动程序还必须支持至少一个参数绑定方法,通过索引或名称。

  • 驱动程序必须支持数据库事务。

  • 对列和参数的索引引用是从零开始的。也就是说,第一个索引从 0 开始。

最终,这些要求将作为实现正式符合 R2DBC 规范的路线图,并大致描述了对任何驱动程序实现的期望。

规范实施要求

您将在后续章节中了解到,R2DBC 规范中有许多接口可以在驱动程序中实现,但并不是所有的接口都必须实现。至少,不是全部。这又回到了数据库解决方案之间的差异,并为每个驱动程序提供了广泛的灵活性,以便能够利用它所提供的独特功能和能力。

首先,所有司机都必须

  • 实现非阻塞 I/O 层。换句话说,驱动程序内到目标数据库的所有通信必须完全无阻塞,并且遵守反应式编程的价值观(例如,提供功能性的和符合要求的反应式流实现)。

  • 通过ConnectionFactoryProviderJava 服务加载器支持ConnectionFactory发现。

Note

Java 服务加载器,或者更具体地说,ServiceLoader类,用于发现和延迟加载服务实现。它使用上下文类路径来定位服务供应器实现,并将它们放在内部缓存中。

除了这两个一般要求之外,正如 R2DBC 规范文档所指出的,还存在对实现单个接口的要求。所有的接口都需要全部或部分实现。

所有驱动程序必须完全实现以下接口:

  • io.r2dbc.spi.ConnectionFactory

  • io.r2dbc.spi.ConnectionFactoryMetadata

  • io.r2dbc.spi.ConnectionFactoryProvider

  • io.r2dbc.spi.Result

  • io.r2dbc.spi.Row

  • io.r2dbc.spi.RowMetadata

  • io.r2dbc.spi.Batch

所有驱动程序必须部分实现以下接口:

  • 实现io.r2dbc.spi.Connection接口,以下可选方法除外:
    • 对于不支持保存点的驱动程序,调用这个方法应该会抛出一个UnsupportedOperations异常。

    • 对于不支持保存点释放的驱动程序来说,调用这个方法是不可行的。

Note

无操作,或称无操作,是一条占用少量空间但不指定任何操作的计算机指令。实际上,这是一个充当占位符的方法,什么也不做。

  • rollbackTransactionToSavepoint:调用这个方法应该为不支持保存点的驱动程序抛出一个UnsupportedOperations异常。

Note

保存点通过标记事务中的中间点来提供细粒度的控制机制。一旦创建了保存点,事务就可以回滚到该保存点,而不会影响前面的工作。

  • 实现io.r2dbc.spi.Statement接口,以下可选方法除外:

    • 对于不支持密钥生成的驱动程序,调用此方法应该是无操作(no-op)。

    • 对于不支持提取大小提示的驱动程序来说,调用这个方法是不可行的。

  • 实现io.r2dbc.spi.ColumnMetadata接口,以下可选方法除外:

    • getPrecision

    • getScale

    • getNullability

    • getJavaType

    • getNativeTypeMetadata

规范扩展

驱动程序还可以选择使用核心 R2DBC 接口的扩展。扩展可用于补充规范接口,以提供 R2DBC 实现不需要的功能。

包装接口

R2DBC 规范包含一个名为Wrapped的接口,如清单 3-1 所示,它为实例提供了一种访问已包装资源的方式。它还允许 R2DBC 实现能够公开包装的资源。

public interface Wrapped<T> {
    T unwrap();
}

Listing 3-1The Wrapped interface

Note

unwrap方法可用于返回实现指定接口的对象,允许访问特定于供应商的方法。

R2DBC SPI 包装器可以通过实现Wrapped接口来创建,使得调用者能够提取原始实例。任何 R2DBC SPI 接口都可以包装。考虑下面包装一个Connection的例子,如清单 3-2 所示。

class ConnectionWrapper implements Connection, Wrapped<Connection> {
    private final Connection wrapped;
    @Override
    public Connection unwrap() {
        return this.wrapped;
    }
    // Construction and implementation details omitted for brevity.
}

Listing 3-2A Wrapped interface implementation example.

可关闭的界面

R2DBC 规范包含一个名为Closeable的接口,如清单 3-3 所示,它为对象提供了一种方式来释放不再以非阻塞方式使用的相关资源。

import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;

@FunctionalInterface
public interface Closeable {
    Publisher<Void> close();
}

Listing 3-3The Closable interface

Note

close方法用于返回一个Publisher来开始关闭操作,并在完成时得到通知。如果对象已经关闭,则订阅成功完成,关闭操作无效。

需要可关闭的对象来实现Closeable接口。一旦实例化,调用者可以在任何获得的Publisher对象上使用来自Closeable的关闭选项,并在完成时得到通知。

以扩展了Closeable接口的Connection接口为例,如清单 3-4 所示。

import org.reactivestreams.Publisher;
public interface Connection extends Closeable {
...
}

Listing 3-4A Closable interface usage example

这里,Connection是实现Connection接口的实例化对象。然后,如清单 3-5 所示,因为Connection接口扩展了Closeable接口,所以可以使用close功能。

Publisher<Void> close = connection.close();

Listing 3-5Using a close method implementation

测试和验证

声称驱动程序符合 R2DBC 的过程是非正式的。事实上,非常非正式,任何人都可以声称他们已经创建了一个 R2DBC 驱动程序。除了我之前提到的文档指南,GitHub 上的 R2DBC 存储库中只有一个小的测试兼容性工具包(TCK)。在我写这篇文章的时候,TCK 是相当轻量级的,并且包含基本的测试。所有的测试和合规工作都完全依赖于驱动程序的开发人员。

摘要

在本章中,我回顾了创建 R2DBC 驱动程序的高级过程。您了解了 R2DBC 规范是以一种反应式的方式在核心关系数据库交互的标准化和提供支持创建健壮驱动程序的高度灵活性和可扩展性之间取得平衡的方式创建的。

本章有助于为接下来的章节打下基础,在这些章节中,我们将更深入地研究 R2DBC 规范的功能,这将帮助您更深入地了解 R2DBC 驱动程序实现是如何结合在一起的。

四、连接

支持到数据源的连接是任何应用中的一项重要功能。事实上,根据应用的不同,它很可能是最重要的功能。理解了连接功能的重要性,R2DBC 规范提供了各种接口和类,这些接口和类允许驱动程序实现不仅建立连接,而且以纯反应的方式有效地管理它们。

在这一章中,我将研究 R2DBC SPI 的最重要的方面,创建和管理连接。我将深入研究 API 中可用的实体的层次结构,以扩展它们是如何被设计和组合起来为实现驱动程序提供一个极其健壮和灵活的解决方案的。

建立联系

最终,一切都归结于Connection接口,R2DBC 用它来定义到底层数据源的连接 API。在很大程度上,目标数据源可能是一个使用 SQL 作为数据访问方法的关系数据库,但是,正如我前面指出的,这不是一个硬性要求。然而,出于我们的目的,在本章中,我将把重点放在专门针对关系数据库管理系统(RDMSs)的连接上。

Note

R2DBC 驱动程序实现不需要使用关系数据库作为底层数据源,事实上,它可以使用任何数据源,包括面向流和面向对象的系统。

解剖学

随着时间的推移,使用数据库连接的应用通常需要管理多个连接,甚至可能是多个连接。R2DBC SPI 使应用能够管理到一个或多个数据源的连接。虽然Connection对象的目的是使驱动程序能够建立和维护单个客户端连接,但应用通常需要比这更复杂的东西。

但是因为一个Connection对象代表一个单独的客户端会话,并且有相关的状态信息,比如用户标识和什么事务语义有效,Connection对象对于多个订阅者的并发状态改变交互是不安全的。

事实上,通过使用适当的同步机制,甚至有可能在串行运行操作的多个线程之间共享Connection对象。

幸运的是,R2DBC 驱动程序并不要求应用直接创建和管理Connection对象,而是通过

  1. 使用一个ConnectionFactory实现来创建一个Connection对象

  2. 使用 R2DBC 提供的ConnectionFactories类,通过利用一个或多个ConnectionFactoryProvider实现,然后可以使用这些实现来获得一个ConnectionFactory实现

在本章的后面,我们将深入探讨连接工厂以及它们在各种其他连接类和接口之间的关系中所扮演的角色。但是现在,让我们把注意力放在通过使用连接工厂来创建连接的途径上(图 4-1 )。

img/504354_1_En_4_Fig1_HTML.png

图 4-1

R2DBC 连接层次结构

R2DBC 后见之明

如果你曾经使用过 JDBC API,你就会知道这可能是一个复杂的、经常令人沮丧的过程。多年来,JDBC 变得非常固执己见,要求开发者坚持其底层技术和功能限制。

您已经了解到 R2DBC 的创建是为了给驱动程序实现提供高度的灵活性,允许它们利用目标数据源的独特特性和功能。同时,R2DBC 旨在标准化所有驱动程序所需的功能。这方面的一个实际例子是使用标准的统一资源定位符(URL)格式。

Note

数据库连接 URL 是 JDBC 驱动程序用来连接数据库的字符串。它可以包含在哪里搜索数据库、要连接的数据库的名称以及许多其他配置选项等信息。

与要求每个驱动程序实现创建需求(包括 URL 解析工作流)的 JDBC 相反,R2DBC 定义了一个标准的 URL 格式。该格式是请求注解(RFC) 3986 统一资源标识符(URI)的增强形式:通用语法,Java 的java.net .URI类型支持其修改。

img/504354_1_En_4_Fig2_HTML.png

图 4-2

R2DBC 连接 URL 格式

创建 URL 标准允许所有驱动程序实现统一利用以下配置选项:

  • scheme:标识该 URL 是有效的 R2DBC URL。有两种有效的方案, r2dbcr2dbc,用于配置安全套接字层(SSL)的使用。

  • driver:标识具体的驱动实现(如 MySQL、MariaDB 等)。).

  • protocol:可选参数,用于配置驱动专用协议。协议可以分层组织,并用冒号(:)分隔。

  • authority:包含端点和授权。授权可以包含单个主机或主机名和端口元组的集合,用逗号(,)分隔。

  • path:(可选)用作初始模式或数据库名。

  • query:(可选)用于通过使用键名称作为选项名称,以字符串键-值对的形式传递附加配置选项。

连接工厂

在计算机科学中,工厂被视为创建其他对象的对象。这很有用,因为在基于类的编程中,工厂作为目标对象构造函数的抽象,有助于本地化复杂对象的实例化(图 4-3 )。

img/504354_1_En_4_Fig3_HTML.png

图 4-3

工厂对象工作流

利用这种方法,R2DBC ConnectionFactory接口为驱动程序提供了创建对象的蓝图,这些驱动程序负责创建Connection对象(列表 4-1 )。

import org.reactivestreams.Publisher;
public interface ConnectionFactory {
    Publisher<? extends Connection> create();
    ConnectionFactoryMetadata getMetadata();
}

Listing 4-1The R2DBC ConnectionFactory interface

驱动程序实现

使用基于工厂的方法来促进连接创建和管理,允许驱动程序抽象出特定于供应商或特定于数据库的方面,以努力创建更简单、更流畅的应用开发体验。

更深入地研究一下,R2DBC 文档指出了许多需求,这些需求准确地定义了ConnectionFactory实现必须完成什么才能被认为是可行的:

*1. ConnectionFactory表示用于延迟连接创建的资源工厂。它可以自己创建连接,包装一个ConnectionFactory,或者在一个ConnectionFactory之上应用连接池。

  1. A ConnectionFactory通过ConnectionFactoryMetadata提供关于驱动本身的元数据。

  2. A ConnectionFactory使用延迟初始化,并且应该在请求项目之后启动连接资源分配(Subscription.request(1)).

  3. 连接创建必须发出一个Connection或一个错误信号。

  4. 连接创建必须是可取消的(Subscription.cancel())。取消连接创建必须释放(“关闭”)连接和所有关联的资源。

  5. A ConnectionFactory应该预料到可以包装。包装器必须实现Wrapped<ConnectionFactory>接口,并在Wrapped.unwrap()被调用时返回底层的ConnectionFactory

公开元数据

ConnectionFactory接口包括一个名为getMetadata的函数,该函数要求类实现提供元数据来标识目标产品的名称(清单 4-2 )。

public interface ConnectionFactoryMetadata {
String getName();
}

Listing 4-2The ConnectionFactoryMetadata interface

连接工厂

除了提供额外的抽象,SPI 还包含一个名为ConnectionFactories的完全实现的类,它的存在消除了应用开发人员实现Connection发现功能的需要。通过使用 Java 的ServiceLoader机制和ConnectionFactoryProvider接口,ConnectionFactories发现机制使得自动查找和加载在类路径中找到的任何 R2DBC 驱动程序成为可能。

发现

ConnectionFactoryProvider是一个 Java 服务接口,当实现时,它提供检查ConnectionFactoryOptions类的能力(清单 4-3 )。

import java.util.ServiceLoader;
public interface ConnectionFactoryProvider {
ConnectionFactory create(ConnectionFactoryOptions connectionFactoryOptions);
boolean supports(ConnectionFactoryOptions connectionFactoryOptions);
String getDriver();
}

Listing 4-3The ConnectionFactoryProvider interface

ConnectionFactoryOptions类表示一个ConnectionFactory对象向一个ConnectionFactoryProvider对象请求的配置(图 4-4 )。

img/504354_1_En_4_Fig4_HTML.png

图 4-4

高级 ConnectionFactory 发现工作流

将这一切结合在一起

使用ConnectionFactoryProvider对象,ConnectionFactories类提供了两种引导ConnectionFactory对象的方法:

  1. 使用 R2DBC 连接 URL。然后 URL 字符串将被解析以创建一个ConnectionFactoryOptions对象。
ConnectionFactory factory = ConnectionFactories.get("r2dbc:a-driver:pipes://localhost:3306/my_database?locale=en_US");

Listing 4-4Obtaining a ConnectionFactory using an R2DBC URL

  1. 通过直接构建一个ConnectionFactoryOptions对象。
ConnectionFactoryOptions options = ConnectionFactoryOptions.builder()
     .option(ConnectionFactoryOptions.DRIVER, "a-driver")
     .option(ConnectionFactoryOptions.PROTOCOL, "pipes")
     .option(ConnectionFactoryOptions.HOST, "localhost")
     .option(ConnectionFactoryOptions.PORT, 3306)
     .option(ConnectionFactoryOptions.DATABASE, "my_database")
     .option(Option.valueOf("locale"), "en_US")
     .build();

ConnectionFactory factory = ConnectionFactories.get(options);

Listing 4-5Obtaining a ConnectionFactory using ConnectionFactoryOptions programmatically

一旦你获得了一个ConnectionFactory对象,你就有能力获得一个Connection对象。

连接

Connection接口的每个连接实现实例化(清单 4-6 )提供了到数据库的单个连接。

import org.reactivestreams.Publisher;
public interface Connection extends Closeable {
    Publisher<Void> beginTransaction();
    @Override Publisher<Void> close();
    Publisher<Void> commitTransaction();
    Batch createBatch();
    Publisher<Void> createSavepoint(String name);
    Statement createStatement(String sql);
    boolean isAutoCommit();
    ConnectionMetadata getMetadata();
    IsolationLevel getTransactionIsolationLevel();
    Publisher<Void> releaseSavepoint(String name);
    Publisher<Void> rollbackTransaction();
    Publisher<Void> rollbackTransactionToSavepoint(String name);
    Publisher<Void> setAutoCommit(boolean autoCommit);
    Publisher<Void> setTransactionIsolationLevel(IsolationLevel isolationLevel);
    Publisher<Boolean> validate(ValidationDepth depth);
}

Listing 4-6The R2DBC Connection interface

Tip

注意,Connection接口利用了反应流 API,将许多函数的返回类型作为类型Publisher提供。回到第二章的第二章,记住的发布者的作用是承诺在未来未知的时间做出回应或结果。

当使用Connection对象建立连接时,可以执行 SQL 语句并随后返回结果。根据 R2DBC 文档,Connection对象可以由到底层数据库的任意数量的传输连接组成,或者代表多路传输连接上的一个会话。为了获得最大的可移植性,应该同步使用连接。

最终,Connection对象的存在是为了启动数据库对话、事务管理和语句执行。

表 4-1

连接接口功能

|

名字

|

描述

|
| --- | --- |
| beginTransaction | 开始新的事务。调用此方法将禁用自动提交模式。 |
| close | 释放由Connection对象持有的任何资源。 |
| commitTransaction | 提交当前事务。 |
| createBatch | 创建一个新的批次(参见第八章)。 |
| createSavePoint | 在当前事务中创建新的保存点。 |
| createStatement | 为构建基于语句的(SQL)请求创建新语句。 |
| isAutoCommit | 返回连接的自动提交模式。 |
| getMetadata | 返回连接所连接的产品的ConnectionMetaData(例如,MariaDB 数据库)。 |
| getTransactionIsolationLevel | 返回连接的IsolationLevel。 |
| releaseSavePoint | 释放当前事务中的保存点。 |
| rollbackTransaction | 回滚当前事务。 |
| rollbackTransactionToSavepoint | 将当前事务回滚到保存点。 |
| setAutoCommit | 为当前事务配置自动提交模式。 |
| setTransactionIsolationLevel | 为当前事务配置隔离级别。 |
| validate | 根据给定的ValidationDepth验证连接。 |

获取连接

Connection对象只能通过ConnectionFactory对象来创建和获取,这一点我之前已经详细说明过了。一旦获得了一个ConnectionFactory对象,就可以使用create方法来访问一个连接。

Publisher<? extends Connection> publisher = factory.create();

Listing 4-7Creating a connection from a ConnectionFactory object

获取元数据

R2DBC 规范要求连接通过实现ConnectionMetadata接口(清单 4-8 )来公开关于它们所连接的数据库的元数据。

public interface ConnectionMetadata {
    String getDatabaseProductName();
    String getDatabaseVersion();
}

Listing 4-8ConnectionMetadata interface

Note

ConnectionMetadata对象中发现的信息通常是基于连接初始化时获得的信息动态发现的。

验证连接

一旦实例化了一个Connection对象,就可以使用validate方法来获得连接的状态。validate 方法接受一个ValidationDepth参数,该参数指示应该验证连接的深度。

ValidationDepth是包含两个常数的枚举:

  • ValidationDepth.LOCAL:表示只进行客户端验证。

  • ValidationDepth.REMOTE:指示进行远程连接验证。

关闭连接

回头看看Connection接口,您会注意到它实现了Closable接口(清单 4-9 )。

import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;

@FunctionalInterface
public interface Closeable {
    Publisher<Void> close();
}

Listing 4-9The Closeable interface

Closable接口公开了一个名为close的方法,当这个方法被调用时,它将释放实现对象Connection持有的所有资源。

Publisher<Void> close = connection.close();

Listing 4-10Closing a connection

摘要

提供连接到底层数据源的能力是 R2DBC 规范最重要的功能之一。虽然有许多相似之处,但关系数据库也可能包含不同的连接要求。R2DBC 仅使用少量的接口和类,就能够提供所有数据源共享的通用功能,并为驱动程序提供灵活性,以整合其目标产品的独特功能。

你在本章中学到的所有信息将在接下来的章节中对你有所帮助。毕竟,如果没有连接的能力,它最终会是一本非常短的书。*

五、事务

支持事务的能力是所有关系数据库的一个重要方面。事实上,这是它们最重要的特性之一,因为它们的使用通常是维护任何规模和复杂性的数据库的关键部分。

鉴于这种理解,R2DBC 规范为驱动程序实现提供支持和指导以公开核心事务管理功能是有意义的。在此过程中,我们将探索规范要求实现哪些事务性特性,同时还将研究如何以及何时利用这些特性。所有这些都是为了了解 R2DBC 如何帮助利用事务来提供数据完整性、隔离性、正确的应用语义,以及在反应式、并发数据库访问期间的一致数据视图。

事务基础

在深入 R2DBC 规范如何处理事务的细节之前,理解关系数据库中事务管理的基本概念、需求和用法很重要,即使只是为了更新您的现有知识。

事务被定义为包含一个或多个 SQL 语句的逻辑工作单元(图 5-1 )。

img/504354_1_En_5_Fig1_HTML.png

图 5-1

一个事务包含一个或多个 SQL 语句,作为单个工作单元启动

更简单地说,您可以将事务视为对数据库的一个或多个更改的传播。

事务的需要

但是为了理解为什么将 SQL 语句分组为事务是必要的,让我们首先考虑一个简单的、相关的场景。举个例子,把钱从一个银行账户转移到另一个银行账户的过程,为了简单起见,让我们假设这个过程仅仅包括更新余额(清单 5-1 )。

UPDATE savings_account SET balance = 0 WHERE account_id = 1;
UPDATE checking_account SET balance = 100 WHERE account_id = 1;

Listing 5-1A sample update to account tables using SQL

当然,正如你可以想象的那样,重要的是钱在你期望的地方,当你期望它在的时候。这意味着对每个帐户的更改要么都成功,要么都失败。否则,你只能从一个账户中取出或向另一个账户中加入钱,造成信息的不匹配。在数据库管理系统中,支持这些类型的期望被称为 ACID 合规性。

酸性顺应性

正如我之前定义的,事务是工作单元,但是,进一步说,事务更具体地被称为原子工作单元。这意味着当事务对数据库进行一个或多个更改时,要么在提交事务时所有更改都成功,要么在事务回滚时所有更改都被撤消。这种“全有或全无”的特性被称为原子性

事务应该是一个独立的单元,因此,在其中执行的操作不能与流程中不涉及的其他数据库操作合并。然而,由于事务可以由多个 SQL 命令组成,甚至可能访问多个数据库,因此数据库管理系统必须确保操作不会受到可能同时或并发运行的其他数据库命令的干扰。这是一个被称为隔离的特性。

为了确保数据库系统不会因为后来的故障而错过成功完成的事务,事务的动作必须在故障之间持续。此外,一个事务的结果只能由另一个事务撤消。这种特性被称为耐久性

由于这三个属性,事务的作用是通过一个被恰当地称为一致性的属性来保持数据库的一致性。

如图 5-2 所示,这些特性组合在一起形成了首字母缩略词 ACID。

img/504354_1_En_5_Fig2_HTML.png

图 5-2

酸性质的高度概括

控制方法

实际上,事务从第一个可执行 SQL 语句开始,在提交或回滚时结束(图 5-3 )。

img/504354_1_En_5_Fig3_HTML.png

图 5-3

简单的事务性工作流

Caution

并非所有数据库管理系统的所有 SQL 语句都能够回滚。例如,MySQL 和 MariaDB 不支持回滚修改,如数据描述语言(DDL),即创建和修改数据库对象(如表、索引和用户)的语法。有关更多信息,请查看目标数据库的文档。

提交事务

提交事务是将更改永久保存到数据库的过程(清单 5-2 )。

START TRANSACTION;
UPDATE savings_account SET balance = 0 WHERE account_id = 1;
UPDATE checking_account SET balance = 100 WHERE account_id = 1;
COMMIT;

Listing 5-2Committing a MariaDB transaction using SQL

回滚事务

回滚事务意味着撤销对未提交事务中数据的任何更改。如果在事务范围内执行 SQL 时出现错误,事务会自动回滚,但也可以手动回滚,如清单 5-3 所示。

START TRANSACTION;
UPDATE savings_account SET balance = 0 WHERE account_id = 1;
UPDATE checking_account SET balance = 100 WHERE account_id = 1;
ROLLBACK;

Listing 5-3Rolling back a MariaDB transaction using SQL

保存点

许多关系数据库也支持保存点,一个命名的子事务。保存点提供了在事务中标记中间点的能力,可以回滚到这些中间点而不影响前面的工作(清单 5-4 )。

START TRANSACTION;
UPDATE savings_account SET balance = 0 WHERE account_id = 1;
SAVEPOINT savings_account_updated;
UPDATE checking_account SET balance = 100 WHERE account_id = 1;
ROLLBACK TO SAVEPOINT savings_account_updated;

Listing 5-4Rolling back to a MariaDB savepoint using SQL

R2DBC 事务管理

R2DBC 规范支持通过代码控制事务性操作,而不是通过所有驱动程序都需要实现的Connection接口直接使用 SQL。

自动提交模式

事务可以隐式或显式启动。当一个连接对象处于自动提交模式时,这将在本章后面详细讨论,当一个 SQL 语句通过一个Connection对象执行时,事务被隐式启动。

Stay Tuned

在第六章中,我们将发现如何使用Connection对象来准备和执行 SQL 语句。

连接对象提供了两种与自动提交模式交互的方法,如表 5-1 所示。

表 5-1

用于查看和编辑事务自动提交功能的连接对象方法

|

方法

|

返回类型

|

描述

|
| --- | --- | --- |
| 设置自动提交 | 出版商 | 为当前事务配置自动提交模式。 |
| isAutoCommit | 布尔 | 返回事务的自动提交模式。isAutoCommit 的默认值由驱动程序实现决定。 |

正如 R2DBC 规范文档中所指出的,应用应该通过调用setAutoCommit方法来更改自动提交模式,而不是执行 SQL 命令来更改底层连接配置。无论出于何种原因,如果在活动事务期间自动提交的值被更改,则当前事务将被提交。

Caution

如果调用了setAutoCommit方法,并且自动提交的值没有从当前值改变,它将被视为空操作。

要知道,修改一个Connection对象的自动提交模式很可能会在底层数据库上启动某种动作,这也是为什么setAutoCommit会返回一个Publisher对象。相比之下,使用isAutoCommit通常会涉及到使用驱动程序的状态,而不是必须与数据库通信。

显性事务

但是,当自动提交模式被禁用时,事务必须显式启动。这可以通过在一个Connection对象上调用beginTransaction方法来完成。

Publisher<Void> begin = connection.beginTransaction();

Listing 5-5Creating a publisher to begin a transaction

提交事务

一旦事务被显式启动,它也必须被显式提交。

Publisher<Void> commit = connection.commitTransaction();

Listing 5-6Creating a publisher to commit a transaction

回滚事务

如果由于某种原因,事务执行的查询之一失败,可以使用rollbackTransaction方法回滚所有查询。

try {
   Publisher<Void> begin = connection.beginTransaction();
   Publisher<Void> updateSavings = connection.createStatement("UPDATE savings_account SET balance = 0 WHERE account_id = 1").execute();
   Publisher<Void> updateChecking = connection.createStatement("UPDATE checking_account SET balance = 100 WHERE account_id = 1").execute();
   Publisher<Void> transaction = connection.commitTransaction();
}
catch (SQLException ex) {
   Publisher<Void> transaction = connection.rollbackTransaction();
}

Listing 5-7Handling an error with a committed transaction and rolling back

Tip

注意,我已经提供了创建Publisher对象的例子。然而,为了执行Publisher的功能,必须订阅它。为了订阅一个Publisher对象,它必须有一个正式的 R2DBC 实现。要了解这一点,请看一下第十四章中提供的实用 R2DBC 驱动程序示例。

管理保存点

Connection接口提供了三种方法,如表 5-2 所示,可用于管理保存点。

表 5-2

用于管理保存点的连接对象方法

|

方法

|

返回类型

|

描述

|
| --- | --- | --- |
| createSavepoint() | 出版商 | 在当前事务中创建保存点。 |
| 释放保存点() | 出版商 | 释放当前事务中的保存点。 |
| 回滚事务到保存点(字符串名称) | 出版商 | 回滚到当前事务中的保存点。 |

createSavepoint方法可用于在事务范围内设置保存点。如果没有活动的事务,调用createSavepoint将启动一个事务。

Note

在事务中创建保存点将禁用包含连接的自动提交。

创建保存点后,可以使用rollbackTransactionToSavepoint方法回滚工作,而不用回滚整个事务。

Publisher<Void> begin = connection.beginTransaction();
Publisher<Void> updateSavings = connection.createStatement("UPDATE savings_account SET balance = 0 WHERE account_id = 1").execute();
Publisher<Void> savepoint = connection.createSavepoint("savepoint");
Publisher<Void> updateChecking = connection.createStatement("UPDATE checking_account SET balance = 100 WHERE account_id = 1").execute();
Publisher<Void> partialRollback = connection.rollbackTransactionToSavepoint("savepoint");
Publisher<Void> commit = connection.commitTransaction();

Listing 5-8Rolling back a transaction to a savepoint.

Note

根据 R2DBC 规范文档,不支持保存点创建和回滚到保存点的驱动程序将抛出一个UnsupportedOperationException来表示这些特性不受支持。

释放保存点

值得注意的是,保存点直接在数据库上分配资源。因此,一些数据库供应商可能要求释放保存点来处置资源。

使用releaseSavepoint方法将释放不再需要的保存点。在以下情况下,保存点也将被释放

  • 提交一个事务。

  • 事务被完全回滚。

  • 事务回滚到该保存点。

  • 事务回滚到前一个保存点。

根据 R2DBC 规范文档,在不支持保存点释放功能的驱动程序实现中调用releaseSavepoint方法将导致无操作。

隔离级别

数据库提供了在事务中指定隔离级别的能力。事务隔离的概念定义了一个事务与其他事务执行的数据或资源修改的隔离程度,从而在多个事务处于活动状态时影响并发访问。

管理隔离

R2DBC 规范包含一个名为IsolationLevel的类,用于表示给定Connection的隔离级别常数。使用一个Connection对象,你可以利用一个IsolationLevel对象分别用getTransactionIsolationLevelsetTransactionIsolationLevel,来获取和设置事务隔离级别。

IsolationLevel类包含四个隔离级别常量,由 ANSI/ISO SQL 标准定义,如下所示:

  • READ_COMMITTED: 基于锁的并发控制 DBMS 实现保持写锁,直到事务结束。但是,一旦执行了 SELECT 操作,读锁就会被释放。

  • READ_UNCOMMITTED: 脏读是允许的,因此没有一个事务可以看到其他事务尚未提交的更改。这是最低的隔离等级。

  • REPEATABLE_READ: 基于锁的并发控制 DBMS 实现保持读写锁,直到事务结束。在此级别中,允许出现幻像读取,即当另一个事务向当前正在读取的记录添加新行或从中删除新行时。

  • 可序列化:基于锁的并发控制 DBMS 实现需要在事务结束时释放读写锁。在这一级,避免了幻像读取。这是最高的隔离等级。

性能考虑因素

请注意,更改事务隔离级别会对性能产生负面影响。如前所述,在IsolationLevel选项中,数据库通常会修改用于确保隔离级别语义的锁定量和资源开销。

根据在任何给定时间支持的并发访问的可用性,可能会影响应用的性能。考虑到这一点,R2DBC 规范文档建议,在确定哪个事务隔离级别合适时,事务管理功能应负责权衡数据一致性需求和性能需求。

摘要

在这一章中,我们研究了事务的基本原理。我们学习或更新了事务的基本解剖、控制机制以及为什么它们是必要的记忆。我们还了解到 R2DBC 规范支持启动和管理事务,目标是在符合 ACID 的数据库之间共享核心事务特性和功能。在这个过程中,我们了解了如何在自己的代码中利用 R2DBC 事务能力。此外,我们还了解了事务隔离级别的复杂性及其在规范中的支持。

六、语句

在前一章中,您已经了解到建立数据库连接是数据库驱动程序最关键的要求之一。虽然这肯定是对的,但是如果您不能向底层数据存储发送信息或从底层数据存储接收信息,那么仅仅连接到数据库对您的应用来说并不是那么有用。

在本章中,我们将看看如何使用 R2DBC 创建和执行 SQL 语句。我们将首先研究对象的基本层次结构,以及使用 R2DBC 与数据库进行交互所涉及的工作流。然后,在对核心功能有了更好的理解之后,我们将看看更复杂的特性。

SQL 语句

如您所知,数据库引擎本质上只是数据仓库,包含外部需求所需的数据,如应用。存储库本身可能包含各种用于组织数据的结构和机制。每个关系数据库供应商都有不同的组织机制,从微妙到非常不同。最终,他们都依靠一个标准来解决所有问题:SQL。

但是深入研究关系数据库如何优化、解析和执行 SQL 的具体细节,最终超出了我们在本书中要研究的范围。相反,我们应该关注一般要点,即 SQL 语句被发送到数据库,并且某种结果是预期的。

img/504354_1_En_6_Fig1_HTML.png

图 6-1

基本的数据库/SQL 工作流

R2DBC 语句

除了连接到底层数据源,执行 SQL 语句可能是 R2DBC 驱动程序最常见的用途之一。

Connection对象负责创建和管理Statement对象,这些对象将用于从数据库中获取查询结果(图 6-2 )。

img/504354_1_En_6_Fig2_HTML.png

图 6-2

使用 R2DBC 执行 SQL 语句的类流

R2DBC Statement接口定义了输入、组织和执行 SQL 语句的方法。Statement接口提供了两种创建和执行语句的方法:非参数化和参数化。

不同于 JDBC 规范,它提供了StatementPreparedStatement对象,R2DBC 只依赖一个对象实现来适应通用和参数确定的 SQL 语句的创建和执行。最终,驱动程序实现必须包含确定执行哪种语句的功能。

基础知识

如前所述,底层数据库上的 SQL 语句交互是通过使用Statement对象来实现的。在最简单的情况下,完全独立的静态 SQL 语句可以用来创建Statement对象。

创建语句

Connection对象公开了一个名为createStatement的方法,该方法返回一个新的Statement对象。createStatement方法接受必须包含有效 SQL 的单个字符串值(清单 6-1 )。

Statement statement = connection.createStatement("SELECT title FROM movies");

Listing 6-1Creating a statement using the Connection object

运行语句

一旦构造完成,包含在Statement对象中的 SQL 语句就可以通过调用execute方法对数据库运行(清单 6-2 )。

Publisher<? extends Result> publisher = statement.execute();

Listing 6-2Executing the SQL statements contained within a Statement object

根据已执行的 SQL 命令的性质,结果 Publisher 对象可能返回一个或多个 Result 对象。

先睹为快

一个Result对象是 R2DBC 规范提供的Result接口的实现(清单 6-3 )。

public interface Result {
    Publisher<Integer> getRowsUpdated();
    <T> Publisher<T> map(BiFunction<Row, RowMetadata, ? extends T> mappingFunction);
}

Listing 6-3The Result interface

Result对象负责提供两种结果类型:

  • 通过getRowsUpdated方法执行 SQL 语句后更新的记录或行数

  • 通过map方法以表格形式组织的结果集

在本书的后面部分,将会更详细地讨论Result接口、它的特性以及它的用途。

动态输入

当然,通常情况下,SQL 语句需要包含信息,如过滤器,以针对特定的数据。您可能还想通过简单地动态交换值来重用特定的 SQL 语句(清单 6-4 )。

String artist = "Johnny Data”;
Statement statement = connection.createStatement("SELECT title FROM songs WHERE artist = ‘" + artist + "'");

Listing 6-4Appending a value directly to the SQL statement string

不幸的是,像我在前面的代码中所做的那样做可能会导致意想不到的后果,比如将您的语句暴露给像 SQL 注入这样的漏洞。

Note

SQL 注入是一种用于攻击数据驱动应用的代码注入技术,在这种技术中,恶意语句被插入到条目字段中以供执行。

幸运的是,R2DBC 规范允许驱动程序实现利用参数化的能力,或者将参数添加到 SQL 语句的过程,方法是在分配给Statement对象的 SQL 字符串中使用特定于供应商的绑定标记。绑定标记是用于表示查询字符串中变量的特殊字符。绑定变量可以通过标记的索引或名称进行绑定。

Caution

必须提供 SQL 语句中指示的所有绑定变量,并且这些变量的类型必须正确,否则在尝试执行该语句时将会出错。

创建参数化语句

创建参数化的Statement对象的过程与创建非参数化的语句是一样的,通过Connection对象上的createStatement方法(清单 6-5 、 6-6 和 6-7 )。

Statement statement = connection.createStatement("SELECT title FROM songs WHERE artist = $1");

Listing 6-7Creating a named parameterized statement for PostgreSQL

Statement statement = connection.createStatement("SELECT title FROM songs WHERE artist = @P0");

Listing 6-6Creating a named parameterized statement for Microsoft SQL Server

Statement statement = connection.createStatement("SELECT title FROM songs WHERE artist = :artist");

Listing 6-5Creating a named parameterized statement for MariaDB or MySQL

因为绑定标记是在语句对象中标识的,所以参数化语句可以被缓存以供重用。

Note

缓存参数化或准备好的语句是一种用于高效地重复执行相同或相似的数据库语句的方法。

绑定参数

一旦创建了参数化语句,就需要为定义的参数赋值。Statement接口定义了两个方法来为绑定标记替换提供参数值,bindbindNull

绑定方法接受两个参数:

  1. 从零开始的序号位置或命名占位符参数

  2. 要分配给参数的值

Statement statement = connection.createStatement("SELECT title FROM songs WHERE title = $1 and artist = $2");
statement.bind(0, "No Errors");
statement.bind(1, "Lil Data");

Listing 6-9Binding parameters to a Statement object by index

Statement statement = connection.createStatement("SELECT title FROM songs WHERE title = $1 and artist = $2");
statement.bind("$1", "No Errors");
statement.bind("$2", "Lil Data");

Listing 6-8Binding parameters to a Statement object using placeholders

在语句运行之前,Statement对象中的每个绑定标记必须有一个相关的值。语句对象中的 execute 方法负责验证参数化语句。如果缺少绑定标记,将抛出一个IllegalStateException

批处理语句

Statement对象还支持多个参数的绑定,这些参数被组织成可以在底层数据库上执行的批处理命令。

Note

批处理是一组一起提交并作为一个组依次执行的 SQL 语句。

可以通过使用bind方法首先提供参数,然后使用add方法来创建批处理。从那里,可以提供下一组参数绑定。

Statement statement = connection.createStatement("INSERT INTO songs (title, artist) VALUES ($1, $2)");
statement.bind(0, "Give me that SQL").bind(1, "Johnny Data").add();
statement.bind(0, "Doo-Doo-Data").bind(1, "Susie SQL").add();
statement.bind(0, "Relationship woes").bind(1, "Column Crew");
Publisher<? extends Result> publisher = statement.execute();

Listing 6-10Creating and running a Statement batch

驱动程序实现负责从Statement批处理中创建相应的 SQL 语句。

INSERT INTO songs (title, artist) VALUES ('Give me that SQL', 'Johnny Data'); INSERT INTO songs (title, artist) VALUES ('Doo-Doo-Data', 'Susie SQL');

Listing 6-11An example set of MariaDB-based SQL statements

最终,批处理运行会发出一个或多个Result对象,这取决于实现如何准备和执行Statement批处理。

使用空值

NULL值使用一种叫做bindNull的方法单独处理,该方法有两个参数:

  1. 从零开始的序号位置或命名占位符参数

  2. 参数的可空的值类型

statement.bindNull(0, String.class);

Listing 6-12Binding a NULL value parameter by index

自动生成的值

我们经常需要利用数据库管理系统自动生成的表中的数据,通常是标识符。许多数据库系统在插入行时会自动生成值,该行可能是唯一的,也可能不是唯一的。

由于数据库系统创建和访问自动生成的值的方式不同,R2DBC 规范在Statement接口上提供了一个名为returnGeneratedValues的方法,特定于供应商的驱动程序可以为其提供实现。该方法接受一个变元参数,用于精确定位包含自动生成值的列名(清单 6-13 )。

Statement statement = connection.createStatement("INSERT INTO songs (title, artist) VALUES ('Primary Key to My Heart', 'Tina Tables')").returnGeneratedValues("id");

Listing 6-13Creating a statement with the returnGeneratedValues method

发出的Result对象包含在Row对象中可用的列,用于请求的每个自动生成的值。用于Row对象实现的Row接口将在本书后面更详细地描述。

Publisher<? extends Result> publisher = statement.execute();
publisher.map((row, metadata) -> row.get("id"));

Listing 6-14Retrieving auto-generated values

性能提示

Statement接口提供了一个名为fetchSize的方法,可以用来向 R2DBC 驱动程序提供背压提示。从高层次来看,该方法的作用是将 fetch size SQL 提示应用于语句产生的每个查询。

Note

一个提示是对 SQL 标准的补充,它指示数据库引擎如何执行查询。例如,提示可以告诉引擎使用或不使用索引。

更具体地说,fetchSize方法的目的是在从查询中获取结果时检索固定数量的行,而不是从背压中获取大小。如果多次调用,将只应用最后一次调用中配置的提取大小。如果指定的值为零,则忽略提示。方法的默认实现是空操作,默认值为零。

驱动程序可以使用背压提示来导出适当的提取大小。为了优化性能,在每个语句的基础上向驱动程序提供提示是有用的,以避免背压提示传播的不必要的干扰。

Caution

背压应被视为流量控制的一种工具,而不是限制结果的大小。结果大小限制应该是查询语句的一部分。

如果通过Statement接口提供给驱动程序的提示不合适或不被底层数据库支持,驱动程序可能会忽略这些提示。

摘要

为数据库提供信息以便检索某种结果或反馈是创建数据库支持的应用中最重要的任务之一,如果不是最重要的话。

在本章中,我们学习了 R2DBC 规范规定的功能和准则,这些功能和准则使 SQL 语句与底层数据库的通信成为可能。我们还研究了提供参数化语句的可用选项,以提高数据库通信的安全性和效率。

七、处理结果

连接到数据库并执行 SQL 语句是很棒的,但是最终,如果我们不能从数据库中获取数据,那还有什么意义呢?在本章中,我们将了解 R2DBC 规范如何组织和公开使从数据库中检索数据变得轻而易举的功能。

您将从了解获取数据的基本步骤开始。然后,我们将更深入地研究对象领域,检查那些支持对关系存储数据进行真正反应式访问的过多功能。

基本面

正如在第六章中简要提到的,一个Result对象被创建并作为在一个Statement对象中运行 SQL 语句的结果而获得。Statement对象的execute方法返回一个Publisher,作为运行底层 SQL 语句的结果,发出 Result对象(清单 7-1 )。

Statement statement = connection.createStatement("SELECT album_id, title, artist FROM songs");
Publisher<? extends Result> results = statement.execute();

Listing 7-1Obtaining a result via SQL statement execution.

Tip

查看第十三章以了解我们将在本章中研究的方法的Statement对象实现。

Result对象是允许消费两种结果类型的对象(图 7-1 ):

img/504354_1_En_7_Fig1_HTML.png

图 7-1

R2DBC 结果对象中有两种类型的结果,更新的记录数和表格结果集

  • 由于执行 SQL 语句而更新的行数

  • 由 SQL 语句检索的一组以表格形式组织的结果

R2DBC 规范提供了一个名为Result的接口,驱动程序实现用它来创建一个Result对象实现(清单 7-2 )。

import org.reactivestreams.Publisher;
import java.util.function.BiFunction;
public interface Result {
    Publisher<Integer> getRowsUpdated();
    <T> Publisher<T> map(BiFunction<Row, RowMetadata, ? extends T> mappingFunction);
}

Listing 7-2The Result interface.

消费结果

获得结果的过程包括处理Row对象的发射,其中结果从第一个Row前进到最后一个。发出最后一行后,Result对象失效,来自同一个Result对象的行不再被使用。

结果中包含的行取决于底层数据库如何实现结果。也就是说,它包含运行查询时或检索行时满足查询的行。

Note

在本章的后面将会更详细地检查Row对象。

光标

R2DBC 驱动程序可以直接或通过使用游标来获得Result,游标是一种控制结构,可以遍历数据库中的记录(图 7-2 )。

img/504354_1_En_7_Fig2_HTML.png

图 7-2

光标的工作流程

通过使用Row对象,R2DBC 驱动程序负责推进光标位置。如果取消了对表格结果的订阅,游标读取过程将会停止,与Result对象相关联的任何资源都将被释放。

更新计数

通过使用getRowsUpdated方法,Result对象报告受 SQL 语句影响的行数,例如 SQL 数据操作语言(DML)语句的更新。

Note

SQL 数据操作语言是一种子语言 SQL,它由涉及添加、删除或修改数据库中数据的操作组成。

Publisher<Integer> rowsUpdated = result.getRowsUpdated();

Listing 7-3Consuming a Result update count.

发出更新计数后,Result对象失效,来自同一个Result对象的行不再被使用。对于不修改行的语句,更新计数可以为空。

行和列

正如我之前指出的,Result接口提供了一个 map 方法,用于从Row对象中检索值。然而,代表表格结果的单行的行只是拼图的一部分。如你所知,表格由两种类型的实体组成:行和列(图 7-3 )。

img/504354_1_En_7_Fig3_HTML.png

图 7-3

表格数据由行和列组成

基于此,通过定位包含的列字段从行对象中检索数据(图 7-4 )。

img/504354_1_En_7_Fig4_HTML.png

图 7-4

R2DBC 结果层次结构

行解剖

实现驱动程序用来提供一个Row对象的Row接口包含四个方法,都被命名为 get:

  • Object get(int)

  • 对象获取(字符串)

  • T get(int,Class )

  • T get(字符串,类)

get(int)get(int, Class<T>)方法都接受一个整数值,从 0 开始,用于查找并返回指定索引处的列值(图 7-5 )。

img/504354_1_En_7_Fig5_HTML.png

图 7-5

使用索引从行对象中检索数据

get(String)get(String, Class<T>)方法都接受一个字符串值,用于查找并返回指定名称列的值(图 7-6 )。

img/504354_1_En_7_Fig6_HTML.png

图 7-6

使用列名从行对象中检索数据

用作get方法输入的列名不区分大小写,不一定反映基础表中的列名,而是反映列在结果中的表示方式或别名

Note

别名用于为数据库表或表中的列提供临时名称。别名通常用于使列名更具可读性或描述性。别名只在包含它的查询期间存在。

检索值

Row只在映射函数回调期间有效,在映射函数回调之外无效。因此,行对象必须完全由映射函数使用。

Tip

映射函数指的是本章前面提到的Result对象中的映射方法。

通用对象

使用没有指定目标类型的get方法将返回一个合适的值表示(清单 7-4 和 7-5 )。

Publisher<Object> values = result.map((row, rowMetadata) -> row.get("title"));

Listing 7-5Creating and consuming a Row object using a column name.

Publisher<Object> values = result.map((row, rowMetadata) -> row.get(0));

Listing 7-4Creating and consuming a Row object using an index.

指定类型

将类型作为参数包含在 get 方法中会提示 R2DBC 驱动程序尝试将从Row对象中检索到的值转换为指定的类型。

Publisher<String> titles = result.map((row, rowMetadata) -> row.get("title", String.class));

Listing 7-7Creating and consuming a Row object with type conversion using a column name.

Publisher<String> values = result.map((row, rowMetadata) -> row.get(0, String.class));

Listing 7-6Creating and consuming a Row object with type conversion using an index.

多列

您可以从一个Row对象中指定和使用多个列。

Publisher<Song> values = result.map((row, rowMetadata) -> {
   String title = row.get("title", String.class);
   String artist = row.get("artist", String.class);
   Integer albumId = row.get(“album_id”, Integer.class);

   return new Song(title, artist, albumId);
});

Listing 7-8Consuming multiple columns from a row using column names.

摘要

在这一章中,我们学习了 R2DBC 规范中定义的驱动程序中可用对象的层次结构。我们学习了Statement对象如何利用反应式编程方法来提供 SQL 语句执行的结果。

此外,我们更深入地剖析了Result对象,以便更好地理解检索数据的能力。除了了解检索由 SQL 语句更新的记录数量的能力之外,我们还进一步了解了游标和数据映射如何使访问 R2DBC 对象实现中的表格存储数据成为可能。

八、结果元数据

在前一章中,您了解了 R2DBC 使您能够非常容易地访问和使用从执行的 SQL 语句返回的结果。但是为了充分利用从数据库返回的结果,理解关于被返回数据的信息通常也同样重要。

更深入一点,在本章中,我们将看看R2DBC 规范如何通过使用元数据不仅可以检索和使用 SQL 结果,还可以洞察模式本身的技术信息。

由于各种原因,元数据在开发人员手中可能非常有用;正因为如此,通过 R2DBC 驱动程序访问它的能力至关重要。

关于数据的数据

然而,在开始理解如何使用元数据之前,理解它是什么是很重要的。简单地说,元数据是提供关于其他数据的数据。对于关系数据库,这意味着元数据提供了关于表格数据或其中的表和部分表的基本和相关信息。

R2DBC 规范为访问语句结果的元数据提供了两个接口,库和应用可以使用这两个接口来确定一个及其列的属性。

行元数据

第一个接口,RowMetadata(清单 8-1 ),用于确定一行的属性。该接口通过为结果中的每一列公开ColumnMetadata来实现这一点。

import java.util.Collection;
import java.util.NoSuchElementException;

public interface RowMetadata {
    ColumnMetadata getColumnMetadata(int index);
    ColumnMetadata getColumnMetadata(String name);
    Iterable<? extends ColumnMetadata> getColumnMetadatas();
    Collection<String> getColumnNames();
}

Listing 8-1The RowMetadata interface

使用getColumnMetadata方法,可以使用索引或列名来检索关于各个列的数据。RowMetadata对象还公开了通过getColumnMetadatas方法访问整个列数据集合以及通过getColumnNames方法访问列名集合的能力。

列元数据

列元数据通常是语句执行的副产品,信息量取决于驱动程序和底层数据库供应商。因为元数据检索可能需要额外的查找,通过使用内部查询来提供一组完整的元数据,所以数据库的工作流可能与 R2DBC 的反应式流特性相冲突。

因此,ColumnMetadata接口(清单 8-2 )声明了两组方法:需要实现的方法和驱动程序可选实现的方法。

public interface ColumnMetadata {

    String getName();

    @Nullable
    default Class<?> getJavaType() {
        return null;
    }

    @Nullable
    default Object getNativeTypeMetadata() {
        return null;
    }

    default Nullability getNullability() {
        return Nullability.UNKNOWN;
    }

    @Nullable
    default Integer getPrecision() {
        return null;
    }

    @Nullable
    default Integer getScale() {
        return null;
    }

}

Listing 8-2The ColumnMetadata interface

必需的方法

列元数据作为语句执行的副产品是可选的,并且是在“尽力而为”的基础上提供的。唯一需要驱动程序实现的方法是getName,它返回列的名称。该名称不一定反映列名在基础表中的样子,而是反映列在结果中的表示方式,包括别名。

可选方法

ColumnMetadata中的所有其他方法都是可选的。然而,根据 R2DBC 文档,建议驱动程序尽可能多地实现,但是支持将根据底层数据库的能力而因驱动程序而异。

getJavaType

getJavaType方法返回列值的主要 Java 类型。返回的类型被认为是本机表示形式,用于以最小的精度损失交换值。

R2DBC 文档建议驱动程序应该实现getJavaType,以便返回实际类型,避免返回Object类型。根据返回的类型,getJavaType的响应时间会有所不同。

getNativeTypeMetadata

getNativeTypeMetadata方法以类型Object的形式返回本机类型描述符,这可能会公开更多的元数据。R2DBC 文档建议,如果驱动程序能够提供公开任何附加信息的特定于驱动程序的类型元数据对象,则仅在实现getNativeTypeMetadata

getNullability

getNullability方法通过Nullabilty枚举(列表 8-3 )返回列值的可空性。

public enum Nullability {
    NULLABLE,
    NON_NULL,
    UNKNOWN
}

Listing 8-3The Nullability enumeration

getNullability方法返回的默认值是Nullability.UNKNOWN

设定精度

getPrecision方法返回列的精度。返回的精度值取决于列的基础类型。

例如

  • 数值数据返回最大精度值。

  • 字符数据返回字符的长度。

  • 日期时间数据返回表示该值所需的长度(以字节为单位),假设小数秒部分的最大允许精度。

设置模型等比缩放比例

getScale方法返回列的小数位数,即数字数据小数点右边的位数

正在检索元数据

获取 R2DBC 元数据对象需要我们回想一下我们在第七章中学习的关于结果的信息。记住,Result对象公开了一个名为map的方法,它的功能是映射在Result中返回的行。

获取 RowMetadata 对象

在使用Result.map(…)消费表格结果的过程中会创建一个RowMetadata对象。对于每个被创建的Row,一个RowMetadata对象也被创建。由此,如清单 8-4 所示,您可以使用RowMetadata对象来获取列元数据。

// result is a Result object
result.map(new BiFunction<Row, RowMetadata, Object>() {

    @Override
    public Object apply(Row row, RowMetadata rowMetadata) {
        ColumnMetadata my_column = rowMetadata.getColumnMetadata("column_name");
        Nullability nullability = my_column.getNullability();
    }

});

Listing 8-4Using a RowMetadata object to access and retrieve column metadata

访问列元数据

一旦你成功地从一个Result中获得了一个RowMetadata对象,你就能够访问在ColumnMetadata对象中可用的实现方法。

// row is a RowMetadata object
row.getColumnMetadatas().forEach(columnMetadata -> {
    String name = columnMetadata.getName();
    Integer precision = columnMetadata.getPrecision();
    Integer scale = columnMetadata.getScale();
});

Listing 8-5Retrieving column information through the ColumnMetadata object

摘要

应用可以用各种方式使用描述数据及其存储方式的信息。事实上,检索和消费元数据可能是您工具箱中非常强大的工具。

在本章中,我们了解到 R2DBC 规范使用Result对象通过RowMetadataColumnMetadata接口公开元数据。更进一步,根据驱动程序的供应商,我们检查了提取与我们的结果一致的列的关键信息的可能方法。

九、映射数据类型

在计算机科学中,数据类型是一种数据存储格式,可以包含特定类型的值范围。基本上,当数据被存储时,每个单元必须被分配到一个特定的类型,以便通用。

不同编程语言的数据类型特征和要求可能会有很大差异。当然,结构化查询语言(SQL)也不例外。在本章中,我们将研究 R2DBC 规范如何为常见的 SQL 数据类型提供支持,如何将它们映射到 Java 编程语言,以及如何在应用中使用它们。

数据类型差异

SQL 数据类型定义了可以存储在表列中的值的类型。有许多数据类型可用,如图 9-1 所示。

img/504354_1_En_9_Fig1_HTML.png

图 9-1

SQL 数据类型

然而,并不是所有关系数据库供应商都支持上图中列出的所有数据类型,有些供应商甚至还提供其他数据类型。尽管一些供应商可能会支持特定的数据类型,但很可能他们对每种类型都有不同的大小限制。

如您所知,应用不仅仅是用 SQL 构建的。请记住,R2DBC 驱动程序旨在为 JVM 语言提供支持,JVM 语言可用于构建应用,以一种反应式的方式与关系数据库进行通信。例如,Java 编程语言包含自己的数据类型,这些数据类型分为两大类:原语和非原语(图 9-2 )。

img/504354_1_En_9_Fig2_HTML.png

图 9-2

Java 数据类型

这就产生了这样一种情况,为了能够使用 SQL 数据类型,不管供应商是谁,使用像 Java 或任何其他应用开发语言这样的编程语言,必须完成映射或数据转换的过程。

幸运的是,R2DBC 规范提供了应用对 SQL 中定义的数据类型的访问。事实上,R2DBC 不仅限于 SQL 类型,因为它是类型不可知的,这一点我们将在本章后面深入探讨。

Tip

根据 R2DBC 规范文档,如果数据源不支持本章中描述的数据类型,则该数据源的驱动程序不需要实现与该数据类型关联的方法和接口。

映射简单数据类型

R2DBC 规范文档指出了可用作实现驱动程序指南的数据类型列表。R2DBC 驱动程序应该使用现代数据类型和类型描述符来与应用交换数据。

字符类型

字符和可变字符数据类型分别接受固定或可变长度的字符串或线性字符序列(表 9-1 )。

表 9-2

布尔类型的 SQL/Java 类型映射

|

SQL 类型

|

Java 类型

|

描述

|
| --- | --- | --- |
| BOOLEAN | java.lang.Boolean | 一个表示布尔(真/假)状态的值。 |

表 9-1

字符类型的 SQL/Java 类型映射

|

SQL 类型

|

Java 类型

|

描述

|
| --- | --- | --- |
| 性格;角色;字母(字符) | java.lang.String | 字符串,固定长度。 |
| 字符变化(VARCHAR) | java.lang.String | 会厌 |
| 民族性格(NCHAR) | java.lang.String | 类似于字符,但包含标准化的多字节或 Unicode 字符。 |
| 民族变体(NVARCHAR) | java.lang.String | 类似于字符变化,但保留标准化的多字节或 Unicode 字符。 |
| 字符大对象 | java.lang.String,io.r2dbc.spi.Clob | CLOB 是数据库管理系统中字符数据的集合。 |
| 国家字符大对象(NLOB) | java.lang.String,io.r2dbc.spi.Clob | 类似于 CLOB,但包含标准化的多字节字符或 Unicode 字符。 |

布尔类型

布尔数据类型支持两个值的存储:TRUE 和 FALSE。

Note

尽管这是一个相当简单的类型,但是需要注意的是,并不是所有的数据库管理系统都包含显式的布尔类型。例如,MySQL 和 MariaDB 使用TINYINT(1),来指定长度为 1 的INTEGER来包含值 1(表示真)或 0(表示假)。

二进制类型

关系数据库使用二进制数据类型来存储二进制数据,二进制数据用于图像、文本文件、音频文件等。

数字类型

数字可以用许多不同的方式表示。例如,它们可以是整数、分数、正数或负数。因此,关系数据库提供了多种数据类型来满足这种需求。

Note

在上表中, p 代表精度s 代表刻度

日期时间类型

日期和时间使用适应两者的类型以及两者的组合来存储。数据库管理系统还提供管理时区的功能。

集合类型

一些数据库供应商提供了集合数据的类型,如数组和多重集。

映射高级数据类型

到目前为止,我们已经研究了 R2DBC 规范支持的简单数据类型。但是,在处理数据时,可能会出现存储大量数据的需求。

虽然早期版本的关系数据库倾向于用现有类型如VARCHARNVARCHAR来处理这种情况,但很快就发现需要更高级的数据类型。

创建大型对象(lob)是为了以优化空间的方式存储数据,并为访问大型数据提供更有效的方法。在下一节中,我们将进一步了解 R2DBC 如何处理两种类型的 lob:blob 和 CLOBs。

斑点和土块

BLOBs 或二进制大型对象是一种存储二进制数据的数据类型,它不同于存储字母和数字的关系数据库中使用的其他数据类型,如整数和字符。允许存储二进制数据使得数据库包含图像、视频或其他多媒体文件成为可能。

Note

因为 BLOBs 用于存储照片、音频或视频文件等对象,所以它们通常比其他数据类型需要更多的空间。BLOB 可以存储的数据量因数据库管理系统而异。

CLOBs 或字符大对象与 BLOBs 相似,它们也是为了存储大量数据而存在的。然而,关键的区别在于 CLOB 数据是使用文本编码方法存储的,如 ASCII 和 Unicode。

Tip

这里的要点是,您可以认为 BLOBs 包含大量的二进制数据,而 CLOBs 包含大量的字符或文本数据。

BlobClob对象的驱动程序实现可以是基于定位器的,也可以是驱动程序中完全物化的对象。根据 R2DBC 规范文档,驱动程序应该更喜欢基于定位器的BlobClob实现,以减轻实现结果的客户机的压力。

创建对象

在 Java 中,lob 由一个发出特定类型大型对象的组件类型的Publisher对象支持,例如ByteBuffer用于BLOB,Java 接口类型CharSequence用于CLOB

Note

最终,CLOBs 作为一个String类型来处理,它实现了CharSequence接口。

R2DBC BlobClob接口提供了创建实现的工厂方法,这些实现可以被Statement对象使用(清单 9-1 )。

表 9-6

集合类型的 SQL/Java 类型映射。

|

SQL 类型

|

Java 类型

|

描述

|
| --- | --- | --- |
| 集合(数组,多重集) | 对应 Java 类型的数组变量(例如,Integer[]表示整数数组) | 表示具有基类型的项的集合。 |

表 9-5

日期时间类型的 SQL/Java 类型映射。

|

SQL 类型

|

Java 类型

|

描述

|
| --- | --- | --- |
| 日期 | java.time.LocalDate | 表示一个日期,但不指定时间部分和时区。 |
| 时间 | Java . time . local time-Java .时间。本机时间 | 表示没有日期部分和时区的时间。 |
| 时区时间 | java.time.OffsetTime | 表示带有时区偏移量的时间。 |
| 时间戳 | java.time.LocalDateTime | 表示不带时区的日期和时间。 |
| 带时区的时间戳 | java.time.OffsetDateTime | 表示带有时区偏移量的日期和时间。 |

表 9-4

数字类型的 SQL/Java 类型映射

|

SQL 类型

|

Java 类型

|

描述

|
| --- | --- | --- |
| 整数 | java.lang.Integer | 表示一个整数。最小值和最大值取决于数据库管理系统(通常精度为 4 字节)。 |
| 蒂尼因特 | java.lang.Byte | 类似于 INTEGER,但它可能包含较小范围的值,具体取决于数据库管理系统(通常精度为 1 字节)。 |
| 斯莫列特 | java.lang.Short | 类似于 INTEGER,但它可能包含更小范围的值,这取决于数据库管理系统(通常是 1 或 2 字节精度)。 |
| 比吉斯本 | java.lang.Long | 类似于 INTEGER,但它可能包含更大范围的值,这取决于数据库管理系统(通常是 8 字节精度)。 |
| 十进制数字 | java.math.BigDecimal | 具有精度(p)和小数位数(s)的固定精度和小数位数。可以表示小数点值的数字。 |
| 浮动 | 双精度浮点数 | 表示尾数精度为(p)的近似数值。根据精度参数(p ),使用 IEEE 表示法的数据库可以将值映射到 32 位或 64 位浮点类型。 |
| 真实的 | java.lang.Float | 像 FLOAT,但是数据库管理系统定义了精度。 |
| 双倍精密度 | java.lang.Double | 像 FLOAT,但是数据库管理系统定义了精度。 |

表 9-3

二进制类型的 SQL/Java 类型映射

|

SQL 类型

|

Java 类型

|

描述

|
| --- | --- | --- |
| 二进制的 | Java . 9 .字节缓冲区 | 二进制数据,固定长度。 |
| 二进制变量 | Java . 9 .字节缓冲区 | 可变长度字符串,使用其最大长度。 |
| 二进制大对象 | java.nio .字节缓冲区,io.r2dbc.spi.Blob | BLOB 是数据库管理系统中二进制数据的集合。 |

static Blob from(Publisher<ByteBuffer> p) {
    Assert.requireNonNull(p, "Publisher must not be null");
    DefaultLob<ByteBuffer> lob = new DefaultLob<>(p);
    return new Blob() {
        @Override
        public Publisher<ByteBuffer> stream() {
            return lob.stream();
        }
        @Override
        public Publisher<Void> discard() {
            return lob.discard();
        }
    };
}

Listing 9-1Blob factory method used to provide a usable implementation.

类似的方法存在于Clob接口中,同样,创建和使用BlobClob对象的步骤也是类似的。

// characterStream is a Publisher<String> object
// statement is a Statement object
Clob clob = Clob.from(characterStream);
statement.bind("text", clob);

Listing 9-3Creating and using a Clob

// binaryStream is a Publisher<ByteBuffer> object
// statement is a Statement object
Blob blob = Blob.from(binaryStream);
statement.bind("image", blob);

Listing 9-2Creating and using a Blob

检索对象

BLOB 和 CLOB 数据类型被视为基本的内置类型。事实上,BLOB 和 CLOB 值可以通过使用Row对象的 get 方法来检索。

Publisher<Clob> clob = result.map((row, rowMetadata) -> row.get("clob", Clob.class));

Listing 9-5Retrieving a Clob object

Publisher<Blob> blob = result.map((row, rowMetadata) -> row.get("blob", Blob.class));

Listing 9-4Retrieving a Blob object

消费对象

BlobClob接口公开了一个名为stream的方法,该方法为客户端提供了一种消费各自内容的方式。与反应流规范保持一致,内容流用于传输大型对象。

Publisher<CharSequence> characterStream = clob.stream();

Listing 9-7Accessing a Clob object using the stream method

Publisher<ByteBuffer> binaryStream = blob.stream();

Listing 9-6Accessing a Blob object using the stream method

值得注意的是,流只能被使用一次,在这个过程中,可以通过执行discard方法随时调用数据。

释放对象

因为BlobClob对象在其事务期间保持有效,所以长时间运行的事务可能会导致应用耗尽资源。记住这一点,R2DBC 规范为实现提供了一种叫做discard的方法,应用可以用它来释放BlobClob对象资源。

Publisher<Void> characterStream = clob.discard();
characterStream.subscribe();

Listing 9-9Releasing Clob object resources

Publisher<Void> binaryStream = blob.discard();
binaryStream.subscribe();

Listing 9-8Releasing Blob object resources

摘要

理解数据类型是有效管理信息的重要组成部分。事实上,如果你仔细想想,对数据类型进行分类是关系数据库管理系统最关键的概念之一。没有它们,像数据完整性这样的强制原则就不可能实现。

在本章中,我们学习了 R2DBC 旨在支持的数据类型以及如何使用它们。我们还研究了更高级的数据类型,如 BLOBs 和 CLOBs,以及它们如何利用反应式编程功能,以及如何被创建、消费和销毁。

十、处理异常

想象一下这样一个场景:你正在编写一个应用,每次编译和运行代码时,一切都完美无缺。好了,你可以不笑了。无论您是新手还是经验丰富的开发人员,有一点是不变的:异常时有发生。虽然如果我们的代码没有问题就很好,但是由于这样或那样的原因,我们一定会遇到问题。幸运的是,我们有能力编写代码来处理和管理异常。

在这一章中,我们将看看 R2DBC 定义的异常类型,并使用它来提供您可能遇到的各种类型的故障的信息。

一般例外

R2DBC 驱动程序抛出异常有两个原因。

  1. 在与驾驶员本身的交互过程中

  2. 在与底层数据源交互的过程中

R2DBC 区分一般错误(或可能出现在驱动程序代码中的错误)和特定于数据源的错误。

非法数据异常

驱动程序抛出IllegalArgumentException来表示 R2DBC 对象中的方法收到了非法或无效的参数。无效参数包括值超出界限或预期参数为空的情况。

IllegalArgumentException扩展了RuntimeException类,因此属于那些可以在 JVM 运行期间抛出的异常。它是一个未检查的异常,因此不需要在方法或构造函数的 throws 子句中声明。

Note

未检查的异常是在执行时发生的异常。这些也称为运行时异常。这些包括编程错误,如逻辑错误或 API 使用不当。编译时会忽略运行时异常。

IllegalStateException

IllegalStateException,也是RuntimeException的扩展,表示一个方法在非法或不适当的时间被调用。R2DBC 驱动程序抛出IllegalStateException来指示方法在当前状态下收到了无效的参数,或者当无参数方法涉及到不允许执行当前状态的状态时,例如,如果试图与已经关闭其连接的对象进行交互。

不支持操作异常

扩展了RuntimeException,UnsupportedOperationException类是相当自明的。它被抛出以指示所请求的操作不受支持。对于 R2DBC 驱动程序,这意味着当驱动程序不支持某些功能时,例如当没有提供方法实现时,它将被抛出。

R2DBCException

R2dbcException抽象类扩展了RuntimeException并作为根异常运行,这意味着 R2DBC 实现中所有与服务器相关的异常都将扩展它。当在与数据源的交互过程中出现错误时,驱动程序将抛出一个R2dbcException实例。

一个R2dbcException对象将包含以下信息:

  • 错误的描述。描述是文本的,可以在驱动程序实现中本地化,并且可以通过调用getMessage方法来检索。

  • 一个SQLState,它通过调用getSqlState方法被检索为一个String。根据 R2DBC 规范文档,SQLState字符串的值取决于底层数据源,并遵循 XOPEN SQLState 或 SQL:2003 约定。

  • 一个错误代码,通过调用getErrorCode方法将其作为Integer进行检索。错误代码值的值和含义是特定于供应商实现的。

  • 一个原因,作为导致抛出R2dbcExceptionThrowable返回。

Note

Throwable类是 Java 语言中所有错误和异常的超类。只有作为这个类(或它的一个子类)实例的对象才会被 JVM 抛出,或者可以被 Java throw 语句抛出。

驱动程序实现能够通过几个构造函数创建R2dbcException对象,并接受reasonsqlStateerrorCodecause参数的可变组合。设置好值后,R2dbcException提供获取异常细节的 getter 方法(清单 10-1 )。

package io.r2dbc.spi;

/**
 * A root exception that should be extended by all server-related exceptions in an implementation.
 */
public abstract class R2dbcException extends RuntimeException {

    private final int errorCode;

    private final String sqlState;

    /**
     * Creates a new {@link R2dbcException}.
     */
    public R2dbcException() {
        this((String) null);
    }

    /**
     * Creates a new {@link R2dbcException}.
     *
     * @param reason the reason for the error.  Set as the exception's message and retrieved with {@link #getMessage()}.
     */
    public R2dbcException(@Nullable String reason) {
        this(reason, (String) null);

    }

    /**
     * Creates a new {@link R2dbcException}.
     *
     * @param reason   the reason for the error.  Set as the exception's message and retrieved with {@link #getMessage()}.
     * @param sqlState the "SQLState" string, which follows either the XOPEN SQLState conventions or the SQL:2003
     *                 conventions
     */
    public R2dbcException(@Nullable String reason, @Nullable String sqlState) {
        this(reason, sqlState, 0);
    }

    /**
     * Creates a new {@link R2dbcException}.
     *
     * @param reason    the reason for the error.  Set as the exception's message and retrieved with {@link #getMessage()}.
     * @param sqlState  the "SQLState" string, which follows either the XOPEN SQLState conventions or the SQL:2003
     *                  conventions
     * @param errorCode a vendor-specific error code representing this failure
     */
    public R2dbcException(@Nullable String reason, @Nullable String sqlState, int errorCode) {
        this(reason, sqlState, errorCode, null);
    }

    /**
     * Creates a new {@link R2dbcException}.
     *
     * @param reason    the reason for the error.  Set as the exception's message and retrieved with {@link #getMessage()}.
     * @param sqlState  the "SQLState" string, which follows either the XOPEN SQLState conventions or the SQL:2003
     *                  conventions
     * @param errorCode a vendor-specific error code representing this failure
     * @param cause     the cause
     */
    public R2dbcException(@Nullable String reason, @Nullable String sqlState, int errorCode, @Nullable Throwable cause) {
        super(reason, cause);
        this.sqlState = sqlState;
        this.errorCode = errorCode;
    }

    /**
     * Creates a new {@link R2dbcException}.
     *
     * @param reason   the reason for the error.  Set as the exception's message and retrieved with {@link #getMessage()}.
     * @param sqlState the "SQLState" string, which follows either the XOPEN SQLState conventions or the SQL:2003
     *                 conventions
     * @param cause    the cause
     */
    public R2dbcException(@Nullable String reason, @Nullable String sqlState, @Nullable Throwable cause) {
        this(reason, sqlState, 0, cause);
    }

    /**
     * Creates a new {@link R2dbcException}.
     *
     * @param reason the reason for the error.  Set as the exception's message and retrieved with {@link #getMessage()}.
     * @param cause  the cause
     */
    public R2dbcException(@Nullable String reason, @Nullable Throwable cause) {
        this(reason, null, cause);
    }

    /**
     * Creates a new {@link R2dbcException}.
     *
     * @param cause the cause
     */
    public R2dbcException(@Nullable Throwable cause) {
        this(null, cause);
    }

    /**
     * Returns the vendor-specific error code.
     *
     * @return the vendor-specific error code
     */
    public final int getErrorCode() {
        return errorCode;
    }

    /**
     * Returns the SQLState.
     *
     * @return the SQLState
     */
    @Nullable
    public final String getSqlState() {
        return this.sqlState;
    }

    @Override
    public String toString() {

        StringBuilder builder = new StringBuilder(32);
        builder.append(getClass().getName());

        if (getErrorCode() != 0 || (getSqlState() != null && !getSqlState()
            .isEmpty()) || getMessage() != null) {
            builder.append(":");
        }

        if (getErrorCode() != 0) {
            builder.append(" [").append(getErrorCode()).append("]");
        }

        if (getSqlState() != null && !getSqlState().isEmpty()) {
            builder.append(" [").append(getSqlState()).append("]");
        }

        if (getMessage() != null) {
            builder.append(" ").append(getMessage());
        }

        return builder.toString();
    }
}

Listing 10-1The R2dbcException abstract class

分类异常

R2DBC 规范旨在对异常进行分类,以便提供到常见错误状态的一致映射。R2DBC 规范文档指出

分类的异常为 R2DBC 客户端和 R2DBC 用户提供了一种标准化的方法,将常见异常转换为特定于应用的状态,而无需实现基于 SQLState 的异常转换,从而产生更可移植的错误处理代码。

在最高级别,R2DBC 将异常分为两类:非瞬态瞬态

非暂时性异常

非暂时性异常是指在问题的根本原因得到纠正之前,重试时会再次失败的异常。R2DBC 非瞬态异常必须扩展抽象类R2dbcNonTransientException,它是R2dbcException的子类。

R2DBC 规范包含四个非瞬态异常子类(表 10-1 )。

表 10-1

非暂时性异常子类

|

亚纲

|

描述

|
| --- | --- |
| R2 dbcbadgram exception | 当 SQL 语句的语法有问题时抛出。 |
| R2 dbcdataintegrityionexception | 当尝试插入或更新数据导致违反完整性约束时引发。 |
| R2 dbcpermissions exception | 当基础资源拒绝访问特定元素(如特定数据库表)的权限时引发。 |
| R2dbcNonTransientException | 当资源完全失败并且失败是永久性的时抛出。如果引发此异常,连接可能被视为无效。 |

暂时性异常

暂时性异常是那些重试时可能成功而不改变任何东西的异常。当先前失败的操作可能能够成功时,如果重试该操作而不干预应用级功能,则会抛出 R2DBC 瞬态异常。R2DBC 瞬态异常必须扩展抽象类R2dbcTransientException,它是R2dbcException的子类。

R2DBC 规范包含瞬态异常的两个子类(表 10-2 )。

表 10-2

暂时异常子类

|

亚纲

|

描述

|
| --- | --- |
| R2 dbcroll back 异常 | 当提交事务的尝试由于死锁或事务序列化失败而导致意外回滚时引发。 |
| R2 dbctimroute exception | 当超过数据库操作指定的超时时间时引发。 |

摘要

在使用数据库时,不可避免的是,在某些时候,您会遇到错误,并且必须处理已经抛出的异常。不管您的经验如何,异常处理是现代应用开发的正常部分。

在本章中,我们研究了 R2DBC 用来说明可能遇到的问题的各种异常类。您了解了 R2DBC 驱动程序使用的一般异常,以及用于提供对非瞬态和瞬态异常的更深入了解的自定义异常类层次结构。

十一、R2DBC 入门

现在,您已经对 R2DBC 规范、其整体结构和一般工作流程有了一个概述,并对其功能有了一点了解,您已经准备好开始实现了。因为,至此,你已经对什么是 R2DBC 和为什么它是必要的有了坚实的理解,现在是时候学习如何使用它了。

在这一章中,我们将简要研究官方 R2DBC 驱动程序和客户机库,它们有助于使用关系数据库创建反应式解决方案。最后,我们将浏览创建一个新的、非常基础的 Java 项目的过程,该项目将有助于 R2DBC 驱动程序的使用。我们在本章中创建的项目将为后面的章节打下基础,因为我们将务实地深入 R2DBC 的功能。

数据库驱动程序

到目前为止,您已经了解到,R2DBC 驱动程序是实现 R2DBC 服务供应器接口(SPI)的自包含编译库。请记住,R2DBC SPI 被有意设计得尽可能小,同时仍然包含对任何关系数据存储都至关重要的特性。这都确保了 SPI 不会将任何可能特定于数据存储的扩展作为目标。

]

即使在我写这篇文章的时候,R2DBC SPI 还不是正式的大众(GA),仍然有各种各样的驱动程序实现,跨越多个数据库供应商。表 11-1 显示目前有 7 个官方的、开源的 R2DBC 驱动。

表 11-1

官方 R2DBC 驱动程序

|

驾驶员

|

目标数据库

|

源位置

|
| --- | --- | --- |
| 云扳手-r2dbc | 谷歌云扳手 | https://github.com/GoogleCloudPlatform/cloud-spanner-r2dbc |
| jassync SQL | MySQL,一种数据库系统 | https://github.com/jasync-sql/jasync-sql |
| r2dbc-h2 | 氘 | https://github.com/r2dbc/r2dbc-h2 |
| r2dbc-mariadb | 马里亚 DB | https://github.com/mariadb-corporation/mariadb-connector-r2dbc |
| r2d DBC-MSSQL | 搜寻配置不当的 | https://github.com/r2dbc/r2dbc-mssql |
| r2dbc-mysql | 关系型数据库 | https://github.com/mirromutth/r2dbc-mysql |
| r2dbc-postgres | 一种数据库系统 | https://github.com/r2dbc/r2dbc-postgresql |

Note

出于本书的目的,我们将使用 MariaDB R2DBC 驱动程序。本章稍后将提供更多信息。

R2DBC 客户端库

您已经了解到 R2DBC 规范是有意创建的轻量级规范,旨在为潜在的驱动程序实现提供大量创造性的灵活性。规范的简单性也阻止了更多固执己见的用户空间功能被包含在驱动程序实现中。

相反,根据官方文档,R2DBC 将“人性化”API 功能的责任留给了客户端库:

R2DBC 规范的意图是鼓励库以客户端库的形式提供“人性化”API。

从实现的角度来看,这意味着应用可以使用客户端库,然后间接使用每个 R2DBC 驱动程序与底层数据源进行反应式通信(图 11-1 )。

img/504354_1_En_11_Fig1_HTML.png

图 11-1

R2DBC 驱动程序和客户端解决方案充当应用与底层数据源通信的抽象

客户端库作为一个抽象层,有助于减少使用 R2DBC 驱动程序实现所需的样板代码或脚手架代码的数量。最后,如果您想在解决方案中包含 R2DBC 客户端库,有两种方法可以实现;创建新的客户端或使用现有的客户端。

创建客户端

由于 R2DBC SPI 的极简特性,创建新客户端的过程应该简单明了。首先,客户端库只需要包含 R2DBC SPI 作为依赖项。这可以通过两种方式实现。

包含 R2DBC SPI 作为依赖项的第一种方法是将组和工件标识符添加到基于 Maven 的项目的 pom.xml 文件中的依赖项列表中(清单 11-1 )。

<dependency>
    <groupId>io.r2dbc</groupId>
    <artifactId>r2dbc-spi</artifactId>
</dependency>

Listing 11-1The R2DBC SPI dependency settings for pom.xml

Tip

如果您不熟悉向基于 Maven 的 Java 应用添加依赖项的过程,不要担心。在这一章的后面,我们将深入讨论这个过程的更多细节!

第二种替代方法是通过使用 Apache Maven 包装器项目直接从源代码构建 R2DBC SPI,GitHub 上有这个项目。

使用现有的客户端库

目前有几个官方 R2DBC 支持客户端(表 11-2 )甚至更多正在调查中。如图 11-1 所示,现有的 R2DBC 客户端库通过利用 R2DBC 驱动程序实现来处理与底层数据源的实际交互。

表 11-2

现有 R2DBC 客户端示例

|

名字

|

描述

|
| --- | --- |
| 春季数据 R2DBC | 一个支持使用 Spring 数据和反应式开发原则和存储库抽象的项目。 |
| 科斯塔亚 | 一个使用 Kotlin 编程语言的类型安全且可协同运行的 SQL 引擎。 |
| jOOQ | 为流行的类型安全 SQL 查询构造库提供 R2DBC 支持。 |

每个 R2DBC 客户端库支持的 R2DBC 驱动程序各不相同。

展望未来

毫无疑问,包含 R2DBC 客户端库的决定是实现新的真正反应式 R2DBC 解决方案的关键一步。然而,在我们能跑之前,我们需要学习如何走路。因此,在这一章中,我们将逐步完成建立一个项目的过程,这个项目只利用了一个 R2DBC 驱动程序实现。然后,在第 12 、 13 和 14 章中,我们将仔细查看R2DBC 驱动程序实现如何利用特定的反应流实现来促进与目标关系数据库的反应交互。**

最后,在第十六章中,在您对创建直接使用 R2DBC 驱动程序的应用有了扎实的理解和必要的努力之后,您将了解一个名为 Spring Data R2DBC 的特定 R2DBC 客户端库如何帮助显著简化 R2DBC 解决方案的创建。

MariaDB 和 R2DBC

如前所述,有多家供应商提供 R2DBC 驱动程序,所有这些驱动程序都提供了基本的 R2DBC 支持和独特的数据库功能。对于本书的其余部分,我们将使用 MariaDB 提供的单一、特定的 R2DBC 驱动程序,用于所有示例、代码片段和演练。

Note

MariaDB 是社区开发的、商业支持的 MySQL 关系数据库管理系统的分支,旨在 GNU 通用公共许可证下保持自由和开源软件。

决定使用 MariaDB 作为 R2DBC 驱动程序和关系数据库并不是出于任何特殊的技术原因,而是因为它提供了一个简单的、开放源代码的、最重要的是免费的数据库解决方案来帮助展示 R2DBC 的能力。

MariaDB 简介

因为我将使用 MariaDB R2DBC 驱动程序作为示例 R2DBC 驱动程序实现来演示如何被动地与 MariaDB 数据库实例进行通信,所以为了继续学习,您还需要有一个正在运行的 MariaDB 数据库实例。因此,如果您已经可以访问 MariaDB 的实例,那太好了!请随意跳到下一部分。然而,如果你不熟悉 MariaDB,不用担心。有许多方法可以开始使用一个新的免费数据库实例。

正如我前面提到的,MariaDB 提供了一个免费的开源关系数据库。事实上,MariaDB Server,作为免费的开源版本,是世界上最流行的开源关系数据库之一,因此,在包括 Windows、macOS 和 Linux 在内的各种操作系统上都受到支持。

下载并安装

MariaDB 的广泛使用不仅使安装变得轻而易举;您还有几个选项可供选择。

直接下载

首先打开一个浏览器,导航到 http://mariadb.org/download ,它由 MariaDB 基金会支持和维护。

Note

MariaDB 基金会是一个非营利组织,作为 MariaDB 服务器协作的全球联系点。

在那里,您可以找到为您的操作系统(OS)检索 mariadb-server 包的说明,或者,通过选择页面上的各种选项,您可以指明您想要下载的包(图 11-2 )。

img/504354_1_En_11_Fig2_HTML.jpg

图 11-2

指定要下载的 MariaDB 服务器包

坞站集线器下载

如果您正在使用 Windows 或 Linux,或者对从源代码构建 MariaDB 服务器感兴趣,上一节提供了下载和安装 MariaDB 的绝佳选择。但是如果你使用 macOS 作为你的操作系统呢?不要害怕!访问 MariaDB 的另一种方式是使用容器,更具体地说,在本例中是 Docker 容器。

Note

容器是一个标准的软件单元,它封装了代码及其所有依赖项,以便应用可以从一个计算环境可靠地运行到另一个计算环境。

首先导航至 www.docker.com/get-started 。在那里,您可以找到关于下载、安装和运行软件的要求的更多信息,这些软件能够为您的环境提供 Docker 容器。

从那里你可以访问最新的 MariaDB 服务器 Docker 容器,以及如何下载、安装和运行的说明,在 https://hub.docker.com/r/mariadb/server ,这是一个官方的 MariaDB 社区服务器 Docker 镜像,由 MariaDB 公司提供。

Note

MariaDB Corporation 是一家提供企业级 MariaDB 产品和服务的营利性公司。随着 MariaDB 企业平台的开发,MariaDB Corporation 也为 MariaDB 服务器的开源社区版本做出了贡献。

访问并准备

一旦您下载并安装了 MariaDB 实例,您应该确认您有能力访问它。这通常是通过使用数据库客户端来完成的。虽然有多种数据库客户机可供选择,但 MariaDB Server 的下载和安装还包括一个可以通过终端访问的客户机。

只需打开一个终端窗口并执行清单 11-2 中所示的命令,连接到您的 MariaDB 实例。

mariadb --host 127.0.0.1 --port 3306 --user root

Listing 11-2Connecting to a local MariaDB Server instance

Note

清单 11-2 中指出的连接值假设您已经使用默认设置在本地机器上安装了一个 MariaDB 服务器实例。

或者,由于默认设置,您也可以通过使用清单 11-3 中的命令连接到本地 MariaDB 服务器实例。

mariadb

Listing 11-3Connecting to a local MariaDB Server instance using the default connection configuration

一旦成功连接到 MariaDB 服务器,就可以通过执行清单 11-4 中的 SQL 来添加一个名为app_user的新用户。

CREATE USER 'app_user'@'%' IDENTIFIED BY 'Password123!';
GRANT ALL PRIVILEGES ON *.* TO 'app_user'@'%' WITH GRANT OPTION;

Listing 11-4Creating a new MariaDB database instance user and password

Note

为了保持前后一致,我将在所有后续的凭证相关示例中使用app_user用户和Password123!密码设置。

使用 R2DBC

建立了对关系数据库的访问之后,我们就可以在 Java 应用中使用 R2DBC 驱动程序了。事实上,在本章的剩余部分,我们将逐步创建和运行一个新的 Java 应用,该应用包含 MariaDB R2DBC 驱动程序作为依赖项。

要求

在开发、构建和运行 Java 应用之前,您需要确保您的系统满足这样做的必要要求。下面的小节描述了在继续阅读本书之前你应该完成的一些必要任务。

安装 Java 开发工具包

首先是安装 Java 开发工具包(JDK),这是一个全功能的软件开发工具包(SDK),必须安装它才能开发和运行 Java 应用。JDK 依赖于平台,因此下载和安装的要求因目标平台而异。

有关如何下载和安装最新版 JDK 的更多信息,请访问 www.oracle.com/java/technologies/javase-downloads.html

安装 Apache Maven

接下来,您应该安装 Apache Maven。构建管理是高级软件语言编译的一个极其重要的方面。Apache Maven 是一个非常流行的构建自动化和管理工具,主要用于 Java。事实上,它是同类 Java 中最受欢迎的工具。

正是出于这个原因,我们将使用 Apache Maven 作为 R2DBC 应用示例的构建管理工具。有关 Apache Maven 的更多信息,包括如何下载和安装,请访问 http://maven.apache.org

Apache Maven 基础知识

我们已经准备好创建一个新的 Java 应用,并开始使用 R2DBC 驱动程序来直接了解它的功能!您可能已经知道,有几种机制可以用来创建新的 Java 项目。

事实上,如果您已经有了开发 Java 应用的经验,那么您甚至可能对如何创建一个新的应用或项目有自己的偏好。无论是通过在终端中手动执行命令,使用特定的集成开发环境(IDE),甚至可能是项目生成网站,都不缺少选项。

创建新项目

为了保持简单和统一,我们将首先利用 Apache Maven 软件配置管理(SCM)客户端(一个简单的命令行工具)来创建一个新的 Java 项目。

首先,在系统中选择一个位置来存储新项目。然后,如清单 11-5 所示,使用 Maven 客户端,通过–DgroupId执行以下指定组的命令;神器,via–DartifactId;和一些其他样板参数来创建一个新的 Java 项目。

mvn archetype:generate -DgroupId=com.example -DartifactId=r2dbc-demo ​-DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.4 ​-DinteractiveMode=false

Listing 11-5The Apache Maven SCM command to create a new project

Tip

使用 Maven 客户机创建新项目只是创建新 Java 项目的一种可能方法。最终,如何创建一个新的 Java 项目并不重要,重要的是你创建了一个新的项目。

-DarchetypeArtifactId=maven-archetype-quickstart参数和值用于指定一个示例 Maven 项目的生成(图 11-3 )。

img/504354_1_En_11_Fig3_HTML.jpg

图 11-3

Maven 项目结构示例

但是现在我们已经使用 Maven 客户端创建了一个新的 Java 项目,什么样的使 Java 项目 Maven 就绪?

最终,Maven 项目是用一个名为 pom.xml 的 XML 定义的,它包含了项目的名称版本,最重要的是,一个一致的、统一的方法来标识外部库的依赖关系。

添加依赖关系

依赖关系是在 pom.xml 的dependencies节点中定义的。向项目添加依赖项涉及到修改 pom.xml 文件。

使用您选择的文本或代码编辑器,打开 pom.xml 文件,该文件位于新创建项目的根文件夹中。导航到dependencies节点,为 MariaDB R2DBC 驱动程序版本 1.0.0 添加一个新的dependency。,如清单 11-6 中的片段所示。

<dependency>
      <groupId>org.mariadb</groupId>
      <artifactId>r2dbc-mariadb</artifactId>
      <version>1.0.0</version>
</dependency>

Listing 11-6MariaDB R2DBC driver dependency

Note

Maven 使用包含在dependencies节点中的值,如groupIdartifactIdversion,来搜索要添加到项目中的内部或外部库。

构建项目

在将 MariaDB R2DBC 驱动程序依赖项添加到项目中之后,现在是构建项目以确认一切都已正确配置的最佳时机。

首先,打开一个终端窗口,导航到包含 pom.xml 文件的目录,并执行清单 11-7 中所示的命令。

mvn package

Listing 11-7The command to build a Maven project

如果一切都已正确配置,您应该会看到类似以下内容的控制台打印输出:

[INFO] --------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] --------------------------------------------------------------------
[INFO] Total time:  1.644 s
[INFO] Finished at: 2020-11-01T05:17:25-06:00
[INFO] --------------------------------------------------------------------

给我看看代码

您可以在专门针对本书的 GitHub 资源库中找到一个完整的、完全可编译的示例应用。只需导航到 https://github.com/apress/r2dbc-revealedgit clone或者直接下载存储库的内容。在那里,您可以在 ch11 文件夹中找到专门用于这一章的示例应用。

展望未来

现在,您已经对什么是 Apache Maven 有了基本的了解,并且对它的基础设施以及如何使用它来管理依赖关系有了较高的认识,您已经对如何将 R2DBC 驱动程序添加到应用中有了基本的了解。

在接下来的几章中,我们将基于您创建的示例应用,用 Java 展示如何通过与 MariaDB R2DBC 驱动程序直接集成来利用 R2DBC 功能。如前一节所述,本章和其余所有章节的示例应用可以在本书专用的 GitHub 资源库中找到。

此外,正如我在本章前面提到的,R2DBC 规范的目标之一是通过使用 R2DBC 客户端库为创建更人性化的 API 提供一条途径。因此,尽管接下来的几章将重点关注方向与 R2DBC 驱动程序的集成,但第十六章将更深入地了解使用 Spring Data R2DBC 客户端库开发完全反应式应用的情况。基本上,如果你喜欢代码,你会喜欢接下来的几章!

摘要

在本章中,您了解了当前可用的不同 R2DBC 驱动程序和客户端解决方案。您还了解了这些解决方案是如何直接通过它们的开源代码或作为基于 Maven 的包提供的。

最后,您浏览了创建一个 Java 项目的过程,该项目包括一个 R2DBC 驱动程序和一个客户机,将在后续章节中使用它来检查与关系数据库 MariaDB 的反应式交互的能力。

十二、管理连接

在前一章中,向您介绍了创建一个新的 Java 项目的过程,并利用 Apache Maven 的功能,将 MariaDB R2DBC 驱动程序作为一个依赖项添加到项目中。现在,您已经成功地创建了一个能够利用 R2DBC 实现的应用,是时候深入了解该规范的功能了。

在这一章中,我们将扩展那个项目来检查驱动程序中可用的连接对象实现。在继续之前,如果您还没有这样做,我强烈推荐您阅读第四章,其中深入了 R2DBC 规范连接层次和工作流的更多细节。

配置

出于本章的目的,我将强调我认为的常规连接参数,以举例说明如何建立到 MariaDB 的连接。更具体地说,我将提供针对一个本地数据库实例的例子。

Note

在本地运行一个程序意味着在你所在的机器上运行它(或者在它自己运行的机器上运行它),而不是让它在某个远程机器上运行。

除了这个示例之外,如果您想了解更多关于 MariaDB 或任何其他 DBMS 的配置选项的信息,我强烈建议您查阅 MariaDB 官方文档。

数据库连接

在上一章中,我们介绍了在您的机器上启动并运行数据库实例的过程。这样做是为了让您能够访问 MariaDB 数据库实例,您可以使用该实例来测试 MariaDB R2DBC 驱动程序的功能。我特别提供了关于设置本地数据库实例的指导,因为它需要最少的配置信息来建立连接。

例如,在表 12-1 中,我已经指出了连接到 MariaDB 的本地实例所需的信息。

表 12-1

示例连接参数

|

财产

|

价值

|

描述

|
| --- | --- | --- |
| 主机名 | 127.0.0.1 | MariaDB 服务器的 IP 地址或域。本地数据库实例的默认 IP 地址是 127.0.0.1。 |
| 港口 | Three thousand three hundred and six | 端口号被用作标识特定进程如何被转发的一种方式。MariaDB 的默认端口号是 3306。 |
| 用户名 | 流引擎 | 连接到 MariaDB 数据库实例所需的用户名。 |
| 密码 | 密码 123! | 连接到 MariaDB 数据库实例所需的密码。 |

Note

在第十一章中,我提供了创建一个新用户app_user的 SQL 语句,这个新用户的密码是 MariaDB 数据库实例的Password123!。虽然当然可以使用您想要的任何凭证,但为了简单和一致,我将在本书中对所有连接代码设置使用app_user

驱动程序实现和可扩展性

正如你在表 12-1 中看到的,信息是最少的。这在两个方面帮助了你。首先,它为您提供了连接到 MariaDB 数据库的最简单的方法。第二,对主机、端口号、用户名和密码的要求在所有其他关系数据库中是常见的,因此,这是一个可以应用于其他数据库供应商及其相应 R2DBC 驱动程序实现的示例。

运行反应代码

在继续之前,需要注意的是,本书中的许多例子将使用非阻塞方法执行,比如subscribe方法。

因为我们将使用一个简单的控制台应用,它利用一个类和main方法,由于异步事件驱动通信的性质,应用可能在发布者向他们的订阅者发送信息之前完成执行。

作为一种可能的变通方法,也是我将在接下来的几章中使用的方法,可以添加代码,通过阻塞当前线程来保持应用的运行。

首先,修改main方法以允许抛出一个InterruptedException。这样做将允许您添加代码来加入当前线程,这将阻止main方法退出(清单 12-1 )。

public static void main(String[] arg     s) throws InterruptedException {
    // This is where we’ll be executing R2DBC sample code

    Thread.currentThread().join();
}

Listing 12-1Keep the current thread threading to allow time for publishers and subscribers to complete processing

Caution

清单 12-1 中的代码块仅仅是出于演示目的的变通方法。您不太可能想在更实际的或“真实世界”的解决方案中使用这样的代码。

建立联系

在第四章中,你学习了 R2DBC Connection对象实现的创建是通过驱动程序的ConnectionFactory对象实现来管理的。

在继续之前,需要注意的是,MariaDB R2DBC 驱动程序实现了连接接口的完整层次结构,并通过在每个对象的名称前加上“MariaDB”为每个对象提供了一个简单的命名约定。因此,驱动程序将ConnectionConnectionFactory接口分别实现为MariadbConnectionMariadbConnectionFactory

Note

这种类型的命名约定在其他 R2DBC 驱动程序实现中很常见。

正在获取 MariadbConnectionFactory

最重要的是,MariadbConnectionFactory对象用于管理MariadbConnection对象。当然,为了能够管理MariadbConnection对象,它们需要存在,在此之前,您需要得到一个ConnectionFactory实现。MariaDB 驱动程序提供了三种获取MariadbConnectionFactory的方法。

创建新的连接工厂

一种方法是使用MariadbConnectionConfiguration对象,它允许您提供各种信息,比如主机地址、端口号、用户名和密码,以标识目标 MariaDB 服务器实例。然后可以使用一个MariadbConnectionConfiguration实例来构造MariadbConnectionFactory对象(清单 12-2 )。

MariadbConnectionConfiguration connectionConfiguration = MariadbConnectionConfiguration.builder()
                            .host("127.0.0.1")
                            .port(3306)
                            .username("app_user")
                            .password("Password123!")
                            .build();

MariadbConnectionFactory connectionFactory = new MariadbConnectionFactory(connectionConfiguration);

Listing 12-2Creating a new MariadbConnectionFactory object using MariadbConnectionConfiguration

MariadbConnectionConfiguration

MariadbConnectionConfiguration类特定于 MariaDB 驱动程序,因为它不从 R2DBC SPI 中存在的实体派生,也不实现 R2DBC SPI 中存在的实体。

然而,[DriverName]ConnectionConfiguration对象在迄今为止的每个驱动程序实现中都是通用的,但不仅仅是因为命名约定。连接配置对象用于管理标准和特定于供应商的连接选项。

一旦创建了 ConnectionConfiguration 对象,您可以使用各种get方法来读取其当前的配置设置(清单 12-3 )。

// Where connectionConfiguration is an existing MariadbConnectionConfiguration object
String host = connectionConfiguration.getHost();
int post = connectionConfiguration.getPort();
String username = connectionConfiguration.getUsername();
int prepareCacheSize = connectionConfiguration.getPrepareCacheSize();

Listing 12-3Example MariadbConnectionConfiguration getter method usages

连接配置对象也提供了一种桥梁,通过ConnectionFactoryProvider实现,使用ConnectionFactoryOptions类帮助ConnectionFactory实现发现过程。

ConnectionFactory Discovery(连接工厂发现)

再次回到第四章的,记住ConnectionFactories,R2DBC SPI 中的一个类,提供了两种方法,都使用get方法来检索驱动程序ConnectionFactory的实现。

第一种方法是使用一个ConnectionFactoryOptions对象为目标数据库实例指定适当的连接设置(清单 12-4 )。

ConnectionFactoryOptions connectionFactoryOptions = ConnectionFactoryOptions.builder()
                        .option(ConnectionFactoryOptions.DRIVER, "mariadb")
                        .option(ConnectionFactoryOptions.PROTOCOL, "pipes")
                        .option(ConnectionFactoryOptions.HOST, "127.0.0.1")
                        .option(ConnectionFactoryOptions.PORT, 3306)
                        .option(ConnectionFactoryOptions.USER, "app_user")
                        .option(ConnectionFactoryOptions.PASSWORD, "Password123!").build();
MariadbConnectionFactory connectionFactory = (MariadbConnectionFactory)ConnectionFactories.get(connectionFactoryOptions);

Listing 12-4Retrieving an existing MariadbConnectionFactory object using a ConnectionFactoryOptions object

Note

记住,在第十一章中,我为您提供了向 MariaDB 数据库实例添加新用户app_user的 SQL 命令。

第二,你还可以选择将 R2DBC URL 传递到ConnectionFactories’ get方法中(清单 12-5 ),这一点你在第四章中已经了解过了。

MariadbConnectionFactory connectionFactory = (MariadbConnectionFactory)ConnectionFactories.get("r2dbc:mariadb:pipes://app_user:Password123!@127.0.0.1:3306");

Listing 12-5Retrieving an existing MariadbConnectionFactory object using an R2DBC connection URL

最终,R2DBC URL 被解析以创建一个ConnectionFactoryOptions对象,然后被ConnectionFactories类用来获得一个ConnectionFactory,就像它在清单 12-4 中所做的那样。

创建连接

一旦创建了一个MariadbConnectionFactory对象,就可以使用create方法获得一个MariadbConnection对象(清单 12-6 )。

Mono<MariadbConnection> monoConnection = connectionFactory.create();

monoConnection.subscribe(connection -> {
     // Do something with connection
});

Listing 12-6Creating a new database connection

Note

一个Mono是一个 Reactive Streams Publisher 对象实现,由 Project Reactor 库提供,专门用于流式传输0–1元素。

注意,ConnectionFactory接口的 create 方法返回了一个Mono<MariadbConnection>,它是 Reactive Streams API 的Publisher<T>和 R2DBC 规范的Publisher<Connection>的项目反应器实现。

请记住,由于基于事件开发的本质,没有人知道何时会发布对象。因此,在某些情况下,在继续之前等待发布者发送一个Connection对象可能是有用的。

在这种情况下,您可以使用block方法在继续之前等待一个MariadbConnection对象(清单 12-7 )。

MariadbConnection connection = connectionFactory.create().block();

Listing 12-7Creating and waiting on a new database connection

验证和关闭连接

在获得一个MariadbConnection对象后,您可以使用validate方法来检查连接是否仍然有效。

validate方法返回Publisher<Boolean>,它可以被项目反应器库使用,使用Mono.from创建一个Mono<Boolean>发布者。然后,如清单 12-8 所示,在订阅monoValidated时,可以使用validated变量中的Boolean值。

Publisher<Boolean> validatePublisher = connection.validate(ValidationDepth.LOCAL);
            Mono<Boolean> monoValidated = Mono.from(validatePublisher);
            monoValidated.subscribe(validated -> {
                if (validated) {
                    System.out.println("Connection is valid");
                }
                else {
                    System.out.println("Connection is not valid");
                }
            });

Listing 12-8Validating a connection

Note

由于本地连接,此示例显示了ValidationDepth.LOCAL的用法。有关ValidationDepth选项的更多信息,请务必查看第四章。

同样,close方法可以用来释放连接及其相关资源。在清单 12-9 中,您可以看到如何订阅close方法。

Publisher<Void> closePublisher = connection.close();
Mono<Void> monoClose = Mono.from(closePublisher);
monoClose.subscribe();

Listing 12-9Closing a connection

把这一切放在一起

在清单 12-10 中,我已经将本章提到的所有代码片段累积到一个可运行的样本中。此示例的目的是演示如何首先建立到 MariaDB 数据库实例的连接,然后验证它。然后,连接将被关闭,并再次检查有效性。

package com.example;

import org.mariadb.r2dbc.MariadbConnectionConfiguration;
import org.mariadb.r2dbc.MariadbConnectionFactory;
import org.mariadb.r2dbc.api.MariadbConnection;
import org.reactivestreams.Publisher;

import io.r2dbc.spi.ConnectionFactories;
import io.r2dbc.spi.ConnectionFactoryOptions;
import io.r2dbc.spi.ValidationDepth;
import reactor.core.publisher.Mono;

public class App
{
    public static void main( String[] args )
    {
        // Initialize Connection
        MariadbConnection connection = obtainConnection();

        // Validate Connection
        validateConnection(connection);

        // Close Connection
        closeConnection(connection);

        // Validate Connection
        validateConnection(connection);
    }

    public static MariadbConnection obtainConnection() {
        try {
            MariadbConnectionFactory connectionFactory;

            // Create a new Connection Factory using MariadbConnectionConfiguration
            connectionFactory = createConnectionFactory();

            // Discover Connection Factory using ConnectionFactoryOptions
            //connectionFactory = discoverConnectionFactoryWithConfiguration();

            // Discover Connection Factory using Url
            //connectionFactory = discoverConnectionFactoryWithUrl();

            // Create a MariadbConnection
            return connectionFactory.create().block();
        }
        catch (java.lang.IllegalArgumentException e) {
           printException("Issue encountered while attempting to obtain a connection", e);
           throw e;
        }
    }

    public static MariadbConnectionFactory createConnectionFactory() {
        try{
            // Configure the Connection
            MariadbConnectionConfiguration connectionConfiguration = MariadbConnectionConfiguration.builder()
                              .host("127.0.0.1")
                              .port(3306)
                              .username("app_user")
                              .password("Password123!")

                              .build();

            // Instantiate a Connection Factory
            MariadbConnectionFactory connectionFactory = new MariadbConnectionFactory(connectionConfiguration);

            print("Created new MariadbConnectionFactory");

            return connectionFactory;
        }
        catch(Exception e) {
            printException("Unable to create a new MariadbConnectionFactory", e);
            throw e;
        }
    }

    public static MariadbConnectionFactory discoverConnectionFactoryWithConfiguration() {
        try{
            ConnectionFactoryOptions connectionFactoryOptions = ConnectionFactoryOptions.builder()
                 .option(ConnectionFactoryOptions.DRIVER, "mariadb")
                 .option(ConnectionFactoryOptions.PROTOCOL, "pipes")
                 .option(ConnectionFactoryOptions.HOST, "127.0.0.1")
                 .option(ConnectionFactoryOptions.PORT, 3306)
                 .option(ConnectionFactoryOptions.USER, "app_user")
                 .option(ConnectionFactoryOptions.PASSWORD, "Password123!")
                 .option(ConnectionFactoryOptions.DATABASE, "todo")
                 .build();

            MariadbConnectionFactory connectionFactory = (MariadbConnectionFactory)ConnectionFactories.get(connectionFactoryOptions);

            return connectionFactory;
        }
        catch(Exception e) {
            printException("Unable to discover MariadbConnectionFactory using ConnectionFactoryOptions", e);
            throw e;
        }
    }

    public static MariadbConnectionFactory discoverConnectionFactoryWithUrl() {
        try {
            MariadbConnectionFactory connectionFactory = (MariadbConnectionFactory)ConnectionFactories.get("r2dbc:mariadb:pipes://app_user:Password123!@127.0.0.1:3306/todo");
            return connectionFactory;
        }
        catch (Exception e) {
            printException("Unable to discover MariadbConnectionFactory using Url", e);
            throw e;
        }
    }

    public static void validateConnection(MariadbConnection connection) {
        try {
            Publisher<Boolean> validatePublisher = connection.validate(ValidationDepth.LOCAL);
            Mono<Boolean> monoValidated = Mono.from(validatePublisher);
            monoValidated.subscribe(validated -> {
                if (validated) {
                    print("Connection is valid");
                }
                else {
                    print("Connection is not valid");
                }
            });
        }
        catch (Exception e) {
           printException("Issue encountered while attempting to verify a connection", e);
        }
    }

    public static void closeConnection(MariadbConnection connection) {
        try {
            Publisher<Void> closePublisher = connection.close();
            Mono<Void> monoClose = Mono.from(closePublisher);
            monoClose.subscribe();
        }
        catch (java.lang.IllegalArgumentException e) {
           printException("Issue encountered while attempting to verify a connection", e);
        }
    }

    public static void printException(String description, Exception e) {
        print(description);
        e.printStackTrace();
    }

    public static void print(String val) {
        System.out.println(val);
    }
}

Listing 12-10The complete connection sample

成功运行清单 12-10 中的示例代码应该会产生类似于清单 12-11 中所示的结果。

Connection is valid
Connection is not valid

Listing 12-11The resulting output from executing the code in Listing 12-10

清单 12-10 中的代码虽然非常简单,但提供了基本的连接功能,我将在接下来的章节中继续详述。

[计]元数据

最后,可以检查关于ConnectionFactoryConnection对象实现的信息或元数据(清单 12-12 ,清单 12-13 )。

MariadbConnectionFactoryMetadata对象公开了一个方法getName,该方法返回 R2DBC 所连接的产品的名称。

ConnectionFactoryMetadata metadata = connectionFactory.getMetadata();
String name = metadata.getName();

Listing 12-12Using MariadbConnectionFactoryMetadata

如清单 12-13 所示,您还可以使用MariadbConnection对象检索关于已建立连接的元数据。MariadbConnectionMetadata对象实现了ConnectionMetadata接口及其所需的方法。MariadbConnectionMetadata还公开了几个特定于 R2DBC 的 MariaDB 驱动程序实现的附加方法。

MariadbConnectionMetadata metadata = connection.getMetadata();

// Methods required by the ConnectionMetadata interface
String productName = metadata.getDatabaseProductName();
String databaseVersion = metadata.getDatabaseVersion();

// Extension methods added to MariadbConnectionMetadata
boolean isMariaDBServer = metadata.isMariaDBServer();
int majorVersion = metadata.getMajorVersion();
int minorVersion = metadata.getMinorVersion();
int patchVersion = metadata.getPatchVersion();

Listing 12-13Using MariadbConnectionMetadata

给我看看代码

您可以在专门针对本书的 GitHub 资源库中找到一个完整的、完全可编译的示例应用。如果您还没有这样做,只需导航到 https://github.com/apress/r2dbc-revealedgit clone或者直接下载存储库的内容。在那里,您可以在 ch12 文件夹中找到一个专门用于这一章的示例应用。

摘要

毫无疑问,连接到数据源的能力是数据库驱动程序最重要的能力之一。这一章对于获取你在第四章中学到的理论信息以及通过使用驱动程序实现来观察它的作用是至关重要的。

在本章中,您了解了如何使用 MariaDB R2DBC 驱动程序来创建和管理数据库连接。本章提供的基础将对后面的章节非常有用,在后面的章节中,我们将深入了解语句执行、事务管理、连接池等概念。

十三、管理数据

到目前为止,您已经掌握了反应式编程、反应式流和 R2DBC 的基础知识。在过去的两章中,您甚至亲眼目睹了 R2DBC 规范如何在驱动程序实现中发挥作用。我们甚至创建了一个简单的应用,它使用 MariaDB R2DBC 驱动程序并连接到一个数据库实例。这才是真正有趣的地方。

在本章中,我们将扩展您在过去两章中获得的知识,深入研究如何使用 MariaDB R2DBC 驱动程序执行 SQL 语句。读,写,参数,没有参数,是的,我们将涵盖它。

简而言之,如果你喜欢代码,你将会爱上这一章!

mariadb 语句

继续在 R2DBC 接口的类名前面加上 Mariadb 的惯例,MariadbStatement对象提供了在 Mariadb 数据库上执行 SQL 语句的能力。

语句层次结构

图 13-1 应该看着眼熟。在第六章中,你了解到 R2DBC Connection接口公开了一个方法来获取一个叫做createStatementStatement对象。因此,使用前一章中详细讨论的MariadbConnection对象,您可以获得一个MariadbStatement对象,然后可以使用它来执行 SQL 语句,并在适当的时候通过MariadbResult对象访问数据。

img/504354_1_En_13_Fig1_HTML.png

图 13-1

用于执行 SQL 语句的 MariaDB 类流

依赖性免责声明

在上一章中,我指出 MariaDB R2DBC 驱动程序包含对 Project Reactor 的依赖,这是一个流行的 Reactive Streams 实现。因此,本章自然会提供许多使用 Project Reactor Reactive Streams 类实现的例子。

然而,请记住,正如我在本书中多次强调的,Reactive Streams 是一个规范,因此使用下面的例子作为指南是很重要的,而不仅仅是 Reactive Streams 实现的一个真实来源。有各种各样的选择,其中许多功能与我们将在本章中讨论的例子非常相似。

基础知识

使用 MariaDB R2DBC 驱动程序有多种方法可以执行和管理 SQL,我希望在本章中介绍这些方法。本节旨在为我们在后续章节中浏览 R2DBC 驱动程序的数据管理功能提供基础。

创建语句

这一切都从创建一个Statement开始,或者在我们的例子中,由 MariaDB R2DBC 驱动程序提供的Statement实现对象:MariadbStatement

使用一个MariadbConnection对象,您可以通过createStatement方法获得一个新的MariadbStatement对象,该方法为您想要运行的 SQL 语句获取一个String值(清单 13-1 )。

MariadbStatement createDatabaseStatement = connection.createStatement("CREATE DATABASE todo");

Listing 13-1Creating a new MariadbStatement using a MariadbConnection object

获得出版商

不管实现如何,Statement对象的execute方法的目的是提供一个反应流Publisher<T>实现,在本例中更具体地说是Publisher<Result>

因此,默认情况下,MariadbStatement对象的 execute 方法提供了一个Flux<MariadbResult>(清单 13-2 )。

 Flux<MariadbResult> publisher = createDatabaseStatement.execute();

Listing 13-2Using the execute method on a Statement object to create a publisher

Note

Flux对象是 Reactive Streams Publisher 接口的项目反应器实现。它的功能是发射 0-N 个元素。

订阅执行

为了开始执行MariadbStatement对象和其中包含的 SQL 语句,必须订阅publisher对象。

publisher.subscribe();

Listing 13-3Subscribing to a publisher

把所有的东西放在一起,完成这个例子,下面的例子(清单 13-4 )说明了我们如何从清单 13-2 和 13-3 向我们新创建的数据库添加一个新的 MariaDB 表。

MariadbStatement createTableStatement = connection.createStatement("CREATE TABLE todo.tasks (" +
                "id INT(11) unsigned NOT NULL AUTO_INCREMENT, " +
                "description VARCHAR(500) NOT NULL, " +
                "completed BOOLEAN NOT NULL DEFAULT 0, " +
                "PRIMARY KEY (id))"
            );
createTableStatement.execute().subscribe();

Listing 13-4Creating a table using R2DBC statement execution

Note

本节使用创建的 todo 数据库和 tasks 表来提供一个简单的例子,说明如何使用 R2DBC 驱动程序执行 SQL。在接下来的小节中,我们将使用利用了 todo.tasks 的 SQL 语句。

检索结果

已经对使用 R2DBC 创建和执行 SQL 语句有了基本的了解,现在是时候更进一步了!现在,我们当然有可能创建只涉及运行 SQL 语句的应用,而我们并不期望从这些语句中接收数据,但这种情况并不常见,也不有趣。更有可能的是,我们编写的任何应用都希望既写又从底层数据库读取

行更新计数

首先,R2DBC 提供的一种方便的机制是检索受数据操作语言(DML)语句影响的行数。

Note

INSERT、UPDATE 和 DELETE 语句都称为 DML 语句。

对于 DML 语句,订阅者可能会收到一个提供了getRowsUpdated方法的MariadbResult对象。调用getRowsUpdated方法使您能够获得一个Publisher<Integer>对象,一旦被订阅,您就可以获得被执行的 SQL 语句影响的行数(清单 13-5 )。

MariadbStatement insertStatement = connection.createStatement("INSERT INTO todo.tasks (description,completed) VALUES ('New Task 1',0)");

// Retrieve and print the number of rows affected
insertStatement.execute().subscribe(result -> result.getRowsUpdated().subscribe(count -> System.out.println (count.toString())));

Listing 13-5Subscribing to a Statement object’s getRowsUpdated method

Caution

发出更新计数后,Result对象失效,来自同一个Result对象的行不再被使用。

映射结果

在上一个示例中,我们使用了一个MariadbResult对象来获取受 SQL 语句影响的行数,但是,正如我们在第七章中所了解到的,这实际上只是一个Result对象实现所包含内容的冰山一角。

MariaDB R2DBC 驱动程序的Result对象实现MariadbResult,提供数据值和元数据(图 13-2 )。

img/504354_1_En_13_Fig2_HTML.png

图 13-2

玛丽亚的解剖结果

使用行对象

回想一下,在第七章中,您了解到Result接口提供了从Row对象中检索值的map方法。既然我们能够直接看到这一点,就有必要详细说明一下。

Row对象检索的过程是可能的,因为map方法接受一个BiFunction,也称为映射函数,接受RowRowMetadata的对象。

Note

双函数表示接受两个参数并产生一个结果的函数(例如,RowRowMetadata)。

在使用RowRowMetadata对象进行行发射时调用映射函数。然后,在BiFunction对象的功能块中,您可以访问row对象,通过使用索引或列/别名来提取每个指定列的数据。

MariadbStatement selectStatement = connection.createStatement("SELECT id, description AS task, completed FROM todo.tasks");

selectStatement.execute()
               .flatMap(result -> result.map((row,metadata) -> {
                    Integer id = row.get(0, Integer.class);
                    String descriptionFromAlias = row.get("task", String.class);
                    String isCompleted = (row.get(2, Boolean.class) == true) ? "Yes" : "No";
                    return String.format("ID: %s - Description: %s - Completed: %s",
                        id,
                        descriptionFromAlias,
                        isCompleted
                    );
                }))
                .subscribe(result -> System.out.println(result));

Listing 13-6Extracting SQL SELECT statement results using the MariadbRow get method

Caution

Row只在映射函数回调期间有效,在映射函数回调之外无效。因此,Row对象必须完全被映射函数消耗。

映射结果

mapflatMap方法都接收一个映射函数,该函数应用于Stream<T>的每个元素并返回一个Stream<R>.。这两个函数之间的关键区别在于,flatMap方法接收的映射函数产生一串新的值,而 map 方法接收的映射函数用于为每个输入元素产生一个值。

因为flatMap方法的映射函数用于返回一个新的流,我们实际上是在接收一个流的流。然而,flatMap方法也用于用流的内容替换每个生成的流。换句话说,由该函数生成的所有单独的流都被展平为一个单独的流。

处理元数据

你可能已经注意到清单 13-6 中的BiFunction还包括一个名为metadata的对象,它是一个MariadbRowMetadata对象。在第八章中,您了解到RowMetadata对象实现提供了关于从执行的 SQL 语句返回的表格结果的信息。更具体地说,RowMetadata可以用来确定行的属性,还可以公开包含在Result对象中的每一列的ColumnMetadata实现。

在清单 13-7 中,重用清单 13-6 中的样本代码,我们可以专注于从metadata对象中提取信息。

selectStatement.execute()
                          .flatMap(result -> result.map((row,metadata) -> {
                            List<String> columnMetadata = new ArrayList<String>();
                            Iterator<String> iterator = metadata.getColumnNames().iterator();
                            while (iterator.hasNext()) {
                                String columnName = iterator.next();
                                columnMetadata.add(String.format("%s (type=%s)", columnName, metadata.getColumnMetadata(columnName).getJavaType().getName()));
                            }
                            return columnMetadata.toString();
                          }))
                          .subscribe(result -> System.out.println("Row Columns = "  + result));

Listing 13-7Accessing row and column metadata

生成的值

接下来,让我们考虑使用我们在本章中学到的Statement工作流创建一个新的记录的过程。

MariadbStatement insertStatement = connection.createStatement("INSERT INTO todo.tasks (description,completed) VALUES ('New Task 2',0)");
insertStatement.execute().subscribe();

Listing 13-8Inserting data

好了,我们已经创建了一个新的MariadbStatement并订阅了所包含的 INSERT 语句的执行。很好,但是如果我们想要获得根据表的规范自动生成的 id 的值呢?关于必须使用映射函数来简单地访问自动生成的值似乎很麻烦、冗长,而且是不必要的。当然,还有更好的选择。

没错,R2DBC 再次出手相救!如果您还记得,在第六章中,向您介绍了returnGeneratedValues,这是一种通过Statement接口可用的方法,可用于获取自动生成的值,这些值是作为 SQL 语句的一部分创建的。该方法只是接收一个可变参数,该参数用于确定自动生成的值要存储到的列名。

MariadbStatement insertStatement = connection.createStatement("INSERT INTO todo.tasks (description,completed) VALUES ('New Task 3',0)");

Flux<Object> publisher =  insertStatement.returnGeneratedValues("id").execute().flatMap(result -> result.map((row,metadata) -> row.get("id")));

 publisher.subscribe(id -> System.out.println
(id.toString()));

Listing 13-9Retrieving a generated value from an INSERT statement execution

多重陈述

作为开发人员,我们知道,由于各种原因(包括提高性能的可能性),能够在一个操作中执行多个 SQL 语句通常是有用的。幸运的是,由于 R2DBC 的灵活性,有几种方法可以有效地批处理 SQL 语句。

使用 MariadbBatch

R2DBC SPI 提供了一个名为Batch的接口,该接口定义了运行多组无参数 SQL 语句的方法。

Stay Tuned

在本章的后面,我们将看看可以批处理的参数化 SQL 语句。

因此,可以创建 MariaDB 实现MariadbBatch,并用于运行多个 SQL 语句,这有助于提高应用性能。

首先通过使用createBatch方法创建一个MariadbBatch的实例,该方法在一个Connection对象上可用。然后使用add方法输入您希望作为批处理的一部分执行的每个 SQL 语句,并使用execute方法,就像您处理Statement对象一样(清单 13-10 )。

MariadbBatch batch = connection.createBatch();

batch.add("INSERT INTO todo.tasks (description,completed) VALUES ('New Task 4', 0)")
     .add("SELECT * FROM todo.tasks WHERE id = last_insert_id()")
     .execute()
     .flatMap(result -> result.map((row,metadata) -> {
                    return row.get(0, Integer.class)  + " - " + row.get(1, String.class);
     }))
     .subscribe(result -> System.out.println(result));

Listing 13-10Using MariadbBatch to execute multiple SQL statements

使用 MariadbStatement

由于 R2DBC 规范提供的灵活性,MariaDB R2DBC 驱动程序还可以在用于构造Statement对象的单个String值中包含多个 SQL 语句。

首先,在构建用于创建执行 SQL 语句的MariadbConnectionMariadbConnectionConfiguration对象的过程中,需要包含一个特定于供应商的方法allowMultiQueries(清单 13-11 )。

MariadbConnectionConfiguration connectionConfiguration = MariadbConnectionConfiguration.builder()
   .host("127.0.0.1")
   .port(3306)
   .username("app_user")
   .password("Password123!")
   .allowMultiQueries(true)
   .build();

Listing 13-11Enabling the ability to run multiple SQL statements in a single Statement object execution using allowMultiQueries

一旦您启用了执行多个 SQL 查询的能力,您就能够在单个String值中添加多个查询,用分号分隔(清单 13-12 )。

MariadbStatement multiStatement = connection.createStatement("INSERT INTO todo.tasks (description,completed) VALUES ('New Task 5', 0); SELECT * FROM todo.tasks WHERE id = last_insert_id();");

multiStatement.execute().flatMap(result -> result.map((row,metadata) -> {
                            return row.get(0, Integer.class)  + " - " + row.get(1, String.class);
                          }))
                          .subscribe(result -> System.out.println(result));

Listing 13-12Running multiple INSERT statements within a single Statement object execution

参数化语句

在第六章中,您了解到用于创建Statement对象的同一个非参数化 SQL String值也可以通过使用特定于供应商的绑定标记被参数化。事实上,参数化的Statement对象是由Connection对象以与非参数化语句相同的方式创建的。

事实上,正如我在第六章中指出的,供应商使用的绑定标记可以不同,但总体方法是相同的。对于 MariaDB,您有两个绑定标记选项:

  1. 通过使用单个问号(?)作为参数占位符

  2. 通过在命名参数占位符前面加上分号(😃

绑定参数

不管使用什么绑定标记,您都可以提供一个参数的索引号来绑定一个值(清单 13-13 )。

MariadbStatement selectStatement = connection.createStatement("SELECT * FROM todo.tasks WHERE completed = ? AND id >= ?");
selectStatement.bind(0, true);
selectStatement.bind(1, 4);
selectStatement.execute()
               .flatMap(result -> result.map((row,metadata) -> {
                    return row.get(0, Integer.class)  + " - " + row.get(1, String.class) + " - " + row.get(2, Boolean.class);
               }))
              .subscribe(result -> System.out.println(result));

Listing 13-13Binding by index

您还可以提供一个特定的占位符参数(清单 13-14 )。

MariadbStatement selectStatement = connection.createStatement("SELECT * FROM todo.tasks WHERE completed = :completed AND id >= :id");
selectStatement.bind("completed", true);
selectStatement.bind("id", 4);
selectStatement.execute()
               .flatMap(result -> result.map((row,metadata) -> {
                    return row.get(0, Integer.class)  + " - " + row.get(1, String.class) + " - " + row.get(2, Boolean.class);
               }))
              .subscribe(result -> System.out.println(result));

Listing 13-14Binding with a named placeholder

批处理语句

在第六章中,还向您介绍了 R2DBC 利用Statement对象的 add 方法支持批量参数化Statement对象的能力(清单 13-15 )。

MariadbStatement batchedInsertStatement = connection.createStatement("INSERT INTO todo.tasks (description,completed) VALUES (?,?)");

batchedInsertStatement.bind(0, "New Task 6").bind(1, false).add();
batchedInsertStatement.bind(0, "New Task 7").bind(1, false).add();
batchedInsertStatement.bind(0, "New Task 8").bind(1, true);

batchedInsertStatement.execute().subscribe();

Listing 13-15Batching parameterized statements with MariadbStatement

反应控制

您可能还记得第二章,Reactive Streams API 提供了一个结构化的过程,用于通过异步、非阻塞的交互来管理事件驱动的开发。基本上,我们了解到,Reactive Streams 围绕争夺数据流的想法添加了一些结构,以帮助我们创建完全反应式应用。

到目前为止,您已经对 R2DBC 规范以及它如何利用 Reactive Streams API 与关系数据库进行交互有了深入的了解。以此为基础,在本章和最后一章中,您还了解了 R2DBC 驱动程序实现实际上是如何利用反应流实现来支持事件驱动开发的。

虽然有许多 Reactive Streams 实现及其方法、类结构和细粒度功能可能会有所不同,但总体前提是相同的。随着您对如何使用 R2DBC 有了更多的经验和了解,从基础开始会很有帮助。

阻止执行

在您完成后续操作之前,需要完成某些数据库交互。例如,在本章的开始,我提供了一个简单的例子(清单 13-1 和 13-2 )来创建一个语句,执行一个发布器,并订阅结果。但是为了能够查询数据库或表,它必须首先存在。对于这种情况,在一个通量对象(清单 13-16 )中使用一个阻塞函数可能是有用的。

MariadbStatement createDatabaseStatement = connection.createStatement("CREATE DATABASE todo");
createDatabaseStatement.execute().blockFirst();

Listing 13-16Subscribing to a Flux and blocking indefinitely until the upstream signals its first value or completes

Note

blockFirst方法订阅一个Flux并无限期地阻塞它,直到上游发出它的第一个值或完成。

另一方面,如果您有一批要执行的语句,您也可以选择使用blockLast方法。

MariadbStatement batchedInsertStatement = connection.createStatement("INSERT INTO todo.tasks (description,completed) VALUES (?,?)");

batchedInsertStatement.bind(0, "New Task 9").bind(1, false).add()
                      .bind(0, "New Task 10").bind(1, false).add()
                      .bind(0, "New Task 11").bind(1, true);

MariadbResult result = batchedInsertStatement.execute().blockLast();

Listing 13-17Subscribing to a Flux and blocking indefinitely until the upstream signals its last value or completes

管理背压

回到第一章,您了解了背压的概念,以及它对于在反应式应用中控制数据流的重要性。然后,第二章通过学习反应流 API 如何使用背压,通过一个正式的 API 结构来实现无阻塞的交互,扩展了这种理解(图 13-3 )。

img/504354_1_En_13_Fig3_HTML.png

图 13-3

反应流 API 工作流

虽然从架构上讲,有多种方法可以利用反应流的能力来做一些事情,比如控制背压,但是让我们看一个简单的例子来说明一种可能的方法。

在清单 13-18 中,我创建了一个新的MariadbStatement并使用flatMap方法返回一个task记录的description值。足够简单,在这一点上应该感觉非常熟悉。然而,您会注意到我已经创建了一个新的Subscriber<String>对象,而不是简单地使用 subscribe 从已执行的 SQL 语句中获取值。

下列范例说明订阅者、订阅和发行者之间的关系。检查新订户,您可以看到有几个被覆盖的方法:onSubscribeonNextonErroronComplete。这些应该看起来很熟悉,因为它们与第一章和图 13-3 中详述的反应流 API 方法函数一致。

MariadbStatement selectStatement = connection.createStatement("SELECT * FROM todo.tasks");
            selectStatement.execute()
                           .flatMap(result -> result.map((row,metadata) -> {
                                return row.get("description", String.class);
                           }))
                           .subscribe(new Subscriber<String>() {
                                private Subscription s;
                                int onNextAmount;
                                int requestAmount = 2;

                                @Override
                                public void onSubscribe(Subscription s) {
                                    System.out.println("onSubscribe");
                                    this.s = s;
                                    System.out.println("Request (" + requestAmount + ")");
                                    s.request(requestAmount);
                                }

                                @Override
                                public void onNext(String itemString) {
                                    onNextAmount++;
                                    System.out.println("onNext item received: " + itemString);
                                    if (onNextAmount % 2 == 0) {
                                        System.out.println("Request (" + requestAmount + ")");
                                        s.request(2);
                                    }
                                }

                                @Override
                                public void onError(Throwable t) {
                                    System.out.println("onError");
                                }

                                @Override
                                public void onComplete() {
                                    System.out.println("onComplete");
                                }
                            });

Listing 13-18Controlling back pressure with a new Subscriber object

通过创建一个自定义的Subscriber对象,类型为String以说明我们的映射结果,您可以更深入地了解 MariaDB R2DBC 驱动程序和您的应用之间的反应性交互。

注意,在清单 13-18 中,我包含了System.out.println用法来帮助说明反应流。执行代码应该会产生类似于清单 13-19 的输出。

onSubscribe
Request (2)
onNext item received: Task 1
onNext item received: Task 2
Request (2)
onNext item received: Task 3
onNext item received: Task 4
Request (2)
onNext item received: Task 5
onComplete

Listing 13-19Sample output for the custom subscriber in Listing 13-18

给我看看代码

您可以在专门针对本书的 GitHub 资源库中找到一个完整的、完全可编译的示例应用。如果您还没有这样做,只需导航到 https://github.com/apress/r2dbc-revealedgit clone或者直接下载存储库的内容。在那里,您可以在 ch13 文件夹中找到一个专门用于这一章的示例应用。

摘要

咻!在这一章中,我们已经涉及了相当多的内容。我们首先看一下利用Statement对象实现MariadbStatement来促进反应式 SQL 语句执行的基础知识。从这里开始,您了解了如何处理 SQL 语句结果,从而扩展了自己的知识。

参数,没有参数,我们几乎涵盖了一切!最后,您甚至体验了使用 R2DBC 和背压概念控制事件驱动的完全反应式信息流的感觉。

十四、管理事务

在第五章中,您已经或者可能再次了解了事务的基本概念及其在关系数据库解决方案中的重要性。最重要的是,您了解了 R2DBC 规范提供的事务特性支持。

在这一章中,我们将使用 MariaDB R2DBC 驱动程序来获得在反应式解决方案中创建、管理和利用事务的第一手资料。

数据库事务支持

不同关系数据库解决方案之间的差异在于它们支持的事务特性的数量。在第五章中,您了解了 R2DBC 规范中可用的事务处理能力。

继续我们在过去几章中设定的趋势;我们将使用 MariaDB R2DBC 驱动程序来看看这些功能。我们将避免深入研究 MariaDB 特有的复杂特性,而是涵盖使用 R2DBC 可能实现的功能。

数据库准备

接下来,我们将查看 Java 代码示例,使用 MariaDB R2DBC 驱动程序,它依赖于一个名为 tasks 的 SQL 表,该表存在于数据库 todo 中,我们在上一章中将其添加到 MariaDB 实例中。

为了让我们达成一致,您可以执行清单 14-1 中的 SQL 来重置 todo.tasks 表。

TRUNCATE TABLE todo.tasks; INSERT INTO todo.tasks (description) VALUES ('Task A'), ('Task B'), ('Task C');

Listing 14-1Truncating the existing records and adding new records to todo.tasks

Tip

在 SQL 中,TRUNCATE TABLE语句是一种数据定义语言(DDL)操作,它将表的范围标记为解除分配。截断任务表将删除所有先前存在的信息,并重新开始自动生成的 id 为列的值计数。

执行清单 14-1 中的 SQL 将确保我们的表包含三条记录,分别包含 id 列值 1、2 和 3。

+----+-------------+-----------+
| id | description | completed |
+----+-------------+-----------+
|  1 | Task A      |         0 |
|  2 | Task B      |         0 |
|  3 | Task C      |         0 |
+----+-------------+-----------+

Listing 14-2The contents of todo.tasks after executing the SQL in Listing 14-1

事务基础

R2DBC 规范支持通过代码控制事务性操作,而不是通过所有驱动程序都需要实现的Connection接口直接使用 SQL。

事务可以隐式或显式启动。当一个Connection对象处于自动提交模式时,当一条 SQL 语句通过一个Connection对象执行时,事务被隐式地启动

自动提交

在第五章中,您了解到Connection对象的自动提交模式可以使用isAutoCommit方法来检索,并通过调用setAutoCommit方法来更改(清单 14-3 )。

boolean isAutoCommit = connection.isAutoCommit();
if (isAutoCommit) {
      connection.setAutoCommit(false).block();
}

Listing 14-3Disabling auto-commit for a Connection object

Tip

在 MariaDB R2DBC 驱动程序中,默认情况下自动提交启用

显性事务

一旦自动提交模式被禁用,事务必须被显式启动。使用 MariaDB 驱动程序,这可以通过在一个MariadbConnection对象上调用beginTransaction方法来完成(清单 14-4 )。

connection.beginTransaction().subscribe();

Listing 14-4Beginning a MariaDB transaction

Tip

对一个MariadbConnection对象使用beginTransaction方法将自动禁用连接的自动提交

提交事务

一旦您开始了显式处理数据库事务的道路,无论您创建和执行了多少 SQL 语句,您都需要调用commitTransaction方法来使对数据的更改永久化(清单 14-5 )。

MariadbStatement insertStatement = connection.createStatement("INSERT INTO tasks (description) VALUES ('Task D'));

insertStatement.execute()
               .then(connection.commitTransaction())
               .subscribe();

Listing 14-5Beginning and committing a MariaDB transaction

Note

在清单 14-5 中,由项目反应器提供的then方法用于建立链式的、声明性的交互。

执行清单 14-5 中的代码将导致一个新的任务行被添加到任务表中。当事务被提交时,INSERT语句的改变变成永久的。您可以通过查看任务表中的内容来确认结果(清单 14-6 )。

SELECT * FROM todo.tasks;
+----+-------------+-----------+
| id | description | completed |
+----+-------------+-----------+
|  1 | Task A      |         0 |
|  2 | Task B      |         0 |
|  3 | Task C      |         0 |
|  4 | Task D      |         0 |
+----+-------------+-----------+

Listing 14-6Output that results after committing the transaction

回滚事务

但是,如果有一个场景需要您撤销 SQL 语句,或者由于某种原因,事务失败,那么可以通过执行和订阅rollbackTransaction方法(清单 14-7 )来回滚所有事务。

connection.rollbackTransaction().subscribe();

Listing 14-7Rolling back a MariaDB transaction

执行清单 14-7 中的代码将回滚INSERT语句的更改,防止它被提交。当这种情况发生时,任务表的内容将类似于清单 14-8 。

SELECT * FROM todo.tasks;
+----+-------------+-----------+
| id | description | completed |
+----+-------------+-----------+
|  1 | Task A      |         0 |
|  2 | Task B      |         0 |
|  3 | TASK C      |         0 |
+----+-------------+-----------+

Listing 14-8Output that results after rolling back the transaction

迫切的观点

回想一下第一章,在那里你学习了命令式和声明式编程。作为复习,记住阻塞操作在命令式或分步式编程范式和语言中很常见。相比之下,声明式方法并不关注如何完成一个特定的目标,而是关注目标本身。

现在,你已经知道 R2DBC 和整个反应式编程的目的是提供一个声明性的解决方案。也就是说,有时候我们的大脑更容易理解命令式的心流。

为了最清楚地展示事务性工作流,我利用了清单 14-9 中的blockblockLast方法,这在真正的反应式应用中可能不会发生,但有助于更清楚地说明发生了什么。

try {
       connection.beginTransaction().block();

       MariadbStatement multiStatement = connection.createStatement("DELETE FROM tasks; INSERT INTO tasks (description) VALUES ('Task D');SELECT * FROM tasks;");

multiStatement.execute().blockLast();

      connection.commitTransaction().subscribe();
}
catch(Exception e) {
     connection.rollbackTransaction().subscribe();
     // More exception handling code
}

Listing 14-9Handling exceptions and transactions

清单 14-9 利用了 MariaDB R2DBC 驱动程序在单个MariadbStatement对象中执行多个 SQL 语句的能力。

Tip

参见第十三章了解更多相关信息。

SELECT * FROM todo.tasks;
+----+-------------+-----------+
| id | description | completed |
+----+-------------+-----------+
|  1 | Task A      |         0 |
|  2 | Task B      |         0 |
|  3 | Task C      |         0 |
+----+-------------+-----------+

Listing 14-11Encountering an exception and rolling back the transactions from Listing 14-9

SELECT * FROM todo.tasks;
+----+-------------+-----------+
| id | description | completed |
+----+-------------+-----------+
|  4 | Task D      |         0 |
+----+-------------+-----------+

Listing 14-10After successfully committing the transactions from Listing 14-9

管理保存点

在第五章中,您了解到当有必要回滚事务的一部分时,保存点会很有用。当事务的一部分出现错误的可能性较低,并且事先验证操作的准确性成本过高时,通常会出现这种情况。

img/504354_1_En_14_Fig1_HTML.png

图 14-1

保存点的基本工作流程

使用保存点

使用 MariaDB 驱动程序,可以使用createSavepoint方法创建保存点,该方法在MariadbConnection对象中可用。

Boolean rollbackToSavepoint = true;

MariadbStatement insertStatement = connection.createStatement("INSERT INTO tasks (description) VALUES ('TASK X');");

MariadbStatement deleteStatement = pconnection.createStatement("DELETE FROM tasks WHERE id = 2;");

insertStatement.execute().then(connection.createSavepoint("savepoint_1").then(deleteStatement.execute().then(rollBackOrCommit(connection,rollbackToSavepoint)))).subscribe();

Listing 14-12Chaining subscribers to commit transactions

在这个场景中,清单 14-12 中使用的rollbackOrCommit方法包含条件功能,该功能要么将事务回滚到保存点 _1 ,然后提交事务,要么提交整个事务。

private Mono<Void> rollBackOrCommit(MariadbConnection connection, Boolean rollback) {
        if (rollback) {
            return connection.rollbackTransactionToSavepoint("savepoint_1").then(connection.commitTransaction());
        }
        else {
            return connection.commitTransaction();
        }
}

Listing 14-13The rollbackOrCommit method

清单 14-14 包含了清单 14-12 和 14-13 的更迫切的方法。

Boolean rollbackToSavepoint = true;

MariadbStatement insertStatement = connection.createStatement("INSERT INTO tasks (description) VALUES ('TASK D');");
insertStatement.execute().blockFirst();

connection.createSavepoint("savepoint_1").block();

MariadbStatement deleteStatement = connection.createStatement("DELETE FROM tasks WHERE id = 2;");
deleteStatement.execute().blockFirst();

if (rollbackToSavepoint) {
    connection.rollbackTransactionToSavepoint("savepoint_1").block();
}

connection.commitTransaction();

Listing 14-14Blocked equivalent of Listings 14-12 and 14-13

无论您使用清单 14-12 和 14-13 中的声明式方法还是清单 14-14 中的命令式流程,输出都是一样的,如清单 14-15 和 14-16 所示。

SELECT * FROM todo.tasks;
+----+-------------+-----------+
| id | description | completed |
+----+-------------+-----------+
|  1 | Task A      |         0 |
|  3 | Task C      |         0 |
|  4 | Task D      |         0 |
+----+-------------+-----------+

Listing 14-16Output that results after committing the entire transaction

SELECT * FROM todo.tasks;
+----+-------------+-----------+
| id | description | completed |
+----+-------------+-----------+
|  1 | Task A      |         0 |
|  2 | Task B      |         0 |
|  3 | Task C      |         0 |
|  4 | Task D      |         0 |
+----+-------------+-----------+

Listing 14-15Output that results after rolling back to savepoint_1

释放保存点

因为保存点直接在数据库上分配资源,所以数据库供应商可能要求释放保存点来处理资源。你在第五章中了解到有多种方法可以释放保存点,包括使用releaseSavepoint方法(清单 14-17 )。

connection.releaseSavepoint("savepoint_1").subscribe();

Listing 14-17Release a savepoint

处理隔离级别

数据库提供了在事务中指定隔离级别的能力。事务隔离的概念定义了一个事务与其他事务执行的数据或资源修改的隔离程度,从而在多个事务处于活动状态时影响并发访问。

可以通过调用getTransactionIsolationLevel方法来检索IsolationLevel枚举值,该方法可通过MariadbConnection对象获得。

Tip

有关IsolationLevel的更多信息,请参见第五章。

IsolationLevel level = connection.getTransactionIsolationLevel();

Listing 14-18Getting the default MariaDB R2DBC driver IsolationLevel setting

Note

这些示例中使用的 MariaDB 存储引擎 InnoDB 的缺省值IsolationLevel是可重复读取的。

要更改IsolationLevel,您可以使用setTransactionIsolationLevel方法,可通过MariadbConnection对象获得。

connection.setTransactionIsolationLevel(IsolationLevel.READ_UNCOMMITTED);

Listing 14-19Changing the MariaDB R2DBC driver IsolationLevel setting

给我看看代码

您可以在专门针对本书的 GitHub 资源库中找到一个完整的、完全可编译的示例应用。如果您还没有这样做,只需导航到 https://github.com/apress/r2dbc-revealedgit clone或者直接下载存储库的内容。在那里,您可以在 ch14 文件夹中找到专门用于这一章的示例应用。

摘要

使用和控制事务的能力是构建使用关系数据库的解决方案的一个关键特性。这是因为事务用于在并发数据库访问期间提供数据完整性、隔离、正确的应用语义和一致的数据视图。

在第五章中,您了解到 R2DBC 兼容驱动程序需要提供事务支持。在这一章中,你可以看到它的作用。使用 MariaDB R2DBC 驱动程序,您学习了如何创建、提交和回滚事务。您还学习了如何创建和管理保存点。最后,您了解了如何使用 R2DBC 处理 MariaDB 数据库中的隔离级别。

十五、连接池

即使我们在本书中挖掘了反应式开发和 R2DBC 的所有优势,打开数据库的基本过程也是一个昂贵的操作,尤其是如果目标数据库是远程的。就资源利用而言,连接到数据库的过程是昂贵的,因为建立网络连接和初始化数据库连接的开销很大。反过来,连接会话初始化通常需要耗时的处理来执行用户身份验证和建立事务上下文以及后续数据库使用所需的会话的其他方面。

因此,在这一章中,我们将看一看连接池的概念。您不仅将了解什么是连接池和为什么它们是必要的,还将了解如何在基于 R2DBC 的应用中使用连接池来帮助提高应用的性能和效率。

连接池基础

切入正题,连接池的概念有助于消除不断重新创建和重新建立数据库连接的需要。如图 15-1 所示,连接池充当数据库连接对象的缓存。

img/504354_1_En_15_Fig1_HTML.png

图 15-1

简单的连接池工作流

连接池的好处

您选择在应用中实现连接池的原因有很多,最明显的是重用现有连接的能力。减少必须创建的连接数量有助于提供几个关键优势,例如

  • 减少创建新连接对象的次数

  • 促进连接对象重用

  • 加速获得连接的过程

  • 减少管理连接对象所需的工作量

  • 最大限度地减少陈旧连接的数量

  • 控制用于维护连接的资源量

最终,一个应用的数据库越密集,它就越能从连接池的使用中获益。

如何开始

我们已经知道 R2DBC 的一个关键优势是它的可扩展性,这是通过规范的简单性实现的。事实上,它非常简单,你可能已经注意到连接池的概念直到本章才被提及。R2DBC 不支持连接池。

滚动您自己的连接池

R2DBC 的构建考虑了可扩展性。由于规范主要包含需要实现的接口,这些实现可以被补充以包括对连接池的支持。基本上,这意味着像ConnectionFactoryConnection这样的 R2DBC 对象可以以这样的方式创建,默认情况下,包括对连接池的支持。

当然,这可以在驱动程序级别完成,但是,理想情况下,由于连接池概念的普遍性,将它作为一个独立、自包含的库来支持可能更有意义。

最后,深入讨论如何使用尚未创建的自定义 R2DBC 实现来创建和维护连接池的细节已经超出了本书的范围。这里真正的要点是,它可以通过从 R2DBC 规范(位于 https://github.com/r2dbc/r2dbc-spi )创建一个实现来完成,其他库可以帮助实现反应流,等等。

R2DBC 池简介

然而,从实用的角度来看,从头开始开发对连接池的支持并不简单。因此,除了其他原因之外,最好不要创建自定义解决方案,而是利用现有的库来创建和管理连接池。幸运的是,这样一个库作为一个 GitHub 存储库存在于 R2DBC 帐户中。

r2dbc-pool 项目是一个支持反应式连接池的库。位于 https://github.com/r2dbc/r2dbc-pool 的开源项目使用 reactor-pool 项目,该项目提供支持通用对象池的功能,作为完全非阻塞连接池的基础。

Note

对象池模式是一种软件设计模式,它使用一组或一个池的初始化对象,这些对象随时可以使用,而不是根据命令分配和销毁它们。

更具体地说,根据 reactor-pool 文档,该项目旨在提供反应式应用中的通用对象池

  • 公开一个反应式 API ( Publisher输入类型,Mono返回类型)

  • 是非阻塞的(从不阻塞试图获取资源的用户)

  • 有懒惰的习得行为

Note

惰性加载(或称获取)是一种设计模式,它专注于将对象的初始化推迟到需要它的时候。

反应堆池项目是完全开源的,可以在 GitHub 的 https://github.com/reactor/reactor-pool 找到。

接下来,我们将使用 r2dbc-pool 项目来研究如何使用连接池来管理支持 r2dbc 的应用中的连接。继续前几章设定的趋势,对于所有后续示例,我将结合使用 MariaDB R2DBC 驱动程序和 r2dbc-pool。

R2DBC 池

在这一节中,我们将了解如何在应用中使用 r2dbc-pool 项目来管理 r2dbc 连接。

添加新的依赖关系

r2dbc-pool 工件可以在 Maven 中央存储库中找到, https://search.maven.org/search?q=r2dbc-pool ,并且可以使用清单 15-1 中所示的示例直接添加到应用的 pom.xml 文件中。

<dependency>
  <groupId>io.r2dbc</groupId>
  <artifactId>r2dbc-pool</artifactId>
  <version>0.8.5.RELEASE</version>
</dependency>

Listing 15-1Adding the dependency for r2dbc-pool

Note

您还可以选择通过直接从源代码构建来使用最新版本的 r2dbc-pool。有关更多信息,请参见 https://github.com/r2dbc/r2dbc-pool 的文档。

在进入下一节之前,再次强调 r2dbc-pool 项目不提供任何实际连接到底层数据库的机制是很重要的。它需要与现有的驱动程序结合使用才能工作。接下来,我将提供假设使用我们在前面章节中使用的 MariaDB R2DBC 驱动程序的示例。

连接池配置

你已经知道了ConnectionFactoryOptions对象,它首先在第四章中提到,然后在第十二章中提到,它的存在是为了保存用于最终创建ConnectionFactory对象的配置选项。R2DBC 池项目通过ConnectionFactoryOptions扩展了可用的选项,以包括发现设置来支持连接池。

在表 15-1 中,您可以看到通过 R2DBC 池公开的连接池设置的支持选项和相关描述。

表 15-1

支持的 ConnectionFactory 发现选项

|

[计]选项

|

描述

|
| --- | --- |
| 驾驶员 | 必须是。 |
| 草案 | 驱动程序标识符。该值由池传播到驱动程序属性。 |
| 收购测量 | 第一次连接获取尝试失败时的重试次数。默认为1。 |
| 初始化 | 池中包含的连接对象的初始数量。默认为10。 |
| maxSize(最大值) | 池中包含的最大连接对象数。默认为10。 |
| 最大寿命 | 池中连接的最长生存期。 |
| 连续时间 | 池中连接的最长空闲时间。 |
| maxAcquireTime | 从池中获取连接的最长允许时间。 |
| maxCreationConnectionTime | 创建新连接的最大允许时间。 |
| 池 | 连接池的名称。 |
| registerJMX | 是否将游泳池注册到 JMX。 |
| 验证深度 | 用于验证 R2DBC 连接的验证深度。默认为LOCAL。 |
| 验证查询 | 就在从池中接收连接之前执行的查询。查询执行用于验证到数据库的连接仍然有效。 |

Tip

Java 管理扩展(JMX)是一种 Java 技术,它提供了管理和监控应用、系统对象、设备和面向服务的网络的工具。这些资源由称为 MBeans 的对象表示。在 API 中,类可以动态加载和实例化。

连接工厂发现

最终,为了能够使用连接池管理连接,您必须能够访问一个ConnectionPool对象。然而,获取一个ConnectionPool对象是从获取一个兼容ConnectionPoolConnectionFactory对象开始的。创建一个兼容ConnectionPoolConnectionFactory对象有两种方法。

首先,您可以选择使用 R2DBC URL。如清单 15-2 所示,允许 R2DBC 池ConnectionPool对象使用ConnectionFactory对象的 R2DBC URL 需要driver的值protocol的值 mariadb

ConnectionFactory connectionFactory = ConnectionFactories.get("r2dbc:pool:mariadb://app_user:Password123!@127.0.0.1:3306/todo?initialSize=5");
Publisher<? extends Connection> connectionPublisher = connectionFactory.create();

Listing 15-2Using an R2DBC URL to discover a pool-capable ConnectionFactory

Tip

其他可选的发现选项可以添加在问号(?)在 R2DBC URL 中。

或者,如清单 15-3 所示,您可以使用ConnectionFactoryOptions以编程方式创建一个新的ConnectionFactory对象。

ConnectionFactoryOptions connectionFactoryOptions = ConnectionFactoryOptions.builder()
.option(ConnectionFactoryOptions.DRIVER, "pool")
.option(ConnectionFactoryOptions.PROTOCOL, "mariadb")
.option(ConnectionFactoryOptions.HOST, "127.0.0.1")
.option(ConnectionFactoryOptions.PORT, 3306)
.option(ConnectionFactoryOptions.USER, "app_user")
.option(ConnectionFactoryOptions.PASSWORD, "Password123!")
.option(ConnectionFactoryOptions.DATABASE, "todo")
 .build();

Listing 15-3Programmatically discovering a pool-capable ConnectionFactory

连接池配置

然后用一个ConnectionFactory对象创建一个ConnectionPoolConfiguration对象(清单 15-4 )。

ConnectionPoolConfiguration configuration = ConnectionPoolConfiguration.builder(connectionFactory)
            .maxIdleTime(Duration.ofMillis(1000))
            .maxSize(5)
            .build();

Listing 15-4Building a ConnectionPoolConfiguration object using ConnectionFactory

创建连接池

前面几节描述了创建一个ConnectionPoolConfiguration对象的工作流程,这是创建一个新的ConnectionPool对象所必需的(清单 15-5 )。

ConnectionPool connectionPool = new ConnectionPool(configuration);

Listing 15-5Creating a new connection pool

ConnectionPool对象只是 R2DBC SPI ConnectionFactory接口的一个定制实现(清单 15-6 ,正如我们将在下一节看到的,这就是它如何使获取Connection对象成为可能。

public class ConnectionPool implements ConnectionFactory, Disposable, Closeable, Wrapped<ConnectionFactory> {
...
}

Listing 15-6Class implementation of ConnectionPool

管理连接

我们已经知道 R2DBC Connection对象是反应性交互的基础,为了获得Connection对象,我们必须通过ConnectionFactory对象。

在上一节中,我们还了解到 R2DBC Pool 项目的ConnectionPool对象是ConnectionFactory对象的一个实现。

获得连接

为了利用 R2DBC Pool 项目提供的连接池管理,您需要通过一个PooledConnection对象获取Connection对象。

PooledConnection对象是Connection接口的自定义实现(清单 15-7 )。

final class PooledConnection implements Connection, Wrapped<Connection> {
...
}

Listing 15-7A high-level class implementation view of PooledConnection

使用在清单 15-5 中获得的ConnectionPool对象,我们可以访问包含在池中的Connection,更准确地说是PooledConnection,对象(清单 15-8 )。

PooledConnection pooledConnection = connectionPool.create().block();

Listing 15-8Obtaining a PooledConnection object

然后,正如预期的那样,我们可以使用PooledConnection对象来与数据库通信,以执行语句、管理事务等等。

释放连接

连接池的目的是通过使用连接来提高性能。对于要重用的连接,它们必须被释放回连接池中(图 15-2 )。

img/504354_1_En_15_Fig2_HTML.png

图 15-2

客户端正在使用的连接池中的所有连接

释放连接是另一个客户端能够从ConnectionPool获取并使用Connection对象的唯一方式,如图 15-3 所示。

img/504354_1_En_15_Fig3_HTML.png

图 15-3

在被释放回连接池之后,连接 C 可供客户机 4 使用

当不再使用某个连接对象时,可以通过调用 close 方法将它释放回连接池中(清单 15-9 )。

pooledConnection.close().subscribe();

Listing 15-9Releasing a connection back into the connection pool

清理

除了ConnectionFactory接口,ConnectionPool对象还实现了Disposable接口,通过调用dispose方法(清单 15-10 ),该接口能够适当地释放它可能正在使用的任何和所有资源。

connectionPool.dispose();

Listing 15-10Disposing a connection pool

或者,您可以使用close方法,通过实现Closeable接口(清单 15-11 )来实现。

connectionPool.close();

Listing 15-11Closing a connection pool

Note

在幕后,close 方法只是调用 dispose 方法。

给我看看代码

您可以在专门针对本书的 GitHub 资源库中找到一个完整的、完全可编译的示例应用。如果您还没有这样做,只需导航到 https://github.com/apress/r2dbc-revealedgit clone或者直接下载存储库的内容。在那里,您可以在 ch15 文件夹中找到一个专门用于这一章的示例应用。

摘要

连接池实质上是数据库内存中维护的数据库连接的缓存,以便在数据库接收未来的数据请求时可以重用这些连接。最终,连接池用于增强在数据库上执行命令的性能。

在本章中,您基本了解了什么是连接池,以及在应用中使用连接池是如何非常有益的,尤其是那些具有大量数据库密集型操作的应用。您了解了 R2DBC Pool 项目,并获得了如何在应用中利用它的第一手知识。

十六、Spring 数据和 R2DBC 的实际应用

到目前为止,您已经了解了 R2DBC 驱动程序提供了一种利用 R2DBC API 对关系数据库使用反应式代码的方法。但是您可能已经注意到,创建一个完整的数据访问层(DAL)需要做大量的工作,这并不是 R2DBC 所独有的。事实上,R2DBC 的一个主要优点是它并不打算成为一个通用的数据访问 API。

相反,R2DBC 侧重于由关系数据交互产生的反应式数据访问和常见使用模式。最终,R2DBC 打算让通用数据访问功能成为 R2DBC 客户端库的职责,这在第十一章中有简要提及。在本章中,我们将研究一个名为 Spring Data R2DBC 的客户端库,它是 Spring 应用框架的一部分,以了解 R2DBC 客户端如何帮助减少创建完全反应式应用的时间和复杂性。

春天简介

Spring Framework 是 Java 平台的应用框架和控制容器的反转。Spring Framework 为现代基于 Java 的应用提供了全面的编程和配置模型。

在这一章中,我将利用 Spring 框架和构建在它之上的库来说明如何创建一个完全反应式的 web 应用,该应用通过 Spring Data R2DBC 客户端使用 R2DBC 与关系数据库(更具体地说是 MariaDB)进行交互。

Spring Boot

Spring Boot 是一个由 Pivotal 开发的开源 Java 框架,Pivotal 也是 R2DBC 的主要贡献者,它旨在简化开发和部署 Java 企业 web 应用的任务。这是一个建立在 Spring 框架之上的项目。

Spring Boot 让创建基于 Spring 的独立的生产级应用变得简单,你可以“直接运行”。

—Spring Boot 官方文档

为了增加简单性,为了减少开发人员的配置和代码搭建,框架在 Spring 平台和第三方库上采取了固执己见的立场。

在本章中,我们将研究一个 Spring Boot 应用。应用将通过表述性状态转移(REST)端点公开应用编程接口(API)(图 16-1 )。在内部,应用将结合使用 Spring Data R2DBC 库和 MariaDB R2DBC 驱动程序来连接和通信我们在第十一章中建立的 MariaDB 数据库。

img/504354_1_En_16_Fig1_HTML.png

图 16-1

Spring Boot 应用架构

关于 Spring Boot 的更多信息,请查看 https://spring.io/projects/spring-boot 的官方文档。

春季数据

Spring Data 的存在是为了统一和简化对不同类型的持久性存储的访问,包括关系型和非关系型。根据官方文档,Spring Data 的任务是为数据访问提供一个熟悉的、一致的、基于 Spring 的编程模型,同时仍然保留底层数据存储的特性。

因为实现应用的数据访问层可能很麻烦,通常需要编写大量的样板代码,所以 Spring Data 提供了存储库抽象来帮助减少创建数据访问层和持久层所需的工作。

我将在本章中使用的几个关键特性是

  • 创建存储库和自定义对象映射抽象

  • 从存储库方法名动态派生查询

  • 定制存储库代码

  • 通过注释利用 Spring 集成

春季数据 R2DBC

与其他 Spring 数据库类似,Spring Data R2DBC 使用核心的 Spring 概念来帮助开发与目标数据源集成的解决方案。与使用 JDBC 驱动程序的 Spring Data JPA 一样,Spring Data R2DBC 使用 R2DBC 驱动程序与关系数据库进行交互,并使用 Spring 数据存储库管理持久性(图 16-2 )。

img/504354_1_En_16_Fig2_HTML.png

图 16-2

Spring Data R2DBC 架构

项目反应堆

默认情况下,Spring Data R2DBC 需要 Project Reactor 作为核心依赖项,但是它可以通过 reactive Streams 规范与其他 Reactive 库进行互操作。Spring Data R2DBC 存储库接受一个 Reactive Streams API Publisher作为输入,并在内部使其适应一个项目反应器类型,以返回一个Mono或一个Flux输出。

然而,正如我们在前面的章节中了解到的,任何Publisher都可以被证明为输入并在输出上应用操作;只需修改输出,就可以使用另一个 Reactive Streams API 实现库。

出于本章的目的,我将坚持使用默认的基于 Project Reactor 的输出,因为 Spring Data R2DBC 和 MariaDB R2DBC 连接器共享 Project Reactor 作为依赖项。

入门指南

在第十一章中,我们介绍了使用 Apache Maven 客户端创建新 Java 应用的过程。希望这有助于您理解创建使用 R2DBC 驱动程序的 Java 应用的基础。在本章中,我将更多地关注应用代码,而不是项目基础设施,因此,我将使用一个名为 Spring Initializr 的 web 应用来生成一个包含各种依赖项的 Java 项目,包括 Spring 数据 R2DBC。

弹簧初始化 zr

Spring Initializr 是一个 web 应用,可以用来为您生成 Spring Boot 项目结构。它不会生成任何应用代码,但是它会给你一个基本的项目结构和一个 Maven 或 Gradle 构建规范来构建你的代码。你所需要做的就是编写应用代码。

Spring Initializr 项目可以以几种方式使用,包括

  • 基于网络的界面

  • 使用 Spring Boot CLI

  • Via Spring 工具套件

Note

Spring Tool Suite (STS)是一套用于创建 Spring 应用的工具。这个工具集既可以作为一个插件安装到 Eclipse JEE 的现有安装中,也可以独立安装。

然而,为了简单起见,我将在 https://start.spring.io 浏览 Spring Initializr 的 web 界面。web 界面包含一个页面,该页面提供了生成新 Spring Boot 应用的可配置选项(图 16-3 )。

img/504354_1_En_16_Fig3_HTML.jpg

图 16-3

spring 初始化网页

项目配置

在 Spring Initializr 页面的左侧,您将看到用于配置新 Spring Boot 应用的各种选项。

首先选择 Maven 项目的项目设置,因为您将使用 Apache Maven 和 Maven 客户端来构建管理应用。

接下来,选择 Java 作为语言,并随意保留默认的 Spring Boot 版本。在我写这本书的时候,默认版本是 2.4.0。

图 16-4 提供了一个我用于项目元数据设置的例子,但是你可以根据自己的需要和喜好随意定制。

img/504354_1_En_16_Fig4_HTML.jpg

图 16-4

为新的 Spring Initializr 生成的应用提供项目设置

添加依赖关系

在 Spring Initializr 页面的右边是一个名为“Dependencies”的部分,它可以用来将 Maven 工件添加到将要生成的项目中。对于示例项目,您将需要表 16-1 中所示的四个依赖项。

表 16-1

示例项目中使用的 Maven 工件的名称和描述

|

工件名称

|

描述

|
| --- | --- |
| MariaDB 驱动程序 | MariaDB R2DBC 驱动程序工件。 |
| 春季数据 R2DBC | Spring Data R2DBC 客户端库工件。 |
| 弹簧反应网 | 一个用于 web 应用的反应式框架库。 |
| 龙目岛 | 一个 Java 注释库,有助于减少样板代码,即模型对象的 getter 和 setter 方法。 |

单击“添加相关性”按钮,提示工作流搜索要生成的项目并向其添加相关性。

当依赖项被成功添加后,您应该会看到它们被填充在类似于图 16-5 的页面上。

img/504354_1_En_16_Fig5_HTML.jpg

图 16-5

添加到新的 Spring Initializr 生成的项目中的依赖项

生成新项目

最后,在提供了项目设置并添加了依赖项之后,您可以通过单击页面底部的“generate”按钮来生成一个新的 Spring Boot 应用。

这样做会自动将包含项目的压缩文件下载到系统上的默认下载位置。

解压缩文件后,您可以使用自己选择的代码编辑器或 IDE 来打开项目并检查文件。正如我前面指出的,Spring Initializr 有助于减少构建项目所花费的时间,包括拼凑依赖层次结构。

如果你想知道我这么说是什么意思,请打开并检查位于 r2dbc-spring-data-demo 文件夹顶层的 pom.xml 文件。在 pom.xml 文件中,您会注意到关于该项目的大量信息,包括依赖项部分(清单 16-1 ),其中包括您使用 Spring Initializr web 接口添加的依赖项。

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-r2dbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mariadb</groupId>
            <artifactId>r2dbc-mariadb</artifactId>
            <version>0.8.4-rc</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

Listing 16-1The dependencies for the Spring Initializr–generated project

配置连接

在前面的章节中,我解释了如何直接使用 R2DBC 驱动程序来配置与底层数据源的连接。最终,驱动程序的代码负责处理与目标数据源的所有通信,但是 Spring Data R2DBC 客户端的部分角色是帮助管理连接工作流。

通过向名为application.properties的单个文件添加信息,可以配置 Spring Data R2DBC 使用特定的驱动程序和到目标数据库的连接。

Tip

Spring Boot 属性文件用于在单个文件中保存 N 个属性,以便在不同的环境中运行应用。在 Spring Boot,属性保存在类路径下的application.properties文件中。

为此,导航到r2dbc-spring-data-demo/src/main/resources/application . properties以及清单 16-2 中详细列出的连接信息。

spring.r2dbc.url=r2dbc:mariadb://127.0.0.1:3306/todo
spring.r2dbc.username=app_user
spring.r2dbc.password=Password123!

Listing 16-2Spring Data R2DBC connection settings in the application.properties file

Tip

有关 Spring Data R2DBC 配置设置的更多信息,包括如何设置连接池,请务必查看位于 https://docs.spring.io/spring-data/r2dbc/docs/current/reference/html/#reference 的官方参考文档。

Spring 数据仓库

正如我在本章开始时提到的,Spring 数据仓库抽象的目标之一是帮助减少为各种持久性存储实现数据访问层所需的样板代码的数量。

Note

在计算机科学中,持久性是一个名词,描述在创建数据的过程之后仍然存在的数据。

在 Spring 中,持久性可以通过使用数据访问对象(DAO)层来处理。在这种情况下,如图 16-6 所示,Spring DAO 层的作用是作为一个持久性管理器,所以无论使用 JDBC、R2DBC、JPA 还是一个本地 API,都会给出相同的应用数据访问 API。

img/504354_1_En_16_Fig6_HTML.png

图 16-6

一个示例应用层架构,包括一个持久层

映射实体

Spring 数据 R2DBC 实体是普通的旧 Java 对象(POJOs ),使用 Spring 数据注释可以映射到关系数据库表。

使用@Table注释,Spring Data R2DBC 在 MariaDB todo 数据库(清单 16-3 )中的Task类和 tasks 表之间建立映射。

@Data
@RequiredArgsConstructor
@Table("tasks")
class Task {
    @Id
    private Integer id;
    @NonNull
    private String description;
    private Boolean completed;
}

Listing 16-3A Spring Data R2DBC mapped entity object

Tip

清单 16-3 中使用的@Data注释是从 Lombok 库中获得的注释,有助于消除向Task类添加 gettersetter 方法的需要。更多关于龙目岛项目的信息,请务必查看 https://projectlombok.org/

创建新的存储库

接下来,您可以利用名为ReactiveCrudRepository的 Spring Data R2DBC 接口,该接口为特定类型的存储库提供通用 CRUD 操作。

要使用ReactiveCrudRepository,创建一个名为TasksRepository的新接口,它扩展了ReactiveCrudRepository,并提供了一个目标实体类型Task和一个主键类型Integer(清单 16-4 )。

interface TasksRepository extends ReactiveCrudRepository<Task, Integer> {
}

Listing 16-4Creating a new repository implementation

ReactiveCrudRepository遵循反应范式,使用建立在反应流之上的项目反应器类型。基本上,准备好处理一些Publisher对象。

查询数据

TasksRepository实现了ReactiveCrudRepository,它提供了几个 CRUD 方法,可以用来与 todo 数据库中的任务表进行交互。

例如,在清单 16-5 、 16-6 和 16-7 中,您可以看到一些可用的 CRUD 方法选项,其中tasksRepository是一个实例化的TasksRepository对象。

Task task = new Task("New Task");
Mono<Task> saveTaskPublisher = tasksRepository.save(task);

Listing 16-7Create a new task record

Mono<Task> taskPublisher = tasksRepository.findById(1);

Listing 16-6Select a single task record by providing a primary key value to the findById method

Flux<Task> tasksPublisher = tasksRepository.findAll();

Listing 16-5Select all records from the tasks table using the findAll method

请记住,清单 16-5 、 16-6 和 16-7 只是通过ReactiveCrudRepository提供的几个 CRUD 操作。有关ReactiveCrudRepository的更多信息,请查阅官方文档。

使用自定义查询

您还可以通过向存储库接口添加方法签名,向存储库添加定制的方法(清单 16-8 )。

interface TasksRepository extends ReactiveCrudRepository<Task, Integer> {
    @Modifying
    @Query("UPDATE tasks SET completed = :completed WHERE id = :id")
    Mono<Integer> updateStatus(Integer id, Boolean completed);
}

Listing 16-8Custom defined query

Note

@Modifying注释表明查询方法应该被认为是修改查询,因为它改变了需要执行的方式。对于所有数据操作语言(DML)和数据定义语言(DDL)查询,Spring Data R2DBC 存储库也需要它。

参数化

在第 6 和 13 章中,您了解了 R2DBC 中参数化的概念,以及占位符值如何被用来动态地向 SQL 语句提供信息。

仔细查看添加到清单 16-8 中的TasksRepository示例的方法,清单 16-9 关注于updateStatus方法的输入参数,以及它们如何在通过使用@Query注释指定的定制查询中用作参数。

@Query("UPDATE tasks SET completed = :completed WHERE id = :id")
Mono<Integer> updateStatus(Integer id, Boolean completed);

Listing 16-9Querying with parameters

将这一切结合在一起

现在,您已经学习了如何使用 Spring Framework 正确配置连接信息,研究了如何建立Task类和底层 tasks 表之间的映射,最后,了解了如何使用自定义的ReactiveCrudRepository实现来处理持久性,现在您已经有了使用 R2DBC 从 MariaDB 数据库读取信息和向其中写入信息的基础!

在清单 16-10 中,我将所有东西都放在了一个内聚的示例中,您可以用它来替换/r2dbc-spring-data-demo/src/main/Java/com/example/r 2 dbcspringdatademodemo/r 2 dbcspringdatademoapplication . Java中的所有内容。

package com.example.r2dbcspringdatademo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.annotation.Id;
import org.springframework.data.r2dbc.repository.Modifying;
import org.springframework.data.r2dbc.repository.Query;
import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;
import org.springframework.data.relational.core.mapping.Table;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import lombok.Data;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@SpringBootApplication
@EnableR2dbcRepositories
public class R2dbcSpringDataDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(R2dbcSpringDataDemoApplication.class, args);
    }

}

@RestController
@RequestMapping("/tasks")
class TasksController {

    @Autowired
    private TaskService service;

    @GetMapping()
    public ResponseEntity<Flux<Task>> get() {
        return ResponseEntity.ok(this.service.getAllTasks());
    }

    @PostMapping()
    public ResponseEntity<Mono<Task>> post(@RequestBody Task task) {
        if (service.isValid(task)) {
            return ResponseEntity.ok(this.service.createTask(task));
        }
        return ResponseEntity.status(HttpStatus.I_AM_A_TEAPOT).build();
    }

    @PutMapping()
    public ResponseEntity<Mono<Task>> put(@RequestBody Task task) {
        if (service.isValid(task)) {
            return ResponseEntity.ok(this.service.updateTask(task));
        }
        return ResponseEntity.status(HttpStatus.I_AM_A_TEAPOT).build();
    }

    @PutMapping("/updatestatus")
    public ResponseEntity<Mono<Integer>> updateStatus(@RequestParam int id, @RequestParam Boolean completed) {
        if (id > 0) {
            return ResponseEntity.ok(this.service.updateTaskStatusById(id,completed));
        }
        return ResponseEntity.status(HttpStatus.I_AM_A_TEAPOT).build();
    }

    @DeleteMapping()
    public ResponseEntity<Mono<Void>> delete(@RequestParam int id) {
        if (id > 0) {
            return ResponseEntity.ok(this.service.deleteTask(id));
        }
        return ResponseEntity.status(HttpStatus.I_AM_A_TEAPOT).build();
    }
}

@Service
class TaskService {

    @Autowired
    private TasksRepository repository;

    public Boolean isValid(final Task task) {
        if (task != null && !task.getDescription().isEmpty()) {
            return true;
        }
        return false;
    }

    public Flux<Task> getAllTasks() {
        return this.repository.findAll();
    }

    public Mono<Task> createTask(final Task task) {
        return this.repository.save(task);
    }

    @Transactional
    public Mono<Task> updateTask(final Task task) {

        return this.repository.findById(task.getId())
                .flatMap(t -> {
                    t.setDescription(task.getDescription());
                    t.setCompleted(task.getCompleted());
                    return this.repository.save(t);
                });
    }

    public Mono<Integer> updateTaskStatusById(Integer id, Boolean completed) {
        return this.repository.updateStatus(id, completed);
    }

    @Transactional
    public Mono<Void> deleteTask(final int id){
        return this.repository.findById(id)
                .flatMap(this.repository::delete);
    }
}

interface TasksRepository extends ReactiveCrudRepository<Task, Integer> {
    @Modifying
    @Query("UPDATE tasks SET completed = :completed WHERE id = :id")
    Mono<Integer> updateStatus(Integer id, Boolean completed);
}

@Data
@RequiredArgsConstructor
@Table("tasks")
class Task {
    @Id
    private Integer id;
    @NonNull
    private String description;
    private Boolean completed;
}

Listing 16-10The complete code sample for R2dbcSpringDataDemoApplication.java

结合设置必要的 application.properties 配置和你在本章中学到的持久性实现,清单 16-10 还包括两个额外的类:TaskServiceTaskController

使用通过 Spring Web Reactive library 提供的库(以前作为依赖项添加的),TaskController类公开了 API 端点。

使用 Spring Framework 提供的控制反转(IoC),TaskController类创建并使用一个TaskService对象作为中介、业务级机制来与TasksRepository交互。

Note

在软件工程中,控制反转(IoC)是一种编程原则。与传统的控制流相比,IoC 颠倒了控制流。在 IoC 中,计算机程序的自定义部分从通用框架接收控制流。

测试它

将所有的部分放在一起之后,是时候检查一下正在运行的应用,看看您的劳动成果了。您将从构建和运行应用开始。从那里,您将能够向使用TaskController公开的端点发出 HTTP 请求。

构建并运行项目

虽然我们非常欢迎您使用任何构建过程或工具,比如 Eclipse 之类的 IDE,只要您能够轻松地构建和运行应用,您也可以像我们在前面章节中所做的那样使用 Apache Maven 客户端。

在打开一个新的终端窗口并导航到 r2dbc-spring-data-demo 项目的根位置后,您可以执行清单 16-11 中的命令来使用 Maven 构建 Spring Boot 应用。

mvn package

Listing 16-11Build the application using the Apache Maven client

一旦成功构建了应用,就可以使用清单 16-12 中的命令来运行应用。

mvn spring-boot:run

Listing 16-12Run the application using the Apache Maven client

呼叫端点

最后,结束本章的编码练习,现在您已经成功构建并运行了应用,是时候调用您通过TaskController类公开的端点了。

发出 HTTP 请求有许多选择。出于以下示例的目的,我选择使用 curl,这是一个使用 url 语法发送和检索数据的命令行工具。您可以在 https://curl.se/ 找到更多关于 curl 支持以及如何下载客户端的信息。

使用 curl,如清单 16-13 所示,您可以执行 GET 请求来检索 MariaDB todo.tasks 表中的所有任务。

curl http://localhost:8080/tasks

Listing 16-13Call the tasks endpoint to retrieve all the records in the MariaDB todo.tasks table

给我看看代码

您可以在专门针对本书的 GitHub 资源库中找到一个完整的、完全可编译的示例应用。如果您还没有这样做,只需导航到 https://github.com/apress/r2dbc-revealedgit clone或者直接下载存储库的内容。在那里,您可以在 ch16 文件夹中找到一个专门用于这一章的示例应用。

摘要

毫无疑问,使用 R2DBC 大大减少了构建可重用数据访问工作流所花费的开发时间和精力。在本章中,您学习了 Spring Framework、Spring Boot,以及 Spring Data 如何帮助抽象出数据持久层中存在的大量复杂性。在这些基础之上,您还了解了 Spring Data R2DBC 客户端库,以及它如何使用 R2DBC 驱动程序来创建完全反应式的应用。

第一部分:反应式运动和 R2DBC

第二部分:R2DBC 服务供应器接口

第三部分:R2DBC 和 MariaDB 入门

posted @ 2024-10-01 21:04  绝不原创的飞龙  阅读(0)  评论(0编辑  收藏  举报