R2DBC-揭秘-二-

133 阅读1小时+

R2DBC 揭秘(二)

原文:R2DBC Revealed

协议:CC BY-NC-SA 4.0

十、处理异常

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

在这一章中,我们将看看 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 = 6047854")
     .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 = 6047854;");

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 项目,并获得了如何在应用中利用它的第一手知识。