R2DBC-揭秘-一-

181 阅读1小时+

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,以及它们如何利用反应式编程功能,以及如何被创建、消费和销毁。