Python-MySQL-Shell-入门指南-全-

Python MySQL Shell 入门指南(全)

原文:Introducing MySQL Shell

协议:CC BY-NC-SA 4.0

一、MySQL Shell 简介

甲骨文继续履行其承诺,让 MySQL 变得更好。这使得 MySQL 工程部门能够在每个新版本中达到更高和更远的目标。最新版本 MySQL 8 包含了比其他版本更多的新特性和增强功能。因此,MySQL 仍然是世界上最受欢迎的开源数据库系统。

为了充分理解 MySQL 8 版本的重要性,让我们考虑一下,虽然 MySQL 过去的版本一直在不断改进产品,但这些版本往往包含一些新功能,重点是改进更受欢迎的功能。因此,以前的版本在很大程度上是渐进的,而不是革命性的。

MySQL 8 在几个方面打破了这一传统。最值得注意的可能是版本号本身。以前的版本是 5。x 范围的数字,但 Oracle 选择使用 8。x 系列标志着技术复杂性的革命性飞跃,并最终脱离了 5 的持续发展。持续了超过 14 年的 x 代码库。

MySQL 8.0 的革命性变化包括致力于高可用性、更高可靠性和复杂性的功能,以及全新的用户体验和革命性的数据处理方式。这本书研究了实现新用户体验的最重要的附加功能之一 MySQL Shell。在这一章中,我们将简要介绍 MySQL 8 的最新特性。但是首先,让我们更好地了解一下 MySQL Shell。

了解 MySQL Shell

许多 MySQL 用户的一个痛点是缺省客户机实用程序的局限性。几十年来,客户机的选择(因为没有其他选择)一直是名为mysql的 MySQL 客户机实用程序,它包含在服务器版本中。

也许旧的 mysql 客户端(MySQL)最大的缺失就是没有任何形式的脚本功能。有人可能会说,用旧客户机来处理一批 SQL 命令,编写 SQL 命令脚本是可能的。其他人可能会指出,MySQL 支持的 SQL 语言对编写存储例程(过程和函数)的支持有限。然而,对于那些想要创建和使用脚本语言来管理他们的数据库(和服务器)的人来说,已经有了外部工具选项,包括 MySQL Workbench 和 MySQL Utilities(现在已经退休了),但是没有专门用于合并脚本语言的工具。

注意

MySQL Workbench 是一个 GUI 工具,设计为基于工作站的工具,具有许多功能,包括设计和建模、开发、数据库迁移等等。参见 http://dev.mysql.com/doc/workbench/en/ 了解更多关于 MySQL Workbench 的信息。

除了这些产品之外,向 MySQL 客户端添加脚本语言的请求还没有得到回应。也就是说,直到现在。

注意

我使用术语“shell”来指代 MySQL Shell 支持的特性或对象。我用“MySQL Shell”来指代产品本身。

概观

MySQL Shell 是 MySQL 的下一代命令行客户端。您不仅可以执行传统的 SQL 命令,还可以使用包括 Python 和 JavaScript 在内的几种编程语言之一与服务器进行交互。此外,如果您还安装了 X 插件,那么您可以使用 MySQL Shell 来处理传统的关系数据以及 JavaScript Object Notation (JSON)文档。这有多酷?

如果你在想,“是时候了!”Oracle 开发了新的 MySQL 客户端,你并不孤单。MySQL Shell 代表了一种大胆的与 MySQL 交互的新方式。有许多选项,甚至有不同的方式来配置和使用 shell。虽然我们将在接下来的章节中看到更多关于 shell 的内容,但是让我们快速地看一下 shell。图 1-1 显示了新 MySQL Shell 的快照。请注意,它提供了一个非常熟悉的界面,尽管更加现代,功能也更加强大。还要注意新的提示。它不仅更加丰富多彩,还提供了一个快速检查,看看你在什么模式。这里显示的是 JS,是 JavaScript 模式(默认模式 1 )。您也可以根据自己的喜好修改提示。

img/478423_1_En_1_Fig1_HTML.jpg

图 1-1

MySQL Shell

小费

如果你想密切关注 MySQL Shell 版本,请加入书签 https://dev.mysql.com/downloads/shell/ ,它包含了流行平台的文档和下载链接。

如果您已经了解到 MySQL Shell 使用全新的机制来访问数据,并且您必须学习所有新的命令,那么您可能已经被引入歧途。虽然 MySQL Shell 确实支持使用脚本语言访问数据的新的应用编程接口(API ),从这个意义上说,还需要学习新的命令(方法),但是 MySQL Shell 继续支持对数据的 SQL 接口。事实上,您所知道的所有 SQL 命令都是完全受支持的。事实上,MySQL Shell 旨在成为您使用离散 SQL 命令的主要工具。

让我们看一个使用 MySQL Shell 和 SQL 命令的例子。图 1-2 显示了一组典型的 SQL 命令,用于创建数据库、插入数据以及选择一些数据进行查看。

img/478423_1_En_1_Fig2_HTML.jpg

图 1-2

使用 MySQL Shell 与 SQL 命令

在这里,我们看到几件事正在发生。首先,我们使用\sql命令将 shell 的模式从 JavaScript 更改为 SQL。然后,我们使用\connect命令连接到服务器。请注意,该命令请求用户密码,如果这是第一次使用该连接,您可以让 shell 为您保存密码(一个很好的安全特性)。从这里,我们可以看到几个运行 SQL 命令的普通例子。清单 1-1 显示了本例中使用的命令。

\sql
\connect root@localhost:3306
CREATE DATABASE testdb;
CREATE TABLE testdb.t1 (a int auto_increment not null primary key, b timestamp, c char(20));
INSERT INTO testdb.t1 (c) VALUES ('one'), ('two'), ('three');
SELECT * FROM testdb.t1 WHERE c = 'two';

Listing 1-1Sample Commands (Getting Started with MySQL Shell)

现在我们已经了解了 MySQL Shell,让我们看看它令人印象深刻的特性列表。你可能会发现有几个可以让你的 MySQL 体验更好。

特征

MySQL Shell 有许多特性,包括支持传统的 SQL 命令处理、脚本原型,甚至支持定制 Shell。下面列出了 shell 的一些主要特性。大多数功能都可以通过命令行选项或特殊的 shell 命令来控制。这个列表是为了让您对 shell 中的特性有一个大致的了解,并且没有给出示例。在后面的章节中,我们将深入探讨一些更重要的特性。

小费

这里的一些术语可能看起来很陌生。在这一点上理解这些并不重要,但是我们将在后面的章节中发现每一个。

  • 自动完成:shell 允许自动完成 SQL 模式下的关键字以及 JavaScript 或 Python 中的所有主要类和方法。只需键入几个字符,然后按TAB键自动完成关键字。当学习新的 API 并试图回忆一个很少使用的 SQL 关键字或 MySQL 函数的拼写时,这可能是一个非常方便的工具。

  • API:Shell 支持 JavaScript 和 Python,与以下应用编程接口交互:

    • X DevAPI :这个 API 允许您使用关系数据或文档存储(JSON)与 MySQL 服务器进行交互。

    • AdminAPI :这个 API 允许您与 MySQL InnoDB 集群进行交互,以设置、配置和维护高可用性集群。

  • 批处理代码执行:如果您想在没有交互会话的情况下运行您的脚本,您可以使用 shell 以批处理模式运行脚本——就像旧客户端一样。

  • 命令历史:shell 保存您输入的命令,允许您使用上下箭头键调用它们。

  • 定制提示:您也可以通过使用特殊格式更新名为~/.mysqlsh/prompt.json的配置文件或定义名为MYSQLSH_PROMPT_THEME的环境变量来更改默认提示。

  • 全局变量:shell 提供了一些全局变量,您可以在使用交互模式时访问这些变量。其中包括以下内容。我们将在第三章中学习更多关于会话和变量的知识。

    • session:全局会话对象(如果已建立)

    • db:通过连接建立的模式

    • dba:用于使用 InnoDB 集群的 AdminAPI 对象

    • shell:使用 Shell 的通用功能

    • util:与服务器一起工作的实用功能

  • JSON 导入:键入 JavaScript 对象符号(JSON)可能会有点繁琐。shell 允许用户将 JSON 文档导入到 shell 中,这使得使用 JSON 变得更加容易。交互式命令和 API 函数中都启用了导入功能。

  • 交互式代码执行:使用 shell 的默认模式是交互式模式,它的工作方式类似于旧的 MySQL 客户端,在这里输入命令并获得响应。

  • 日志:你可以为你的会话创建一个日志,用于以后的分析或者保存消息的记录。您可以使用--log-level选项设置详细程度,范围从 1(无记录)到 8(最大调试)。

  • 多行支持:shell 允许你输入命令,并将它们缓存起来作为单个命令执行。

  • 输出格式:Shell 支持三种格式选项;table ( --table),这是您在旧客户端中习惯使用的传统网格格式,tab(--tabbed),它使用制表符分隔显示信息并用于批处理执行,以及 JSON ( --json),它以更易于阅读的方式格式化 JSON 文档。这些是您在启动 shell 时指定的命令行选项。

  • 脚本语言:shell 支持 JavaScript 和 Python,尽管你一次只能使用一种。

注意

在本书中,我们将重点介绍 Python,但使用 JavaScript 的 API 是相同的。唯一的区别是类和方法是如何拼写的。这是因为 JavaScript 对大写和多字标识符使用了不同的约定。精明的 JavaScript 开发人员翻译本书中的例子不会有问题。

  • 会话:会话本质上是到服务器的连接。shell 允许您处理会话,包括在需要时存储和检索会话。

  • 启动脚本:您可以定义一个脚本在 shell 启动时执行。您可以用 JavaScript 或 Python 编写脚本。

  • 升级检查器(Upgrade Checker): shell 还包括一个方便的升级检查工具,可以让你检查给定的服务器,看看它是否可以升级到 MySQL 8。对于那些将现有 MySQL 服务器迁移到 MySQL 8 的人来说,这是一个真正的时间节省器。

  • 用户凭证“秘密”存储:也许最节省时间的特性是能够将用户密码保存到平台或特定平台存储中常见的“秘密存储”或加密凭证存储机制中。shell 支持 MySQL 登录路径、MacOS keychain 和 Windows API。默认情况下,此功能是打开的,但可以根据用户凭据禁用(如果不想存储密码,可以不存储)。如果您使用单个系统或跨多个系统的受保护帐户,这将通过从 secret store 中取回该用户的密码来节省您的时间。我们将在第三章中看到更多关于这个特性的内容。

MySQL Shell 的最新版本(8.0.16)包括许多错误修复和一些新功能,肯定会受到欢迎并经常使用。其中包括以下内容。

  • 用户自定义报告:您现在可以设置报告来显示来自服务器的实时信息,如状态和性能数据。如果您想监控您的服务器元数据和状态变量,请参见 https://dev.mysql.com/doc/mysql-shell/8.0/en/mysql-shell-reporting.html 了解关于此新功能的更多信息。

  • SQL 模式执行:如果你正在使用 JavaScript 模式的 Python 中的 shell,并且想要运行 SQL 命令,那么\sql shell 命令现在允许你指定一个要运行的 SQL 命令。例如,您可以执行\sql SHOW DATABASES,而不必切换到 SQL 模式(或者反过来)。

  • AdminAPI :现在在status()describe()rescan()方法中报告关于服务器版本的信息。

MySQL Shell 并不是 MySQL 8 中唯一的新特性。事实上,MySQL 的最新版本中有很多值得喜欢和探索的地方。事实上,有一些已经改进的特性,像 MySQL Shell 这样引入的新特性,以及一些将改变您使用 MySQL 方式的非常独特的特性。其中一些特性将 MySQL Shell 作为一个关键组件。因为我们计划探索如何在 shell 中使用这些特性,所以让我们花点时间了解一下 MySQL 8 中的新特性。

旧功能再创新

这个类别包括那些在 MySQL 早期版本中作为独立下载或插件引入的特性。有些被认为是实验性的,即使它们可能是在正式发布(g a)期间引入的(该特性可能不是正式发布的)。虽然将来可能会以这种方式引入一些功能,但目前所有这些功能都是 MySQL 8 GA 的一部分,而且形式更加精炼。其中包括以下内容。这里没有列出的是该版本中包含的数百个小到中等的增强和缺陷修复。

  • JSON 数据类型:对数据最具革命性的改变包括加入了 JSON 数据类型,它允许使用 MySQL 作为真正的 NoSQL 数据库系统。

  • X Plugin、X Protocol 和 X DevAPI :服务器现在支持新的客户端协议,所有新的 API 都建立在该协议之上。

  • InnoDB 改进:除了作为默认的存储引擎,InnoDB 已经成为一个更加健壮的企业级原子性、一致性、隔离性和持久性(ACID)兼容的存储引擎。

JSON 数据类型

从 MySQL 版本 5.7.8 开始,MySQL 支持 JSON 数据类型。JSON 数据类型可用于在关系表中存储 JSON 文档。因此,您的表中可以有 JSON 列!在一个表中可以有多个 JSON 列(字段)。

JSON 数据类型也是将 MySQL 用作文档存储的一个关键组件。简而言之,JSON 是一种用于交换数据的标记语言。它不仅可读,还可以直接在应用中使用,在其他应用、服务器甚至 MySQL 之间存储和检索数据。

注意

下面是对 JSON 数据类型和 JSON 文档的简要概述。我们将在第六章看到对 JSON 的深入研究。

事实上,程序员对 JSON 很熟悉,因为它类似于其他标记方案。JSON 也非常简单,因为它只支持两种类型的结构:(1)包含(name,value)对的集合,(2)有序列表(或数组)。当然,您也可以混合和匹配一个对象中的结构。当我们创建一个 JSON 对象时,我们称之为 JSON 文档。 2

与 MySQL 中的普通数据类型不同,JSON 数据类型允许您将 JSON 格式的对象(文档)存储在一行的一列中。虽然您可以对文本或 BLOB 字段这样做(很多人都这样做),但是 MySQL 中没有内置与文本和 BLOB 字段中的数据进行交互的功能。因此,数据的操作在很大程度上取决于应用。此外,数据的结构通常是每行都有相同的列“格式”。在文本和 BLOB 字段中存储数据并不新鲜,很多人已经这样做了很多年。

使用 JSON 数据类型,我们不必编写任何专门的代码来存储和检索数据。这是因为 JSON 文档很好理解,并且许多编程环境和脚本语言本身就支持它。JSON 允许你存储当时的数据。与典型的数据库表不同,我们不必担心默认值(它们是不允许的),也不必担心我们是否有足够的列,甚至是主/从关系来将所有数据规范化并存储在一个漂亮、整洁、结构化的包中。

让我们先来看看 JSON 数据类型。假设您想在数据库中存储地址,但是您不能保证您存储的所有项目都有一个地址,有些可能有多个地址。更糟糕的可能是你掌握的地址信息不一致。也就是说,地址的形式和所提供的数据各不相同。例如,您可能有一行、两行甚至三行“街道”地址,但其他地址可能只有一行带有邮政信箱号码。或者,有些地址包含五位数字的邮政编码,而有些地址包含九位数字的邮政编码,甚至有些地址可能包含邮政编码(如加拿大邮政)。

在这种情况下,您可以将地址字段添加到现有的表中(但这并不能解决行可能有多个地址的情况),或者更好的方法是创建一个关系表来存储地址并将数据硬塞进字段中。对于不符合的地址,您可能会被迫使用缺省值,甚至为缺失的项目存储 NULL。尽管所有这些都是可能的,但它会在关系数据库中增加一层复杂性,这可能意味着需要额外的代码来处理丢失的数据。

图 1-3 显示了一个典型的关系数据库的模式,它包含了作为一个单独的表存储的地址。这段摘录虽然非常简洁和不完整,但展示了数据库设计人员在处理数据(如地址)时采用的典型方法,这些数据可能因项目而异。

img/478423_1_En_1_Fig3_HTML.jpg

图 1-3

示例关系数据库摘录

这种方法没有错,但是为了理解 JSON 数据类型给我们带来的优势,让我们看一组典型的 SQL 语句来创建示例并插入一些数据。清单 1-2 显示了用于创建表格的示例 SQL 语句。我包含了切换到 SQL 模式并连接到服务器的特定于 shell 的命令。

DROP DATABASE IF EXISTS my dB;
CREATE DATABASE mydb;
CREATE TABLE mydb.customers (id int auto_increment NOT NULL PRIMARY KEY, first_name char(30), last_name char(30));
CREATE TABLE mydb.addresses (id int NOT NULL, caption char(20) NOT NULL, street1 char(100), street2 char(100), city char(50), state_code char(2), zip char(10), PRIMARY KEY(id, caption));
INSERT INTO mydb.customers VALUES (NULL, 'Sam', 'Blastone');
SELECT LAST_INSERT_ID() INTO @last_id;
INSERT INTO mydb.addresses VALUES (@last_id, 'HOME', '9001 Oak Row Road', Null, 'LaPlata', 'MD', '33532');
INSERT INTO mydb.addresses VALUES (@last_id, 'WORK', '123 Main Street', Null, 'White Plains', 'MD', '33560');
SELECT first_name, last_name, addresses.∗ FROM mydb.customers JOIN mydb.addresses ON customers.id = addresses.id \G

Listing 1-2Sample Relational Database SQL (no JSON)

到目前为止,添加的地址有些正常。但是考虑到我们想要添加另一个地址但是这个地址不完整的可能性。例如,我们只知道这个客户的仓库位置所在的城市和州,我们了解到客户在那里花费了一些时间。我们希望存储这些信息,这样我们就可以知道客户在该地区的存在,但我们可能不知道任何更多的细节。如果我们继续使用这个关系示例,我们将添加几个空字段(这是可以的)。当我们在插入不完整的地址后运行SELECT查询时,我们得到了这个信息。

> SELECT first_name, last_name, addresses.∗ FROM mydb.customers JOIN mydb.addresses ON customers.id = addresses.id \G
...
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 2\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
first_name: Sam
 last_name: Blastone
        id: 1
   caption: WAREHOUSE
   street1: NULL
   street2: NULL
      city: Carson Creek
state_code: CO
       zip: NULL

我们最终在一个表(customers)中得到一行,在另一个表(addresses)中得到三行,但是有几个空字段。现在,让我们看看这个例子,只是这次我们将使用 JSON 数据类型来存储地址。

在下一个示例中,我们用 customers 表中分配了 JSON 数据类型的一个列替换了第二个表(address detail 表)。除了明显地删除第二个表和查询数据必须遍历的关系之外,我们还获得了只存储我们需要的内容的能力。清单 1-3 显示了构建这个版本的修改后的 SQL 语句。

DROP DATABASE IF EXISTS mydb_json;
CREATE DATABASE mydb_json;
CREATE TABLE mydb_json.customers (id int auto_increment NOT NULL PRIMARY KEY, first_name char(30), last_name char(30), addresses JSON);
INSERT INTO mydb_json.customers VALUES (NULL, 'Sam', 'Blastone', '{"addresses":[
    {"caption":"HOME","street1":"9001 Oak Row Road","city":"LaPlata","state_code":"MD","zip":"33532"},
    {"caption":"WORK","street1":"123 Main Street","city":"White Plains","state_code":"MD","zip":"33560"},
    {"caption":"WAREHOUSE","city":"Carson Creek","state_code":"CO"}
]}');
SELECT first_name, last_name, JSON_PRETTY(addresses) FROM mydb_json.customers \G

Listing 1-3Sample Relational Database SQL (JSON)

在这里,我们看到定义表的 SQL 缩短了很多。要使用 JSON 数据类型,我们只需指定JSON,在这里我们可以使用任何其他数据类型。然而,输入带有 JSON 值的数据需要更多的输入,但是正如您所看到的,它允许我们使用表达式,用一种容易理解的语言描述代码。查询这些数据会将 JSON 字符串作为单个字符串返回,但是我们可以使用 MySQL JSON 函数之一来帮助提高输出的可读性。为此,我们使用清单 1-4 中所示的JSON_PRETTY()函数,它在从服务器返回的字符串中放置换行符和空格。

> SELECT first_name, last_name, JSON_PRETTY(addresses) FROM mydb_json.customers \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
            first_name: Sam
             last_name: Blastone
JSON_PRETTY(addresses): {
  "addresses": [
    {
      "zip": "33532",
      "city": "LaPlata",
      "caption": "HOME",
      "street1": "9001 Oak Row Road",
      "state_code": "MD"
    },
    {
      "zip": "33560",
      "city": "White Plains",
      "caption": "WORK",
      "street1": "123 Main Street",
      "state_code": "MD"
    },
    {
      "city": "Carson Creek",
      "caption": "WAREHOUSE",
      "state_code": "CO"
    }
  ]
}
1 row in set (0.0036 sec)

Listing 1-4Querying Rows with JSON Data

注意,我们现在只有一个表,地址已经“折叠”到 JSON 列中,每一行都存储地址数组。并且,只存储已知的数据。因此,对于仓库地址,我们只存储城市和州。虽然在 shell 输出中不容易阅读(稍后我们将看到一些提高输出格式可读性的方法),但我们仍然可以很容易地看到数据。而且,在我们的应用中使用时,接收 JSON 将比检查每一列的数据容易得多。我们将在第六章中了解更多。

正如我们所发现的,JSON 数据类型使得我们的数据存储更加灵活。正如我们将在第六章中发现的那样,我们可以通过 X 插件、X 协议和 X DevAPI,借助内置于 MySQL 和 MySQL Shell 中的支持,使用文档存储将我们所有的数据存储为 JSON 文档,从而进一步发展这个概念。事实上,现在让我们通过研究新的 X 插件和 X 协议来发现是什么让 shell 变得强大。

X 插件、X 协议和 X DevAPI

MySQL 引入了新的协议和 API 来处理 JSON 文档。除了支持 JSON 数据类型,我们还有三种以简单名称“X”为前缀的技术:X 插件、X 协议和 X DevAPI。X 插件是一个启用 X 协议的插件。X 协议被设计成使用 X DevAPI 与服务器通信。X DevAPI 是一个应用编程接口,它允许您为 MySQL 开发 NoSQL 解决方案,并将 MySQL 用作文档存储。我们将在后面的小节中了解更多关于文档存储的内容。

您可能想知道 shell 和插件是如何与服务器交互的。图 1-4 显示了组件是如何“堆叠”的。

img/478423_1_En_1_Fig4_HTML.jpg

图 1-4

x 协议栈

注意,我们有一个允许使用 X DevAPI 的 shell,它通过 X 插件与服务器进行通信。因此,X 插件是一种使能技术,真正的力量是 X 协议和 X DevAPI。

现在我们已经了解了将 MySQL 用作文档存储的技术,让我们看看 InnoDB 存储引擎在最近的版本中有什么变化。

InnoDB 改进

自 MySQL 5.6 以来,InnoDB 一直是 MySQL 的旗舰存储引擎(也是默认引擎)。Oracle 已经慢慢脱离了多存储引擎模型,专注于现代数据库服务器应该做的事情—支持事务存储机制。InnoDB 是满足这一需求的答案。

什么是存储引擎?

存储引擎是一种以各种方式存储数据的机制。例如,有一种存储引擎允许您与逗号分隔值(文本)文件(CSV)进行交互,另一种为写日志文件(归档)进行了优化,一种只在内存中存储数据(内存),甚至还有一种根本不存储任何东西(黑洞)。您可以通过使用 ENGINE = table 选项将它们用于您的表。除了 InnoDB,MySQL 服务器还附带了 Archive、Blackhole、CSV、Memory 和 MyISAM 存储引擎。InnoDB 存储引擎是唯一支持事务的引擎。有关其他存储引擎的更多信息,包括每个引擎的功能及其使用方法,请参见在线参考手册中的“备用存储引擎”一节。

在早期,InnoDB 是一家独立的公司,因此它是一款独立的产品,既不是 MySQL 的一部分,也不属于 MySQL AB(MySQL 的最初所有者,现在完全归 Oracle 所有)。最终,Oracle 同时拥有了 InnoDB 和 MySQL,因此将两者结合起来是有意义的,因为它们具有相互包容的目标。虽然仍然存在一个独立的 InnoDB 工程团队,但他们已经与核心服务器开发团队完全集成。

这种紧密的集成带来了 InnoDB 的许多改进,包括许多性能增强,甚至支持微调等等。这一点在 InnoDB 不断改进的过程中显而易见,尤其是 MySQL 8 中的 InnoDB。

虽然大多数改进都相当微妙,从某种意义上说,你不会注意到它们(除了通过更好的性能和可靠性,这是不可轻视的),但大多数都显示出致力于使 InnoDB 成为最佳的事务存储机制,并通过扩展 MySQL 成为强大的事务数据库系统。

最显著的改进包括性能和稳定性。同样,对于较小的数据库,您可能看不到太多的差异,但是较大的数据库和企业级系统将会看到显著的改进。例如,崩溃恢复和日志记录得到了相当大的改进,使得恢复更快,正常关机和启动也更快。

类似地,死锁检测、临时表、自动增量甚至 Memcached 支持方面的改进表明了 Oracle 想尽一切办法纠正缺陷和改进 InnoDB 的愿望。虽然这个列表似乎集中在小的改进上,但是其中一些对于寻求帮助调优和规划数据库服务器安装的系统管理员来说非常重要。

小费

如果您想了解更多关于这些改进的信息或查看所有最新变化的列表,请参见在线 MySQL 8 参考手册( http://downloads.mysql.com/docs/refman-8.0-en.pdf )。

下一节将描述 MySQL 8 新增的和独有的特性。

新功能

除了那些在 5.7 服务器版本中开发的特性之外,还有一些 MySQL 8 独有的特性。它们目前还没有(甚至不可能被合并到)旧版本中。这部分是因为服务器代码库为了适应新特性而做了大量的修改。MySQL 8.0 中的新特性包括:

  • 数据字典:系统中所有对象的事务性元数据存储机制

  • 账户管理:用户、密码、权限管理的重大改进

  • 删除的选项、变量和特性:希望从旧版本升级的用户应该查看新版本中有所变化的较小细节,例如删除的选项、变量和特性

除了那些在 5.7 服务器版本中开发的特性之外,还有一些 MySQL 8 独有的特性。事实上,它们目前还没有(甚至不可能被合并到)旧版本中。这部分是因为服务器代码库为了适应新特性而做了大量的修改。MySQL 8.0 中的新特性包括新的数据字典和新的账户管理系统。

数据字典

如果您曾经使用 MySQL 试图获取数据库中包含的对象的信息,或者发现那里有什么对象,搜索具有特定名称前缀的对象,或者试图发现存在什么索引,那么您很可能必须访问INFORMATION_SCHEMA中的表和视图,或者必须导航的特殊的mysql数据库。也许最糟糕的是一些表格的定义被存储在一个特殊的结构化文件中,叫做.frm 3 (表单)文件。例如,database1中一个名为table1的表格有一个名为/data/database1/table1.frm.frm文件。

注意

INFORMATION_SCHEMAmysql数据库仍然可见,这些视图和表格中的信息仍然可以在数据字典之前以类似的方式使用,但是缺少数据字典中的附加信息。

这种组合要求数据库管理员通过了解数据所在的位置来学习如何查找数据。根据您要查找的内容,您可能需要查询某个数据库,或者在可怕的情况下,破译.frm文件。更重要的是,由于数据是在非事务性的表(和元数据文件)中,所以这些机制不是事务性的,并且,通过扩展,不是崩溃安全的。

新的数据字典改变了这一切,使我们可以创建一个单一的事务性(与 InnoDB 相同的 ACID 支持)存储库来存储系统中对象的所有元数据。所有基于文件的元数据已经被移动到数据字典中,包括.frm文件、分区、触发器和其他选项文件(例如.par.trn.trg.isl.opt)。

但是,您不会在数据库列表中看到数据字典(例如,SHOW DATABASES)。数据字典表是不可见的,不能直接访问。您不会很容易找到数据字典表(尽管如果您足够努力的话是有可能的)。

这样做主要是为了使数据字典崩溃安全,并且您不必进行管理。幸运的是,您可以通过INFORMATION_SCHEMA数据库甚至是SHOW命令来访问存储在数据字典中的信息。mysql 数据库仍然存在,但它主要包含额外的信息,如时区、帮助和类似的非重要信息。事实上,INFORMATION_SCHEMASHOW命令使用数据字典来表示信息。

那么,看不到数据字典怎么用呢?简单地说,INFORMATION_SCHEMA视图从数据字典中获取信息。因此,您可以继续使用您习惯使用的相同查询,但是在这种情况下,数据更加可靠和具有事务性。酷!

有关数据字典的更多信息,包括存储了什么以及它如何与INFORMATION_SCHEMA视图交互的细节,请参见在线 MySQL 参考手册( https://dev.mysql.com/doc/refman/8.0/en/ )中的“MySQL 数据字典”一节。

添加数据字典最终使得许多人一段时间以来一直想实现的几个特性成为可能。最新的一项是对账户管理的重大改革。

账户管理

MySQL 数据库管理员(尤其是那些使用企业级系统的管理员)的另一个棘手问题是需要为一组用户分配相同的权限并管理密码。MySQL 8 在帐户管理和 MySQL 中的特权系统方面提供了许多改进。下面列出了最重要的改进。

  • 角色:管理员可以将授权语句分配给一个角色,该角色可以分配给多个用户。

  • 用户帐户限制:管理员可以设置资源限制,帮助限制对关键数据的访问。

  • 密码管理:管理员可以设置密码形成和失效的条件。

  • 用户账户锁定:管理员可以暂时锁定用户账户,禁止其访问数据。

注意

MySQL 8 禁止使用GRANT语句创建用户帐户。您必须首先使用CREATE USER语句显式创建用户。

角色

一个非常常见的场景是必须创建一组具有相同权限的用户。过去,您必须保存或存档GRANT语句,并为每个用户重复这些语句。也就是说,您可以为两个或更多用户重用GRANT语句。幸运的是,随着数据字典的出现,MySQL 中的支持角色在 MySQL 8 中已经成为现实!

可以创建、删除角色,授予或撤销权限。我们还可以向用户授予角色或从用户处撤销角色。角色最终使得管理 MySQL 上的用户账户变得更加简单。

用户帐户限制

企业系统的另一个管理问题包括需要在特定时间段内进一步限制对用户帐户的访问,或者甚至限制帐户发出特定数量的语句。

在 MySQL 8 中,管理员可以为用户帐户设置每小时查询数、每小时事务数、每小时连接数,甚至每小时同时连接数的限制。这允许管理员为安全目标、工作效率限制等设置限制。

密码管理

帐户管理功能中最受欢迎的变化之一是设置密码限制和标准的能力。在当今充满挑战的安全环境中,我们必须确保我们的密码不会被轻易破解,为此,我们需要控制密码的长度以及包含多少小写或大写字符或特殊字符。

幸运的是,MySQL 8 有这些功能,你可以设置密码有效期,旧密码限制的重用,密码的验证,当然还有密码强度。这些功能和前面提到的两个功能帮助推动 MySQL 8 在更好的安全性方面向前迈出了一大步。

用户帐户锁定

有时,您必须暂时限制对一个或多个用户帐户的访问。这可能是由于维护计划、诊断,甚至是员工的临时休假。不管是什么原因,MySQL 在过去要么需要更改密码(并且不告诉用户——但这并不阻止帐户被使用),要么删除帐户并在以后重新创建它。如果您的用户帐户被授予了复杂的权限(或几个角色),这是有问题的。

MySQL 8 包含一个特性,支持使用ACCOUNT LOCK子句锁定帐户和使用ACCOUNT UNLOCK子句解锁帐户来锁定和解锁用户帐户。您可以在CREATE USER语句或ALTER USER语句中使用这些子句。

帐户管理功能还有许多小的改进。要了解更多有关变更的信息,请参阅在线参考手册中的“用户帐户管理”一节( https://dev.mysql.com/doc/refman/8.0/en/ )。

删除了选项、变量和功能

如果您阅读了 MySQL 8 的发行说明,您可能会注意到 MySQL 8 对启动选项、变量等进行了许多小的更改。可以在 https://dev.mysql.com/doc/relnotes/mysql/8.0/en/ 找到 MySQL 8 每个版本所有变更的完整列表。

幸运的是,大多数变化都与支持最新功能和删除旧的和过时的设置有关。所以,大多数不会对那些想开始使用 MySQL 的人有太大影响。但是,从旧版本升级时,有几个问题可能不太重要。

请记住,在 MySQL 5.7(及更早版本)中,许多选项、变量和特性都被标记为不推荐使用。它们现在在 MySQL 8 中被正式移除。如果您使用 MySQL 已经有一段时间了,那么您很可能已经为这些变化做好了准备。

然而,有一些变化可能会影响一些想升级到 MySQL 8 的人。其中包括以下内容。

  • 删除了--bootstrap选项。它用于控制服务器如何启动,通常用于创建 MySQL 特权表,而无需启动完整的 MySQL 服务器。

  • --innodb_file_format_∗选项已更改。这些用于配置 InnoDB 存储引擎的文件格式。

  • 删除了--partition--skip分区选项。它们用于控制 MySQL 服务器中用户定义的分区支持。

  • 作为数据字典功能的一部分,.frm和相关元数据文件被删除。

  • 一些 SSL 选项已经改变,并且引入了新的认证插件(caching_sha2_password)来提高安全连接。

  • 在最新版本中,许多错误代码都进行了更改,包括删除了几十个鲜为人知的错误代码。如果您的应用使用 MySQL 服务器错误代码,您应该检查文档以确保错误代码没有被更改或删除。

像这样的变化是主要版本的典型特征。在任何情况下,您都应该在计划任何升级时考虑发行说明。像这样的更改可能会导致问题的地方是在您的定制和配置中。例如,如果您已经定义了使用选项和变量或与之交互的调优过程、存储过程、DevOps 或其他机制,您应该仔细检查 MySQL 8 文档中的条目,以确保您可以相应地修改您的工具。

小费

参见 http://dev.mysql.com/doc/refman/8.0/en/added-deprecated-removed.html 获取 MySQL 8 中要删除的特性的完整列表。

范式转变特征

MySQL 8 中的一些特性对于 MySQL 生态系统来说确实是开创性的。事实上,它们可能会改变人们使用 MySQL 的方式,并扩展 MySQL 日益增长的用例列表。这些包括以下范式转变特征。

  • 文档存储:一种新的结构化存储机制,它将改变您可以存储的内容,以及您与 MySQL 交互的方式,从而为数据可能发生变化的应用存储数据,使您的应用无需重新构建存储层即可适应变化

  • 组复制:一种新的、强大的自我修复高可用性选项

  • InnoDB Cluster :一种新的管理高可用性的方法,建立在组复制的基础上,结合了新的 shell 和 MySQL 路由器,实现了易于设置和维护的高可用性安装

文档存储

在讨论 JSON 数据类型时,我们已经了解了一些关于文档存储的知识。MySQL 文档存储将 JSON 存储概念带到了一个新的高度。虽然 JSON 数据类型允许在我们的关系数据库中引入非结构化数据,但是文档存储是真正的 NoSQL 数据存储。

更具体地说,文档存储允许在 MySQL 中以 JSON 文档的形式存储非结构化数据。也就是说,MySQL 现在支持 SQL 和 NoSQL 选项。NoSQL 选项使用我们之前发现的 X 技术,包括 X 协议和 X DevAPI。这些允许您编写直接与 MySQL 接口的应用,而不使用任何 SQL 或关系结构。多酷啊。

我知道 SQL,但是 NoSQL 是什么?

如果您曾经使用过关系数据库系统,那么您无疑非常熟悉结构化查询语言(SQL ),在 SQL 中,我们使用特殊的语句(命令)来与数据进行交互。事实上,大多数数据库系统都有自己的 SQL 版本,包括操作数据的命令(DML)以及定义存储数据的对象的命令(DDL ),甚至包括管理服务器的管理命令。

也就是说,您获得结果集,并且必须使用命令来搜索数据,然后将结果转换为内部编程结构,使数据看起来像是一个辅助组件,而不是解决方案的一个组成部分。NoSQL 接口打破了这种模式,它允许您使用 API 来处理数据。更具体地说,您使用编程接口,而不是基于命令的接口。

遗憾的是,NoSQL 可能意味着几件事,这取决于你的观点,包括“非 SQL”,“不仅仅是 SQL”,或者“非关系”。但是它们都是指这样一个事实,即你所使用的机制并没有使用基于命令的接口,而且这个术语的大多数用法都表明你使用的是编程接口。对于 MySQL 8,可以通过 SQL 或 NoSQL 使用 X 协议访问 JSON 文档,通过 X 插件访问 X DevAPI。

MySQL 文档存储库的起源在于几种技术,它们被结合在一起形成了文档存储库。具体来说,Oracle 将键、值机制与新的数据类型、新的编程库和新的访问机制结合起来,创建了现在的文档存储。这不仅允许我们使用带有 NoSQL 接口的 MySQL,还允许我们构建混合解决方案,利用关系数据的稳定性和结构,同时增加 JSON 文档的灵活性。

我们将在第六章中了解更多关于文档存储的信息。

组复制

如果您使用过 MySQL 复制,那么您无疑非常熟悉如何在构建高可用性解决方案时利用它。事实上,您很可能已经发现了许多使用 MySQL 复制来提高应用可用性的方法。

此外,显而易见的是,您的高可用性需求越多,您的解决方案越扩展(越复杂),您就越需要采用更好的方法来管理节点丢失、数据完整性和集群的一般维护(复制数据的服务器组,有时称为副本集)。事实上,大多数高可用性解决方案已经超越了基本的主从拓扑结构,演变成由服务器集群组成的层,一些服务器复制一部分数据以获得更快的吞吐量,甚至用于分区存储。所有这些导致许多人发现他们需要更多的 MySQL 复制。Oracle 通过组复制满足了这些需求以及更多需求。

通过组复制,您可以建立一组要在组中使用的服务器,这不仅可以减少服务器之间的事务,还可以实现自动故障转移和容错。此外,组复制还可以与 MySQL 路由器一起使用,以允许您的应用拥有一个与集群隔离的层。当我们研究 InnoDB 集群时,我们将会对路由器有所了解。

组复制和标准复制的一个重要区别是,组中的所有服务器都可以参与更新数据,并自动解决冲突。是的,您不再需要精心设计您的应用来发送写入(更新)到特定的服务器!但是,您可以将组复制配置为只允许一台服务器(称为主服务器)进行更新,其他服务器充当辅助服务器或备份服务器(用于故障转移)。

我们将在第八章中了解更多关于组复制的信息。

InnoDB 集群

另一个新出现的特性叫做 InnoDB 集群。它旨在使高可用性更易于设置、使用和维护。InnoDB Cluster 通过 MySQL Shell 和 AdminAPI、组复制和 MySQL 路由器与 X AdminAPI 协同工作,将高可用性和读取可伸缩性提升到一个新的水平。也就是说,它将 InnoDB 中用于克隆数据的新功能与组复制、MySQL Shell 和 MySQL 路由器相结合,提供了一种设置和管理高可用性的新方法。

注意

AdminAPI 是一个特殊的 API,可通过 MySQL Shell 进行配置并与 InnoDB 集群交互。因此,AdminAPI 具有旨在简化 InnoDB 集群工作的特性。

在此使用案例中,群集设置有一个主节点(在标准复制术语中称为主节点),它是所有写入(更新)的目标。多个辅助服务器(从属服务器)维护数据的副本,这些副本可以被读取,因此能够在不给主服务器增加负担的情况下读取数据,从而实现读取可伸缩性(但是所有服务器都参与协商和协调)。组复制的引入意味着集群是容错的,并且组成员是自动管理的。MySQL 路由器缓存 InnoDB 集群的元数据,并执行到 MySQL 服务器实例的高可用性路由,从而更容易编写应用来与集群进行交互。

您可能想知道这与使用标准复制的读出可伸缩性设置有何不同。从高层次来看,这些解决方案似乎正在解决同一个用例。但是,使用 InnoDB Cluster,您可以从 MySQL shell 创建、部署和配置集群中的服务器,从而提供一个易于管理的完整的高可用性解决方案。也就是说,您可以通过 shell 使用 InnoDB Cluster AdminAPI,使用 JavaScript 或 Python 以编程方式创建和管理 InnoDB 集群。

我们将在第十章中了解更多关于 InnoDB 集群的信息。

摘要

MySQL 8 有很多新特性。在许多方面,它代表了包括高可用性和 NoSQL 在内的几个领域的重大飞跃。然而,一个宣传较少但非常重要的新特性是新的 MySQL Shell。正如您将在接下来的章节中看到的,新的 Shell 是使所有新特性以无缝方式一起工作的粘合剂。

例如,如果没有 shell,使用 MySQL 文档存储将需要使用第三方编程环境来与 X DevAPI 交互。shell 使得使用新的 API 变得更加容易,因为它不仅提供了一个熟悉的环境(想想旧的客户机),而且对用户更加友好。

类似地,如果没有实现 AdminAPI 的 shell,使用 InnoDB Cluster 不会比 MySQL 复制所需的手动管理更好。虽然您仍然可以手动配置 MySQL 中的任何高可用性特性,但现在我们有了 shell 来使这一切变得更加容易,除非在某些特定的情况下,否则没有必要这样做。

然而,要真正理解 shell 对 MySQL 8 的重要性和重大贡献,我们必须看到它在每个新特性中的表现。本书的其余部分将介绍使用 MySQL 8 中每个主要特性的简短教程,以及如何使用 shell 实现该特性的示例。其中包括以下内容:

  • 对 SQL 数据库使用 shell

  • 将 Shell 与文档存储一起使用

  • 将 shell 用于组复制

  • 将 shell 与 InnoDB 集群一起使用

但是首先,我们将在下一章看到如何安装 MySQL Shell。

二、安装 MySQL Shell

虽然 MySQL 服务器仍然包含旧的 MySQL 客户端(mysql),但是新的 MySQL Shell 应该被视为与 MySQL 服务器交互的默认客户端。它比之前与服务器捆绑的客户端有很多优势;最强大的是直接从 shell 中使用 Python 或 JavaScript 的能力。那么,我们如何获得新的 MySQL Shell 呢?

在这一章中,我们将发现如何为三个最流行的平台下载和安装 MySQL ShellWindows、macOS 和 Linux。对于 Windows,我们将使用一个特殊的一体化安装程序,使安装任何 MySQL 产品变得容易。对于 macOS,我们将看到如何使用 macOS 友好的安装程序下载和安装 shell。对于 Linux,我们将了解如何使用 Oracle 的高级打包工具(APT)库来简化在 Linux 上添加 MySQL 产品。

让我们从下载 MySQL Shell 并检查其先决条件开始。

准备安装 MySQL Shell

MySQL Shell 可以安装在 Oracle 支持 MySQL Server 的任何平台上。在大多数平台上,shell 包含在单独的安装中。一个例外是 Windows 平台,其中的 shell 包含在 MySQL Windows Installer 中。在这一节中,我们将看到为各种平台下载 shell 的快速概述。

先决条件

如果您没有安装 MySQL Server 8.0,您可能希望在完成本教程之前在您的系统上安装它。虽然您可以在旧版本的 MySQL 上使用 shell,但是您需要最新版本的服务器才能使用所有功能。

除了在一个系统(或者用于实验或开发目的的台式机或笔记本电脑)上安装 MySQL Server 8.0 之外,shell 还要求您安装以下软件。

注意

对于本书中的示例,您只需要安装 Python 和 Connector/Python。检查您的系统,看看您是否安装了其中的一个。注意,在 Windows 上,可以将 Connector/Python 与 shell 一起安装。

现在让我们看看如何从 Oracle 网站下载 MySQL Shell。

如何获得 MySQL Shell

像大多数 MySQL 产品一样,MySQL Shell 有社区版和企业版。社区版是开源的,因此可以免费下载和使用。如果您是 Oracle 企业客户,您可以通过首选的客户渠道获得 MySQL 的安装程序。但是,如果您愿意,也可以免费下载社区版。在本节中,我们将了解如何下载社区版。

要下载任何 MySQL 产品的社区版,请访问甲骨文的 MySQL 下载页面 https://dev.mysql.com/downloads/ 。在这里,您将看到所有产品的列表。

单击您想要下载的产品( https://dev.mysql.com/downloads/shell/ ),网站将为您提供适用于您的操作系统的可供下载的文件。也就是说,网站会预先选择运行浏览器的操作系统。例如,如果你正在运行 macOS 并点击 MySQL Shell,你会看到如图 2-1 所示的文件列表。如果愿意,您可以单击选择操作系统下拉控件并选择不同的操作系统。

img/478423_1_En_2_Fig1_HTML.jpg

图 2-1

下载 MySQL Shell (macOS)

如果您使用的是 Windows,您可以选择不同的选项。对于 Windows,Oracle 提供了一个全面的指导安装程序,称为 MySQL Installer for Windows。如果您访问任何 MySQL 产品的下载网站,您将在文件列表的顶部看到 MySQL 安装程序的条目。如果您愿意,您仍然可以下载单独的安装程序,但是推荐的机制是使用 MySQL 安装程序。

要下载 MySQL 安装程序( https://dev.mysql.com/downloads/installer/ ,点击所示链接。这将把你带到如图 2-2 所示的另一个页面,该页面显示了可供下载的文件。选择与您的系统相匹配的链接(32 位或 64 位)。

img/478423_1_En_2_Fig2_HTML.jpg

图 2-2

下载 Windows 版 MySQL 安装程序

如果您想继续观看安装程序的运行,请确保为您的 Windows 系统选择正确的下载选项并立即下载。

Windows 版 MySQL 安装程序有两个版本;一个包含通常安装的产品,另一个 web 版本只下载您想要安装的产品。如果你只想安装几个产品,网页版可能是更好的选择。另一方面,如果您打算安装服务器和 shell 以及文档和连接器,您应该下载完整版本。

现在让我们看看如何在 Windows、macOS 和 Linux (Ubuntu)上安装 shell。您可以阅读与您选择的平台相匹配的部分。

使用 MySQL 安装程序在 Windows 上安装

用 MySQL Installer for Windows 安装 MySQL Shell,也就是 MySQL Installer 或简称 Installer,遵循了在 Windows 上安装应用的类似模式。MySQL 安装程序包括所有的 MySQL 产品,允许您安装您想要的组件。虽然 MySQL 安装程序是 Windows 的推荐安装选项,但是您可以下载 shell 并单独安装。但是最好使用 Windows installer。

在本演示中,我们还将安装 MySQL 服务器和使用本书中示例的 shell 的最小组件。您可能希望在您的系统上安装 MySQL。如果您已经安装了 MySQL Server,您可以跳过安装服务器的部分。

当您第一次启动安装程序时(随后的启动将显示添加、修改、升级对话框),您将看到一个显示许可证的欢迎对话框。您必须接受许可证才能继续。图 2-3 显示了安装人员的欢迎面板。

img/478423_1_En_2_Fig3_HTML.jpg

图 2-3

安装人员欢迎面板-许可协议

要接受许可条款,勾选复选框,然后单击下一个的继续。这将使您转到安装类型对话框,您可以在其中选择几个选项之一,包括安装典型开发人员安装的预设、仅安装服务器、仅安装客户端或自定义安装。如果要安装多个产品或删除一些产品,应该使用自定义安装类型。图 2-4 显示了选择设置类型对话框。

img/478423_1_En_2_Fig4_HTML.jpg

图 2-4

选择安装类型

由于我们将安装 MySQL 服务器以及 MySQL Shell 和相关组件,您应该勾选自定义安装类型并点击下一步。这将显示“选择产品和功能”对话框。在这里,我们将选择要安装的组件。对于本教程,我们必须安装以下组件。

  • MySQL 服务器

  • MySQL Shell

  • MySQL 路由器

  • 连接器

  • 连接器/Python

  • MySQL 文档

  • 样本和示例

  • (可选)MySQL 工作台

我们使用自定义选项,因为其他选项将包括我们可能不需要的附加组件(但安装它们不会有什么坏处)。

图 2-5 显示了选择产品和功能对话框。该对话框显示两列,其中左边的一列包含安装程序中的所有产品,右边的一列是选择要安装的产品。

img/478423_1_En_2_Fig5_HTML.jpg

图 2-5

选择产品和功能(默认选择)

要从安装中删除组件,请选择右侧的产品,然后单击向左箭头图标。这将删除产品。要添加产品,只需导航左侧的树找到组件(例如,MySQL Shell),选择它,然后单击绿色箭头将其添加到右侧的列中。图 2-6 显示了正确的选择。

小费

要删除所有组件,请单击双左箭头。同样,要添加所有组件,请单击双右箭头。

img/478423_1_En_2_Fig6_HTML.jpg

图 2-6

选择产品和功能(选定的组件)

当上面列出的所有组件都添加到右边的栏中后,点击下一个按钮继续。这将显示安装对话框面板,其中列出了要安装的组件以及是否必须下载该组件。这是安装程序的一个常见误解。虽然它涵盖了所有组件,但在您下载安装程序时,它可能不包括所有组件。幸运的是,这意味着我们可以只下载那些我们想安装的组件,仅此而已。图 2-7 显示安装对话框。

花点时间检查列表,以确保您已经将所有要安装的组件放在队列中。如果需要进行更改,可以点击返回按钮返回上一个对话框并选择缺少的组件。

img/478423_1_En_2_Fig7_HTML.jpg

图 2-7

安装(暂存)

当您准备好继续时,单击执行按钮。这不会显示一个新的对话框,相反,您会看到每个组件的状态随着下载和安装而变化。图 2-8 显示了正在进行组件安装的对话框的典型示例。

img/478423_1_En_2_Fig8_HTML.jpg

图 2-8

安装(进行中)

一旦所有组件安装完毕,安装对话框面板将显示所有安装完成的状态,并将底部的按钮改为显示下一个,如图 2-9 所示。准备就绪后,一旦所有产品安装完毕,点击下一步

img/478423_1_En_2_Fig9_HTML.jpg

图 2-9

安装(安装完成)

点击下一个的,出现如图 2-10 所示的产品配置对话框。此时,在配置完每个组件后,安装程序将返回到此对话框。

img/478423_1_En_2_Fig10_HTML.jpg

图 2-10

产品配置

请注意,我们有三个组件需要配置;服务器、路由器和示例。安装程序将按顺序执行这些操作。只需点击下一步即可开始配置服务器。图 2-11 显示了配置服务器的第一步——设置组复制。

选项包括正常安装组复制(通过选择独立选项)或在沙盒中安装组复制。如果您想在系统上测试组复制,并且不想安装额外的服务器,沙盒选项可能会有所帮助。除了这种情况,您应该始终选择独立选项。

img/478423_1_En_2_Fig11_HTML.jpg

图 2-11

组复制

勾选独立选项,然后点击下一个的。这将显示如图 2-12 所示的类型和联网对话框。

img/478423_1_En_2_Fig12_HTML.jpg

图 2-12

类型和网络

在此对话框中,您可以使用一个预配置的选项或用法集,如开发(例如,开发机器)、带应用的服务器或专用服务器(仅安装了 MySQL)。对于笔记本电脑或台式机上的大多数安装,您应该选择开发计算机选项。

如果您想要使用默认的监听端口(3306 和 33060)、命名管道或共享内存,请选择开发计算机选项。如果没有勾选 Windows 防火墙选项,您需要勾选以确保 MySQL 正确运行,并且您可以在 Windows 上连接到它。当您对所有设置满意后,点击下一步。这将显示“验证方法”对话框。

“身份验证方法”对话框允许您选择最新选项,包括强密码加密(强烈推荐)或使用旧的身份验证方法(不推荐用于生产)。

注意

如果您选择强密码加密方法,并希望使用 MySQL 客户端的旧版本(而不是 shell),您可能会遇到连接错误。您必须使用带有强密码加密的新客户端。

图 2-13 显示了认证方法对话框。请注意,有大量的文本描述每个选项,默认情况下使用强密码加密。

img/478423_1_En_2_Fig13_HTML.jpg

图 2-13

认证方法

您应该勾选强密码加密选项,然后单击下一步。这将显示“帐户和角色”对话框。此对话框用于设置 root 密码,并可以选择创建您想要使用的任何角色。如果您计划设置一个服务器供应用和许多用户使用,这可能会很有帮助。但是,在本演示中,我们只需要选择一个 root 密码。

由于我们使用强密码和加密选项,我们在对话框中输入的密码将根据最佳实践(强密码)进行评估。然而,在我们配置服务器使用密码验证插件之前,我们可以使用任何我们想要的密码。无论你是否安装了插件,最好使用强密码。

小费

参见 https://dev.mysql.com/doc/refman/8.0/en/password-management.html 来设置密码选项和验证。

图 2-14 显示了账户和角色对话框。继续键入您选择的 root 用户密码,然后在重复对话框中再次键入。请注意密码强度显示。在这种情况下,我的 10 个字符的密码只有中等强度,尽管使用了特殊字符,没有字典中的单词。苛刻,是吧?

img/478423_1_En_2_Fig14_HTML.jpg

图 2-14

客户和角色

输入 root 密码后,点击下一步的。这将显示 Windows 服务对话框,我们可以在其中选择在 Windows 上启动服务器并将其作为服务运行。图 2-15 显示了窗口服务对话框。

img/478423_1_En_2_Fig15_HTML.jpg

图 2-15

Windows 服务

这里建议使用默认选择,包括将 MySQL Server 配置为名为 MySQL80 的 Windows 服务,在启动时启动服务器,使用系统用户启动服务器。但是,如果您想更改这些,并且您熟悉如何在 Windows 上手动启动 MySQL,您可以进行这些更改。

准备好后,点击下一个按钮。这将显示“应用配置”对话框。在这个对话框中没有什么可选择的,所以准备好之后,只需点击执行按钮。这将启动配置过程,以绿色复选标记的形式提供反馈,每个步骤完成后都会出现。

所有步骤完成后,将显示“完成”对话框。图 2-16 显示了完成对话框。点击完成开始下一阶段的配置(MySQL 路由器)。这将返回到产品配置对话框(为简洁起见省略-参见图 2-10 )。

img/478423_1_En_2_Fig16_HTML.jpg

图 2-16

服务器配置完成

在产品配置对话框中,单击下一个的进入 MySQL 路由器配置对话框,该对话框允许您更改路由器的安装方式及其监听的端口。虽然我们没有讨论过路由器,但有些内容可能并不熟悉。幸运的是,缺省值是可以接受的,并且包括用于经典客户机协议和 X 协议客户机的端口。

注意

我们将在第八章和第十章中讨论 MySQL 路由器,包括如何设置和使用它。

图 2-17 显示了 MySQL 路由器配置对话框。我们可以使用默认值,所以我们只需要点击完成返回到产品配置对话框(为简洁起见省略–参见图 2-10 )。

img/478423_1_En_2_Fig17_HTML.jpg

图 2-17

MySQL 路由器

在产品配置对话框中,点击下一个的继续。下一个要配置的组件是示例组件。虽然看起来这里没有什么可做的(毕竟,它们只是使用 MySQL 的例子),但是这个对话框有一个非常有趣的特性。该对话框允许您通过连接到服务器来检查服务器安装。这不仅可以测试您的服务器,还可以确保您的安装正常工作。

当对话框出现时,键入您之前选择的 root 密码,然后单击 Check 按钮。这将检查您与服务器的连接(和密码)。

您可能还会注意到,该对话框显示了独立服务器选项和沙箱选项。如果你安装了沙箱选项,你也可以检查运行在沙箱中的 MySQL 服务器。在本演示中,我们仅选中了独立选项。

图 2-18 显示了连接到服务器对话框。

img/478423_1_En_2_Fig18_HTML.jpg

图 2-18

连接到服务器

检查完连接后,点击下一步继续。下一个对话框简单地应用了我们之前看到的配置,但是在本例中,安装程序配置了示例和范例的所有子组件。只需点击执行即可开始。当所有配置完成后,可以点击完成进入下一步。图 2-19 显示了样本和示例的完整配置对话框。

当您点击完成时,您将返回到产品配置对话框,该对话框将显示所有已配置的组件(为简洁起见,此处省略——参见图 2-10 )。点击下一步继续。

img/478423_1_En_2_Fig19_HTML.jpg

图 2-19

样品和示例已完成

所有产品配置完成后,您将看到如图 2-20 所示的安装完成对话框。单击完成退出安装程序。

img/478423_1_En_2_Fig20_HTML.jpg

图 2-20

安装完成

如果您一直在自己的机器上安装 MySQL,那么恭喜您!现在,您已经有了 MySQL Server 和 MySQL Shell 以及完成本书中的示例所需的其他组件。

在 macOS 上安装

在 macOS 上安装 MySQL 产品是通过从 MySQL 网站下载每个产品作为一个单独的包来完成的。例如,如果您想在 macOS 上安装 MySQL Server 和 MySQL Shell,您需要下载它们各自的安装程序。在本节中,我们将看到在 macOS 上安装服务器和 shell 的演练。

我们将先安装 MySQL 服务器,然后安装 MySQL Shell。回想一下,我们必须从 https://dev.mysql.com/downloads/ 为每个下载安装程序。例如,在选择社区服务器条目( https://dev.mysql.com/downloads/mysql/ )并在操作系统下拉列表中选择 macOS 条目后,我们将看到可用于在 macOS 上安装服务器的文件。图 2-21 显示了适用于 MySQL 8.0.16 的文件。

img/478423_1_En_2_Fig21_HTML.jpg

图 2-21

为 macOS 下载 MySQL 服务器

请注意,您将看到几个选项,包括带有引导式安装程序的可挂载磁盘映像(.dmg)以及磁带归档(.tar)。在 macOS 上,您应该使用磁盘映像选项。现在就去下载吧。

类似地,您可以返回到社区下载页面,单击 MySQL Shell 的条目,在操作系统下拉框中选择 macOS 之后,您将看到可用于下载 Shell 安装程序的文件。图 2-22 显示了 MySQL Shell 8.0.16 可用文件的示例。

img/478423_1_En_2_Fig22_HTML.jpg

图 2-22

为 macOS 下载 MySQL Shell

在这里,我们再次看到了一个可挂载的磁盘映像,带有一个引导式安装程序(.dmg)和一个磁带归档文件(.tar)。您应该下载磁盘映像。

现在,让我们看看如何安装服务器。

安装 MySQL 服务器

要安装 MySQL Server,请打开可挂载的磁盘镜像文件(如mysql-8.0.16-macos10.14-x86_64.dmg),然后打开安装程序(如mysql-8.0.16-macos10.14-x86_64.pkg)。这将开始安装。

您将看到的第一个对话框是欢迎对话框,它显示了文档链接列表和步骤的简要总结。对于精明的 macOS 爱好者来说,安装进度会非常熟悉。图 2-23 显示了欢迎对话框。

img/478423_1_En_2_Fig23_HTML.jpg

图 2-23

欢迎对话框

阅读欢迎文本并浏览链接后,点击继续继续。下一个对话框是许可证对话框,您可以选择阅读 GNU 通用公共许可证(GNU GPL)许可证(或者企业许可证,如果您选择下载企业版)。您也可以打印许可证或将许可证保存到文件中,以便以后阅读。图 2-24 显示许可对话框。

img/478423_1_En_2_Fig24_HTML.jpg

图 2-24

许可对话框

点击继续查看许可接受对话框。图 2-25 显示许可协议对话框。同样,您可以阅读许可,但您必须接受许可才能继续。要接受许可证,点击接受

img/478423_1_En_2_Fig25_HTML.jpg

图 2-25

接受许可对话框

接受许可后,您将进入安装类型对话框。请注意,目标选择对话框被跳过。图 2-26 显示安装类型对话框。

img/478423_1_En_2_Fig26_HTML.jpg

图 2-26

安装类型对话框

在此对话框中,您可以选择安装目标。对于大多数系统,您可以接受默认值。如果需要更改目的地,可以点击自定义按钮。要继续安装目的地,点击安装

下一个对话框是进度对话框,显示正在安装的文件的进度。您在这里不会看到太多,所以让我们看看下一个对话框,它开始了配置过程。关闭该对话框后,您将开始配置阶段。图 2-27 显示了配置 MySQL 服务器对话框。

img/478423_1_En_2_Fig27_HTML.jpg

图 2-27

配置 MySQL 服务器(密码)对话框

这里,我们必须选择使用强密码加密(强烈推荐)或使用传统身份验证方法(不推荐用于生产)。默认情况下选择强加密,因此我们只需单击下一个的即可进入下一个配置项。

注意

如果您选择强密码加密方法,并希望使用 MySQL 客户端的旧版本(而不是 shell),您可能会遇到连接错误。您必须使用带有强密码加密的新客户端。

下一个配置项是我们为 root 用户帐户选择密码,如图 2-28 所示。选择一个你能记住并且足够复杂的密码,没有人会轻易猜到。建议您使用由字母、数字和其他字符组合而成的至少八个字符的密码。

我们也可以选择在安装完成后启动 MySQL 服务器。建议在安装后启动服务器,以便您可以检查它是否正常工作。

img/478423_1_En_2_Fig28_HTML.jpg

图 2-28

配置 MySQL 服务器(根用户密码/启动服务器)对话框

输入 root 用户密码并决定是否要启动服务器后,单击 Finish 完成配置并转到 summary 对话框。图 2-29 显示了汇总对话框。

img/478423_1_En_2_Fig29_HTML.jpg

图 2-29

摘要对话框

要完成安装,点击关闭。您可能想要关闭可装载的磁盘映像(如果您不关闭它,它将在关机时关闭)。如果您愿意,现在可以连接到服务器,但是您必须使用旧的客户端。与其这样做,不如让我们安装 Shell。

安装 MySQL Shell

安装 MySQL Shell 与安装服务器非常相似。例外是没有配置步骤。

要安装 MySQL Shell,请打开可挂载的磁盘镜像文件(如mysql-shell-8.0.16-macos10.14-x86-64bit.dmg),然后打开安装程序(如mysql-shell-8.0.16-macos10.14-x86-64bit.pkg)。这将开始安装。

您将看到的第一个对话框是欢迎对话框,它显示了文档链接列表和步骤的简要总结。对于精明的 macOS 爱好者来说,安装进度会非常熟悉。图 2-30 显示了欢迎对话框。

img/478423_1_En_2_Fig30_HTML.jpg

图 2-30

欢迎对话框

阅读欢迎文本并浏览链接后,点击继续继续。下一个对话框是许可证对话框,您可以选择阅读 GPL 许可证(或者企业许可证,如果您选择下载企业版)。您也可以打印许可证或将许可证保存到文件中,以便以后阅读。图 2-31 显示许可对话框。

img/478423_1_En_2_Fig31_HTML.jpg

图 2-31

许可对话框

点击继续查看许可接受对话框。图 2-32 显示许可协议对话框。同样,您可以阅读许可,但您必须接受许可才能继续。要接受许可证,点击接受

img/478423_1_En_2_Fig32_HTML.jpg

图 2-32

接受许可对话框

接受许可后,您将进入安装类型对话框。请注意,目标选择对话框被跳过。图 2-33 显示安装类型对话框。

在此对话框中,您可以选择安装目标。对于大多数系统,您可以接受默认值。如果需要更改目的地,可以点击自定义按钮。要继续安装目的地,点击安装

img/478423_1_En_2_Fig33_HTML.jpg

图 2-33

安装类型对话框

下一个对话框是进度对话框,显示正在安装的文件的进度。这里您不会看到太多,所以让我们看看下一个对话框,它显示了摘要对话框。图 2-34 显示了汇总对话框。

img/478423_1_En_2_Fig34_HTML.jpg

图 2-34

摘要对话框

要完成安装,点击关闭。您可能想要关闭可装载的磁盘映像(如果您不关闭它,它将在关机时关闭)。你现在可以使用 MySQL Shell 连接到服务器,如图 2-35 所示。在这里,我打开了一个终端,输入命令 mysqlsh 来启动 shell。

img/478423_1_En_2_Fig35_HTML.jpg

图 2-35

使用 MySQL Shell (macOS)

如果您一直在自己的机器上安装 MySQL,那么恭喜您!现在,您已经有了 MySQL Server 和 MySQL Shell 以及完成本书中的示例所需的其他组件。

使用 APT 库在 Linux (Ubuntu)上安装

在其他平台上安装 MySQL 服务器和 Shell 最好使用特定于平台的存储库。也就是说,您可以访问 MySQL 下载站点,下载特定于平台的安装程序,并按照 Linux 发行版的通用方法安装 MySQL 产品。

事实上,MySQL 服务器分发包不像其他平台那样是单一下载。这是因为服务器包是以模块化方式构建的。也就是说,您可以分部分安装服务器,包括客户端、公共库、服务器核心等。

然而,Oracle 已经为更流行的 Linux 发行版,特别是 Ubuntu 和 Debian 发行版建立了一种更简单的安装方法。这是通过 APT 存储库实现的,它为您的平台建立了包和引用。例如,一旦你在 Ubuntu 上安装了存储库,你就可以使用 apt 来安装任何你想要的服务器产品,它会自动下载合适的安装程序。

但是 APT 库不仅仅如此。安装后,您可以随时了解 MySQL Server 的最新版本。总的来说,这比每次你想安装新版本的时候手动下载安装程序要容易得多。

在这一节中,我们将看到在 Ubuntu 上使用 MySQL APT 库安装 MySQL 服务器和 MySQL Shell 的演示。让我们从下载和安装存储库开始。

下载 APT 存储库

要下载 APT 资源库,请导航至 MySQL 社区下载页面( https://dev.mysql.com/downloads/ )并点击页面顶部的 APT 资源库菜单项,如图 2-36 所示。

img/478423_1_En_2_Fig36_HTML.jpg

图 2-36

选择 APT 仓库

然后,您将看到 APT 存储库可用的文件。对于 MySQL 8.0.16 版本,只有一个选项,如图 2-37 所示。要下载文件,点击下载

img/478423_1_En_2_Fig37_HTML.jpg

图 2-37

下载 APT 存储库

您可能会被要求登录或注册一个免费的 MySQL 帐户。这是可选的,您可以通过单击“不,谢谢,只需启动按钮下方的我的下载链接”来跳过这一步,如图 2-38 所示。

img/478423_1_En_2_Fig38_HTML.jpg

图 2-38

跳过用于下载 APT 存储库的登录

根据您使用的浏览器,接下来会要求您打开或保存文件。你应该将文件保存在你的Downloads文件夹中或者你能找到的地方,如图 2-39 所示。

img/478423_1_En_2_Fig39_HTML.jpg

图 2-39

保存文件

现在我们已经下载了 APT 存储库,我们可以安装它了。

安装 APT 存储库

要安装 APT 存储库,打开一个终端并切换到保存它的目录(例如,您的Downloads文件夹)。安装 APT 存储库所需的步骤包括:

$ sudo dpkg -i mysql-apt-config_0.8.13-1_all.deb
$ sudo apt-get update

第一个命令安装链接 Oracle 存储库所需的软件包源。第二个命令用于更新包源代码并启用新的源代码。清单 2-1 显示了安装 APT 存储库的脚本。你的结果应该差不多。

$ sudo dpkg -i mysql-apt-config_0.8.13-1_all.deb
[sudo] password for cbell:
Selecting previously unselected package mysql-apt-config.
(Reading database ... 212217 files and directories currently installed.)
Preparing to unpack mysql-apt-config_0.8.13-1_all.deb ...
Unpacking mysql-apt-config (0.8.13-1) ...
Setting up mysql-apt-config (0.8.13-1) ...
OK
$ sudo apt-get update
Get:1 http://repo.mysql.com/apt/ubuntu xenial InRelease [19.1 kB]
Hit:2 http://us.archive.ubuntu.com/ubuntu xenial InRelease
Get:3 http://security.ubuntu.com/ubuntu xenial-security InRelease [107 kB]
Get:4 http://repo.mysql.com/apt/ubuntu xenial/mysql-8.0 Sources [994 B]
Get:5 http://us.archive.ubuntu.com/ubuntu xenial-updates InRelease [109 kB]
Get:6 http://repo.mysql.com/apt/ubuntu xenial/mysql-apt-config amd64 Packages [567 B]
Get:7 http://repo.mysql.com/apt/ubuntu xenial/mysql-apt-config i386 Packages [567 B]
Get:8 http://repo.mysql.com/apt/ubuntu xenial/mysql-8.0 amd64 Packages [7,150 B]
Get:9 http://repo.mysql.com/apt/ubuntu xenial/mysql-8.0 i386 Packages [7,143 B]
Get:10 http://repo.mysql.com/apt/ubuntu xenial/mysql-tools amd64 Packages [3,353 B]
Get:11 http://repo.mysql.com/apt/ubuntu xenial/mysql-tools i386 Packages [2,632 B]
Get:12 http://us.archive.ubuntu.com/ubuntu xenial-backports InRelease [107 kB]
Fetched 364 kB in 5s (64.6 kB/s)
Reading package lists... Done

Listing 2-1Installing the APT Repository

此时,只需在 APT 命令中指定产品名称,您的系统就可以安装 MySQL 产品了。接下来,让我们安装 MySQL 服务器。

安装 MySQL 服务器

安装带有 APT 存储库的服务器可以通过安装名为 mysql server 的元包来完成。下面显示了可用于安装服务器及其最常用组件(包括客户端)的命令。

$ sudo apt-get install mysql-server

该命令的输出是大多数 Linux 安装的典型输出,并且大多数人对其内容并不感兴趣,因此我们将跳过对输出的检查。如果你很好奇,你可以看到下载和安装的所有包和包的依赖项。

在安装过程中,系统会提示您选择要启用的软件包。大多数情况下,您可以使用默认值。从列表中选择 Ok 条目,继续默认设置。对话框底部的“确定”将转到下一个屏幕,以配置从列表中选择的软件包。图 2-40 显示了允许您配置特定软件包的对话框。如果您想要配置其中一个软件包,使用向上向下箭头键选择该软件包,然后按选项卡键移动到屏幕底部的确定选项以继续。

img/478423_1_En_2_Fig40_HTML.jpg

图 2-40

在 APT 存储库中配置包

您将看到的下一步是选择 root 密码。您应该选择一个容易记住且足够复杂的密码,没有人会轻易猜到。建议您使用由字母、数字和其他字符组合而成的至少八个字符的密码。图 2-41 显示了设置 root 用户密码的对话框。

img/478423_1_En_2_Fig41_HTML.jpg

图 2-41

设置 Root 用户密码

下一个对话框是您在返回终端提示符之前看到的最后一个对话框。您必须选择使用强密码加密(强烈推荐)或使用传统身份验证方法(不推荐用于生产)。默认情况下会选择强加密,所以我们只需使用选项卡键,然后选择 Ok 继续。图 2-42 显示密码加密配置对话框。

注意

如果您选择强密码加密方法,并希望使用 MySQL 客户端的旧版本(而不是 shell),您可能会遇到连接错误。您必须使用带有强密码加密的新客户端。

img/478423_1_En_2_Fig42_HTML.jpg

图 2-42

密码加密对话框

如果一切顺利,你应该被毫不客气地送回终点站。您应该不会看到任何错误,并且可能会看到安装任何附加组件的结果,例如下面的摘录。

...
update-alternatives: using /var/lib/mecab/dic/ipadic-utf8 to provide /var/lib/mecab/dic/debian (mecab-dictionary) in auto mode
Setting up mysql-server (8.0.16-1ubuntu16.04) ...
Processing triggers for libc-bin (2.23-0ubuntu10) ...

接下来,我们用下面的命令启动服务器。请注意,这可能需要一段时间才能在第一次启动时运行,因为服务器必须设置数据目录。

$ sudo service mysql start

现在,您可以连接到服务器。因为我们还没有安装 shell,所以我们可以使用下面的命令来使用旧的客户机。这些选项指定用户(root)和提示输入用户密码的选项。

$ mysql -uroot -p

这将导致启动旧的客户端,看起来非常类似于我们到目前为止看到的 MySQL Shell 的例子。如果你仔细观察,你会发现欢迎声明中的细微差别,但最大的线索是新的提示。清单 2-2 展示了一个使用旧客户端连接到服务器的例子。

$ mysql -uroot -p
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 8.0.16 MySQL Community Server - GPL

Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> \q
Bye!

Listing 2-2Connecting to MySQL (Old Client)

接下来,让我们安装 MySQL Shell,并通过连接到服务器进行测试。

安装 MySQL Shell

要安装 MySQL Shell,我们必须下载 MySQL Shell 分发包。你可以通过访问 https://dev.mysql.com/downloads/shell/ 来做到这一点。然后,您必须更改操作系统以匹配您的系统(如 Ubuntu)和版本(如 18.04),如图 2-43 所示。或者,您可以将版本设置为“All”来查看所有可用的包。点击下载按钮,下载与您的系统相匹配的软件包。

img/478423_1_En_2_Fig43_HTML.jpg

图 2-43

下载 MySQL Shell (Ubuntu)

例如,我使用的是运行 64 位版本的 Ubuntu 18.04。因此,我下载了名为 mysql-shell _ 8 . 0 . 16-1 Ubuntu 18.04 _ amd64 . deb 的文件。回想一下,根据您使用的浏览器,您可能需要点击 MySQL 下载接受对话框(见图 2-38 )并保存文件(见图 2-39 )。下载完成后,您可以使用以下命令安装 shell。

$ sudo dpkg -i ./mysql-shell_8.0.16-1ubuntu18.04_amd64.deb

运行该命令的输出非常短,如下所示。

(Reading database ... 151363 files and directories currently installed.)
Preparing to unpack .../mysql-shell_8.0.16-1ubuntu18.04_amd64.deb ...
Unpacking mysql-shell:amd64 (8.0.16-1ubuntu18.04) ...
Setting up mysql-shell:amd64 (8.0.16-1ubuntu18.04) ...

您还可以通过使用以下命令,使用 APT 存储库安装 shell。无论您使用哪一种都可以,但是使用 APT 存储库是首选的方法。

$ sudo apt-get install mysql-shell

至此,我们已经安装并配置了 MySQL 服务器和 Shell。现在,让我们通过在终端窗口中发出以下命令来测试这两者。

$ mysqlsh

一旦输入该命令,您将看到 shell 启动,如图 2-44 所示。

img/478423_1_En_2_Fig44_HTML.jpg

图 2-44

使用 MySQL Shell (Ubuntu)

注意,在这个例子中,我发出了几个命令。以斜杠(\)开头的命令是 shell 命令。首先是将模式改为 SQL ( \sql)用于处理 SQL 命令,然后我使用\connect命令连接到服务器,将用户 id、主机和端口作为root@localhost:3306传递。最后,我发出一个 SQL 命令来显示所有的数据库。我们将在下一章学习更多关于使用 shell 的知识。

如果您一直在自己的机器上安装 MySQL,那么恭喜您!现在,您已经有了 MySQL Server 和 MySQL Shell 以及完成本书中的示例所需的其他组件。

摘要

对于 MySQL 客户端来说,MySQL Shell 是技术上的一次巨大飞跃。它不仅被设计为以更智能的方式与 MySQL 中的 SQL 一起工作,还被设计为支持 JavaScript 和 Python 的原型。您可以使用任何您想要的语言,并在它们之间轻松切换,而不必重新启动应用或断开连接。多酷啊。

在本章中,我们学习了如何安装 MySQL Shell。我们还学习了如何安装 MySQL 服务器。演示了使用 MySQL 安装程序在 Windows 上安装 MySQL,使用 APT 库在 macOS 和 Linux (Ubuntu)上安装 MySQL。

在下一章中,我们将看到一个关于 shell 及其主要特性的简短教程。

三、MySQL Shell 教程

既然我们已经知道了 MySQL Shell 是什么以及它在 MySQL 产品套件中的位置,那么是时候了解 Shell 能为我们做什么了。具体来说,它支持哪些命令,如何连接服务器,支持哪些功能?

在这一章中,我们将更详细地探讨 MySQL Shell。我们将了解更多关于它的主要特性和选项,以及如何使用新的 shell 来交互式地执行脚本。正如您将看到的,MySQL Shell 是 MySQL 未来的一个重要元素。让我们从 shell 支持的命令和选项开始。

命令和选项

如果您认为新的 shell 只不过是原始客户端的改进版本,那么您就大错特错了。shell 不仅仅是原始客户端的简单替换。为了让您开始了解它的不同之处,以及它如何比原来的 shell 复杂得多,让我们从检查支持的命令和选项开始。

命令是您可以在提示符下提供的特殊条目,它直接与 MySQL Shell 应用交互。这些命令通常被称为 shell 命令,并以斜杠(\)开头。选项指的是启动 MySQL Shell 时可以指定的许多参数(选项)。因此,shell 支持启动时的定制,比如连接到服务器、设置模式等等。

在接下来的几节中,我们将了解更多关于 shell 的命令和选项,但是首先让我们简单地讨论一下如何启动 shell。

启动 MySQL Shell

根据您的平台和安装 shell 的方式,您可以从系统(例如,开始)菜单启动 shell。但是,在所有平台上,安装都将 shell 可执行文件放在可以从命令行执行的地方。Windows 上的 shell 可执行文件命名为mysqlsh.exe。在其他平台上,简直就是mysqlsh

例如,在 Windows 上,您可以在特殊的终端中使用开始菜单启动 shell,如图 3-1 所示。

img/478423_1_En_3_Fig1_HTML.jpg

图 3-1

从“开始”菜单启动 Shell(Windows)

大多数人会从命令行启动 shell。我们可以通过打开终端(或控制台)并运行 shell 可执行文件来实现这一点。例如,图 3-2 显示了如何从命令窗口启动 Windows 上的 shell。你可以在其他平台上做同样的事情。

img/478423_1_En_3_Fig2_HTML.jpg

图 3-2

从命令行启动 Shell(Windows)

命令

像最初的 MySQL 客户端一样,有一些特殊的命令控制应用本身,而不是与数据交互(通过 SQL 或 X DevAPI)。要执行 shell 命令,请发出带斜线(\)的命令。例如,\help打印所有 shell 命令的帮助。表 3-1 列出了一些更常用的 Shell 命令。

表 3-1

Shell 命令

|

命令

|

捷径

|

描述

|
| --- | --- | --- |
| \ |   | 开始多行输入(仅限 SQL 模式) |
| \connect | \c | 连接到服务器 |
| \help | \?,\h | 打印帮助文本 |
| \history |   | 查看和编辑命令行历史 |
| \js |   | 切换到 JavaScript 模式 |
| \nowarnings | \w | 不显示警告 |
| \option |   | 查询和更改 MySQL Shell 配置选项 |
| \py |   | 切换到 Python 模式 |
| \quit | \q,\exit | 放弃 |
| \reconnect |   | 重新连接到同一个 MySQL 服务器 |
| \rehash |   | 手动更新自动完成名称缓存 |
| \source | \. | 执行指定的脚本文件 |
| \sql |   | 切换到 SQL 模式 |
| \status | \s | 打印有关连接的信息 |
| \use | \u | 设置会话的模式 |
| \warnings | \W | 在每个语句后显示警告 |

有些命令,如\connect 命令,带有一个或多个参数。学习如何使用 shell 命令的最好方法是使用\help命令。您可以使用不带参数的命令来获得相关帮助。例如,要了解更多关于\connect命令的信息,输入\help connect,如清单 3-1 所示。这里,我们从命令行启动 shell,不带任何选项,它以默认的 JavaScript 模式启动,发出几个 help 命令,然后用\q命令退出 shell。为了简洁起见,省略了部分输出,命令以粗体显示,以便于阅读。

C:\>mysqlsh

MySQL Shell 8.0.16
...
 MySQL  JS > \help sql
Found several entries matching sql

The following topics were found at the X DevAPI category:

- mysqlx.Session.sql
- mysqlx.SqlExecute.sql

For help on a specific topic use: \? <topic>

e.g.: \? mysqlx.Session.sql

 MySQL  JS > \help connect
NAME
      connect - Establishes the shell global session.

SYNTAX
      shell.connect(connectionData[, password])

WHERE
      connectionData: the connection data to be used to establish the session.
      password: The password to be used when establishing the session.

DESCRIPTION
      This function will establish the global session with the received
      connection data.

      The connection data may be specified in the following formats:

      - A URI string
      - A dictionary with the connection options
...
 MySQL  JS > \q
Bye!

Listing 3-1Getting Help in MySQL Shell

小费

使用\help <command>了解如何使用新命令。

您可以使用\sql\js\py shell 命令动态切换模式。这不仅意味着您可以在不重启 shell 的情况下更改模式,还允许您随时切换模式,从而使处理 SQL 和 NoSQL 数据变得更加容易。例如,您可以执行一些 SQL 命令,然后连接到 X DevAPI 来运行 JavaScript,返回到 SQL,然后切换到运行 Python 脚本。此外,即使使用了启动选项来设置模式,也可以使用这些 shell 命令。

注意,退出 shell 的方式是使用\q(或\quit)命令。如果您像在旧客户端中习惯的那样键入quit,shell 将根据您所处的模式做出不同的响应。清单 3-2 展示了每种模式下发生的情况。让我们从默认模式(JavaScript)开始,然后切换到 Python,最后是 SQL 模式。

MySQL  JS > quit
ReferenceError: quit is not defined

 MySQL  JS > \py
Switching to Python mode...

 MySQL  Py > quit
Traceback (most recent call last):
  File "<string>", line 1, in <module>
NameError: name 'quit' is not defined

 MySQL  Py > \sql
Switching to SQL mode... Commands end with ;

 MySQL  SQL > quit;
ERROR: Not connected.

 MySQL  SQL >
 MySQL  SQL > \quit
Bye!

Listing 3-2Results of Using quit in Different Modes

如果您习惯了旧的 MySQL 客户端,并且不小心使用了旧的客户端命令,您可能会看到类似的奇怪现象,但是只需要经常使用它就可以提醒您要使用的正确命令。现在,让我们看看 shell 的命令行选项。

选择

可以使用几个控制模式、连接、行为等的启动选项来启动 shell。本节介绍一些您可能想要使用的更常用的选项。我们将在后面的章节中看到更多关于连接选项的内容。表 3-2 显示了常见的 Shell 选项。这些只是众多选择中的几个。

表 3-2

常见的 MySQL Shell 选项

|

[计]选项

|

描述

|
| --- | --- |
| --auth-method=method | 要使用的身份验证方法 |
| --cluster | 确保目标是 InnoDB 集群的一部分 |
| --compress | 启用客户端和服务器之间的压缩 |
| --database=name | - schema 的别名 |
| --dba=enableXProtocol | 在连接到的服务器中启用 X 协议。必须与- mysql 一起使用 |
| --dbpassword=name | 连接到服务器时使用的密码 |
| --dbuser=name, -u | 用于连接的用户 |
| --execute=<cmd>, -e | 执行命令并退出 |
| --file=file, -f | 用于执行的流程文件 |
| --host=name, -h | 用于连接的主机名 |
| --import | 导入一个或多个 JSON 文档 |
| --interactive[=full], -i | 为了在批处理模式下使用,它强制模拟交互模式处理。批处理中的每一行都像在交互模式下一样进行处理 |
| --js | 以 JavaScript 模式启动 |
| --json=[raw&#124;pretty] | 以 raw 格式(无格式)或 pretty(人类可读格式)生成 JSON 格式的输出 |
| --log-level=value | 日志级别。值必须是 1 到 8 之间的整数或[无、内部、错误、警告、信息、调试、调试 2、调试 3]中的任何一个 |
| --mc --mysql | 创建经典(旧协议)会话 |
| --mx --mysqlx | 创建一个 X 协议会话(简称为“会话”) |
| --name-cache | 为默认模式启用表名加载 |
| --no-name-cache | 禁止加载默认模式的表名 |
| --nw, --no-wizard | 禁用执行脚本的向导模式(非交互式) |
| -p | 请求密码提示以设置密码 |
| --password=name | dbpassword 的别名 |
| --port=#, -P | 用于连接的端口号 |
| --py | 以 Python 模式启动 |
| --schema=name, -D | 要使用的架构 |
| --socket=sock, -S | UNIX 中用于连接的套接字名称或 Windows 中的命名管道名称(仅限经典会话) |
| --sql | 以 SQL 模式启动 |
| --sqlc | 使用经典会话在 SQL 模式下启动 |
| --sqlx | 使用创建 X 协议会话在 SQL 模式下启动 |
| --ssl-ca=name | PEM 格式的 CA 文件(查看 OpenSSL 文档) |
| --ssl-capath=dir | CA 目录 |
| --ssl-cert=name | PEM 格式的 X509 证书 |
| --ssl-cipher=name | 要使用的 SSL 密码 |
| --ssl-crl=name | 证书吊销列表 |
| --table | 以表格格式显示结果 |
| --tabbed | 以选项卡格式显示结果 |
| --uri | 以user@host:port的形式提供连接信息 |
| --vertical | 以垂直格式显示结果(如\G) |

请注意,有些选项有别名,其用途与原始客户端相同。如果您有启动客户机来执行操作的脚本,这使得切换到 shell 变得更容易一些。还要注意,有一组使用安全套接字层(SSL)连接的选项。还有一些选项可以控制输出的显示方式——传统的表格(比如 SQL 结果)、垂直方向,甚至是 JSON。花点时间浏览一下这个列表,让自己熟悉哪些是可用的。但是,还有其他不常用的选项。有关可用选项的完整列表,请参见 https://dev.mysql.com/doc/mysql-shell/8.0/en/mysqlsh.html 。现在不要担心记住它们或如何使用它们——我们将在后面的章节中看到它们的实际应用。

MySQL Shell 入门

正如我们所了解的,MySQL Shell 是 MySQL 产品组合中一个令人兴奋的新成员。它不仅是一个新的客户端,还是一个优秀的脚本环境,可以用来开发处理数据的新工具和应用。酷!

注意

我们不会检查 MySQL Shell 的每个方面;相反,我们专注于常用的特性。我们还将在后面的章节中看到许多演示的特性。有关应用日志、启动脚本和使用环境变量( https://dev.mysql.com/doc/mysql-shell/8.0/en/ )等附加功能的更多信息,请参见 MySQL Shell online 用户手册。

让我们再看一次壳牌的行动。清单 3-3 展示了一个使用命令行选项通过统一资源标识符(URI)连接到我们的 MySQL 服务器的例子,设置默认模式,模式为 SQL,输出格式为 vertical。

注意

在以后的例子中,我将使用清单而不是图形来展示运行中的 shell。

C:\Users\cbell>mysqlsh --sql --uri=root@localhost -p -D world --vertical
Creating a session to 'root@localhost/world'
Please provide the password for 'root@localhost': ∗∗∗∗∗∗∗∗∗∗
Save password for 'root@localhost'? [Y]es/[N]o/Ne[v]er (default No):
Fetching schema names for autocompletion... Press ^C to stop.
Fetching table and column names from `world` for auto-completion... Press ^C to stop.
Your MySQL connection id is 11 (X protocol)
Server version: 8.0.16 MySQL Community Server - GPL
Default schema set to `world`.
MySQL Shell 8.0.16

Copyright (c) 2016, 2019, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type '\help' or '\?' for help; '\quit' to exit.

 MySQL  localhost:33060+ ssl  world  SQL > SHOW TABLES;
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
Tables_in_world: city
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 2\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
Tables_in_world: country
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 3\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
Tables_in_world: countrylanguage
3 rows in set (0.0014 sec)

 MySQL  localhost:33060+ ssl  world  SQL > \q
Bye!

Listing 3-3Starting the MySQL Shell with Options

正如您所看到的,您可以使用或不使用选项来启动 shell。您可以选择在启动时连接,或者在启动 shell 后等待并连接到服务器。事实上,无论是否使用命令行选项进行连接,都可以在不重新启动应用的情况下更改到另一台服务器的连接。当使用多个 MySQL 服务器时,这是一个很好的特性,正如我们将看到的,这也是有效设置高可用性解决方案的关键。

正如我们在表格和前面的例子中看到的,您必须使用\connect shell 命令来连接服务器。这些连接称为会话,shell 有几个处理会话的特性。连接可以使用原始的客户机/服务器协议,也可以使用新的 X 协议,通过 X 插件与服务器通信。这意味着 shell 允许您处理关系文档(SQL)和/或 JSON 文档(NoSQL)。我们将在后面的章节中发现更多关于连接的内容。

除了会话,理解 shell 支持的各种模式也很重要。回想一下,这些包括 SQL、JavaScript 和 Python 模式。像连接一样,您可以在命令行上指定模式,并在使用 shell 时随时更改模式。这使您可以根据需要在 Python 和 SQL 之间来回切换,而无需离开应用。

以下部分从较高的层次上介绍了 shell 的会话和模式功能。了解这些特性是理解如何在 shell 中建立连接的关键。我们将在后面的章节中继续学习如何建立连接。有关 MySQL Shell 的更多信息,请参见在线 MySQL 参考手册中标题为“MySQL Shell 用户指南”的部分。

会话和模式

像最初的客户端和大多数 MySQL 客户端应用一样,您需要连接到 MySQL 服务器,以便运行命令。MySQL Shell 支持多种连接 MySQL 服务器的方式和多种与服务器交互的选项(称为会话)。在会话中,您可以更改 shell 接受命令的方式(称为模式),以包括 SQL、JavaScript 或 Python 命令。

考虑到使用服务器的所有不同的新概念,那些初学使用 shell 的人可能会发现其中的细微差别,甚至有时会感到困惑。事实上,在线参考手册和各种博客以及其他报告有时会互换使用模式和会话,但是正如您将看到的,它们是不同的(无论多么微妙)。让我们从查看可用的会话对象开始。

会话对象

关于会话,首先要理解的是,会话是到单个服务器的连接。第二件要理解的事情是,每个会话可以使用两个会话对象中的一个来启动,这两个会话对象公开了一个特定的对象,用于使用特定的通信协议与 MySQL 服务器一起工作。也就是说,会话是到服务器的连接(定义了所有参数),会话对象是 shell 用来以几种方式之一与服务器进行交互的对象。更具体地说,MySQL Shell 会话对象简单地定义了如何与服务器交互,包括支持什么模式,甚至 Shell 如何与服务器通信。shell 支持如下两个会话对象:

  • 会话:X 协议会话用于应用开发,支持 JavaScript、Python、SQL 模式。通常用于开发脚本或执行脚本。要使用该选项启动 shell,请使用--mx ( --mysqlx)选项。

  • 经典会话:使用旧的服务器通信协议,对 DevAPI 的支持非常有限。对没有 X 插件或不支持 X 协议的旧服务器使用这种模式。通常用于旧服务器的 SQL 模式。要使用该选项启动 shell,请使用--mc ( --mysqlc)选项。

通过为经典会话指定-mc或为 X 协议会话指定-mx,可以指定使用\connect shell 命令时要使用的会话对象(协议)。下面依次展示了其中的每一个。注意<URI>指定了统一资源标识符。

  • \connect -mx <URI>:使用 X 协议(会话)

  • \connect -mc <URI>:使用经典协议(经典会话)

本章和其他地方提到的 URI 是指连接到 MySQL 服务器时使用的连接信息的特定格式或布局。下面显示了构建 URI 的格式。

[scheme://][user[:[password]]@]target[:port][/schema][?attribute1=value1&attribute2=value2...

注意,我们可以指定可选的连接(会话)类型、用户、密码(不推荐)、目标或主机、端口,甚至模式以及会话类型使用的任何选项。例如,用于连接到本地 MySQL 服务器的简单 URI 如下所示。这里,我们使用用户名 root 和端口 3306。如果 URI 中没有密码,shell 将提示输入密码。

/connect root@localhost:3306

当然,您仍然可以对用户、主机和端口使用单独的命令行选项。然而,标准做法是将 URIs 与/connect命令或命令行选项一起使用。

小费

有关使用 URIs 的更多信息,请参见 https://dev.mysql.com/doc/refman/8.0/en/connecting-using-uri-or-key-value-pairs.html

召回会话大致等同于连接。但是,会话不仅仅是一个连接,因为它包含了用于建立连接的所有设置(包括会话对象)以及用于与服务器通信的协议。因此,我们有时会遇到术语“协议”来描述会话。

支持的模式

shell 支持三种模式(也称为语言支持或简称为活动语言);SQL、JavaScript 和 Python。回想一下,我们可以使用 shell 命令启动这些模式中的任何一种。你可以随时切换模式(语言),每次都不会断线。下面列出了三种模式以及如何切换到每种模式。

  • \sql:切换到 SQL 语言

  • \js:切换到 JavaScript 语言(默认模式)

  • \py:切换到 Python 语言

您可以随时切换模式,也可以在特定模式下启动 shell。默认模式是 JavaScript。因此,如果您没有在命令行上指定模式,您将会看到 JavaScript 提示。这是你知道自己处于哪种模式的方法。图 3-3 显示了各种模式。它们还用 JavaScript 黄色、SQL 橙色和 Python 蓝色进行了颜色编码。不错!

img/478423_1_En_3_Fig3_HTML.jpg

图 3-3

MySQL Shell 模式提示

现在我们已经了解了会话和模式,我们可以看看如何连接到 MySQL 服务器。

使用连接

在 shell 中建立连接可能需要一些时间来适应与最初的 MySQL 客户端不同的工作方式,最初的 MySQL 客户端需要在命令行中使用几个选项。您可以使用特殊格式的 URI 字符串,或者通过名称使用单个选项连接到服务器(像旧客户端一样)。也支持 SSL 连接。可以通过启动选项、shell 命令和脚本来建立连接。但是,所有连接都需要使用密码。因此,除非您另外声明,否则如果没有给出密码,shell 将提示您输入密码。

注意

如果您想使用没有密码的连接(不推荐),您必须使用--password选项,或者,如果使用 URI,包括一个额外的冒号来代替密码。

下面不是讨论所有可用的连接方式和选项,而是在下面的部分中给出每种连接方式的一个示例。

使用 URI

回想一下,URI 是一个使用特殊格式来包含各种参数值的字符串。密码、端口和模式是可选的,但用户和主机是必需的。在这种情况下,模式是连接时要使用的默认模式(数据库)。旧客户机/服务器协议的默认端口是 3306,X 协议的默认端口是 33060。要在启动 shell 时使用命令行上的 URI 连接到服务器,请使用--uri选项指定如下。

$ mysqlsh --uri root:secret@localhost:3306

小费

如果您省略了--uri但仍然包含一个 URI,那么 shell 将处理作为 URI 出现的字符串。

shell 假定所有连接都需要密码,如果没有提供密码,它将提示输入密码。清单 3-4 显示了前面没有密码的连接。注意 shell 是如何提示输入密码的。

C:\Users\cbell>mysqlsh --uri root@localhost:33060/world_x --sql
Creating a session to 'root@localhost:33060/world_x'
Please provide the password for 'root@localhost:33060': ∗∗∗∗∗∗∗∗∗∗
Save password for 'root@localhost:33060'? [Y]es/[N]o/Ne[v]er (default No):
Fetching schema names for autocompletion... Press ^C to stop.
Fetching table and column names from `world_x` for auto-completion... Press ^C to stop.
Your MySQL connection id is 16 (X protocol)
Server version: 8.0.16 MySQL Community Server - GPL
Default schema set to `world_x`.
MySQL Shell 8.0.16

Copyright (c) 2016, 2019, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type '\help' or '\?' for help; '\quit' to exit.

 MySQL  localhost:33060+ ssl  world_x  SQL > \q
Bye!

Listing 3-4Connecting with a URI

注意,我们还用 URI 中的模式选项指定了默认模式( world_x )。 world_x 数据库是一个示例数据库,您可以从 https://dev.mysql.com/doc/index-other.html 下载。我们将在后面的 MySQL Shell 教程中安装这个数据库。

使用单个选项

您还可以使用单独的选项在 shell 命令行上指定连接。可用的连接选项如表 3-1 所示。清单 3-5 展示了如何使用单独的选项连接到 MySQL 服务器。注意,我用--py选项将模式(语言)改为 Python。

C:\Users\cbell>mysqlsh --user root --host localhost --port 33060 --schema world_x --py --mx
Creating an X protocol session to 'root@localhost:33060/world_x'
Please provide the password for 'root@localhost:33060': ∗∗∗∗∗∗∗∗∗∗
Save password for 'root@localhost:33060'? [Y]es/[N]o/Ne[v]er (default No):
Fetching schema names for autocompletion... Press ^C to stop.
Your MySQL connection id is 17 (X protocol)
Server version: 8.0.16 MySQL Community Server - GPL
Default schema `world_x` accessible through db.
MySQL Shell 8.0.16

Copyright (c) 2016, 2019, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type '\help' or '\?' for help; '\quit' to exit.

 MySQL  localhost:33060+ ssl  world_x  Py > \q

Bye!

Listing 3-5Connecting Using Individual Options

在脚本中使用连接

如果您计划使用 shell 来创建脚本或者仅仅作为一个原型工具,那么您还会希望在脚本中使用会话。虽然有些例子可能看起来很奇怪,并且没有详细的解释,但是在学习如何执行这些步骤的细节(以及为什么)之前,看看事情是如何进行的总是一个好主意。因此,您应该阅读这一部分,以熟悉什么是可能的。在本节中,我们将探讨如何在脚本中使用会话。我们将在后面的章节中学习如何使用这些例子。

在脚本中使用会话的强大之处在于我们可以保存它们并在以后重用它们。在这种情况下,我们将创建一个变量来包含获取的会话。以这种方式创建的会话称为全局会话,因为一旦创建,它就可用于任何模式。然而,根据我们使用的会话对象(回想一下这是经典还是 X 协议),我们将使用不同的方法创建一个 X 或经典会话。我们对 X 协议会话对象使用get_session()方法,对经典会话对象使用get_classic_session()方法。

注意

我们将把重点放在 Python 脚本上,但是许多例子也适用于 JavaScript,尽管类和方法的大小写略有不同。

下面演示如何在 Python 中获取 X 协议会话对象。注意,我在 URI 中指定了密码,并将密码作为单独的参数。为了简洁起见,我省略了 shell 提示符。

> my_session = mysqlx.get_session('root@localhost:33060', 'secret');
> print(my_session)
<Session:root@localhost:33060>

以下演示了如何在 Python 中获取经典会话对象。

Py > my_classic = mysql.get_classic_session('root@localhost:3306', 'secret');
Py > print(my_classic)
<ClassicSession:root@localhost:3306>

使用 SSL 连接

您还可以创建 SSL 连接,以便安全地连接到您的服务器。要使用 SSL,您必须将服务器配置为使用 SSL。要在运行 MySQL 的同一台机器上使用 SSL,可以使用--ssl-mode=REQUIRED选项。

使用 SHOW VARIABLES 命令查看 SSL 变量的状态,以确定您的服务器是否启用了 SSL。清单 3-6 显示了使用%包围的 ssl 运行查询的结果,这是通配符。这将导致显示名称中带有 ssl 的所有变量。我们可以看到,SSL 确实是启用的(见have_ssl,显示的是YES)。如果您的服务器没有设置为使用 SSL,而您想要使用 SSL 连接,请参阅在线 MySQL 参考手册( https://dev.mysql.com/doc/refman/8.0/en/encrypted-connections.html )以了解如何在您的服务器上设置 SSL。

SQL > SHOW VARIABLES LIKE '%ssl%';
+--------------------+-----------------+
| Variable_name      | Value           |
+--------------------+-----------------+
| have_openssl       | YES             |
| have_ssl           | YES             |
| mysqlx_ssl_ca      |                 |
| mysqlx_ssl_capath  |                 |
| mysqlx_ssl_cert    |                 |
| mysqlx_ssl_cipher  |                 |
| mysqlx_ssl_crl     |                 |
| mysqlx_ssl_crlpath |                 |
| mysqlx_ssl_key     |                 |
| ssl_ca             | ca.pem          |
| ssl_capath         |                 |
| ssl_cert           | server-cert.pem |
| ssl_cipher         |                 |
| ssl_crl            |                 |
| ssl_crlpath        |                 |
| ssl_fips_mode      | OFF             |
| ssl_key            | server-key.pem  |
+--------------------+-----------------+
17 rows in set (0.0207 sec)

Listing 3-6Checking for SSL Support

您也可以指定 SSL 选项,如表 3-1 所示。您可以使用命令行选项在命令行上指定它们,或者将其作为\connect shell 命令的扩展。下面显示了如何使用 SSL 和命令行选项连接到服务器。

C:\Users\cbell>mysqlsh -uroot -h127.0.0.1 --port=33060 --ssl-mode=REQUIRED

注意

MySQL 服务器的旧版本可能没有启用 X 插件。请参阅在线 MySQL 参考手册,了解如何在版本 5.7 和 8.0 的早期版本中启用 X 插件。

现在,让我们通过演示 MySQL Shell 的基本特性来看看它的运行情况。

使用 MySQL Shell

以下部分演示了如何在最基本的操作中使用 MySQL shell 选择和插入数据。这些示例使用了 world_x 数据库,旨在提供一个概述,而不是深入探讨。如果您对 MySQL 文档存储或 JSON 数据一无所知,不要绝望;本教程旨在演示如何使用 MySQL Shell,因为该 Shell 旨在用于 JSON 文档,所以我们将这样做。

因此,本教程的目标是在 world_x 数据库中插入新数据,然后执行搜索以检索满足包含新数据的条件的行。我将使用关系表来说明这些概念,因为对于我们这些熟悉“普通”数据库操作的人来说,这更容易理解。

在开始我们的旅程之前,让我们花点时间安装我们将需要的示例数据库,Oracle 的示例 MySQL 数据库。

安装示例数据库

Oracle 提供了几个示例数据库,供您在测试和开发应用时使用。样本数据库可以从 http://dev.mysql.com/doc/index-other.html 下载。我们想要使用的示例数据库被命名为 world_x ,以表明它包含 JSON 文档,并且打算用 X DevAPI、shell 等进行测试。继续前进,导航到该页面并下载数据库。示例数据库包含几个关系表(countrycitycountrylanguage)以及一个集合(countryinfo)。

下载完文件后,解压缩并记下文件的位置。我们进口的时候你会需要的。接下来,启动 MySQL Shell 并连接到您的服务器。使用\sql shell 命令切换到 SQL 模式,然后使用\source shell 命令读取world_x.sql文件并处理其所有语句。清单 3-7 展示了如何使用这些选项并安装示例数据库。为简洁起见,省略了运行\source 命令的响应。

JS > \sql
Switching to SQL mode... Commands end with ;
SQL > \source world_x.sql
Query OK, 0 rows affected (0.0034 sec)
Query OK, 0 rows affected (0.0004 sec)
Query OK, 0 rows affected (0.0003 sec)
…
Query OK, 0 rows affected (0.0003 sec)
Query OK, 0 rows affected (0.0002 sec)
Query OK, 0 rows affected (0.0003 sec)
Query OK, 0 rows affected (0.0003 sec)
Query OK, 0 rows affected (0.0002 sec)
Query OK, 0 rows affected (0.0003 sec)

 MySQL  localhost:3306 ssl  SQL > show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sakila             |
| sys                |
| world_x            |
+--------------------+
6 rows in set (0.0045 sec)
SQL > USE world_x;
Query OK, 0 rows affected (0.00 sec)
SQL > SHOW TABLES;
+-------------------+
| Tables_in_world_x |
+-------------------+
| city      |
| country   |
| countryinfo       |
| countrylanguage   |
+-------------------+
4 rows in set (0.00 sec)

Listing 3-7Installing the world_x Sample Database

注意,我安装了另一个名为 sakila 的示例数据库,您也可以在 Oracle 网站上找到这个数据库和 world_x 数据库。我还展示了位于示例数据库中的表。

小费

如果文件的路径中有空格,应该用双引号将路径括起来。

您还可以使用命令行上的--recreate-schema选项安装示例数据库,如下所示。请注意,如果数据库已经存在,这将删除并重新创建数据库。这是批处理运行 SQL 命令的另一个例子。

C:\Users\cbell\Downloads\world_x-db>mysqlsh -uroot -hlocalhost --sql --recreate-schema --schema=world_x < world_x.sql
Please provide the password for 'root@localhost': ∗∗∗∗∗∗∗∗∗∗

Please pick an option out of [Y]es/[N]o/Ne[v]er (default No):
Recreating schema world_x...

当然,您可以使用 similar source 命令在旧客户机上安装 sample 数据库,但是这有什么意思呢?

现在,让我们看看如何处理数据。

使用数据

在本节中,我们将看到一些在数据库中选择和插入数据的简单示例。我使用 world_x 数据库中的城市表来演示查询中的 JSON 数据类型。正如您将看到的,这开启了一种处理数据的新方式。同样,我们将在下一章看到更多关于 JSON 数据类型的内容。对于这一节,您应该关注 shell 中的交互。也就是说,如何使用 shell 来运行查询。如果您是 SQL 数据库专家,那么除了 JSON 数据类型之外,所有这些都非常熟悉。先说查询数据。

我们要完成的任务是查看城市表中有哪些行。在这种情况下,我们将检索(选择)那些包含美国城市的行。我们将按名称对行进行排序,为了简洁起见,只显示前 20 行。清单 3-8 展示了如何执行查询。即使你不懂 SQL,这个查询读起来也很容易。

SQL > SELECT Name, District, Info FROM city WHERE CountryCode = 'USA' ORDER BY Name DESC LIMIT 20;
+------------------+----------------------+------------------------+
| Name             | District             | Info                   |
+------------------+----------------------+------------------------+
| Yonkers          | New York             | {"Population": 196086} |
| Worcester        | Massachusetts        | {"Population": 172648} |
| Winston-Salem    | North Carolina       | {"Population": 185776} |
| Wichita Falls    | Texas                | {"Population": 104197} |
| Wichita          | Kansas               | {"Population": 344284} |
| Westminster      | Colorado             | {"Population": 100940} |
| West Valley City | Utah                 | {"Population": 108896} |
| West Covina      | California           | {"Population": 105080} |
| Waterbury        | Connecticut          | {"Population": 107271} |
| Washington       | District of Columbia | {"Population": 572059} |
| Warren           | Michigan             | {"Population": 138247} |
| Waco             | Texas                | {"Population": 113726} |
| Visalia          | California           | {"Population": 91762}  |
| Virginia Beach   | Virginia             | {"Population": 425257} |
| Vancouver        | Washington           | {"Population": 143560} |
| Vallejo          | California           | {"Population": 116760} |
| Tulsa            | Oklahoma             | {"Population": 393049} |
| Tucson           | Arizona              | {"Population": 486699} |
| Torrance         | California           | {"Population": 137946} |
| Topeka           | Kansas               | {"Population": 122377} |
+------------------+----------------------+------------------------+
20 rows in set (0.0024 sec)

Listing 3-8
Selecting Rows

注意Info栏。这是一个 JSON 数据类型列,数据显示为 JSON 文档(例如,{"Population": 122377})。这表明 JSON 文档包含一个表示每个城市人口的键/值对。我们可以看到这个表是如何使用清单 3-9 中所示的SHOW CREATE TABLE查询构建的。这里,我们看到了Info列的 JSON 数据类型。

SQL > SHOW CREATE TABLE city\G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
       Table: city
Create Table: CREATE TABLE `city` (
  `ID` int(11) NOT NULL AUTO_INCREMENT,
  `Name` char(35) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
  `CountryCode` char(3) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
  `District` char(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
  `Info` json DEFAULT NULL,
  PRIMARY KEY (`ID`)
) ENGINE=InnoDB AUTO_INCREMENT=4080 DEFAULT CHARSET=utf8
1 row in set (0.0038 sec)

Listing 3-9SHOW CREATE TABLE Example

您可能想知道我们是否可以在查询中的Info列中使用 JSON 文档。答案是,可以,可以!我们可以使用一个特殊的函数(为处理 JSON 数据类型而设计的几十个函数之一)来提取键、值对。在这种情况下,我们将使用JSON_EXTACT()函数传入列名和一个称为路径表达式的特殊格式的字符串。在这种情况下,我们希望选择那些代表人口超过 500,000 的城市的行。清单 3-10 显示了修改后的查询的一个例子。在这种情况下,我们将输出限制为前 10 行,但保留排序选项,并将列限制为 name、district 和 information 列。此外,我们将以批处理模式使用 shell 来启动 shell,运行查询,然后退出。

C:\Users\cbell\Downloads\world_x-db>mysqlsh --uri=root@localhost:3306 --sql --table -e "SELECT Name, District, Info FROM world_x.city WHERE CountryCode = 'USA' AND JSON_EXTRACT(Info, '$.Population') > 500000 ORDER BY Name DESC LIMIT 10;"
Please provide the password for 'root@localhost:3306': ∗∗∗∗∗∗∗∗∗∗
Save password for 'root@localhost:3306'? [Y]es/[N]o/Ne[v]er (default No):
+---------------+----------------------+-------------------------+
| Name          | District             | Info                    |
+---------------+----------------------+-------------------------+
| Washington    | District of Columbia | {"Population": 572059}  |
| Seattle       | Washington           | {"Population": 563374}  |
| San Jose      | California           | {"Population": 894943}  |
| San Francisco | California           | {"Population": 776733}  |
| San Diego     | California           | {"Population": 1223400} |
| San Antonio   | Texas                | {"Population": 1144646} |
| Portland      | Oregon               | {"Population": 529121}  |
| Phoenix       | Arizona              | {"Population": 1321045} |
| Philadelphia  | Pennsylvania         | {"Population": 1517550} |
| Oklahoma City | Oklahoma             | {"Population": 506132}  |
+---------------+----------------------+-------------------------+

Listing 3-10Selecting Rows Using JSON Data Type (Batch Mode)

让我们更仔细地看看这个命令。注意,我们用--uri启动 shell 以提供登录信息,--sql启动 SQL 模式,--table以表格模式显示输出(默认为 tab),而-e选项后跟执行并返回结果的查询。您可以使用这种机制在任何批处理作业中插入 shell。

我们要做的下一个任务是插入数据。为了让事情变得有趣,我们将修改一些行,以包含您在镇上可以访问的有趣网站。在纯关系数据库中,这需要更改表或添加新表来存储新信息。但是 JSON 数据类型允许我们通过更多的键、值对以自由形式添加信息。因此,我们将修改信息列,以包括关于感兴趣的站点的附加信息。换句话说,我们可以添加自己对去过的地方的评论,并推荐给其他人。

对于这个例子,我们将添加两个站点;华盛顿特区的史密森尼国家航空航天博物馆( https://airandspace.si.edu/ )和马里兰州的国家海港( https://www.nationalharbor.com/ )。当你在这个地区时,这是两个值得参观的好地方。

我们知道华盛顿特区的表中有一行,但是巴尔的摩呢?我们可以像前面一样运行一个快速的批处理查询,但是这一次我们将以选项卡的形式看到结果。

C:\Users\cbell\Downloads\world_x-db>mysqlsh --uri=root@localhost:3306 --sql  -e "SELECT Name, Info FROM world_x.city WHERE CountryCode = 'USA' AND DISTRICT = 'Maryland'"
Please provide the password for 'root@localhost:3306': ∗∗∗∗∗∗∗∗∗∗
Save password for 'root@localhost:3306'? [Y]es/[N]o/Ne[v]er (default No):
Name    Info
Baltimore       {"Population": 651154}

现在我们知道两个城市都有行,我们需要做的就是构造我们想要插入的 JSON。在这种情况下,我们将使用另一个 JSON 函数来添加新数据。但首先,让我们看看这些数据是什么样的。在这种情况下,我们希望包括网站的名称、网站的 URL 和网站的类型(例如,博物馆、景点)。为了能够为每个城市添加多个站点,我们将以 JSON 数组的形式添加新数据。这可能看起来很混乱,但是让我们看看巴尔的摩的例子。下面显示了包含现有数据和新数据的完整 JSON 文档。我以一种典型的方式对它进行了格式化,您将看到 JSON 文档使用缩进。

{
      "Population": 651154,
      "Places_of_interest": [
            {
                  "name":"National Harbor",
                  "URL":"https://www.nationalharbor.com/",
                  "type":"attraction"
            }
      ]
}

此时,您可能想知道如何确保 JSON 文档的格式正确。对于大多数人来说,这来自于使用 JSON 的经验。然而,MySQL 提供了一个名为JSON_VALID()的 JSON 函数,您可以用它来验证 JSON 文档。只需将字符串作为参数传递。对于有效的文档,应该得到值 1,对于有错误的文档,应该得到值 0,如下所示。在这里,我们看到文件是有效的。

SQL > SELECT JSON_VALID('{"Population": 651154, "Places_of_interest": [{"name": " National Harbor ", "URL":" https://www.nationalharbor.com/","type":"attraction"}]}')\G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_VALID('{"Population": 651154, "Places_of_interest": [{"name": " National Harbor ", "URL":" https://www.nationalharbor.com/","type":"attraction"}]}'): 1
1 row in set (0.0005 sec)

现在我们准备更新表中的数据。为此,我们将使用 UPDATE SQL 语句,用上面的新 JSON 文档替换信息列。但是,我们必须用更传统的字符串重新格式化文档。您可以在字符串中保留空格和换行符,但这样做并不常见。相反,我们希望形成一个用单引号括起来的字符串。但是首先,我们需要表中该行的键。city 表有一个名为 ID 的关键字字段。我们去查两个城市的身份证。

SQL > SELECT ID, Name FROM world_x.city WHERE Name IN ('Washington', 'Baltimore');
+------+------------+
| ID   | Name       |
+------+------------+
| 3809 | Baltimore  |
| 3813 | Washington |
+------+------------+
2 rows in set (0.0053 sec)

现在,让我们做更新。下面是一个典型的 SQL UPDATE命令,用于替换表中特定行的列。这里没有什么不寻常的,但是请注意,我们使用 SQL 执行的\G选项以更容易阅读的形式显示结果。

SQL > UPDATE world_x.city set Info = '{"Population": 651154,"Places_of_interest":[{"name":"National Harbor","URL":"https://www.nationalharbor.com/","type":"attraction"}]}' WHERE ID = 3809;
Query OK, 1 row affected (0.0499 sec)

Rows matched: 1  Changed: 1  Warnings: 0

SQL > SELECT Name, District, Info FROM world_x.city WHERE ID = 3809\G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
    Name: Baltimore
District: Maryland
    Info: {"Population": 651154, "Places_of_interest": [{"URL": "https://www.nationalharbor.com/", "name": "National Harbor", "type": "attraction"}]}
1 row in set (0.0008 sec)

在这里,我们看到我们已经成功地更新(替换)了 JSON 文档。但是输出不容易阅读。为了更容易阅读,让我们使用JSON_PRETTY()函数,它将输出重新格式化为更令人满意的布局。下面显示了添加了函数的相同查询。那不是更好读吗?

SQL > SELECT Name, District, JSON_PRETTY(Info) FROM world_x.city WHERE ID = 3809\G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
             Name: Baltimore
         District: Maryland
JSON_PRETTY(Info): {
  "Population": 651154,
  "Places_of_interest": [
    {
      "URL": "https://www.nationalharbor.com/",
      "name": "National Harbor",
      "type": "attraction"
    }
  ]
}
1 row in set (0.0005 sec)

现在,让我们看看使用该列的真实更新完成的其他更新。这里,我们将使用另一个名为 JSON_MERGE_PRESERVE()的特殊 JSON 函数,它合并两个 JSON 文档并保留数组(当合并两个数组时)。对于这个例子,它有效地将现有的 JSON 文档与我们添加的新信息合并在一起。

注意

有几种方法可以实现这一更新,但这是精明的数据库管理员常用的一种方法。

为了实现这一点,让我们首先从行中获取当前的 JSON 文档。我们将使用一个局部变量(以@开头)来存储使用SELECT语句的SELECT…INTO版本的结果,如下所示。在这里,我们将值保存到@var1并显示它。

SQL > SELECT Info FROM world_x.city WHERE ID = 3813 INTO @var1;
Query OK, 1 row affected (0.0007 sec)

SQL > SELECT @var1;
+------------------------+
| @var1                  |
+------------------------+
| {"Population": 572059} |
+------------------------+
1 row in set (0.0004 sec)

现在我们可以构建我们想要添加的新信息。下面展示了仅包含新数据的 JSON 文档(population 已经在该行中)。但是不要担心,我们将合并文档。

{
      "Places_of_interest":[
            {
                  "name":"Smithsonian National Air and Space Museum",
                  "URL":"https://airandspace.si.edu/",
                  "type":"museum"
            }
      ]
}

接下来,我们可以使用 JSON 文档和变量来更新行,如下所示。

SQL > UPDATE world_x.city SET Info = JSON_MERGE_PRESERVE(@var1, '{"Places_of_interest":[{"name":"Smithsonian National Air and Space Museum","URL":"https://airandspace.si.edu/","type":"museum"}]}') WHERE ID = 3813;
Query OK, 1 row affected (0.0784 sec)

Rows matched: 1  Changed: 1  Warnings: 0

请注意,我们现在添加了新信息,并保留了总体。酷。

SQL > SELECT Name, District, JSON_PRETTY(Info) FROM world_x.city WHERE ID = 3813\G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
             Name: Washington
         District: District of Columbia
JSON_PRETTY(Info): {
  "Population": 572059,
  "Places_of_interest": [
    {
      "URL": "https://airandspace.si.edu/",
      "name": "Smithsonian National Air and Space Museum",
      "type": "museum"
    }
  ]
}
1 row in set (0.0005 sec)

现在,如果我们只想检索Places_of_interest数组的值呢?在这种情况下,我们可以使用另一个名为JSON_EXTRACT()的 JSON 函数从数组中提取键和值。下面演示了这种技术。请注意以粗体突出显示的部分。这里,我们使用前面看到的路径表达式提取密钥。而且,我们正在对整个表进行查询,所以我们将获得 JSON 文档中具有Places_of_interest键的所有行。

SQL > SELECT Name, District, JSON_EXTRACT(info, '$.Places_of_interest[∗].name') as Sights FROM world_x.city WHERE JSON_EXTRACT(info, '$.Places_of_interest') IS NOT NULL \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
    Name: Baltimore
District: Maryland
  Sights: ["National Harbor"]
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 2\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
    Name: Washington
District: District of Columbia
  Sights: ["Smithsonian National Air and Space Museum"]
2 rows in set (0.0119 sec)

好了,现在看起来容易多了,不是吗?这也是一个有点混乱的 SQL 命令。如果这一切看起来有点痛苦,你是对的,的确如此。在 SQL 中处理 JSON 数据需要借助 JSON 函数,但是这是一个额外的步骤,在语法上可能有点混乱。关于每个 JSON 函数的完整解释,请参阅在线 MySQL 参考手册。 1 我们将在下一章看到更多关于 JSON 函数的内容。

使用格式模式

如果您经常使用旧的 MySQL 客户端来查询具有宽行的数据,那么您很可能会像上面一样使用\G选项来以垂直格式显示结果,这使得读取数据更加容易。有了 shell,我们可以用几种方式显示数据。在这一节中,我们将看到在批处理模式下运行 shell 以各种格式显示数据的简短示例。正如您将看到的,选择格式有助于使数据更容易阅读(或者如果在脚本中阅读输出,则更容易摄取)。

回想一下,我们可以使用命令行选项--table--tabbed--vertical--json来设置 JSON 格式,有两种选择。选择的输出将影响输出在执行模式(SQL、Python 或 JavaScript)下的外观。对于本教程,我们将只看到 SQL 模式。

让我们从表格格式开始。下面以表格格式显示了在批处理模式下执行最后一个查询的结果。请注意,我们看到的输出类似于在 SQL 模式下交互运行时的输出。

C:\Users\cbell>mysqlsh --uri=root@localhost:3306 --sql -e "SELECT Name, District, JSON_EXTRACT(info, '$.Places_of_interest[∗].name') as Sights FROM world_x.city WHERE JSON_EXTRACT(info, '$.Places_of_interest') IS NOT NULL" --table
Please provide the password for 'root@localhost:3306': ∗∗∗∗∗∗∗∗∗∗
Save password for 'root@localhost:3306'? [Y]es/[N]o/Ne[v]er (default No):
+------------+----------------------+-------------------------------------+
| Name       | District             | Sights                              |
+------------+----------------------+-------------------------------------+
| Baltimore  | Maryland             | ["National Harbor"]                 |
| Washington | District of Columbia | ["Smithsonian National Air and Space Museum"]                      |
+------------+----------------------+-------------------------------------+

接下来,让我们看看选项卡式格式。下面显示了以选项卡格式在批处理模式下执行最后一个查询的结果。注意,在这种情况下,输出是一个制表符分隔的视图。在清单中不容易看到,但是当在脚本中运行时,它使得输入输出很容易用制表符分开。

C:\Users\cbell>mysqlsh --uri=root@localhost:3306 --sql -e "SELECT Name, District, JSON_EXTRACT(info, '$.Places_of_interest[∗].name') as Sights FROM world_x.city WHERE JSON_EXTRACT(info, '$.Places_of_interest') IS NOT NULL" --tabbed
Please provide the password for 'root@localhost:3306': ∗∗∗∗∗∗∗∗∗∗
Save password for 'root@localhost:3306'? [Y]es/[N]o/Ne[v]er (default No):
Name    District        Sights
Baltimore       Maryland        ["National Harbor"]
Washington      District of Columbia    ["Smithsonian National Air and Space Museum"]

接下来,我们来看看垂直格式。下面显示了使用垂直格式在批处理模式下执行最后一个查询的结果。注意,在这种情况下,输出类似于在 SQL 模式下使用\G选项。

C:\Users\cbell>mysqlsh --uri=root@localhost:3306 --sql -e "SELECT Name, District, JSON_EXTRACT(info, '$.Places_of_interest[∗].name') as Sights FROM world_x.city WHERE JSON_EXTRACT(info, '$.Places_of_interest') IS NOT NULL" --vertical
Please provide the password for 'root@localhost:3306': ∗∗∗∗∗∗∗∗∗∗
Save password for 'root@localhost:3306'? [Y]es/[N]o/Ne[v]er (default No):
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
    Name: Baltimore
District: Maryland
  Sights: ["National Harbor"]
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 2\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
    Name: Washington
District: District of Columbia
  Sights: ["Smithsonian National Air and Space Museum"]

接下来,让我们看看原始的 JSON 格式。下面显示了使用原始 JSON 格式在批处理模式下执行最后一个查询的结果。注意,在这种情况下,输出与我们通常在 SQL 模式下看到的非常不同。事实上,我们将输出视为一系列 JSON 文档,尽管没有格式化。

C:\Users\cbell>mysqlsh --uri=root@localhost:3306 --sql -e "SELECT Name, District, JSON_EXTRACT(info, '$.Places_of_interest[∗].name') as Sights FROM world_x.city WHERE JSON_EXTRACT(info, '$.Places_of_interest') IS NOT NULL" --json=raw
{"password":"Please provide the password for 'root@localhost:3306': "}
∗∗∗∗∗∗∗∗∗∗
{"prompt":"Save password for 'root@localhost:3306'? [Y]es/[N]o/Ne[v]er (default No): "}

{"executionTime":"0.0063 sec","info":"","rows":[{"Name":"Baltimore","District":"Maryland","Sights":"[\"National Harbor\"]"},{"Name":"Washington","District":"District of Columbia","Sights":"[\"Smithsonian National Air and Space Museum\"]"}],"warningCount":0,"warningsCount":0,"warnings":[],"hasData":true,"affectedRowCount":0,"affectedItemsCount":0,"autoIncrementValue":0}

最后,我们来看看漂亮的 JSON 格式。到目前为止,输出相当简洁。然而,漂亮的 JSON 格式有点冗长。清单 3-11 显示了以批处理模式执行最后一个查询的结果,格式为漂亮的 JSON 格式。在这种情况下,它添加了空白和新行,以便于阅读。

C:\Users\cbell>mysqlsh --uri=root@localhost:3306 --sql -e "SELECT Name, District, JSON_EXTRACT(info, '$.Places_of_interest[∗].name') as Sights FROM world_x.city WHERE JSON_EXTRACT(info, '$.Places_of_interest') IS NOT NULL" --json=pretty
{
    "password": "Please provide the password for 'root@localhost:3306': "
}
∗∗∗∗∗∗∗∗∗∗
{
    "prompt": "Save password for 'root@localhost:3306'? [Y]es/[N]o/Ne[v]er (default No): "
}

{
    "executionTime": "0.0033 sec",
    "info": "",
    "rows": [
        {
            "Name": "Baltimore",
            "District": "Maryland",
            "Sights": "[\"National Harbor\"]"
        },
        {
            "Name": "Washington",
            "District": "District of Columbia",
            "Sights": "[\"Smithsonian National Air and Space Museum\"]"
        }
    ],
    "warningCount": 0,
    "warningsCount": 0,
    "warnings": [],
    "hasData": true,
    "affectedRowCount": 0,
    "affectedItemsCount": 0,
    "autoIncrementValue": 0
}

Listing 3-11The JSON pretty Format

请注意,输出更加冗长,甚至来自 shell 的消息也是 JSON 格式的,但是这确实有助于更好地读取 JSON 数据。

代码/命令历史

像它的前身一样,shell 允许您通过命令历史列表调用最后输入的命令。与原始客户端不同,shell 还允许您搜索历史记录。这在编写脚本时特别有用,因为它允许您搜索以前使用过的操作。

要搜索命令历史,请随时按下 CTRL+R 。这将启动反向搜索,通过命令进行反向搜索。提示会发生变化,表明您正在搜索,您可以键入要搜索的命令的前几个字符。搜索时将显示找到的第一个命令,如下所示。如果这不是你正在寻找的命令,你可以再次按下 CTRL+R 来获得下一个匹配,或者按下 CTRL+C 来取消。如果按下回车,找到的命令将被执行。

(reverse-i-search)`SHOW': SHOW TABLES FROM world_x;

向前搜索有一点不同,当你已经处于搜索模式时,向前搜索仍然有效。也就是说,按下 CTRL+S 将向前搜索历史记录,但如果您不在搜索模式下,则不能向前搜索。然而,一旦你掌握了窍门,使用起来就非常方便。与反向搜索一样,正向搜索会更改提示,如下所示。

(i-search)`SHOW': SHOW DATABASES;

您还可以配置存储在历史记录中的项目数量(默认为 100),以及在名为~/.mysqlsh/history(Windows 上为%AppData%\MySQL\mysqlsh\history)的文件中查看历史记录。但是,您必须使用\history 命令保存历史,或者将 shell 配置为自动保存历史。我们将在后面的章节中看到如何做到这一点。您可以使用\history 命令随时查看历史列表,如下所示。

SQL > \history
    1  \history
    2  \connect root@localhost:3306
    3  SHOW DATABASES;
    4  SHOW TABLES FROM world_x;
    5  SHOW VARIABLES LIKE '%ssh%';
    6  SHOW VARIABLES LIKE '%ssl%';

\history 命令还允许您删除历史记录中的一个或多个条目,清除历史记录(在切换模式时很有用),并保存历史记录。命令选项如下所示。

  • \history del 2-4:从历史记录中删除条目 2、3 和 4。

  • \history clear:清除本次会话的历史记录。

  • \history save:保存历史到文件。

注意

只有那些交互输入的命令才会保存在历史记录中。批处理执行不会将命令保存到历史文件中。

保存密码

现在让我们来讨论 shell 的最新和最有成效的特性之一——秘密存储。这是通过称为可插拔密码存储的功能实现的,该功能支持多种存储机制,包括秘密存储、钥匙串等。

它允许您安全地存储常用的密码,这使得使用 MySQL Shell 更加容易和安全。您可以使用秘密储存装置(如钥匙串)来存储服务器连接的密码。您以交互方式输入连接的密码,该密码作为连接的凭证与服务器 URL 一起存储。

小费

参见 https://dev.mysql.com/doc/mysql-shell/8.0/en/mysql-shell-pluggable-password-store.html 了解更多关于使用可插拔密码存储器的信息。

事实上,到目前为止,您已经看到该消息多次出现在 shell 的输出中,在所有示例中,我只需对该提示按 enter 键就可以回答“no ”,这告诉 shell 不要存储密码。

Please provide the password for 'root@localhost:3306': ∗∗∗∗∗∗∗∗∗∗
Save password for 'root@localhost:3306'? [Y]es/[N]o/Ne[v]er (default No):

现在,让我们补救一下,并在我们的系统上永久保存该密码!清单 3-12 显示了运行 shell 来执行一个简单查询的脚本。第一次,我告诉 shell 记住密码。第二次,我不再需要记住密码,shell 也不会提示我输入密码。

C:\Users\cbell>mysqlsh --uri=root@localhost:3306 --sql -e "SELECT Name, District, JSON_EXTRACT(info, '$.Places_of_interest[∗].name') as Sights FROM world_x.city WHERE JSON_EXTRACT(info, '$.Places_of_interest') IS NOT NULL" --vertical
Please provide the password for 'root@localhost:3306': ∗∗∗∗∗∗∗∗∗∗
Save password for 'root@localhost:3306'? [Y]es/[N]o/Ne[v]er (default No): Y
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
    Name: Baltimore
District: Maryland
  Sights: ["National Harbor"]
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 2\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
    Name: Washington
District: District of Columbia
  Sights: ["Smithsonian National Air and Space Museum"]

C:\Users\cbell>mysqlsh --uri=root@localhost:3306 --sql -e "SELECT Name, District, JSON_EXTRACT(info, '$.Places_of_interest[∗].name') as Sights FROM world_x.city WHERE JSON_EXTRACT(info, '$.Places_of_interest') IS NOT NULL" --vertical
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
    Name: Baltimore
District: Maryland
  Sights: ["National Harbor"]
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 2\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
    Name: Washington
District: District of Columbia
  Sights: ["Smithsonian National Air and Space Museum"]

Listing 3-12Saving Passwords with the Secret Store

如果您想在忘记或更改密码时重置密码,可以使用一个特殊的对象(全局变量),该对象是一个可用于自定义可插拔密码功能的类。由于定制 shell 是一个更大的主题,我们将在下一节讨论如何定制 shell,在下一节我们还将看到如何重置存储的密码。

定制 Shell

我们将讨论的最后一个教程是如何定制 shell。您可以更改几个控制 shell 操作方式的参数。这包括更改提示以及自动完成、等待时间、输出格式等等。让我们从它是如何工作的开始。

有三种方法可以在 shell 中设置配置选项。您可以使用\option命令来列出、设置甚至取消设置选项及其值,或者您可以在 Python 或 JavaScript 模式下使用shell.option对象,或者您可以修改磁盘上的配置文件。让我们来看看这些技术的实际应用。

使用\option命令

\option命令在任何模式下都有效。\option命令是设置选项的更常用方法。清单 3-13 显示了\option命令的帮助文本以及配置命令历史的选项列表。

Py > \help \option
NAME
      \option - Allows working with the available shell options.

SYNTAX
      \option [args]

DESCRIPTION
      The given [args] define the operation to be done by this command, the
      following values are accepted

      - -h, --help [<filter>]: print help for the shell options matching
        filter.
      - -l, --list [--show-origin]: list all the shell options.
      - <shell_option>: print value of the shell option.
      - <shell_option> [=] <value> sets the value for the shell option.
      - --persist causes an option to be stored on the configuration file
      - --unset resets an option value to the default value.
Py > \option --help history
 history.autoSave           Shell's history autosave.
 history.maxSize            Shell's history maximum size
 history.sql.ignorePattern  Shell's history ignore list.

Py > \option history.autoSave
false

Py > \option history.maxSize
1000

Py > \option history.sql.ignorePattern
∗IDENTIFIED∗:∗PASSWORD∗

Listing 3-13Using the \option Command

这里,我们看到了一个使用\option \help命令列出命令历史功能可用选项的例子。虽然这仅仅显示了选项的名称,但是我们可以使用\option命令来查看每个特定选项的值。如果你想看到所有可用的选项及其值,使用\option --list命令。

小费

如果设置选项时没有使用--persist参数,当 shell 关闭时,更改不会被保存。您必须使用参数来保存更改,以便在以后的执行中保持(保存)更改。

回想一下上一节,我们可以让 shell 自动保存历史,这样我们就不必手动保存了。正如我们在清单 3-13 中看到的,我们可以将history.autoSave选项设置为true,如下所示。注意,这里我们只是使用赋值参数(等号)来设置值,然后保存(持久化)更改。

Py > \option --persist history.autoSave = true

使用shell.option对象

我们还可以通过使用shell.option对象和set()方法(为 shell 的当前执行设置值)或者使用set_perist()方法永久保存值,在其中一种脚本模式中设置选项。为了设置选项,我们使用选项类别和选项的名称作为引号(单引号或双引号)内的点字符串。例如,下面的代码设置了自动历史保存,就像我们在上一节中看到的那样。

Py > shell.options.set_persist("history.autoSave", True)

shell 对象有几个方法可以与选项一起使用,如下所示。

  • shell.options.set(<option_name>, <value>):将<option_name>设置为此会话的值,不保存更改。

  • shell.options.set_persist(<option_name>, <value>):将<option_name>设置为此会话的值,并将更改保存到配置文件。

  • shell.options.unset(<option_name>):将<option_name>重置为该会话的默认值,更改不会保存到配置文件中。

  • shell.options.unset_persist(<option_name>):将<option_name>重置为该会话的默认值,并将更改保存到配置文件。

清单 3-14 展示了一个使用 shell 对象和自动完成特性(通过按下 TAB 两次来启动)来取消设置值,将其更改为缺省值并保存它的例子。最后,我们看看如何将所有选项恢复到它们的默认值。

Py > shell.options.<TAB><TAB>
autocomplete.nameCache          devapi.dbObjectHandles          pager
batchContinueOnError            history.autoSave                passwordsFromStdin
credentialStore.excludeFilters  history.maxSize                 sandboxDir
credentialStore.helper          history.sql.ignorePattern       showWarnings
credentialStore.savePasswords   interactive                     useWizards
dba.gtidWaitTimeout             logLevel
defaultMode                     outputFormat
Py > shell.options.set("history.autoSave", True)

Py > print(shell.options["history.autoSave"])
True
Py > shell.options.unset_persist("history.autoSave")

Listing 3-14Using the shell.option Object

使用配置文件

更改和保留的选项以 JSON 格式存储在配置文件中。值在启动时读取,当您使用持久化功能时,设置会保存到配置文件中。因此,您还可以通过将选项添加到配置文件中来更改它们,或者如果选项已经存在,您可以在文件中更改它们并重新启动 shell 以使它们生效。下面显示了配置文件的一个示例。所有选项和值都作为键、值对存储在同一个 JSON 文档中。

C:\Users\cbell\AppData\Roaming\MySQL\mysqlsh>more options.json
{
    "history.autoSave": "true"
}

配置文件的位置是用户配置路径,该文件被命名为options.json。在 Windows 上,该文件位于%APPDATA%\MySQL\mysqlsh\options.json,或者在 Linux 上,位于~/.mysqlsh/options.json

第一次更改选项时会创建配置文件。虽然您可以编辑此文件来更改选项,但必须非常小心。这是因为该文件被视为内部文件,用户不能更改。如果您犯了一个错误,设置了错误的选项(比如拼错了名称),shell 可能不会启动并抛出一个错误。因此,您应该注意首先以交互方式设置选项,然后编辑文件来更改它。虽然这仍不被认为是“安全的”,但可以通过编辑文件来更改选项。

警告

不建议直接编辑options.json文件。

使用保存的密码

您可以在 shell 中配置的事情之一是可插入的密码身份验证,或者用更实际的术语来说,保存在 secret store 中的凭证。在这种情况下,我们可能想要撤销某些密码,因为我们已经更改了它们,或者我们可能想要查看存储了哪些凭证。

我们可以使用 shell 对象中的一个或多个函数与这个特性进行交互。例如,下面显示了如何列出存储的凭据。这将返回所有存储的凭据的列表(或者,如果缺少变量,则打印返回的列表)。在这种情况下,只存储一个凭据。请注意,不打印任何密码。

Py > shell.list_credentials()
[
    "root@localhost:3306"
]

以下函数允许您使用可插拔密码存储。您可以列出可用的秘密存储帮助程序,以及列出、存储和检索凭据。要使用其中的任何一个,您必须以一种脚本模式(Python 或 JavaScript)执行它们。然而,值得注意的是,由于命名约定的不同,Python 和 JavaScript 模式之间的方法名称略有不同。例如,JavaScript 名称遵循类似于 camelCase 的不同模式,Python 在名称中使用下划线。我们将在本书中使用 Python 示例。

  • cred_list = list_credentials():返回存储的所有凭证列表(无密码!)

  • delete_credential(<URI>):删除给定 URI 的凭证

  • delete_all_credentials():删除当前存储的所有凭证

  • cred_helpers = list_credential_helpers():返回凭证帮助者列表

  • store_credential(<URI>, [<password>]):存储给定 URI 的凭证,可选地指定密码(如果未提供,则提示输入密码)

如果您想要替换其中一个凭证——比方说更改与之相关的密码——您可以使用提供相同 URI 的shell.store_credential()方法,如下所示。在本例中,我省略了 password 参数,所以 shell 会提示输入密码。

Py > shell.store_credential("root@localhost:3306")
Please provide the password for 'root@localhost:3306': ∗∗∗∗∗∗∗∗∗∗

如果我们想刷新所有凭证,我们可以使用shell.delete_all_credentials()方法。

更改提示

最后,shell 允许您更改交互式会话的提示。如果您想要显示一个提醒或类似的提示,例如使用不同的数据库或服务器,您可能需要这样做。

更改提示需要编辑系统上的文件。这个文件称为提示主题文件,可以使用MYSQLSH_PROMPT_THEME环境变量或者将主题模板文件保存到 Linux 上的~/.mysqlsh文件夹中的一个名为prompt.json的文件或者 Windows 上的%AppData%\MySQL\mysqlsh目录中来指定。

您可以在安装 shell 的目录share\mysqlsh\prompt中找到示例提示主题文件。例如,在 Windows 中,文件存储在c:\Program Files\MySQL\MySQL Shell 8.0中。下面显示了提示主题文件的列表。

10/04/2018  02:54 AM             1,245 prompt_16.json
10/04/2018  02:54 AM             2,137 prompt_256.json
10/04/2018  02:54 AM             1,622 prompt_256inv.json
10/04/2018  02:54 AM             2,179 prompt_256pl+aw.json
10/04/2018  02:54 AM             1,921 prompt_256pl.json
10/04/2018  02:54 AM               183 prompt_classic.json
10/04/2018  02:54 AM             2,172 prompt_dbl_256.json
10/04/2018  02:54 AM             2,250 prompt_dbl_256pl+aw.json
10/04/2018  02:54 AM             1,992 prompt_dbl_256pl.json
10/04/2018  02:54 AM             1,205 prompt_nocolor.json

在这里,我们看到了几个为各种常见的提示定制而预先格式化的文件。有不同的颜色主题以及在问题中显示的内容。例如,下面显示了经典主题:

{
  "symbols" : {
    "separator" : "-",
    "separator2" : "-",
    "ellipsis" : "-"
  },
  "segments": [
    {
      "text": "mysql"
    },
    {
      "text": "%mode%"
    }
  ]
}

如果您想进行自己的更改,可以在位于应用安装的share\mysqlsh\prompt目录下的README.prompt文件中找到记录的格式。您可以使用主题来指定特殊字体、终端颜色等。

但是,在创建提示主题文件时要小心,因为如果在提示主题文件中发现错误,将会打印一条错误消息并使用默认的提示主题。我建议在构建自己的提示主题文件之前,花些时间阅读一下README.prompt文件并研究一下示例。此外,请记住,这些文件可能会指定依赖于平台的设置,并且可能不会普遍适用。

摘要

MySQL Shell 代表了 MySQL 用户生产力的一次重大飞跃。shell 不仅是一个更好的 MySQL 客户端,它还是一个代码编辑器和测试环境。在这一章中,我们对 shell 及其主要特性进行了一个简短的浏览,包括内置命令、如何格式化输出,甚至如何定制 shell。在接下来的章节中,我们将再次应用我们到目前为止所学到的知识,探索如何在各种任务中使用 shell。

虽然在本章中我们还没有学到关于 MySQL Shell 的所有知识,但是我们已经了解了它是如何工作的,以及如何使用 MySQL Shell 提高工作效率。如果您想了解构成 MySQL Shell 的所有细微差别,请参阅在线用户指南( https://dev.mysql.com/doc/mysql-shell/8.0/en/ )。

在下一章中,我们将进行一次使用 SQL 数据库的向导之旅。我们将看到使用 SQL 接口的简要概述,但是我们将把重点放在使用 X DevAPI 处理关系数据库上。如果您非常熟悉 SQL 数据库和使用 SQL 命令,您可能希望浏览本章的 SQL 部分,然后浏览第五章中的示例,这些示例演示了如何将 MySQL Shell 用于 SQL 数据库,包括如何使用新的 X DevAPI for SQL 数据库。

四、对 SQL 数据库使用 Shell

大多数使用 MySQL 的人使用结构化查询语言(SQL)接口来利用关系数据库功能与他们的数据进行交互。正如我们所见,MySQL Shell 是一个非常强大的客户端,您可以使用结构化查询语言(SQL)语句来处理您的数据。然而,MySQL Shell 也是一个强大的脚本语言编辑器和执行引擎。

在本章中,我们将简要了解什么是 SQL 数据库以及如何在 shell 中使用它们,包括使用 SQL 的简要概述。然而,我们不会在这里花太多时间,因为很多人都熟悉 SQL。无论你是否是 MySQL 和 SQL 的新手,我建议你阅读这些章节,这样你就能理解我们将在本章后面使用的访问方法。

虽然我们会看到一些简短的例子,但第五章包含了一个更详细的例子,展示了如何将 MySQL Shell 用于 SQL 数据库,包括如何使用新的 X DevAPI for SQL 数据库。

让我们先简单介绍一下 MySQL 的 SQL 接口。

重温关系数据库

如你所知,MySQL 是作为后台进程(Windows 中的一项服务)运行的。如果您从命令行启动它,也可以将其作为前台进程运行。像大多数数据库系统一样,MySQL 支持 SQL。您可以使用 SQL 创建数据库和对象(使用数据定义语言[DDL]),写入或更改数据(使用数据操作语言[DML]),以及执行各种命令来管理服务器。

DDL 语句是我们用来在数据库中创建存储机制(对象,如表)的语句——包括数据库本身。另一方面,DML 语句是为存储和检索数据(行)而设计的。MySQL 还支持其他实用的 SQL 命令,比如显示系统状态、变量和类似元数据的命令。清单 4-1 展示了每种形式(DDL 和 DML)的一个例子,以及一些实用的 SQL 命令。 1

C:\Users\cbell>mysqlsh --sql --uri root@localhost:3306
...
SQL > CREATE DATABASE test_db;
Query OK, 1 row affected (0.0586 sec)

SQL > USE test_db;
Query OK, 0 rows affected (0.0003 sec)

SQL > CREATE TABLE test_tbl (id int auto_increment, name char(20), primary key(id));
Query OK, 0 rows affected (0.0356 sec)

SQL > INSERT INTO test_tbl VALUES (NULL, 'one');
Query OK, 1 row affected (0.1117 sec)

SQL > INSERT INTO test_tbl VALUES (NULL, 'two');
Query OK, 1 row affected (0.0078 sec)

SQL > INSERT INTO test_tbl VALUES (NULL, 'three');
Query OK, 1 row affected (0.0109 sec)

SQL > SELECT ∗ FROM test_tbl;
+----+-------+
| id | name  |
+----+-------+
|  1 | one   |
|  3 | three |
|  4 | one   |
|  5 | two   |
|  6 | three |
+----+-------+
5 rows in set (0.0011 sec)

SQL > DELETE FROM test_tbl WHERE id = 2;
Query OK, 0 rows affected (0.0005 sec)

SQL > SELECT ∗ FROM test_tbl;
+----+-------+
| id | name  |
+----+-------+
|  1 | one   |
|  3 | three |
|  4 | one   |
|  5 | two   |
|  6 | three |
+----+-------+
5 rows in set (0.0005 sec)

SQL > SHOW TABLES;
+-------------------+
| Tables_in_test_db |
+-------------------+
| test_tbl          |
+-------------------+
1 row in set (0.0015 sec)

SQL > SHOW DATABASES;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sakila             |
| sys                |
| test_db            |
| world              |
| world_x            |
+--------------------+
8 rows in set (0.0009 sec))

SQL > SELECT @@version;
+-----------+
| @@version |
+-----------+
| 8.0.16    |
+-----------+
1 row in set (0.0004 sec)

SQL > DROP DATABASE test_db;
Query OK, 1 row affected (0.2659 sec)

Listing 4-1Example DDL and DML Statements

注意

您必须用分号(;\G来结束每个 SQL 命令。

这个例子演示了以CREATE DATABASECREATE TABLE语句形式的 DDL 语句,以INSERTDELETESELECT语句形式的 DML 语句,以及几个实用程序语句,包括一个简单的管理命令来检索全局服务器变量(@@version)。

请注意创建数据库和表来存储数据,在表中添加几行,删除一行,最后检索表中的数据。注意我是如何用大写字母表示 SQL 命令关键字的。这是一种常见的做法,有助于使 SQL 命令更容易阅读,更容易找到用户提供的选项或数据(小写)。

MySQL 中有很多可用的命令。幸运的是,你只需要掌握几个比较常见的。以下是您最常使用的命令。<>中包含的部分表示用户提供的命令组件,而[...]表示需要额外的选项。

  • CREATE DATABASE <database_name>:创建数据库

  • USE <database>:设置默认数据库(不是 SQL 命令)

  • CREATE TABLE <table_name> [...]:创建一个表格或结构来存储数据

  • INSERT INTO <table_name> [...]:向表格中添加数据

  • UPDATE [...]:更改特定行的一个或多个值

  • DELETE FROM <table_name> [...]:从表格中删除数据

  • SELECT [...]:从表格中检索数据(行)

  • SHOW [...]:显示对象、系统变量等的列表

虽然这个列表只是一个简短的介绍,并不像一个完整的语法指南,但有一个很好的在线参考手册详细解释了每个命令(以及更多)。当你对 MySQL 有任何疑问时,你应该参考在线参考手册。你可以在 https://dev.mysql.com/doc/refman/8.0/en/sql-syntax.html 找到 MySQL 支持的每个 SQL 命令的解释和细节。

显示的一个更有趣的命令允许您查看对象列表。例如,您可以看到带有SHOW DATABASES的数据库,带有SHOW TABLES的表列表(一旦您用USE命令设置了默认数据库),甚至带有SHOW GRANTS的用户权限。我发现自己经常使用这些命令。

如果你认为 MySQL 不仅仅是几个简单的命令,那你就错了。尽管 MySQL 易于使用且启动时间快,但它是一个成熟的关系数据库管理系统(RDBMS)。比你在这里看到的要多得多。有关 MySQL 的更多信息,包括所有高级特性,请参见在线参考手册。

什么是关系数据库管理系统?

RDBMS 是一种基于数据关系模型的数据存储和检索服务,由 E. F. Codd 于 1970 年提出。这些系统是结构化数据的标准存储机制。大量的研究致力于改进 Codd 提出的基本模型,正如 C. J. Date 在《数据库关系模型:回顾和分析》中所讨论的那样。这种理论和实践的演变最好地记录在 Date 的第三宣言中。

关系模型是存储库(数据库)的直观概念,可以通过使用一种称为查询语言的机制来检索、更新和插入数据,从而方便地查询存储库。许多供应商已经实现了关系模型,因为它具有完善的系统理论、坚实的数学基础和简单的结构。最常用的查询机制是 SQL,它类似于自然语言。虽然关系模型中不包括 SQL,但它提供了关系模型在 RDBMSs 中的实际应用的一个组成部分。

现在,您已经知道了什么是 MySQL,并且已经看到了一个用于处理数据的 SQL 命令的简洁示例,让我们来探索一些成功部署和使用 MySQL 来存储和检索数据所需的更常见的概念和操作。

使用 MySQL 命令和函数

学习和掌握数据库系统需要训练、经验和极大的毅力。精通所需的主要知识是如何使用常见的 SQL 命令和概念。本节通过介绍最常见的命令和概念来完成 MySQL 和 SQL 入门。

本节介绍了高级命令和概念,而不是重复参考手册。如果您决定使用任何命令或概念,并需要更多信息,请参考在线参考手册,了解更多详细信息、完整的命令语法和其他示例。但首先,让我们澄清一些我们在使用传统关系数据库(SQL)处理 MySQL 时使用的术语。

术语

在 MySQL 中,像其他关系数据库系统一样,我们以固定的方式存储数据,我们使用数据库来存储给定任务、工作、应用、域等的数据。我们使用来存储相似的数据(具有相同格式的数据)。在一个中,数据被表示为,每一行都具有相同的格式(或模式)。

如果您以前从未使用过数据库,您可以松散地关联一个关系数据库表,如电子表格 2 ,其中定义了列,每一行包含每一列的值。因此,插入数据或检索数据需要将数据作为表中的行来形成或查看。

创建用户和授予访问权限

要开始处理数据,您需要在使用 MySQL 之前了解两个管理操作:创建用户帐户和授予对数据库的访问权限。MySQL 可以用CREATE USERGRANT语句来执行这些操作。要创建一个用户,您可以发出一个CREATE USER命令,后跟一个或多个GRANT命令。例如,下面显示了名为的用户的创建,并授予该用户对名为 store_inventory 的数据库的访问权限:

CREATE USER 'jane'@'%' IDENTIFIED BY 'secret';
GRANT SELECT, INSERT, UPDATE ON store_inventory.∗ TO 'jane'@'%';

第一个命令创建名为 jane 的用户,但是该名称也有一个@后跟另一个字符串。第二个字符串是与用户相关联的机器的主机名。也就是说,MySQL 中的每个用户都有一个用户名和一个主机名,以user@host的形式来惟一地标识他们。这意味着用户和主机jane@10.0.1.16和用户和主机jane@10.0.1.17是不同的。但是,%符号可以用作通配符,将用户与任何主机关联起来。IDENTIFIED BY子句为用户设置密码。

警告

为您的应用创建没有 MySQL 系统完全访问权限的用户帐户并为管理员保留完全访问权限总是一个好主意。此外,您应该避免对主机使用通配符,这样您就可以将用户限制到已知的机器(IP 地址)、子网等。这是为了最大限度地减少任何意外更改,并防止被利用。

对主机使用通配符%时要小心。虽然创建单个用户并让用户从任何主机访问数据库服务器变得更加容易,但这也使得一些怀有恶意的人更容易从任何地方访问您的服务器(一旦他们发现了密码)。

第二个命令允许访问数据库。您可以授予用户许多权限。这个例子展示了您最有可能给数据库用户的集合:read ( SELECT)、add data ( INSERT)和 change data ( UPDATE)。有关安全性和帐户访问权限的更多信息,请参见在线参考手册。 3

该命令还指定要授予权限的数据库和对象。因此,可以给用户一些表的读(SELECT)权限,给另一些表的写(INSERTUPDATE)权限。这个例子给了用户访问所有对象(表、视图等)的权限。)在商店 _ 库存数据库中。

小费

MySQL 的新版本不再允许你用GRANT语句创建用户。您必须首先显式创建用户。

创建数据库和表

您需要学习和掌握的最基本的命令是CREATE DATABASECREATE TABLE命令。回想一下,MySQL 之类的数据库服务器允许您创建任意数量的数据库,您可以用逻辑方式添加表和存储数据。

创建数据库

要创建一个数据库,使用CREATE DATABASE后跟一个数据库名称。发出命令后,shell 不会将上下文“切换”到该数据库(像其他一些客户端一样)。相反,如果您想要设置默认数据库,您必须使用USE <database>命令。每当您决定在 SQL 命令的名称限定符中省略使用数据库时,这是需要的。

例如,通过以<database>.<table>的形式指定数据库和表,您可以使用SELECT命令从任何数据库的任何表中选择行。请注意,我们用句点分隔名称。此外,SELECT ∗ FROM db1.table1将执行,不管默认的数据库设置。您应该养成在命令中指定数据库的习惯。下面显示了创建和更改数据库焦点的两个命令:

CREATE DATABASE factory_sensors;
USE factory_sensors;

创建表格

要创建一个表格,使用CREATE TABLE命令。该命令有许多选项,不仅允许您指定列及其数据类型,还允许您指定附加选项,如索引、外键等。下面显示了如何创建一个简单的表来存储装配线的传感器数据。

CREATE TABLE `factory_sensors`.`trailer_assembly` (
  `id` int auto_increment,
  `sensor_name` char(30) NOT NULL,
  `sensor_value` float DEFAULT NULL,
  `sensor_event` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `sensor_units` char(15) DEFAULT NULL,
  PRIMARY KEY `sensor_id` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

注意,我在这里指定了表名( trailer_assembly )和四列( sensor_namesensor_valuesensor_eventsensor_units )。我使用了几种数据类型。对于 sensor_name ,我使用了一个最多 30 个字符的字符字段,一个浮点数据类型用于 sensor_value ,一个时间戳值用于 sensor_event ,另一个字符字段用于 15 个字符的 sensor_units

我还添加了一个带有自动增量列的索引,以确保我们可以用相同的名称存储传感器值。也就是说,我们可能会在一段时间内对相同的传感器进行多次采样。也可以使用CREATE INDEX命令创建索引。

注意

这个虚构的表来自一个工作概念,在这个概念中,传感器数据在一个给定的时间段(比如 24 小时)内放在一个表中,然后移动到另一个系统进行分析。因此,该表不是为长时间存储传感器数据而设计的。

注意TIMESTAMP栏。这种数据类型的列在传感器网络或物联网(IOT)解决方案中,或者在您想要记录事件或操作的日期和时间的任何时候特别有用。例如,知道何时读取传感器值通常是有帮助的。通过向表中添加一个TIMESTAMP列,您不需要在数据收集时计算、读取或格式化日期和时间。

还要注意,我指定将 sensor_name 列定义为一个键,这将创建一个索引。在这种情况下,它也是主键。PRIMARY KEY短语告诉服务器确保表中存在且只有一行匹配列的值。通过重复关键字,可以指定几个要在主键中使用的列。注意,所有主键列都不允许空值(NOT NULL)。

注意

此示例是工厂设置中典型传感器网络的高级概念,具有代表性,用于教学目的。

如果您不能确定唯一标识一行的一组列(您希望这样做——有些人喜欢没有这种限制的表,但是一个好的数据库管理员(DBA)不会这样做),那么您可以为整数字段使用一个称为AUTO INCREMENT的人工数据类型选项。当用于某一列(必须是第一列)时,服务器会为插入的每一行自动增加该值。这样,它就创建了一个默认主键。有关自动递增列的更多信息,请参见联机参考手册。

但是,最佳实践表明,在某些情况下,在字符字段上使用主键并不是最佳选择,例如表中的每一列都有很大的值或者有许多唯一值。这可能会降低搜索和索引的速度。在这种情况下,您可以使用 auto increment 字段来人工添加一个更小的主键(但有点神秘)。

可用的数据类型比上一个示例中显示的多得多。您应该查阅联机参考手册,以获得数据类型的完整列表。参见“数据类型一节。"如果你想知道一个表格的布局或“模式”,使用如下所示的SHOW CREATE TABLE命令。

SQL > SHOW CREATE TABLE factory_sensors.trailer_assembly \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
       Table: trailer_assembly
Create Table: CREATE TABLE `trailer_assembly` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `sensor_name` char(30) NOT NULL,
  `sensor_value` float DEFAULT NULL,
  `sensor_event` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `sensor_units` char(15) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=latin1
1 row in set (0.0009 sec)

像数据库一样,您也可以使用SHOW TABLES命令获得数据库中所有表的列表。

存储数据

现在您已经创建了一个数据库和表,您将希望向表中加载或插入数据。您可以使用INSERT INTO语句来实现。这里,我们为行指定表和数据。下面是一个简单的例子:

INSERT INTO factory_sensors.trailer_assembly (sensor_name, sensor_value, sensor_units) VALUES ('paint_vat_temp', 32.815, 'Celsius');

在本例中,我为拖车装配线上的一个传感器手动插入数据。你想知道其他的柱子呢?在这种情况下,其他列包括一个时间戳列,它将由数据库服务器填充。所有其他列(只有一列)将被设置为 NULL,这意味着没有值可用、缺少值、值不为零或值为空。对于自动增量和时间戳列,NULL 触发它们的行为,例如将值设置为下一个唯一的整数或捕获当前日期和时间。下面显示了在表中插入这一行的示例。

SQL > SELECT ∗ FROM factory_sensors.trailer_assembly \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
sensor_name: paint_vat_temp
sensor_value: 32.815
sensor_event: 2019-02-01 14:59:35
sensor_units: Celsius
1 row in set (0.0005 sec)

请注意,我在该行的数据之前指定了列。当您希望插入的列数少于表中包含的列数时,这是必要的。更具体地说,关闭列列表意味着您必须为表中的所有列提供数据(或NULL)。此外,列出的列的顺序可以不同于它们在表中的定义顺序。关闭列列表将导致列数据根据它们在表中的显示方式进行排序。

您也可以使用逗号分隔的行值列表,使用相同的命令插入几行,如下所示:

INSERT INTO factory_sensors.trailer_assembly (sensor_name, sensor_value, sensor_units) VALUES ('tongue_height_variance', 1.52, 'mm'), ('ambient_temperature', 24.5, 'Celsius'), ('gross_weight', 1241.01, 'pounds');

这里我用相同的命令插入了几行。请注意,这只是一种简化机制,除了自动提交之外,与发出单独的命令没有什么不同。

更新数据

有时,您需要更改或更新数据。您可能需要更改一列或多列的值,替换多行的值,或者更正数字数据的格式甚至比例。为了更新数据,我们使用UPDATE命令。

您可以更新一列、更新一组列、对一列或多列执行计算等等。本节中使用的示例—工厂传感器网络—不太可能需要更改数据(IOT 就是按照记录存储数据,只要数据相关就存储),但有时在传感器读取代码出错或类似数据输入问题的情况下,这可能是必要的。

更有可能的是,您或您的用户想要重命名数据库中的对象。例如,假设我们确定甲板上的植物实际上不是蕨类植物,而是一种外来开花植物。在这种情况下,我们希望将工厂名称为 gross_weight 的所有行更改为 trailer_weight 。以下命令执行更改。注意这里的关键操作符是SET操作符。这告诉数据库为指定的列分配一个新值。您可以在命令中列出多个 set 操作。

UPDATE factory_sensors.trailer_assembly SET sensor_name = 'trailer_weight' WHERE sensor_name = 'gross_weight';

还要注意,我在这里使用了一个WHERE子句将UPDATE限制为一组行。这就是你在SELECT语句中看到的同一个WHERE子句,它做同样的事情;它允许您指定限制受影响的行的条件。如果不使用WHERE子句,更新将应用于所有行。

删除数据

有时,您最终会得到需要删除的表中的数据。也许您使用了测试数据,并希望去除虚假的行,或者也许您希望压缩或清除您的表,或者希望消除不再适用的行。要删除行,使用DELETE FROM命令。

我们来看一个例子。假设您有一个正在开发的工厂监控解决方案,您发现您的一个传感器或传感器节点读取的值太低,原因是编码、布线或校准错误。在这种情况下,我们希望删除传感器值小于 0.001 的所有行(可能是虚假数据)。以下命令可以做到这一点:

DELETE FROM factory_sensors.trailer_assembly WHERE sensor_value < 0.001;

当形成从句时,你应该小心。我喜欢使用带有SELECTWHERE子句来确保我对我想要的行进行操作。使用SELECT以这种方式测试潜在受影响的行比简单地盲目发出命令要安全得多。例如,我将首先发出下面的命令来检查我是否要删除我想要的行,并且只删除那些行。注意这是同一个WHERE条款。

SELECT ∗ FROM factory_sensors.trailer_assembly WHERE sensor_value < 0.001;

警告

发出不带WHERE子句的UPDATEDELETE命令将影响表中的所有行!

选择数据(结果)

您需要知道的最常用的基本命令是从表中返回数据的命令(也称为结果集或行)。为此,您可以使用SELECT语句。这个 SQL 语句是数据库系统的核心。所有对数据的查询都将使用该命令执行。因此,我们将花更多的时间来看看可以使用的各种子句(部分),从列列表开始。

SELECT语句允许您指定想要从数据中选择哪些列。该列表作为语句的第一部分出现。第二部分是FROM子句,它指定了要从中检索行的表。FROM子句还允许您合并两个或多个表中的数据。这被称为连接,并使用JOIN操作符来链接表。在后面的小节中,您将看到一个简单的连接示例。

指定列的顺序决定了结果集中的显示顺序。如果需要所有列,请使用星号()代替。清单 4-2 展示了生成相同结果集的三条语句。也就是说,在每个的输出中将显示相同的行。事实上,为了简单起见,我使用了一个只有四行的表。

SQL > SELECT sensor_name FROM factory_sensors.trailer_assembly;
+------------------------+
| sensor_name            |
+------------------------+
| ambient_temperature    |
| paint_vat_temp         |
| tongue_height_variance |
| trailer_weight         |
+------------------------+
4 rows in set (0.0006 sec)

SQL > SELECT sensor_name, sensor_value, sensor_event, sensor_units FROM factory_sensors.trailer_assembly \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
 sensor_name: ambient_temperature
sensor_value: 24.5
sensor_event: 2019-02-01 15:04:08
sensor_units: Celsius
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 2\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
 sensor_name: paint_vat_temp
sensor_value: 32.815
sensor_event: 2019-02-01 14:59:35
sensor_units: Celsius
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 3\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
 sensor_name: tongue_height_variance
sensor_value: 1.52
sensor_event: 2019-02-01 15:04:08
sensor_units: mm
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 4\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
 sensor_name: trailer_weight
sensor_value: 1241.01
sensor_event: 2019-02-01 15:06:17
sensor_units: pounds
4 rows in set (0.0004 sec)

SQL > SELECT ∗ FROM factory_sensors.trailer_assembly \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
 sensor_name: ambient_temperature
sensor_value: 24.5
sensor_event: 2019-02-01 15:04:08
sensor_units: Celsius
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 2\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
 sensor_name: paint_vat_temp
sensor_value: 32.815
sensor_event: 2019-02-01 14:59:35
sensor_units: Celsius
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 3\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
 sensor_name: tongue_height_variance
sensor_value: 1.52
sensor_event: 2019-02-01 15:04:08
sensor_units: mm
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 4\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
 sensor_name: trailer_weight
sensor_value: 1241.01
sensor_event: 2019-02-01 15:06:17
sensor_units: pounds
4 rows in set (0.0005 sec)

SQL > SELECT sensor_value, sensor_name, sensor_units FROM factory_sensors.trailer_assembly;
+--------------+------------------------+--------------+
| sensor_value | sensor_name            | sensor_units |
+--------------+------------------------+--------------+
|         24.5 | ambient_temperature    | Celsius      |
|       32.815 | paint_vat_temp         | Celsius      |
|         1.52 | tongue_height_variance | mm           |
|      1242.00 | trailer_weight         | pounds       |
+--------------+------------------------+--------------+
4 rows in set (0.0005 sec)

Listing 4-2Example SELECT Statements

请注意,第一条语句列出了表中的传感器名称。接下来的两个语句以相同的顺序生成相同的行和相同的列,但是第三个语句虽然生成相同的行但没有传感器事件,却以不同的顺序显示列。

您还可以使用列列表中的函数来执行计算和类似操作。一个特殊的例子是使用COUNT()函数来确定结果集中的行数,如下所示。注意,我们传入通配符()来计算所有行。关于 MySQL 提供的函数的更多例子,请参阅在线参考手册。 4

SELECT COUNT(∗) FROM factory_sensors.trailer_assembly;

SELECT语句中的下一个子句是WHERE子句。就像我们在更新和删除行中看到的那样,这是您指定想要用来限制结果集中的行数的条件的地方。也就是说,只有那些符合条件的行。这些条件基于列,可能相当复杂。也就是说,您可以基于计算、连接结果等来指定条件。但是为了回答一个问题,大多数条件将是一列或多列上的简单等式或不等式。例如,假设您想要查看传感器读数小于 10.00 的植物。在这种情况下,我们发出以下查询并接收结果。注意,我只指定了两列:传感器名称和从传感器读取的值。

SQL > SELECT sensor_name, sensor_value FROM factory_sensors.trailer_assembly WHERE sensor_value < 10.00;
+------------------------+--------------+
| sensor_name            | sensor_value |
+------------------------+--------------+
| tongue_height_variance |         1.52 |
+------------------------+--------------+
1 row in set (0.0008 sec)

您还可以使用其他子句,包括用于对行进行分组以进行聚合或计数的GROUP BY子句,以及用于对结果集进行排序的ORDER BY子句。让我们从聚合开始,快速地看一下每一个。

假设您想要计算每个传感器在表中读取的传感器值的平均值。在这种情况下,我们有一个包含各种传感器随时间变化的传感器读数的表。虽然该示例只包含四行(因此可能没有统计信息),但是该示例非常清楚地展示了聚合的概念,如清单 4-3 所示。请注意,我们收到的只是四个传感器读数的平均值。

SQL > SELECT sensor_name, sensor_value FROM factory_sensors.trailer_assembly WHERE sensor_name = 'gross_weight';
+--------------+--------------+
| sensor_name  | sensor_value |
+--------------+--------------+
| gross_weight |         1250 |
| gross_weight |         1235 |
| gross_weight |         1266 |
| gross_weight |         1242 |
+--------------+--------------+
4 rows in set (0.0040 sec)

SQL > SELECT sensor_name, AVG(sensor_value) as avg_value FROM factory_sensors.trailer_assembly WHERE sensor_name = 'gross_weight' GROUP BY sensor_name;
+--------------+-----------+
| sensor_name  | avg_value |
+--------------+-----------+
| gross_weight |   1248.25 |
+--------------+-----------+
1 row in set (0.0006 sec)

SQL > SELECT sensor_name, sensor_value FROM factory_sensors.trailer_assembly WHERE sensor_name = 'gross_weight' ORDER BY sensor_value ASC;
+--------------+--------------+
| sensor_name  | sensor_value |
+--------------+--------------+
| gross_weight |         1235 |
| gross_weight |         1242 |
| gross_weight |         1250 |
| gross_weight |         1266 |
+--------------+--------------+
4 rows in set (0.0007 sec)

SQL > SELECT sensor_name, sensor_value FROM factory_sensors.trailer_assembly WHERE sensor_name = 'gross_weight' ORDER BY sensor_value DESC;
+--------------+--------------+
| sensor_name  | sensor_value |
+--------------+--------------+
| gross_weight |         1266 |
| gross_weight |         1250 |
| gross_weight |         1242 |
| gross_weight |         1235 |
+--------------+--------------+
4 rows in set (0.0009 sec)

Listing 4-3GROUP BY Example

请注意,在第二个示例中,我在列列表中指定了 average 函数AVG(),并传入了我想要求平均值的列的名称。MySQL 中有许多这样的函数可以用来执行一些强大的计算。显然,这是另一个例子,说明数据库服务器中存在的能力需要客户机上更多的资源(更不用说对于大型数据集,这意味着在操作之前将数据传输到客户机)。

还要注意,我用关键字AS重命名了平均值列。您可以使用它来重命名任何指定的列,这将更改结果集中的名称,如清单所示。

最后两个示例展示了如何查看按传感器值排序的结果集的结果。我们使用ORDER BY子句按照传感器值的升序和降序对行进行排序。如果将它与 LIMIT 子句结合使用,可以看到最大值(max)和最小值(min ),如下所示。但是最好使用MIN()MAX()函数——参见 https://dev.mysql.com/doc/refman/8.0/en/func-op-summary-ref.html 获得 MySQL 中可用函数的完整列表。

SQL > SELECT sensor_value AS min FROM factory_sensors.trailer_assembly WHERE sensor_name = 'gross_weight' ORDER BY sensor_value ASC LIMIT 1;
+------+
| min  |
+------+
| 1235 |
+------+
1 row in set (0.0008 sec)
SQL > SELECT sensor_value as max FROM factory_sensors.trailer_assembly WHERE sensor_name = 'gross_weight' ORDER BY sensor_value DESC LIMIT 1;
+------+
| max  |
+------+
| 1266 |
+------+
1 row in set (0.0005 sec)

子句的另一个用途是计数。在这种情况下,我们用COUNT()替换了AVG(),得到了与WHERE子句匹配的行数。更具体地说,我们想知道为每个传感器存储了多少个传感器值。

SQL > SELECT sensor_name, COUNT(sensor_value) as num_values FROM factory_sensors.trailer_assembly GROUP BY sensor_name;
+------------------------+------------+
| sensor_name            | num_values |
+------------------------+------------+
| paint_vat_temp         |          1 |
| tongue_height_variance |          1 |
| ambient_temperature    |          1 |
| trailer_weight         |          1 |
| gross_weight           |          4 |
+------------------------+------------+
5 rows in set (0.0008 sec)

正如我提到的,SELECT语句比这里显示的要多得多,但是我们在这里看到的会让你走得更远,尤其是在处理大多数中小型解决方案的典型数据时。

创建索引

创建表时不使用任何排序。也就是说,表是无序的。虽然 MySQL 每次都会以相同的顺序返回数据,但是没有隐含的(或可靠的)顺序,除非您创建一个索引。 5 我这里指的排序并不是你在排序时想的那样(用SELECT语句中的ORDER BY子句就有可能)。

相反,索引是服务器在执行查询时用来读取数据的映射。例如,如果一个表上没有索引,并且希望选择某列中值大于某个值的所有行,则服务器必须读取所有行来查找所有匹配项。但是,如果我们在该列上添加了一个索引,服务器将只能读取那些符合标准的行。

要创建索引,您可以在CREATE TABLE语句中指定索引,或者发出一个CREATE INDEX命令。我们可以使用这个命令在sensor_name列上添加一个索引。清单 4-4 展示了添加索引前后对表结构(模式)的影响。回想一下,我们在前面创建表时添加了主键索引。

SQL > SHOW CREATE TABLE factory_sensors.trailer_assembly \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
       Table: trailer_assembly
Create Table: CREATE TABLE `trailer_assembly` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `sensor_name` char(30) NOT NULL,
  `sensor_value` float DEFAULT NULL,
  `sensor_event` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `sensor_units` char(15) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=latin1
1 row in set (0.0005 sec)

SQL > CREATE INDEX sensor_name ON factory_sensors.trailer_assembly (sensor_name);
Query OK, 0 rows affected (0.2367 sec)

Records: 0  Duplicates: 0  Warnings: 0

SQL > SHOW CREATE TABLE factory_sensors.trailer_assembly \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
       Table: trailer_assembly
Create Table: CREATE TABLE `trailer_assembly` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `sensor_name` char(30) NOT NULL,
  `sensor_value` float DEFAULT NULL,
  `sensor_event` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `sensor_units` char(15) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `sensor_name` (`sensor_name`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=latin1
1 row in set (0.0009 sec)

Listing 4-4Adding Indexes

像这样创建的索引不会影响表中行的唯一性,换句话说,确保存在且只有一行可以被特定列的特定值访问。我所指的是主键(或主索引)的概念,这是在创建表时使用的一个特殊选项,如前所述。

您可以使用如下所示的DROP INDEX命令删除索引。

DROP INDEX sensor_name ON factory_sensors.trailer_assembly;

创建视图

视图是一个或多个表的结果的逻辑映射。它们可以像查询中的表一样被引用,这使它们成为创建数据子集的强大工具。您用CREATE VIEW创建一个视图,并给它起一个类似表格的名字。下面显示了一个简单的例子,其中我们创建了一个测试视图来从表中读取值。在这种情况下,我们限制了视图的大小(行数),但是您可以为视图使用各种各样的条件,包括组合来自不同表的数据。因此,视图可以像表一样用于查询。它们是处理数据子集的便捷方式(当构造正确时)。

SQL > CREATE VIEW list_weights AS SELECT ∗ FROM factory_sensors.trailer_assembly WHERE sensor_units = 'pounds' LIMIT 3;
Query OK, 0 rows affected (0.0525 sec)

SQL > SELECT ∗ FROM factory_sensors.list_weights;
+----+----------------+--------------+---------------------+--------------+
| id | sensor_name    | sensor_value | sensor_event        | sensor_units |
+----+----------------+--------------+---------------------+--------------+
|  4 | trailer_weight |      1241.01 | 2019-02-01 15:40:35 | pounds       |
|  5 | gross_weight   |         1250 | 2019-02-01 15:40:35 | pounds       |
|  6 | gross_weight   |         1235 | 2019-02-01 15:40:35 | pounds       |
+----+----------------+--------------+---------------------+--------------+
3 rows in set (0.0047 sec)

在中小型数据库解决方案中通常不会遇到视图,但是我将它们包括在内是为了在您决定进行额外的分析并希望将数据组织成更小的组以便于阅读时让您了解它们。

简单连接

数据库系统最强大的概念之一是在数据之间建立关系的能力(因此得名关系型)。也就是说,一个表中的数据可以引用另一个(或多个)表中的数据。最简单的形式称为主从关系,其中一个表中的一行引用或关联到另一个表中的一行或多行。

一个常见的(也是经典的)主从关系的例子来自订单跟踪系统,其中一个表包含订单的数据,另一个表包含订单的行项目。因此,我们只存储一次订单信息,如客户号和发货信息,并在检索订单时合并或“连接”这些表。

让我们来看一个名为 world_x 的示例数据库。您可以在 MySQL 网站( http://dev.mysql.com/doc/index-other.html )上找到这个数据库。请随意下载它和任何其他示例数据库。它们都展示了数据库系统的各种设计。您还会发现练习查询数据很方便,因为它包含了许多简单的行。

注意

如果你想运行下面的例子,你需要安装第三章中描述的world_x示例数据库。

清单 4-5 展示了一个简单连接的例子。这里发生了很多事情,所以花点时间检查一下SELECT语句的各个部分,尤其是我是如何指定JOIN子句的。您可以忽略LIMIT选项,因为它只是限制了结果集中的行数。

SQL > SELECT Name, Code, Language FROM world_x.Country JOIN world_x.CountryLanguage ON Country.Code = CountryLanguage.CountryCode LIMIT 10;
+-------------+------+------------+
| Name        | Code | Language   |
+-------------+------+------------+
| Aruba       | ABW  | Dutch      |
| Aruba       | ABW  | English    |
| Aruba       | ABW  | Papiamento |
| Aruba       | ABW  | Spanish    |
| Afghanistan | AFG  | Balochi    |
| Afghanistan | AFG  | Dari       |
| Afghanistan | AFG  | Pashto     |
| Afghanistan | AFG  | Turkmenian |
| Afghanistan | AFG  | Uzbek      |
| Angola      | AGO  | Ambo       |
+-------------+------+------------+
10 rows in set (0.0165 sec)

Listing 4-5Simple JOIN Example

警告

如果您系统的文件系统支持区分大小写的名称,请确保使用一致的命名。比如world_xWorld_X在某些平台上是两个不同的名字。有关区分大小写的标识符的更多信息,请参见 https://dev.mysql.com/doc/refman/8.0/en/identifier-case-sensitivity.html

这里我使用了一个JOIN子句,它接受两个指定的表,这样第一个表使用特定的列及其值连接到第二个表(ON指定匹配)。数据库服务器所做的是从表中读取每一行,并只返回那些列中的值指定匹配的行。一个表中不在另一个表中的任何行都不会被返回。

还要注意,我只包括了几个专栏。在本例中,我从Country表中指定了国家代码和洲,从CountryLanguage表中指定了语言列。如果列名不是惟一的(相同的列出现在每个表中),我就必须用表名来指定它们,比如Country.Name。事实上,总是以这种方式限定列被认为是一种好的做法。

这个例子中有一个有趣的异常,我觉得有必要指出来。事实上,有些人会认为这是一个设计缺陷。注意,在JOIN子句中,我指定了表格和每个表格的列。这是正常且正确的,但是请注意,两个表中的列名不匹配。虽然这真的没有关系,并且只需要一点额外的输入,但是一些 DBA 会认为这是错误的,并且希望在两个表中使用相同的公共列名。

连接的另一个用途是检索公共数据、存档数据或查找数据。例如,假设您有一个表,其中存储了不变(或很少变)的事物的详细信息,例如与邮政编码相关联的城市或与身份证号相关联的名称(例如,社会保险号(SSN))。您可以将这些信息存储在一个单独的表中,并在需要时将数据连接到一个公共列(和值)上。在这种情况下,公共列可以用作外键,这是另一个高级概念。

外键用于维护数据完整性(即,如果一个表中的数据与另一个表相关,但这种关系需要保持一致)。例如,如果您想确保在删除主行时所有的细节行也被删除,您可以在主表中声明一个外键,指向细节表的一列(或多列)。有关外键的更多信息,请参见联机参考手册。 6

关于连接的讨论只涉及最基本的内容。事实上,连接可以说是数据库系统中最困难和最容易混淆的领域之一。如果您发现想要使用联接来组合几个表或扩展数据,以便从几个表中提供数据(外部联接),您应该花一些时间深入学习数据库概念,如 Clare Churcher 的书开始数据库设计(Apress,2012)

其他高级概念

MySQL 中有更多可用的概念和命令,但有两个可能会引起人们的兴趣,那就是PROCEDUREFUNCTION,它们有时被称为例程。我在这里介绍这些概念,以便如果您想探索它们,您可以理解它们是如何在高层次上使用的。

假设您需要运行几个命令来更改数据。也就是你需要在计算的基础上做一些复杂的改变。对于这些类型的操作,MySQL 提供了存储过程的概念。存储过程允许您在调用该过程时执行复合语句(一系列 SQL 命令)。存储过程有时被认为是一种主要用于定期维护的高级技术,但它们在更简单的情况下也很方便。

例如,假设您想要开发您的解决方案,但是由于您正在开发它,您需要定期重新开始,并且想要首先清除所有数据。如果只有一个表,存储过程不会有太大帮助,但是假设有几个表分布在几个数据库中(对于较大的解决方案来说并不少见)。在这种情况下,存储过程可能会有所帮助。

在 shell 中输入带有复合语句的命令时,您需要临时更改分隔符(分号),以便行尾的分号不会终止命令条目。例如,在用复合语句编写命令之前使用DELIMITER //,使用//结束命令,用DELIMITER ;将分隔符改回来。

假设您想执行一个复合语句并返回一个结果——您想将它用作一个函数。您可以使用函数通过执行计算、数据转换或简单的翻译来填充数据。因此,函数可用于提供值来填充列值、提供聚合、提供日期操作等等。

您已经看到了几个函数(COUNTAVG)。这些被认为是内置函数,在线参考手册中有一整节专门介绍它们。但是,您也可以创建自己的函数。例如,您可能希望创建一个函数来对您的数据执行一些数据规范化。更具体地说,假设您有一个传感器,它产生一个特定范围内的值,但是根据该值和来自不同传感器或查找表的另一个值,您想要对该值进行加、减、平均等操作来校正它。您可以编写一个函数来实现这一点,并在触发器中调用它来填充计算列的值。

由于存储过程可能相当复杂,如果您决定使用它们,在尝试开发自己的存储过程之前,请阅读在线参考手册的“CREATE PROCEDURE and CREATE FUNCTION Syntax”一节。 7 创建存储过程的内容远不止这一部分。

换对象呢?

您可能想知道当您需要修改表、过程、触发器等时该怎么做。放心吧,你不必从头开始!MySQL 为每个对象提供了一个ALTER命令。也就是说,有一个ALTER TABLEALTER PROCEDURE等等。关于每个ALTER命令的更多信息,参见在线参考手册标题为数据定义语句的章节。 8

现在,我们已经对使用 SQL 命令和 MySQL 存储和检索关系数据有了一个较高的认识,让我们看看如何使用 X DevAPI 来编写使用相同关系数据的 Python 代码。我们终于要写点 Python 了!

使用 Python 管理数据库

现在我们已经很好地介绍了 SQL 和 MySQL 命令,我们可以将注意力转向一种令人兴奋的在 MySQL 中使用 SQL 数据库的新方法——使用 X DevAPI 编写 Python 脚本来合并代码和数据。

我们将从处理关系数据的角度开始探索 API,而不是对 X DevAPI 必须提供的所有功能进行艰苦、乏味的探索。在这一节中,我们将发现关于 X DevAPI 的足够多的知识,以便能够编写 Python 代码来处理关系数据库对象(数据库和表)。我们把更复杂的部分留到第六章来看 X DevAPI 对 JSON 文档的支持。

然而,我们仍然需要知道 X DevAPI 是什么以及它的主要特性。如前所述,我们将从处理关系数据的角度在本章和下一章探讨其中的一些。API 远不止这些,但是从一个已知的(或者至少是熟悉的)基础开始将有助于那些新手在应用中加入数据库支持。毕竟,X DevAPI 就是为了让从代码中访问数据变得容易!

您应该知道的第一件事是,shell 内置了对我们需要的几个库对象的支持。比如前面列出的mysqlx模块就是 shell 中的内置模块之一。我们将内置模块称为全局变量。shell 还包括以下库作为内置模块(有时称为库或对象)作为全局变量。

  • 会话:如果已经建立了全局会话,则表示全局会话。

  • db :如果已经定义了一个模式,则表示该模式,例如,由 URI 类型的字符串定义。

  • dba :表示 AdminAPI,它是 InnoDB 集群的一个组件,使您能够管理服务器实例集群。有关 InnoDB 集群的更多信息,请参见第十章。

  • mysqlx :提供对连接到 MySQL 服务器产生的会话对象的操作。

  • shell :提供通用功能,例如配置 MySQL Shell。

  • util :提供实用函数,例如,在升级前检查服务器实例。

在学习将 X DevAPI 用于关系数据时,需要理解的关键概念包括。这些在 Python 代码中被表示(实现)为对象。也就是说,我们将使用一个或多个方法来创建对象,然后使用它的一个或多个方法来执行我们的代码以处理数据。

  • CRUD :创建、读取、更新和删除——对数据的基本操作

  • 数据库/模式:一个或多个数据库级对象的容器,如表、视图、触发器等。

  • Result :来自一个读操作(SELECT)或其他操作的一组零行或多行,这些操作产生从服务器返回的值

  • 会话:到 MySQL 服务器的连接,包括管理连接的属性

  • :以特定布局格式化的数据容器,用于将数据存储在具有数据类型的预定义列中

这些概念中的大多数应该是熟悉的,或者至少足够熟悉,以至于学习使用它们不需要太多的努力。例如,作为 SQL 数据库用户,我们理解数据库(模式)、表和结果集的基本概念。代表这些概念的对象只不过是它们行为的模型,我们可以用它们作为方法来调用。

然而,三个最新的概念可能是mysqlx模块、会话和 CRUD 操作。让我们来看看每一个。

MySQL X 模块

mysqlx模块是用 X DevAPI 编写应用的入口点。您可以将该模块视为一个库,其中包含几个可以在 Python 脚本中使用的对象。我们需要的最值得注意的对象是那些可用于连接和处理 MySQL 中的关系数据的类和方法,但是还有更多可用于 JSON 文档的对象。

要理解的关键概念是,对象是从其他对象上调用的方法生成的。更具体地说,当我们在对象a上调用方法x()时,它返回对象b的实例。例如,我们调用方法并将返回的对象赋给另一个变量,就像这个b = a.x()。一旦理解了这一点,就可以检查方法的返回类型,然后引用返回的对象类型来找出它提供了什么方法。

当使用mysqlx模块时,这一切都从返回一个Session对象 9 的连接开始——从get_session()方法返回的也是这个对象。从那里,我们可以调用会话中的方法,它们返回不同的对象。让我们看看mysqlx模块中有哪些类。表 4-1 显示了模块中可用的类别。

表 4-1

mysqlx模块中的类

|

班级

|

描述

|
| --- | --- |
| BaseResult | 服务器返回的不同类型结果的基类 |
| Collection | 集合是一个容器,可用于在 MySQL 数据库中存储文档 |
| CollectionAdd | 集合上文档添加的处理程序 |
| CollectionFind | 集合上文档选择的处理程序 |
| CollectionModify | 更新集合中文档的操作 |
| CollectionRemove | 删除集合中文档的操作 |
| DocResult | 允许遍历由 Collection.find 操作返回的 DbDoc 对象 |
| LockContention | 常数来表示锁争用类型 |
| Result | 允许检索对数据库执行的非查询操作的相关信息 |
| RowResult | 允许遍历表返回的行对象。选择操作 |
| Schema | 表示从使用 X 协议创建的会话中检索的架构 |
| Session | 支持使用 X 协议与 MySQL 服务器进行交互 |
| SqlExecute | SQL 语句执行处理程序,支持参数绑定 |
| SqlResult | 允许在通过 Session.sql 对数据库执行操作后浏览结果信息 |
| Table | 表示模式上的表,通过使用 mysqlx 模块创建的会话进行检索 |
| TableDelete | 从表中删除数据的操作 |
| TableInsert | 表中插入操作的处理程序 |
| TableSelect | 表中记录选择的处理程序 |
| TableUpdate | 表中记录更新操作的处理程序 |
| Type | 常数来表示列对象上的数据类型 |

正如您所看到的,有几个类是由mysqlx模块提供的。如果这看起来有点难以承受,请不要担心。我们不需要所有这些来处理关系数据;大多数是为 JSON 文档(也称为文档存储)设计的。然而,我们将需要使用Session类。

注意

本书中的表格引用了在线 Doxygen 文档( https://dev.mysql.com/doc/dev/mysqlsh-api-python/8.0/ )中记录的 MySQL Shell 中实现的 Python X DevAPI。其他语言的 X DevAPI 在组织以及类和方法的命名方案上可能略有不同。

我们已经在第三章中发现了会话。回想一下,我们使用\connect shell 命令与 shell 中的会话进行交互,这允许您在 shell 交互会话中建立连接。在 Python 中使用会话略有不同。接下来我们来看看Session类。

会话类

Session 类是我们在编写与数据交互的 Python 应用时将使用的主要类。我们使用该模块将连接信息以连接字符串或特定于语言的结构(Python 中的字典)的形式传递给服务器,以将连接参数作为 URI 或连接字典作为参数传递(而不是两者都作为)。获取会话对象最常用的方法如下所示。

get_session(<URI or connection dictionary>)

下面显示了使用连接选项字典获取会话对象实例和使用连接字符串(URI)获取会话对象实例的示例。

import mysqlx
mysqlx_session1 = mysqlx.get_session({'host': 'localhost', 'port': 33060, 'user': 'root', 'password': 'secret'})
mysqlx_session2 = mysqlx.get_session('root:secret@localhost:33060')

如果连接成功,结果变量将指向一个对象实例。如果失败,结果可能是一个错误或未初始化的连接。我们将在下一章看到如何处理这个问题。

一旦我们有了一个会话,我们就可以通过获取一个模式对象来开始处理我们的数据。

模式类

X DevAPI 使用术语“模式”来指代一组集合,它们是文档的集合。然而,当处理关系数据时,我们使用“数据库”来指代表和类似对象的集合。人们可能会认为“模式”是“数据库”的同义词,对于 MySQL 的旧版本来说,这是正确的。然而,当使用文档存储和 X DevAPI 时,应该使用“模式”,而当引用关系数据时,应该使用“数据库”

模式或数据库:重要吗?

从 MySQL 5.0.2 开始,这两个术语通过 SQL 命令CREATE DATABASECREATE SCHEMA成为同义词。然而,其他数据库系统做出了区分。也就是说,在某些情况下,模式是表的集合,而数据库是模式的集合。其他人认为模式是定义数据结构的东西。如果您使用其他数据库系统,请确保检查定义,以便正确使用术语。

开始处理数据时,您需要做的第一件事是选择(获取)一个现有的模式,删除一个现有的模式,或者创建一个新的模式。您可能还想列出服务器上的模式。Session类提供了几个执行这些操作的方法,所有这些方法都返回一个Schema对象。表 4-2 列出了与模式相关的方法、参数和返回值。

表 4-2

会话类–模式方法

|

方法

|

返回

|

描述

|
| --- | --- | --- |
| create_schema(str name) | 计划 | 在数据库上创建一个模式,并返回相应的对象 |
| get_schema(str name) | 计划 | 通过名称从当前会话中检索架构对象 |
| get_default_schema() | 计划 | 检索了配置为会话默认值的架构 |
| get_current_schema() | 计划 | 检索到会话中的活动架构 |
| set_current_schema(str name) | 计划 | 为此会话设置当前架构,并返回其架构对象 |
| get_schemas() | 目录 | 检索了会话中可用的架构 |
| drop_schema(str name) | 没有人 | 删除具有指定名称的架构 |

现在让我们来看看用于执行符合 ACID 的事务的事务方法。

交易方式

事务提供了一种机制,允许一组操作作为单个原子操作执行。例如,如果为一个银行机构建立一个数据库,将资金从一个账户转移到另一个账户的宏操作将优选地被完整地执行(资金从一个账户转移到另一个账户),而不会中断。

事务允许将这些操作封装在一个原子操作中,如果在所有操作完成之前发生错误,该原子操作将取消任何更改,从而避免数据从一个表中删除,并且永远不会进入下一个表。包含在事务命令中的 SQL 语句形式的一组示例操作如下:

START TRANSACTION;
UPDATE SavingsAccount SET Balance = Balance – 100
WHERE AccountNum = 123;
UPDATE CheckingAccount SET Balance = Balance + 100
WHERE AccountNum = 345;
COMMIT;

MySQL 的 InnoDB 存储引擎(默认存储引擎)支持确保数据完整性的 ACID 事务,能够在所有操作成功的情况下提交(保存)结果更改,或者在任何一个操作失败的情况下回滚(撤消)更改。

会话类实现了事务处理的方法,这些方法反映了前面显示的 SQL 命令。表 4-3 列出了交易方式。

表 4-3

交易方式

|

方法

|

返回

|

描述

|
| --- | --- | --- |
| start_transaction() | 没有人 | 在服务器上启动事务上下文 |
| commit() | 没有人 | 提交调用 startTransaction()后执行的所有操作 |
| rollback() | 没有人 | 放弃调用 startTransaction()后执行的所有操作 |
| set_savepoint(str name="") | 潜艇用热中子反应堆(submarine thermal reactor 的缩写) | 创建或替换具有给定名称的事务保存点 |
| release_savepoint(str name) | 没有人 | 删除在事务中定义的保存点 |
| rollback_to(str name) | 没有人 | 将事务回滚到指定的保存点,而不终止事务 |

注意,最后三种方法允许您创建命名事务保存点,这是事务处理的一种高级形式。有关保存点和事务的更多信息,请参见服务器在线参考手册。10

现在,让我们看看与服务器连接相关的方法。

连接方法

下划线连接有两种方法。一个用于检查连接是否打开,另一个用于关闭连接。表 4-4 显示了Session类中剩余的可用实用方法。

表 4-4

连接方法

|

方法

|

返回

|

描述

|
| --- | --- | --- |
| close() | 没有人 | 关闭会话 |
| is_open() | 弯曲件 | 如果已知会话是打开的,则返回 true |

杂项方法

Session 类中还有几个实用方法。表 4-5 列出了附加功能。有关这些方法的更多信息,请参见 X DevAPI 在线参考。

表 4-5

杂项方法

|

方法

|

返回

|

描述

|
| --- | --- | --- |
| quote_name(str id) | 潜艇用热中子反应堆(submarine thermal reactor 的缩写) | 转义标识符 |
| get_uri() | 潜艇用热中子反应堆(submarine thermal reactor 的缩写) | 返回会话的 URI |
| set_fetch_warnings(bool enable) | 没有人 | 启用或禁用警告生成 |
| sql(str sql) | sqlstatesment | 创建一个 SqlStatement 对象,以允许在目标 MySQL 服务器上运行收到的 SQL 语句 |

注意sql()方法。我们可以使用这个方法来发布 SQL 语句,但是一般来说,在处理数据时我们不需要这样做,因为有一个Table对象。一旦我们看到哪些 CRUD 操作可用于处理关系数据,我们将更详细地研究这个类。

CRUD 操作(关系数据)

X DevAPI 实现了一个创建、读取、更新和删除(CRUD)模型,用于处理模式中包含的对象。模式可以包含任意数量的集合、文档、表、视图和其他关系数据对象(如触发器)。在这一节中,我们将看到 schema 和 tables 类的概述。CRUD 模型是为模式中的所有对象实现的,这些对象可以包含文档存储和关系数据的数据。

正如我们将在第六章中看到的,文档存储数据 CRUD 操作使用动词 add、find、modify 和 remove,而关系数据使用匹配等效 SQL 命令(insert、select、update 和 delete)的术语。表 4-6 快速展示了这些方法是如何命名的,以及每个方法的简要描述。注意,我们对文档存储数据使用Collection类,对关系数据使用Table类。

表 4-6

文档存储和关系数据的 CRUD 操作

|

CRUD 操作

|

描述

|

文档存储

|

关系数据

|
| --- | --- | --- | --- |
| 创造 | 添加新项目/对象 | collection.add() | table.insert() |
| 阅读 | 检索/搜索数据 | collection.find() | table.select() |
| 更新 | 修改数据 | collection.modify() | table.update() |
| 删除 | 移除项目/对象 | collection.remove() | table.delete() |

在下面几节中,我们将看到处理关系数据(模式和表)所需的特定于每个类的方法。让我们从查看 Schema 类的细节开始。

模式类

模式是存储数据的对象的容器。回想一下,这可以是文档存储数据的集合,也可以是关系数据的表或视图。就像过去处理关系数据一样,您必须选择(或使用)一个模式来存储集合、表或视图中的数据。

虽然您可以混合使用文档存储数据(集合)和关系数据(表、视图),但为了便于记忆,我们将研究 Schema 类方法,因为它们与处理关系数据有关。

表 4-7 显示了集合和表格的操作方法。同样,在本章和下一章中,我们将只使用这些方法来处理表,但是看一下文档存储方法并没有什么坏处。注意,create 和 get 方法返回一个对象的实例。例如,get_table()方法返回一个Table对象。

表 4-7

模式类方法

|

方法

|

返回

|

描述

|
| --- | --- | --- |
| get_tables() | 目录 | 返回该模式的表列表 |
| get_collections() | 目录 | 返回此架构的集合列表 |
| get_table(str name) | 桌子 | 返回该模式的给定名称的表 |
| get_collection(str name) | 募捐 | 返回此架构的给定名称的集合 |
| get_collection_as_table(str name) | 桌子 | 返回一个代表数据库集合的 Table 对象 |
| create_collection(str name) | 募捐 | 在当前架构中创建具有指定名称的新集合,并检索表示所创建的新集合的对象 |
| drop_collection(str name) | 没有人 | 删除指定的集合 |

现在,让我们看看Table类的方法。

表格类

表的概念是关系数据的主要组织机制。在 X DevAPI 中,表是我们都熟悉的相同的关系数据结构。X DevAPI 有一个Table(您也可以将它们用于视图)类,包含 CRUD 操作(选择、插入、更新和删除)以及用于计算行数或基对象是否是视图的其他方法。表 4-8 显示了Table类的方法。

表 4-8

表格类

|

方法

|

返回

|

描述

|
| --- | --- | --- |
| insert() | 表格插入 | 创建 TableInsert 对象以向表中插入新记录 |
| insert(list columns) | 表格插入 | 使用列列表插入一行 |
| insert(str col1, str col2,...) | 表格插入 | 使用列的参数列表插入一行 |
| select() | 表格选择 | 创建 TableSelect 对象以从表中检索行 |
| select(list columns) | 表格选择 | 创建 TableSelect 对象以从表中检索行 |
| update() | 表格更新 | 创建记录更新处理程序 |
| delete() | 表删除 | 创建记录删除处理程序 |
| is_view() | 弯曲件 | 指示此表对象是否表示数据库上的视图 |
| count() | (同 Internationalorganizations)国际组织 | 返回表格中的行数 |
| get_name() | 潜艇用热中子反应堆(submarine thermal reactor 的缩写) | 返回对象的名称 |
| get_session() | 目标 | 返回该数据库对象的会话对象 |
| get_schema() | 目标 | 返回该数据库对象的架构对象 |
| exists_in_database() | 弯曲件 | 验证该对象是否存在于数据库中 |

请注意,没有创建表的方法。我们必须使用CREATE TABLE SQL 命令或sql()方法来执行 SQL 语句。事实上,没有创建任何关系数据对象的方法。您必须使用 SQL 发出适当的 create 语句来创建对象。例如,在前面的例子中,要为我们的工厂 _ 传感器数据创建一个表,我们可以使用下面的CREATE TABLE语句。虽然我们之前已经看到了这一点,但是下面显示了一个 Python 代码片段,其中我们声明了一个变量来保存查询,并演示了使用sql()方法执行查询。

...
CREATE_TBL = """
CREATE TABLE `factory_sensors`.`trailer_assembly` (
  `id` int auto_increment,
  `sensor_name` char(30) NOT NULL,
  `sensor_value` float DEFAULT NULL,
  `sensor_event` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `sensor_units` char(15) DEFAULT NULL,
  PRIMARY KEY `sensor_id` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
"""
my_session = mysqlx.get_session(user_info)
my_db = my_session.create_schema('factory_sensors')
sql_res = my_session.sql(CREATE_TBL).execute()
my_tbl = my_db.get_table('trailer_assembly')
...

小费

没有创建表或视图的 create 方法。您必须将 SQL 命令传递给sql()方法来创建这些(和其他关系数据)对象。

现在我们已经创建了表格,我们可以使用Table对象插入数据。回想一下,我们在这里有一组对象。我们从Session对象create_schema()方法开始,该方法返回一个Database对象,我们将该对象保存到一个名为my_db的变量中。在我们创建了表格之后,我们调用了my_db.get_table()方法来获取Table对象。

在我们看到处理数据的例子之前,让我们看看处理关系数据的其他类和方法。表 4-9 列出了与关系数据的 CRUD 操作相关的每个类的方法。

表 4-9

关系数据的 CRUD 操作类

|

班级

|

方法

|

返回

|

描述

|
| --- | --- | --- | --- |
| 表格选择 | 对表进行记录检索操作的语句 |
| select (list searchExprStr) | 表格选择 | 定义要从表中检索的列 |
| where (str expression) | 表格选择 | 设置搜索条件以过滤要从表中检索的记录 |
| group_by (list searchExprStr) | 表格选择 | 为检索到的行设置分组标准 |
| having (str condition) | 表格选择 | 为聚合函数运算中要考虑的记录设置条件 |
| order_by (list sortExprStr) | 表格选择 | 设置检索记录的顺序 |
| limit (int numberOfRows) | 表格选择 | 设置选择操作返回的最大行数 |
| offset (int numberOfRows) | 表格选择 | 当定义了限制时,设置结果集上要跳过的行数 |
| bind (str name, Value value) | 表格选择 | 将值绑定到此操作中使用的特定占位符 |
| execute () | RowResult | 使用所有配置的选项执行选择操作 |
| 表格插入 | 对表执行插入操作的语句 |
| insert () | 表格插入 | 初始化记录插入处理程序 |
| insert (list columns) | 表格插入 | 用接收到的列列表初始化记录插入处理程序 |
| insert (str col1, str col2,...) | 表格插入 | 用接收到的列列表初始化记录插入处理程序 |
| values (Value, Value value,...) | 表格插入 | 用给定值向插入操作添加新行 |
| execute () | 结果 | 执行插入操作 |
| 表格更新 | 对表进行记录更新操作的语句 |
| update () | 表格更新 | 已初始化更新操作 |
| set (str attribute, Value value) | 表格更新 | 添加更新操作 |
| where (str expression) | 表格更新 | 设置搜索条件以过滤要更新的记录 |
| order_by (list sortExprStr) | 表格更新 | 设置记录更新的顺序 |
| limit (int numberOfRows) | 表格更新 | 设置操作要更新的最大行数 |
| bind (str name, Value value) | 表格更新 | 将值绑定到此操作中使用的特定占位符 |
| execute () | 结果 | 使用所有配置的选项执行删除操作 |
| 表删除 | 删除表的语句 |
| delete () | 表删除 | 已初始化此记录删除处理程序 |
| where (str expression) | 表删除 | 设置搜索条件以过滤要从表中删除的记录 |
| order_by (list sortExprStr) | 表删除 | 设置删除记录的顺序 |
| limit (int numberOfRows) | 表删除 | 设置操作要删除的最大行数 |
| bind (str name, Value value) | 表删除 | 将值绑定到此操作中使用的特定占位符 |
| execute () | 结果 | 使用所有配置的选项执行删除操作 |

哇,方法真多!请注意,语句类之间有一些相似之处。例如,大多数都有绑定参数、搜索条件等的方法。为了更好地理解这一点,让我们看看 X DevAPI 用户指南中 CRUD 操作的语法图( https://dev.mysql.com/doc/x-devapi-userguide/en/ )。

我们将使用这些类和方法的方式是一个称为方法链的概念,我们可以将我们的类和方法调用组合成一个“链”,在这个“链”中,我们通过使用点标记来扩展语法,为返回的对象调用方法。换句话说,如果method_a()返回一个具有名为count()的方法的对象的实例,我们可以像这样将它链接在一起:method_a().count(),从而避免存储中间对象的需要。

什么是方法链?

方法链接(也称为命名参数习惯用法)是面向对象编程中的一种设计约束,其中每个方法(支持链接)返回一个对象实例。因此,只需将调用添加到第一个方法的末尾,就可以访问(调用)返回对象上的任何方法。

例如,如果一个类X有一个方法a(),它用方法b()返回对象Y,我们可以如下将调用链接在一起。

x = something.get_x()
res = x.a().b()

在这种情况下,x.a()方法首先执行,然后当它返回一个Y对象实例时,它调用 Y 对象实例上的b()方法。

有关方法链接概念的更多信息,请参见 https://en.wikipedia.org/wiki/Method_chaining

以下部分演示了关系数据的 CRUD 操作的简单示例。回想一下,我们将使用从会话中检索的表对象(实例)来执行 CRUD 操作。让我们来看一个例子。

创建数据

创建操作使用一个名为insert()Table对象方法,该方法将一列列表作为参数。然后,我们可以使用方法链(见后面)将值列表作为参数传递,从而将values()方法用于TableInsert对象。这是因为 insert()方法返回了一个TableInsert类的实例。例如,我们用下面的INSERT查询在前面的部分添加了一行。

INSERT INTO factory_sensors.trailer_assembly (sensor_name, sensor_value, sensor_units) VALUES ('paint_vat_temp', 32.815, 'Celsius');

为了在 Python 中执行这个,我们使用下面的语句。注意,我们使用了一个变量来存储列的列表。我们还使用方法链接来调用values()execute()方法,以便在一条语句中完成行插入。

...
COLUMNS = ['sensor_name', 'sensor_value', 'sensor_units']
my_tbl.insert(COLUMNS).values('paint_vat_temp', 32.815, 'Celsius').execute()
...

图 4-1 显示了读取操作的语法图。这里,这个链相当小,因为我们只有两个中间对象(values()execute()的“所有者”)。如果这看起来有点奇怪,不要难过。从 SQL 语句的世界过渡到高级编码技术可能是一个挑战,但是通过实践和更多的例子,像这样链接方法将变得非常自然。

img/478423_1_En_4_Fig1_HTML.jpg

图 4-1

语法图- Table.insert()

阅读日期

从表对象中读取数据是我们使用方法链接将对象方法链接在一起的另一种情况。如果您考虑典型的SELECT语句的复杂性,那么读操作也可能相当复杂就不足为奇了。然而,我们将在这个演示中保持简单,在下一章看更复杂的例子。

下面是获取表中所有行的SELECT语句。

SELECT sensor_value, sensor_name, sensor_units FROM factory_sensors.trailer_assembly;

同样的方法链接也适用于select()方法,它返回一个Table对象,在那里我们链接了where()子句。在这个简单的例子中,我们没有一个WHERE子句,所以我们没有参数。我们仍然添加了execute()方法来运行查询。

...
COLUMNS = ['sensor_name', 'sensor_value', 'sensor_units']
my_res = my_tbl.select(COLUMNS).execute()
...

图 4-2 显示了Table类的select()方法的语法图。在这里,我们看到不同的类和方法是如何通过链末端的execute()方法链接在一起的。链中描述的各种方法是可选的,但是大多数读操作将包括where()方法(子句)。

img/478423_1_En_4_Fig2_HTML.jpg

图 4-2

语法图- Table.select()

更新数据

更新操作类似于读取操作,我们使用一个或多个方法来指定在典型的UPDATE SQL 查询中可以找到的子子句。下面是一个简单的例子,我们用等于 1.52 的传感器值更新行,改变传感器单位。请注意,我们使用了 MySQL 函数库的一个技巧来将浮点值转换为特定的小数,以消除舍入产生的错误(sensor_units = 1.52不起作用)。

UPDATE factory_sensors.trailer_assembly SET sensor_units = 'inches' WHERE sensor_value LIKE 1.52;

为了在 Python 中执行这个,我们使用表对象的update()方法,将它与TableUpdate对象的set()where()方法链接起来,并传入我们的参数。

my_tbl.update().set().where('sensor_value LIKE 1.52').execute()

图 4-3 显示了更新操作的语法图。这类似于 read 操作,因为我们有几个共同的中间步骤,如WHERE子句、order by 等。

img/478423_1_En_4_Fig3_HTML.jpg

图 4-3

语法图- Table.update()

删除数据

删除操作类似于读取和更新操作,我们使用一个或多个方法来指定在典型的DELETE SQL 查询中会找到的子子句。下面是一个简单的例子,我们删除了传感器值为> 30 的行。

DELETE FROM factory_sensors.trailer_assembly WHERE sensor_value > 30;

为了在 Python 中执行这个,我们使用表对象的delete()方法,并用TableDelete对象的where()方法链接它,并传入我们的参数。

my_tbl.delete().where('sensor_value > 30').execute()

图 4-3 显示了更新操作的语法图。这类似于更新操作,因为我们有几个共同的中间步骤,如WHERE子句、order by 等。

img/478423_1_En_4_Fig4_HTML.jpg

图 4-4

语法图- Table.delete()

在我们看到使用关系数据的完整 Python 示例之前,我们还应该探索一件事——使用结果集(有时称为数据集)。

使用结果

到目前为止,我们已经看到了一些处理结果的简单例子,虽然看起来所有的结果都是同一个类,但是有几个结果类。返回的结果类的对象实例取决于操作。例如,每种类型的结果都有一个单独的类。结果有时被称为数据集或结果集。

表 4-10 显示了您在处理数据集和结果时会遇到的所有类及其方法。所有结果类都是从 BaseResult 对象派生的,该对象提供了一组属性和基方法。为了完整起见,我在表中重复了这些。

注意

类方法用()表示,属性用()表示。

表 4-10

处理数据集和结果的类和方法

|

班级

|

方法

|

返回

|

描述

|
| --- | --- | --- | --- |
| 结果 | 允许检索对数据库执行的非查询操作的相关信息 |
| get_affected_item_count() | (同 Internationalorganizations)国际组织 | 最后一道工序中受影响的物件数量 |
| get_auto_increment_value() | (同 Internationalorganizations)国际组织 | (从插入操作中)自动生成的最后一个插入 id |
| get_generated_ids() | 目录 | 返回服务器上生成的文档 id 列表 |
| affected_item_count | (同 Internationalorganizations)国际组织 | 与 get_affected_itemCount()相同 |
| auto_increment_value | (同 Internationalorganizations)国际组织 | 与 get_auto_increment_value()相同 |
| generated_ids | 目录 | 与 get_generated_ids()相同 |
| affected_items_count | (同 Internationalorganizations)国际组织 | 与 get_affected_items_count()相同 |
| warning_count | (同 Internationalorganizations)国际组织 | 与 get_warning_count()相同 |
| warnings_count | (同 Internationalorganizations)国际组织 | 与 get_warnings_count()相同 |
| warnings | 目录 | 与 get_warnings()相同 |
| execution_time | 潜艇用热中子反应堆(submarine thermal reactor 的缩写) | 与 get_execution_time()相同 |
| RowResult | 允许遍历表返回的行对象。选择操作 |
| fetch_one() | 排 | 检索 rowresult 中的下一行 |
| fetch_all() | 目录 | 返回 DbDoc 对象列表,其中包含每个未读文档的元素 |
| get_column_count() | (同 Internationalorganizations)国际组织 | 检索当前结果的列数 |
| get_column_names() | 目录 | 获取当前结果中的列 |
| get_columns() | 目录 | 获取活动结果中列的列元数据 |
| column_count | (同 Internationalorganizations)国际组织 | 与 get_column_count()相同 |
| column_names | 目录 | 与 get_column_names()相同 |
| columns | 目录 | 与 get_columns()相同 |
| affected_items_count | (同 Internationalorganizations)国际组织 | 与 get_affected_items_count()相同 |
| warning_count | (同 Internationalorganizations)国际组织 | 与 get_warning_count()相同 |
| warnings_count | (同 Internationalorganizations)国际组织 | 与 get_warnings_count()相同 |
| warnings | 目录 | 与 get_warnings()相同 |
| execution_time | 潜艇用热中子反应堆(submarine thermal reactor 的缩写) | 与 get_execution_time()相同 |
| SqlResult | 表示 SQL 语句的结果 |
| get_auto_increment_value() | (同 Internationalorganizations)国际组织 | 返回插入的最后一条记录的标识符 |
| get_affected_row_count() | (同 Internationalorganizations)国际组织 | 返回受已执行查询影响的行数 |
| has_data() | 弯曲件 | 如果最后一个语句执行有结果集,则返回 true |
| next_data_set() | 弯曲件 | 准备 SqlResult 以从下一个结果开始读取数据(如果返回了许多结果) |
| next_result() | 弯曲件 | 准备 SqlResult 以从下一个结果开始读取数据(如果返回了许多结果) |
| auto_increment_value | (同 Internationalorganizations)国际组织 | 与 get_auto_increment_value()相同 |
| affected_row_count | (同 Internationalorganizations)国际组织 | 与 get_affected_row_count()相同 |
| column_count | (同 Internationalorganizations)国际组织 | 与 get_column_count()相同 |
| column_names | 目录 | 与 get_column_names()相同 |
| columns | 目录 | 与 get_columns()相同 |
| affected_items_count | (同 Internationalorganizations)国际组织 | 与 get_affected_items_count()相同 |
| warning_count | (同 Internationalorganizations)国际组织 | 与 get_warning_count()相同 |
| warnings_count | (同 Internationalorganizations)国际组织 | 与 get_warnings_count()相同 |
| warnings | 目录 | 与 get_warnings()相同 |
| execution_time | 潜艇用热中子反应堆(submarine thermal reactor 的缩写) | 与 get_execution_time()相同 |
| domresult | 允许遍历由 Collection.find 操作返回的 DbDoc 对象 |
| fetch_one() | 文件 | 在 domresult 上检索下一个 dbdoc |
| fetch_all() | 目录 | 返回 DbDoc 对象列表,其中包含每个未读文档的元素 |
| affected_items_count | (同 Internationalorganizations)国际组织 | 与 get_affected_items_count()相同 |
| warning_count | (同 Internationalorganizations)国际组织 | 与 get_warning_count()相同 |
| warnings_count | (同 Internationalorganizations)国际组织 | 与 get_warnings_count()相同 |
| warnings | 目录 | 与 get_warnings()相同 |
| execution_time | 潜艇用热中子反应堆(submarine thermal reactor 的缩写) | 与 get_execution_time()相同 |

有迭代器的三个类实现了两个方法:fetch_one()fetch_all()。它们像您想象的那样工作,返回一个数据集或一组文档的一组对象。fetch_one()方法返回数据集中的下一个数据项,如果没有更多的数据项,则NULL返回所有的数据项。更具体地说,fetch_one()一次从服务器检索一个数据项,而fetch_all()一次从服务器检索所有数据。您使用哪一个将取决于数据集的大小和您想要如何处理数据。

那么,这在 Python 中是什么样子的呢?下面显示了一个简单的例子,执行一个读操作来获取表中的所有行(没有WHERE子句)。这里,我们首先检索表列的列表,以便打印它们。我们将使用两个循环:一个循环遍历列名列表,另一个循环遍历读取操作返回的行。

column_names = my_res.get_column_names()
column_count = my_res.get_column_count()
for i in range(0,column_count):
    if i < column_count - 1:
        print "{0}, ".format(column_names[i]),
    else:
        print "{0}".format(column_names[i]),
print

在这里,我们看到了一些用于获得结果的 Python 语句。在这种情况下,我们使用的是TableSelect类,但是由于大多数结果类都有相同的方法,所以对于其他结果,您的代码也是类似的。您可能会注意到一些基本的格式化代码,使输出以逗号分隔。这只是为了演示。您自己的应用可能会一次消费一行数据,并对其进行处理。然而,获取列和获取行的概念是相同的。一旦你记下这些概念,我们只需要添加如何开始的概念。

开始编写 Python 脚本

现在是时候获取一些实际的代码了,我们可以使用这些代码来加强到目前为止所讨论的概念。让我们来看一个简单的例子。乍一看,这段代码似乎有点吓人,但它是一个非常简单的示例,包含您需要通过打开一个会话、创建新模式和创建表来连接到服务器的样板代码。从这里,我们看到了从对表对象的select()调用开始的 CRUD 操作的例子,以及使用 CRUD 操作的演示。

清单 4-6 显示了一个 Python 脚本来创建之前使用的数据库和表,包括添加数据和执行简单的 select 查询。只是,这次我们用 Python 来做!如果您想继续,请打开 shell 并连接到您的服务器,如下所示。

#
# Introducing the MySQL 8 Shell
#
# This example shows a simple X DevAPI script to work with relational data
#
# Dr. Charles A. Bell, 2019

from mysqlsh import mysqlx # needed in case you run the code outside of the shell

# SQL CREATE TABLE statement
CREATE_TBL = """
CREATE TABLE `factory_sensors`.`trailer_assembly` (
  `id` int auto_increment,
  `sensor_name` char(30) NOT NULL,
  `sensor_value` float DEFAULT NULL,
  `sensor_event` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `sensor_units` char(15) DEFAULT NULL,
  PRIMARY KEY `sensor_id` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
"""

# column list, user data structure
COLUMNS = ['sensor_name', 'sensor_value', 'sensor_units']
user_info = {
  'host': 'localhost',
  'port': 33060,
  'user': 'root',
  'password': 'secret',
}

print("Listing 4-6 Example - Python X DevAPI Demo with Relational Data.")
# Get a session (connection)
my_session = mysqlx.get_session(user_info)
# Precautionary drop schema
my_session.drop_schema('factory_sensors')
# Create the database (schema)
my_db = my_session.create_schema('factory_sensors')
# Execute the SQL statement to create the table
sql_res = my_session.sql(CREATE_TBL).execute()
# Get the table object
my_tbl = my_db.get_table('trailer_assembly')
# Insert some rows (data)
my_tbl.insert(COLUMNS).values('paint_vat_temp', 32.815, 'Celsius').execute()
my_tbl.insert(COLUMNS).values('tongue_height_variance', 1.52, 'mm').execute()
my_tbl.insert(COLUMNS).values('ambient_temperature', 24.5, 'Celsius').execute()
my_tbl.insert(COLUMNS).values('gross_weight', 1241.01, 'pounds').execute()
# Execute a simple select (SELECT ∗ FROM)
print("\nShowing results after inserting all rows.")
my_res = my_tbl.select(COLUMNS).execute()
# Display the results . Demonstrates how to work with results
# Print the column names followed by the rows
column_names = my_res.get_column_names()
column_count = my_res.get_column_count()
for i in range(0,column_count):
    if i < column_count - 1:
        print "{0}, ".format(column_names[i]),
    else:
        print "{0}".format(column_names[i]),
print

for row in my_res.fetch_all():
    for i in range(0,column_count):
        if i < column_count - 1:
            print "{0}, ".format(row[i]),
        else:
            print "{0}".format(row[i]),
    print

# Update a row
my_tbl.update().set('sensor_units', 'inches').where('sensor_value LIKE 1.52').execute()
print("\nShowing results after updating row with sensor_value LIKE 1.52.")
# Execute a simple select (SELECT ∗ FROM)
my_res = my_tbl.select(COLUMNS).execute()
# Display the results
for row in my_res.fetch_all():
    print row
# Delete some rows
my_tbl.delete().where('sensor_value > 30').execute()
# Execute a simple select (SELECT ∗ FROM)
print("\nShowing results after deleting rows with sensor_value > 30.")
my_res = my_tbl.select(COLUMNS).execute()
# Display the results
for row in my_res.fetch_all():
    print row
# Delete the database (schema)
my_session.drop_schema('factory_sensors')

Listing 4-6Simple Relational Data Example

花点时间通读代码,确保找到 CRUD 操作。同样,这些都是非常简单的例子,只有一些关于WHERE子句表达式的小例子。注释行和额外的打印语句形成了一个指南,有助于使代码更易于阅读。我们将在下一章看到一个更详细的例子,完整地解释如何使用各种方法来限制输出(WHERE子句)。

现在,让我们看看代码的执行。在这种情况下,我们将使用 shell 的批处理执行特性来读取我们之前创建的文件并执行它。清单 4-7 显示了运行脚本的命令和结果。

C:\Users\cbell\MySQL Shell\source\Ch04>mysqlsh --py -f listing4-6.py
Listing 4-6 Example - Python X DevAPI Demo with Relational Data.

Showing results after inserting all rows.
sensor_name,  sensor_value,  sensor_units
paint_vat_temp,  32.815,  Celsius
tongue_height_variance,  1.52,  mm
ambient_temperature,  24.5,  Celsius
gross_weight,  1241.01,  pounds

Showing results after updating row with sensor_value LIKE 1.52.
[
    "paint_vat_temp",
    32.815,
    "Celsius"
]
[
    "tongue_height_variance",
    1.52,
    "inches"
]
[
    "ambient_temperature",
    24.5,
    "Celsius"
]
[
    "gross_weight",
    1241.01,
    "pounds"
]

Showing results after deleting rows with sensor_value > 30.
[
    "tongue_height_variance",
    1.52,
    "inches"
]
[
    "ambient_temperature",
    24.5,
    "Celsius"
]

Listing 4-7Executing the Sample Code

输出显示了第一个读取操作,该操作使用我们前面看到的处理结果的方法打印行,即以逗号分隔的输出形式打印列名和行,每行一行。另一个输出显示了结果是如何返回给 Python 的——它们是列表的列表!这就是为什么我们看到输出显示为行的字符串值列表。

花点时间再次浏览代码,确保您可以看到 CRUD 操作对数据的影响。也就是说,在每次读取、更新和删除操作之后,结果的输出应该略有不同。

连接器/Python 呢?

如果您一直在使用名为 Connector/Python 的 Python 数据库连接器,您可能会想知道这里有什么不同,而连接器却不能做到。在这一点上,你的直觉是正确的。到目前为止,我介绍的所有内容都可以用连接器和优秀的 Python 编程来完成。事实上,正是这种重叠表明 X DevAPI 已经实现了它的一个目标。

现在,您可能有兴趣知道连接器完全支持 X DevAPI,并且 Shell 使用了连接器。我们正在学习如何从一个不同的角度处理我们的数据——数据的角度就是代码。一旦你阅读了第六章,它将全部点击(如果它还没有)。

摘要

MySQL 中传统的数据存储和检索机制是 SQL 接口。这是大多数人熟悉的,事实上大多数人学习 SQL 是为了成为开发人员或数据库管理员。因此,对于许多人来说,学习像 MySQL Shell 这样的新工具最好从熟悉的 SQL 开始。事实上,这就是本章的全部内容。

在这一章中,我们看到了一个简单的游览,首先是在传统的 SQL 交互会话中使用 shell,在这个会话中我们发出 SQL 语句并处理关系数据。它很熟悉,并且演示了关系数据的基本概念。即使您以前没有使用过 SQL,这个小演示也足以让您上手。

然而,趋势是将我们的数据混合到我们的代码中,也就是说,使我们的数据成为代码的一部分。为此,我们需要一个强大的 API,让我们可以像处理代码中的对象一样处理数据。X DevAPI 就是答案。此外,我们还看到了对用于关系数据的 X DevAPI 的简要介绍。我们不仅学习了如何开始使用 X DevAPI,还看到了一些可用的 Python 代码,您可以使用它们开始编写自己的 Python 脚本。

但这仅仅是开始。我们真正需要的是一个更大的例子,可以作为编写更高级 Python 脚本的教程。下一章将更详细地介绍本章中介绍的概念。

五、示例:SQL 数据库开发

在上一章中,我们探索了使用两种模式的 shell:传统的 SQL 命令执行和使用 X DevAPI 编写 Python 代码在没有 SQL 数据库的情况下进行交互的简短旅程。

在这一章中,我们将看到如何使用 shell 开发 Python 代码模块来处理传统的关系数据库。事实上,我们会相信 MySQL Shell 是一个开发工具的说法。

我们将首先检查示例应用的数据库,然后构建数据库代码来访问数据库中的数据。我们将以循序渐进的方式来完成这项工作,以便让您更好地了解如何使用 shell 来开发您自己的代码。最后,我们将看到一个非常简短的演示,演示如何使用 shell 来测试数据库代码。

我们不会详细讨论示例应用,因为重点是如何使用 shell,而不是示例应用本身。然而,本书的附录包含了用于实现示例应用的代码。

让我们从检查示例数据库和简要讨论示例应用开始。

入门指南

能够用散文成功地解释主题,并用实例巩固读者的理解是一回事,但解释使用数据或代码的新方法的好处是完全不同的另一回事。在这些情况下,人们必须以互动的方式演示概念,以便通过例子来证明概念,而不是简单地展示它是如何可能实现的。在这一节中,我们将了解一个努力做到这一点的示例应用——证明如何使用 shell 来开发自己的代码。

然而,为了以这种方式展示 shell 的全部功能,样本必须足够复杂,具有足够的深度(和宽度)来完成它的角色。因此,这一章,我们将重点解决一个重大问题:如何组织你的车库!

好吧,那可能太远了,够不着。让我们回到简单地组织你的车库或车间的工具。如果你有任何工具,或者像我一样,有大量的工具用于各种各样的建造、修理和维护,知道每个工具的位置可能是一个真正的斗争,尤其是当你获得如此多的工具,你需要多个存储位置来存放它们。

示例应用概念

示例应用概念是一个组织概念。因此,我们将存储我们想要组织的事物的描述,包括它们被组织的事物。具体来说,我们想知道我们有哪些工具,它们存放在哪里。如果存储位置是工具箱或橱柜,我们还想知道它在哪个抽屉或架子上。也可能是我们把工具存放在盒子或箱子里,而这些工具又存放在某个地方。因此,我们不仅要建模工具,还要建模工具存储。

车库应用诞生于更好地组织的需要。事实上,拥有一个杂乱的车库或者只是随意存放你的工具可能会满足一些人的需求,像我这样的其他人需要更多的结构。另外,如果你想知道你是否有某种工具,最好不仅知道你有,而且知道它在哪里!

因此,这个应用主要被设计为一个查找工具,因此主要关注显示表中所有行的列表视图。

这就是存储位置是默认视图的原因。如果你走进你的车库(或车间),你首先应该看到的是存储设备——工具箱、架子、搁板等。当你寻找一个工具时,你通常会在一个或多个地方(存储设备)寻找,利用你存储它的最后一个地方或公共地方的记忆。但是,如果您有许多工具,可能无法记住每个工具的位置,尤其是如果您已经有一段时间没有使用它了。

然而,示例应用还提供了一个视图来显示您所有的手动工具和电动工具。这些类别的列表视图显示了为您排序的所有项目。您只需要浏览列表来找到所需的工具,然后查看列来确定工具的存储位置。因此,通过在车库应用中点击几次,您就知道去哪里获得您想要的工具。我们将这个示例应用称为 MyGarage。酷吧。

小费

我们不会解释示例应用的每一个细微差别,而是将重点放在最适合用来证明使用 shell 开发代码的实用性的部分——数据库访问代码模块。

让我们快速看一下样例应用的部分用户界面。图 5-1 显示了手工具记录的详细视图。在这里,我们可以指定供应商、描述、工具大小、类型和位置。通过这种方式,我们可以获取关于一个工具的基本信息,包括哪个公司制造的以及我们把它存放在哪里。

img/478423_1_En_5_Fig1_HTML.jpg

图 5-1

手动工具详细视图

虽然这个视图看起来相当简单,但是底层数据库的设计要复杂一些。例如,人们可以查看表单并预测我们将有某种方式来存储供应商,因为一个供应商可能有许多工具。您还可以预测存储位置也会出现类似的情况。但是,请考虑一下,一件储物设备可能有一个或多个抽屉或搁板,或者两者都有。因此,我们可能希望对这些工具进行建模。

在开始数据库设计之前,让我们更好地理解示例应用中的对象。下面列出了应用中标识的对象及其使用方法。这将有助于理解数据是如何存储的(及其设计)。

  • 手动工具:没有动力的工具

  • 电动工具:依靠空气(气动)或电力运行的工具,有线或无线

  • 储藏设备:有一个或多个地方可以存放工具(东西)的架子、箱子、柜子等

  • 存储位置:存储设备的一个特征,如架子或抽屉

  • 整理器:可以放置一个或多个工具,但需要存放在存放处的容器

  • 厂商:工具的制造商

让我们看看示例应用的存储部分。这可能看起来有点复杂,但是一旦你看到它的实际应用,你就会明白了。假设我们有一个新的工具储物盒,里面有几个抽屉和搁板。如果我们要做一张桌子,只存放箱子,我们怎么知道工具放在哪个抽屉或哪个架子上呢?

例如,我们可以将工具列为在tool_chest_1中,但是如果它有十个抽屉和四个架子,那对我们没有多大帮助。谁想要一个告诉你大概位置的应用?你不得不拉开抽屉或者随意检查架子,直到找到你的工具。但是,如果我们抽象抽屉和架子,我们可以通过引用存储位置(抽屉、架子)来指定工具箱中给定工具的确切位置,存储位置引用存储设备。

让我们看一个例子。图 5-2 显示了一个可从家装商店(Lowe's)买到的 Kobalt 工具箱。注意衣柜有七个抽屉和两个搁板。

img/478423_1_En_5_Fig2_HTML.jpg

图 5-2

Kobalt 工具箱

如果我们在表格中为每个抽屉建模或创建条目,那么我们可以在工具、抽屉和工具箱之间分配一个关系。这不仅展示了我们如何更好地分类(组织)我们的数据,还展示了大多数使用真实数据的应用的一个关键方面——数据中存在多种一对多关系。

既然我们已经了解了示例应用的目标以及我们需要如何对存储特性建模,那么让我们来看看数据库是如何设计的。

数据库设计

让我们从实体关系图(ERD)开始我们的数据库设计之旅。图 5-3 显示了数据库的 ERD。如果您不熟悉这些图,它们通常会显示表、视图或您想要的任何其他对象,以及实体之间的关系(虚线)。该示例中还包括每个表的索引。有一个视图显示为实心矩形。

img/478423_1_En_5_Fig3_HTML.jpg

图 5-3

我的车库数据库 ERD(版本 1)

花些时间研究一下图表,以便熟悉我们将使用的表格。我们将在本节的后面看到这些表的更多细节。我们将数据库命名为garage_v1,因为我们将在第七章中看到如何将该数据库从关系模型迁移到 NoSQL 模型,这将成为garage_v2

您可能会注意到,每个表都有一个定义为自动递增字段的代理键。这是一种很好的、简单的方法,可以确保表中的行是唯一的,并且是一种允许存储多个相同项目的人工机制。例如,在典型的工具收藏中,人们通常有不止一种某种工具,例如锤子、钳子、可调扳手等。使用自动增量键允许我们给每个工具一个独特的Id

现在,让我们看看 ERD 中的每个实体,以便了解它们存储什么。我们将从关系最少的表开始构建,这样您就可以理解它们是如何构建的。

当通读这个设计时,精明的读者可能会看到改进设计的方法。然而,回想一下这个示例应用的目标是双重的:足够复杂以演示重要的示例,并且读者可以自己运行。因此,采取了一些设计折衷以避免过于复杂。 1

供应商表

vendor表包含数据库中工具的供应商或制造商的基本信息。我们记录了名称、供应商网站的 URL 以及可以从该供应商那里购买产品的简短描述。清单 5-1 显示了创建vendor表的 SQL CREATE TABLE命令。

CREATE TABLE `garage_v1`.`vendor` (
  `Id` int(11) NOT NULL AUTO_INCREMENT,
  `Name` char(50) NOT NULL,
  `URL` char(128) DEFAULT NULL,
  `Sources` char(40) DEFAULT NULL,
  PRIMARY KEY (`Id`)
) ENGINE=InnoDB AUTO_INCREMENT=100 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

Listing 5-1Vendor Table

注意,我们使用自动递增字段作为主键,这是一个整数字段。仔细查看表格选项。在这里,我们演示如何设置自动递增字段(列)的初始值。在这种情况下,我们选择起始值 100。我们可以为其他表选择其他起始值,使每个 id 范围都有些独特。例如,如果我们将另一个表的起始值设置为 1000,我们一眼就可以看出Id为 103 的行是供应商,而值为 1022 的行来自另一个表。

当然,大多数精明的数据库管理员会引用一些关系数据库教科书 2 中的章节和版本来描述这种做法有多糟糕,但是在实践中,如果您出于调试目的使用类似这样的通用编码形式,这可能会很方便。因为行在不同的表中,所以编码的恐惧或“罪恶”没有实现。也就是没有碰撞的可能。因此,您可以放心,因为这与关系数据库设计并不完全对立;相反,它是一个调试或编码工具。

组织者表格

organizer表用于存储关于组织者的信息,比如盒子、箱子、塑壳等等。这有助于解决一些工具具有它们自己的特殊情况和必须组合在一起使用的工具的问题,例如套筒组、某些类型的扳手等。我们还使用了自动递增的技巧,从 2000 开始为Id列赋值。清单 5-2 显示了创建organizer表的 SQL CREATE TABLE命令。

CREATE TABLE `garage_v1`.`organizer` (
  `Id` int(11) NOT NULL AUTO_INCREMENT,
  `PlaceId` int(11) NOT NULL,
  `Type` enum('Bin','Box','Case') DEFAULT 'Case',
  `Description` char(40) NOT NULL,
  `Width` int(11) DEFAULT '0',
  `Depth` int(11) DEFAULT '0',
  `Height` int(11) DEFAULT '0',
  PRIMARY KEY (`Id`),
  KEY `OrganizerStorage_FK_idx` (`PlaceId`),
  CONSTRAINT `OrganizerStorage_FK` FOREIGN KEY (`PlaceId`) REFERENCES `place` (`Id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE=InnoDB AUTO_INCREMENT=2000 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

Listing 5-2Organizer Table

请注意,该表包含一个外键。外键可以被视为一个表中的一行与另一个表中的一行之间的链接或关系。它们主要用于加强关系。例如,请注意前面的 SQL 代码中的限制。在这里,我们看到外键被限制在删除和更新操作上,因此如果该表中的一行引用了它的Id列,则不能在place表中删除该行。它还指定了在更新中不能更改 place 表中的Id列。这就是它被称为“foreign”的原因,因为它对另一个表施加了限制。这是另一种关系数据库构造,数据库设计人员使用它来帮助构建数据库的健壮性(以及防止意外更改)。

存储位置表

存储位置表(简称为place)用于存储关于我们可以存储东西的位置的信息,比如抽屉或架子。事实上,这个表通过一个名为Type的枚举列仅限于这两种类型。我们还存储一个描述,这个存储位置所在的存储设备的Id,它的尺寸。清单 5-3 显示了place表的 SQL CREATE TABLE

CREATE TABLE `garage_v1`.`place` (
  `Id` int(11) NOT NULL AUTO_INCREMENT,
  `StorageId` int(11) NOT NULL,
  `Type` enum('Drawer','Shelf') DEFAULT 'Drawer',
  `Description` char(40) NOT NULL,
  `Width` int(11) DEFAULT '0',
  `Depth` int(11) DEFAULT '0',
  `Height` int(11) DEFAULT '0',
  PRIMARY KEY (`Id`),
  KEY `PlaceStorage_FK_idx` (`StorageId`),
  CONSTRAINT `PlaceStorage_FK` FOREIGN KEY (`StorageId`) REFERENCES `storage` (`Id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE=InnoDB AUTO_INCREMENT=1038 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

Listing 5-3Place Table

这个表也像 organizer 表一样有一个外键。同样,这样做是为了在该表中的某一行引用的存储表中不能删除行(或更改Id列)。

存储设备表

存储设备表简称为storage,用于存储工具或一般存储结构的信息,如工具箱、橱柜、工作台或架子。事实上,与存储位置表一样,我们使用一个名为Type的枚举列来指定存储设备类型。

除了存储设备类型,我们还存储了描述、抽屉、搁板和门的数量(如果适用)及其总体尺寸,以及存储其在车库或车间中的位置(物理描述)的通用文本字段。清单 5-4 显示了针对storage表的 SQL CREATE TABLE命令。

CREATE TABLE `garage_v1`.`storage` (
  `Id` int(11) NOT NULL AUTO_INCREMENT,
  `VendorId` int(11) NOT NULL,
  `Type` enum('Cabinet','Shelving','Toolchest','Workbench') DEFAULT 'Toolchest',
  `Description` char(125) DEFAULT NULL,
  `NumDrawers` int(11) DEFAULT '0',
  `NumShelves` int(11) DEFAULT '0',
  `NumDoors` int(11) DEFAULT '0',
  `Width` int(11) DEFAULT NULL,
  `Depth` int(11) DEFAULT NULL,
  `Height` int(11) DEFAULT NULL,
  `Location` char(40) DEFAULT NULL,
  PRIMARY KEY (`Id`),
  KEY `VendorKey_idx` (`VendorId`),
  CONSTRAINT `StorageVendor_FK` FOREIGN KEY (`VendorId`) REFERENCES `vendor` (`Id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE=InnoDB AUTO_INCREMENT=503 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

Listing 5-4Storage Table

我们再次看到这个表和 vendor 表之间有一个外键,这样就不会删除任何供应商,也不会更改它的Id列,只要这个表中有引用它的行。

手工工具桌

handtool表用于存储每个非电动工具的信息。我们收集供应商、描述和尺寸。我们还使用了一个名为Type的枚举字段,它存储了工具的类型,这样我们就可以按类别对工具进行分组。这应该使得发出诸如“显示我所有的螺丝刀”这样的查询更加容易——特别是当一些类别的工具被放在不同的地方时。允许的类型可以在 SQL 语句中看到。

我们还存储供应商和存储位置的链接(的Id值)。因此,我们正在形成这些表之间的关系。清单 5-5 显示了针对handtool表的 SQL CREATE TABLE命令。

CREATE TABLE `garage_v1`.`handtool` (
  `Id` int(11) NOT NULL AUTO_INCREMENT,
  `VendorId` int(11) NOT NULL,
  `Description` char(125) NOT NULL,
  `Type` enum('Adjustable Wrench','Awl','Clamp','Crowbar','Drill Bit','File','Hammer','Knife','Level','Nutdriver','Pliers','Prybar','Router Bit','Ruler','Saw','Screwdriver','Socket','Socket Wrench','Wrench') DEFAULT NULL,
  `ToolSize` char(50) DEFAULT NULL,
  `PlaceId` int(11) NOT NULL,
  PRIMARY KEY (`Id`),
  KEY `VendorKey_idx` (`VendorId`),
  KEY `PlaceKey_idx` (`PlaceId`),
  CONSTRAINT `HandtoolPlace_FK` FOREIGN KEY (`PlaceId`) REFERENCES `place` (`Id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
  CONSTRAINT `HandtoolVendor_FK` FOREIGN KEY (`VendorId`) REFERENCES `vendor` (`Id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE=InnoDB AUTO_INCREMENT=2253 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

Listing 5-5Handtool Table

在这个表中,我们有两个外键;一个用于存放地点(place)表中的Id,另一个用于vendor表中的Id

电动工具台

powertool桌子与handtool桌子相似,只是这里存放的是由空气、电力(主电源)或电池驱动的工具。我们存储了描述和一个名为Typ e 的枚举字段,用于表示工具使用的电源类型。如果我们需要所有气动工具(空气)的列表,这可能会很方便。

我们还存储供应商和存储位置的链接(的Id值)。因此,我们正在形成这些表之间的关系。清单 5-6 显示了针对powertool表的 SQL CREATE TABLE命令。

CREATE TABLE `garage_v1`.`powertool` (
  `Id` int(11) NOT NULL AUTO_INCREMENT,
  `VendorId` int(11) NOT NULL,
  `Description` char(125) DEFAULT NULL,
  `Type` enum('Air','Corded','Cordless') DEFAULT NULL,
  `PlaceId` int(11) NOT NULL,
  PRIMARY KEY (`Id`),
  KEY `VendorKey_idx` (`VendorId`),
  KEY `PlaceKey_idx` (`PlaceId`),
  CONSTRAINT `PowerToolPlace_FK` FOREIGN KEY (`PlaceId`) REFERENCES `place` (`Id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
  CONSTRAINT `PowertoolVendor_FK` FOREIGN KEY (`VendorId`) REFERENCES `vendor` (`Id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE=InnoDB AUTO_INCREMENT=3022 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

Listing 5-6Powertool Table

我们在这个表中拥有与在handtool表中相同的外键;一个用于存放地点(place)表中的Id,另一个用于vendor表中的Id

位置视图

最后,我们将使用一个视图。这个名为location的视图可以让我们快速获得存储位置和存储设备组合的查找表(视图)。我们可以用它在我们的示例应用中创建一个漂亮的下拉列表。图 5-4 显示了下拉列表的结果示例。我们将使用这个列表来创建对前面描述的handtoolpowertool表中存储位置的引用。请注意,我们看到了一些表格的组合,以便于查看和选择适当的位置。这是现实世界中的应用如何利用数据库中的技巧来使用户界面更易于使用的另一个例子。

img/478423_1_En_5_Fig4_HTML.jpg

图 5-4

使用下拉列表中的位置视图

这个视图的 SQL 相当复杂,涉及到一个连接(在关系数据库中很常见),它将两个表中的信息结合起来。清单 5-7 显示了视图location的 SQL CREATE VIEW。请注意,视图重命名了一些列(使用AS关键字),以便更容易区分每个表中的字段。这一点尤其重要,因为我们为每个表的主键使用了一个通用的“Id”。 3

CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`localhost` SQL SECURITY DEFINER VIEW `garage_v1`.`location` AS
    SELECT `garage_v1`.`storage`.`Id` AS `StorageId`,
           `garage_v1`.`storage`.`Description` AS `StorageEquipment`,
           `garage_v1`.`place`.`Id` AS `PlaceId`,
           `garage_v1`.`place`.`Type` AS `Type`,
           `garage_v1`.`place`.`Description` AS `Location`
    FROM (`garage_v1`.`storage` JOIN `garage_v1`.`place` ON
        ((`garage_v1`.`storage`.`Id` = `garage_v1`.`place`.`StorageId`)))

Listing 5-7Location View

现在,让我们花点时间来讨论数据库代码设计。

代码设计

虽然你可能会在数据库设计中看到一些你会以不同方式做的事情 4 并且有几种“正确”的方式来做事情,但是代码设计将它们带到了一个更高的层次。也就是说,给两个程序员和一组要评审的代码,他们可能会花更多的时间仔细检查这种或那种编码方式的细微之处,而不是一开始就去写。这并不是说审查是无益的——它肯定是有益的——相反,这意味着总是有办法使用不同的机制、结构和哲学在代码中做同样的事情。

这适用于为示例应用设计的代码。选择使代码模块化,更容易阅读,最重要的是,演示(一种方式)构建关系数据库应用。因此,您将要遇到的可能不是您应该如何编写代码,但是它应该仍然可以以当前的形式用于演示目的。更具体地说,为示例应用做出的代码设计选择包括:

  • 将 Flask 框架用于基于 web 的界面

  • 使用一个类来表示数据库中的每个表

  • 将单个类放在它自己的代码模块中

  • 将所有数据库代码模块放在它自己的文件夹(名为 database)中

  • 使用类封装到数据库服务器的连接

  • 使用类模块测试每个表/视图类

  • 使用从 shell 运行的代码模块来测试类模块

我们将在演示中看到这些约束中的大部分。如前所述,用户界面的描述包含在附录中。

我们在这一部分关注的代码包括我们需要与数据库交互的代码。因此,我们需要实现创建、读取、更新和删除(CRUD)操作的代码。我们还需要代码来帮助我们连接到数据库服务器。

表 5-1 显示了每个计划的数据库代码文件的代码模块、类名和描述。在下一节中,我们将看到如何使用 shell 来开发这些组件。

表 5-1

数据库代码模块

|

代码模块

|

类别名

|

描述

|
| --- | --- | --- |
| garage_v1 | MyGarage | 实现与服务器和通用服务器接口的连接 |
| handtool.py | Handtool | 为手工工具表建模 |
| location.py | Location | 模拟位置视图 |
| organizer.py | Organizer | 模拟组织者表格 |
| place.py | Place | 模拟位置表 |
| powertool.py | Powertool | 为电动工具表建模 |
| storage.py | Storage | 为存储表建模 |
| vendor.py | Vendor | 对供应商表建模 |

当我们为示例应用编写代码以使用这些代码模块时,我们将使用MyGarage类连接到数据库服务器,并且当被请求时,使用与每个表相关联的类来调用每个表上的 CRUD 操作。唯一的例外是Location类只实现读取操作,因为它是一个视图,而视图被用作查找(读取)表。

现在我们已经了解了示例应用及其设计的目标,让我们开始为示例应用编写数据库代码。

设置和配置

以下演示的设置不需要安装任何东西,甚至不需要使用示例应用;相反,我们只需要加载示例数据库,因为我们将只使用数据库代码模块。虽然图像用于描述示例应用的某些方面,但在本章中并不一定需要它。同样,关于如何设置和使用完整的示例应用,请参见附录。

要安装示例数据库,我们必须从图书网站( https://www.apress.com/us/book/9781484250822 )下载示例源代码。选择本章的文件夹并下载文件。示例源代码包含一个名为database/garage_v1.sql的文件,该文件包含用于创建示例数据库并使用示例数据填充该数据库的 SQL 语句。

这个文件不仅发出CREATE DATABASECREATE TABLE命令,还包含使用INSERT SQL 命令的每个表的一小组数据。也就是说,它是一个典型的车库或车间中一套工具的库存。所以,你不必花宝贵的时间去想出描述、尺寸等。对于要使用的一组数据,已经为您做好了!

由于这个文件是一个 SQL 文件,我们将需要对 shell 使用--sql模式。幸运的是,我们可以使用选项来读取这个文件,导入(源)它,然后退出,如下所示。请记住,您必须指定文件的路径,或者从文件所在的目录执行 shell。

mysqlsh --uri root@localhost:3306 --sql -f garage_v1.sql

转到数据库文件夹,发出以下命令,告诉 shell 打开文件并执行语句。运行它不需要一分钟,因为我们是在批处理模式下运行的,完成后将退出 shell。清单 5-8 显示了运行这些命令的结果。如果您对文件中的命令感到好奇,可以随意打开它,看看 SQL 语句是如何编写的。您应该注意到这是一个使用mysqlpump服务器客户端应用的数据库转储。

小费

关于mysqlpump的更多信息,请参见 https://dev.mysql.com/doc/refman/8.0/en/mysqlpump.html

C:\Users\cbell\Documents\mygarage_v1>cd database
C:\Users\cbell\Documents\mygarage_v1\database>mysqlsh --uri root@localhost:3306 --sql -f garage_v1.sql
Records: 31  Duplicates: 0  Warnings: 0
Records: 6  Duplicates: 0  Warnings: 0
Records: 250  Duplicates: 0  Warnings: 0
Records: 3  Duplicates: 0  Warnings: 0
Records: 0  Duplicates: 0  Warnings: 0
Records: 0  Duplicates: 0  Warnings: 0
Records: 0  Duplicates: 0  Warnings: 0
Records: 0  Duplicates: 0  Warnings: 0
Records: 22  Duplicates: 0  Warnings: 0
Records: 0  Duplicates: 0  Warnings: 0
Records: 0  Duplicates: 0  Warnings: 0
Records: 0  Duplicates: 0  Warnings: 0
Records: 0  Duplicates: 0  Warnings: 0
Records: 22  Duplicates: 0  Warnings: 0
Records: 0  Duplicates: 0  Warnings: 0
Records: 0  Duplicates: 0  Warnings: 0
Records: 2  Duplicates: 0  Warnings: 0
Records: 3  Duplicates: 0  Warnings: 0

Listing 5-8Populating the Example Database (Windows 10)

既然我们已经创建并填充了数据库,我们就可以开始探索数据库代码模块了!

示范

当您从图书网站下载相同的源代码时,您可能已经意识到代码模块包含在下载中并且是完整的。因此,如果你是一个精明的 Python 程序员,你可能会想浏览或跳过这一章的其余部分。但是,您应该继续阅读,因为我们将看到如何使用 shell 来帮助开发这些模块的演示。也就是说,我们将使用 shell 开始为示例应用开发 Python 代码。

注意

本章完整的工作示例应用可以从本书的网站上下载。请参阅附录,了解如何设置您的机器来运行该应用。

如果你以前从未使用 Python 编程,不要担心,因为它是一种非常容易学习的语言。事实上,您只需要遵循本节中的示例,到最后您就会对代码在做什么(以及为什么)有一个坚实的理解。但是,如果您想学习 Python 或者需要更深入的 Python 知识,有几本关于这个主题的优秀书籍。下面我列出了几个我最喜欢的。Python 网站上的文档是一个很好的资源:python.org/doc/.

  • Pro Python ,第二版(Apress 2014),J. Burton Browning,Marty Alchin

  • 学习 Python ,第五版(奥赖利媒体 2013),马克·卢茨

  • 用 Python 自动化枯燥的东西:面向所有初学者的实用编程(无淀粉出版社,2015 年),Al Sweigart

在接下来的部分中,我们将看到如何先创建最简单的类(Location ),然后再创建一些其他的类。正如你将看到的,它们遵循相同的设计模式/布局,所以一旦你看到一两个,其他的就很容易预测了。因此,我们将看到使用两个类的详细演练,为了简洁起见,其余的将被演示和呈现较少的细节。

如果您想跟进,请确保已经加载了示例数据库,并且 MySQL Shell 已经准备就绪。您可能还想使用代码或文本编辑器来编写代码模块。更重要的是,您应该创建一个名为database的文件夹,并从父文件夹启动 shell。

例如,您应该创建一个名为mygarage_v1,的文件夹,并在该文件夹中创建database文件夹。然后,我们将执行来自mygarage_v1\. Why? Because we will usePython import 指令的 shell,并使用文件夹名命名代码模块的路径(例如from database import Location)。我们还将创建单元测试,因此需要一个名为unittests的文件夹来存储测试文件。

让我们从MyGarage类开始。

我的车库班

该类旨在通过提供登录(连接)到服务器的机制和封装一些常见操作(如获取会话、当前数据库、检查到 MySQL 的连接是否活动、断开连接等)来简化 MySQL 服务器的工作。我们还将包括将 SQL 结果或 select 结果转换为 Python 列表(数组)的方法,以便于处理。表 5-2 显示了我们将为这个类创建的方法的完整列表,包括所需的参数(一些方法不需要它们)。

表 5-2

我的车库类方法

|

方法

|

因素

|

描述

|
| --- | --- | --- |
| __init__() | mysqlx_sh(消歧义) | 构造函数-如果从 MySQL Shell 运行,则提供 mysqlx |
| connect() | 用户名,密码,主机,端口 | 连接到主机端口的 MySQL 服务器 |
| get_session() |   | 返回会话以便在其他类中使用 |
| get_db() |   | 返回数据库供其他类使用 |
| is_connected() |   | 检查是否连接到服务器 |
| disconnect() |   | 断开与服务器的连接 |
| make_rows() | sql_select | 为从选择结果的读取操作返回的行返回 Python 数组 |
| make_rows_sql() | sql_res,num_cols | 为从 sql 结果的读取操作返回的行返回 Python 数组 |

编写源代码

对于这个代码模块,我们不会使用 shell 来开发代码,因为这更像是一个方便的类,并且您已经看到了它的大多数方法的例子,或者至少是代码中使用的mysqlx模块中的方法。相反,我们将看到完整的代码,然后看看如何使用 shell 测试该类。该类维护当前会话,并隐藏了与服务器连接和断开连接的大部分机制。

有些人可能倾向于将连接机制转移到类中(您也可以这样做),但是使用单独的类来管理意味着您不会复制任何代码,这总是首选的。

清单 5-9 显示了MyGarage类的完整代码。在您的文本或代码编辑器中打开一个新文件,并将该代码保存在名为garage_v1.py的文件的database文件夹中。花几分钟时间通读代码。即使你正在学习 Python,它也应该易于阅读和理解。

注意

为简洁起见,本章源代码清单中的注释和不必要的行已被删除。

from __future__ import print_function

# Attempt to import the mysqlx module. If unsuccessful, we are
# running from the shell and must pass mysqlx in to the class
# constructor.
try:
    import mysqlx
except Exception:
    print("Running from MySQL Shell. Provide mysqlx in constructor.")

class MyGarage(object):
    def __init__(self, mysqlx_sh=None):
        self.session = None
        if mysqlx_sh:
            self.mysqlx = mysqlx_sh
            self.using_shell = True
        else:
            self.mysqlx = mysqlx
            self.using_shell = False

    def connect(self, username, passwd, host, port):
        config = {
            'user': username,
            'password': passwd,
            'host': host,
            'port': port,
        }
        try:
            self.session = self.mysqlx.get_session(∗∗config)
        except Exception as err:
            print("CONNECTION ERROR:", err)
            self.session = None
            raise

    def get_session(self):
        return self.session

    def get_db(self):
        return self.session.get_schema('garage_v1')

    def is_connected(self):
        return self.session and (self.session.is_open())

    def disconnect(self):
        try:
            self.session.close()
        except Exception as err:
            print("WARNING: {0}".format(err))

    def make_rows(self, sql_select):
        cols = []
        if self.using_shell:
            cols = sql_select.get_column_names()
        else:
            for col in sql_select.columns:
                cols.append(col.get_column_name())
        rows = []
        for row in sql_select.fetch_all():
            row_item = []
            for col in cols:
                if self.using_shell:
                    row_item.append("{0}".format(row.get_field(col)))
                else:
                    row_item.append("{0}".format(row[col]))
            rows.append(row_item)
        return rows

    @staticmethod
    def make_rows_sql(sql_res, num_cols):
        rows = []
        all_rows = sql_res.fetch_all()
        for row in all_rows:
            row_item = []
            for col in range(0, num_cols):
                row_item.append("{0}".format(row[col]))
            rows.append(row_item)
        return rows

    def get_last_insert_id(self):
        return self.get_session().sql(
            "SELECT LAST_INSERT_ID()").execute().fetch_one()

Listing 5-9
garage_v1 Code

注意import行。这被放在一个tryexcept块中,因为当从 shell 中使用代码模块时,shell 并不直接公开mysqlx模块(它是内置模块之一)。相反,我们可以在构造函数中提供内置mysqlx模块的实例。

实际上,__init__()接受一个参数,mysql_sh,我们可以用它从 shell 中运行代码。这是让您的代码既可以在 shell 中使用,也可以交互使用(在应用中)的一个好方法。

还要注意,我们使用了一个变量self.using_shell来存储我们是否在使用 shell。这在make_rows∫方法中是需要的,因为 Shell 中的mysqlx模块与连接器中提供的mysqlx模块略有不同。参见下面的侧栏,了解为什么会这样。

Shell 和连接器的差异

当您进入更高级的应用时,您会注意到 MySQL Shell 中使用的mysqlx模块与 MySQL 连接器(Connector/Python、Connector/J 等)中使用的模块有所不同。).这些差异的原因主要是因为希望保持模块中方法的操作或机制在不同语言之间是相同的。由于连接器支持的语言很多,标准化行为的尝试导致了 shell 如何实现相同方法的一些细微差异。幸运的是,差异很小,很容易纠正。

现在我们已经写好了源代码,让我们使用 MySQL Shell 来测试这个类。

测试类

在我们开始测试这个类之前,我们必须设置 Python path 变量(PYTHONPATH)来包含我们想要运行测试的文件夹。这是因为我们使用的模块没有安装在系统级,而是位于与我们测试的代码相关的文件夹中。在 Windows 中,可以使用以下命令将执行路径添加到 Python 路径中。

C:\Users\cbell\Documents\my_garage_v1> set PYTHONPATH=%PYTHONPATH%;c:\users\cbell\Documents\mygarage_v1

或者,在 Linux 和 macOS 上,您可以使用这个命令来设置 Python 路径。

export PYTHONPATH=$(pwd);$PYTHONPATH

现在我们可以运行 shell 了。为此,我们将使用--py选项在 Python 模式下启动。让我们在课堂上练习一些方法。我们可以尝试所有的方法,除了make_rows()方法。我们稍后会看到这些。清单 5-10 展示了如何在 shell 中导入类,初始化(创建)一个名为mygarage的类实例,然后连接connect(),并执行一些方法。最后,我们调用disconnect()来关闭与服务器的连接。

C:\Users\cbell\Documents\my_garage_v1> mysqlsh --py
MySQL  Py > from database.garage_v1 import MyGarage
Running from MySQL Shell. Provide mysqlx in constructor.
MySQL  Py > myg = MyGarage(mysqlx)
MySQL  Py > myg.connect('root', 'SECRET', 'localhost', 33060)
MySQL  Py > db = myg.get_db()
MySQL  Py > db
<Schema:garage_v1>
MySQL  Py > s = myg.get_session()
MySQL  Py > s
<Session:root@localhost:33060>
MySQL  Py > myg.is_connected()
true
MySQL  Py > myg.disconnect()
MySQL  Py > myg.is_connected()
false

Listing 5-10Testing MyGarage using MySQL Shell

注意这里我们导入了模块,然后在内置的mysqlx模块中创建了一个类实例。然后,我们连接到服务器(确保使用您系统的密码),检索数据库并打印它(通过将变量放在一行上并按下 ENTER ,为会话做同样的事情,然后最后测试is_connected()disconnect()方法。

在执行过程中,我们发出 print()语句来打印方法调用的一些结果。shell 的一个很好的特性是,如果您打印一个类实例变量,它会显示该变量的类名。这是你可以用来帮助你学习课程和选择正确方法的另一个技巧。这将节省你继续学习 X DevAPI 的时间。

警告

该类使用mysqlx模块,这需要 X 协议连接。确保使用 X 协议端口(默认为 33060)。

如果您想将这些命令保存在一个文件中,您可以。事实上,这是测试代码类(单元)的手工单元测试的一种形式。 5 为了在批处理模式下更容易阅读,我们将添加一些print()语句。要运行这个测试,创建一个名为unittests的文件夹,并将名为garage_v1_test.py的文件放在那里。清单 5-11 显示了文件的完整清单。我们还添加了提示用户 Id 和密码的代码,这比硬编码在文件中要好得多!

from getpass import getpass
from database.garage_v1 import MyGarage

print("MyGarage Class Unit test")
mygarage = MyGarage(mysqlx)
user = raw_input("User: ")
passwd = getpass("Password: ")
print("Connecting...")
mygarage.connect(user, passwd, 'localhost', 33060)
print("Getting the database...")
database = mygarage.get_db()
print(database)
print("Getting the session...")
session = mygarage.get_session()
print(session)
print("Connected?")
print(mygarage.is_connected())
print("Disconnecting...")
mygarage.disconnect()
print("Connected?")
print(mygarage.is_connected())

Listing 5-11
garage_v1_test.py

稍后,如果您想执行它,可以使用下面的命令。记得从您之前创建的文件夹(mygarage_v1)中运行这个。这是一个很好的方法,可以确保在不对整个应用进行排序的情况下测试部分代码。清单 5-12 显示了测试代码的执行。

> mysqlsh --py -f unittests\garage_v1_test.py
Running from MySQL Shell. Provide mysqlx in constructor.
MyGarage Class Unit test
User: root
Password:
Connecting...
Getting the database...
<Schema:garage_v1>
Getting the session...
<Session:root@localhost:33060>
Connected?
True
Disconnecting...
Connected?
False

Listing 5-12Running the garage_v1_test Unit Test

现在,让我们看一下最简单的类,它们对一个数据库表建模,或者在本例中,对一个视图建模。

位置类别

这个类是location视图的一个模型。回想一下,location视图执行连接,将所有存储位置和存储设备的列表合并到一个列表中,该列表可以用作查找表。因此,这个类只需要实现 read CRUD 操作。

在下一节中,我们将演示如何使用 MySQL Shell 编写该类的源代码。

编写源代码

使用 shell 编写代码的方法之一是使用交互式会话,一次编写一行代码。这允许您尝试如何组织代码,更重要的是,学习使用哪些方法。

对于这个类,我们只需要 read 操作来填充手工具、电动工具和管理器表的详细信息表单中的下拉列表。因为我们使用database\garage_v1.py代码模块进行数据库连接,所以我们需要首先初始化这个类。一旦我们登录并拥有了一个MyGarage类的实例,我们就可以用它来获取表并读取表中的行。清单 5-13 显示了可以完成这些步骤的代码。

from database.garage_v1 import MyGarage
LOCATION_READ_COLS = ['PlaceId', 'StorageEquipment', 'Type', 'Location']
LOCATION_READ_BRIEF_COLS = ['StorageEquipment', 'Type', 'Location']
mygarage = MyGarage(mysqlx)
mygarage.connect('root', 'SECRET', 'localhost', 33060)
schema = mygarage.get_db()
table = schema.get_table('location')
sql_res = table.select(LOCATION_READ_COLS).order_by(∗LOCATION_READ_BRIEF_COLS).limit(5).execute()
rows = mygarage.make_rows(sql_res)
print(rows)

Listing 5-13
Primitive Code

注意这里我们使用常量来设置列名。这使得select()方法中的代码更好一些,尤其是如果您使用需要列名列表的其他子句。在本例中,我们还使用了limit()方法,该方法将输出限制在前五行,这使得代码的执行很简短。清单 5-14 展示了这个代码在 shell 中的执行。

MySQL  Py > from database.garage_v1 import MyGarage
MySQL  Py > LOCATION_READ_COLS = ['PlaceId', 'StorageEquipment', 'Type', 'Location']
MySQL  Py > LOCATION_READ_BRIEF_COLS = ['StorageEquipment', 'Type', 'Location']
MySQL  Py > mygarage = MyGarage(mysqlx)
MySQL  Py > mygarage.connect('root', 'SECRET', 'localhost', 33060)
MySQL  Py > schema = mygarage.get_db()
MySQL  Py > table = schema.get_table('location')
MySQL  Py > sql_res = table.select(LOCATION_READ_COLS).order_by(∗LOCATION_READ_BRIEF_COLS).limit(5).execute()
MySQL  Py > rows = mygarage.make_rows(sql_res)
MySQL  Py > print(rows)
[['1007', 'Kobalt 3000 Steel Rolling Tool Cabinet (Black)', 'Drawer', 'Bottom'], ['1001', 'Kobalt 3000 Steel Rolling Tool Cabinet (Black)', 'Drawer', 'Left 1'], ['1002', 'Kobalt 3000 Steel Rolling Tool Cabinet (Black)', 'Drawer', 'Left 2'], ['1003', 'Kobalt 3000 Steel Rolling Tool Cabinet (Black)', 'Drawer', 'Left 3'], ['1004', 'Kobalt 3000 Steel Rolling Tool Cabinet (Black)', 'Drawer', 'Right 1']]

Listing 5-14Executing the Primitive Code

虽然这些行的输出不是以易读的方式打印出来的(实际上并不需要),但是如果您想查看细节,可以添加代码来实现这一点,但是打印原始 Python 列表就足以看到返回了 5 行。

现在,让我们从前面的示例代码中构造一个类。我们简单地应用编码构造来创建一个只有一个名为read()的方法的类。我们还使用 mysqlx 对象的实例编写了一个构造函数来获取该表。清单 5-15 显示了修改后的代码。

虽然本章中的清单显示了如何键入创建类所需的代码,但是有一点您必须遵循的过程。具体来说,您必须在类声明及其方法之间输入带有空格的行。这是因为当您在空白行上按下 ENTER 时,shell 将评估类代码。这同样适用于任何多行代码块,包括字典、列表等。

警告

如果您遇到关于意外缩进的错误,即使代码是正确的,也可以尝试使用带有空格的行来分隔方法。请注意,您可以在批处理模式下执行文件,而不需要带有空格的行。

from database.garage_v1 import MyGarage
LOCATION_READ_COLS = ['PlaceId', 'StorageEquipment', 'Type', 'Location']
LOCATION_READ_BRIEF_COLS = ['StorageEquipment', 'Type', 'Location']
class Location(object):
    def __init__(self, myg):
        self.table = myg.get_db().get_table('location')

    def read(self):
        sql_res = self.table.select(LOCATION_READ_COLS).order_by(
                  ∗LOCATION_READ_BRIEF_COLS).limit(5).execute()
        return(mygarage.make_rows(sql_res))

mygarage = MyGarage(mysqlx)
mygarage.connect('root', 'SECRET', 'localhost', 33060)
location = Location(mygarage)
rows = location.read()
print(rows)

Listing 5-15Location Class Primitive

请注意,我们创建了一个带有构造函数的类,该构造函数设置了一个包含该类的类变量。这是从 X DevAPI 运行方法以实现 CRUD 操作所必需的。记住,我们已经有了来自MyGarage类实例(myg)的连接,并且它被传递到构造函数的 Location 类中。

那只是班级代码。我们还需要添加代码来执行或测试该类。我们在课后补充。当我们将这段代码放在一个文件(名为listing5-15.py)中并执行它时,shell 将按照编写的方式创建类并执行类后面的代码行。例如,我们使用以下命令执行清单,该命令告诉 shell 打开文件,并以 Python 模式一次运行一行文件的内容。

$ mysqlsh --py -f listing5-15.py

现在,当我们在 shell 中执行该代码时,我们得到与之前相同的输出,如清单 5-16 所示。

MySQL  Py > from database.garage_v1 import MyGarage
Running from MySQL Shell. Provide mysqlx in constructor.

MySQL  Py > LOCATION_READ_COLS = ['PlaceId', 'StorageEquipment', 'Type', 'Location']
MySQL  Py > LOCATION_READ_BRIEF_COLS = ['StorageEquipment', 'Type', 'Location']
MySQL  Py > class Location(object):
          ->     def __init__(self, myg):
          ->         self.table = myg.get_db().get_table('location')
          ->
          ->     def read(self):
          ->         sql_res = self.table.select(LOCATION_READ_COLS).order_by(∗LOCATION_READ_BRIEF_COLS).limit(5).execute()
          ->         return(mygarage.make_rows(sql_res))
          ->
MySQL  Py > mygarage = MyGarage(mysqlx)
MySQL  Py > mygarage.connect('root', 'SECRET', 'localhost', 33060)
MySQL  Py > location = Location(mygarage)
MySQL  Py > rows = location.read()
MySQL  Py > print(rows)
[['1007', 'Kobalt 3000 Steel Rolling Tool Cabinet (Black)', 'Drawer', 'Bottom'], ['1001', 'Kobalt 3000 Steel Rolling Tool Cabinet (Black)', 'Drawer', 'Left 1'], ['1002', 'Kobalt 3000 Steel Rolling Tool Cabinet (Black)', 'Drawer', 'Left 2'], ['1003', 'Kobalt 3000 Steel Rolling Tool Cabinet (Black)', 'Drawer', 'Left 3'], ['1004', 'Kobalt 3000 Steel Rolling Tool Cabinet (Black)', 'Drawer', 'Right 1']]

Listing 5-16Executing the Location Class Primitive

正如你所看到的,我们不仅能够编写这个类,我们还在最后测试了这个类。这是创建类模块的一种常见且简单的方法。也就是说,shell 使动态编写代码成为可能,而不是在 Python 代码文件中从头开始编写代码,然后再执行它们(很多人都这样做)。这与 Python 解释器的工作方式非常相似。区别在于 shell 使得直接使用 X DevAPI 成为可能。

一旦您完善了您的类,您就可以创建适当的代码模块来存储该类。在示例应用中,这段代码以类的名称放在数据库文件夹中。例如,Location类存储在名为database/location.py的文件中。清单 5-17 中显示了Location类的完整代码。

class Location(object):
    """Location class

    This class encapsulates the location view permitting read operations
    on the data.
    """
    def __init__(self, mygarage):
        """Constructor"""
        self.mygarage = mygarage
        self.schema = mygarage.get_db()
        self.tbl = self.schema.get_table('location')

    def read(self):
        """Read data from the table"""
        sql_res = self.tbl.select(LOCATION_READ_COLS).order_by(
            ∗LOCATION_READ_BRIEF_COLS).execute()
        return self.mygarage.make_rows(sql_res)

Listing 5-17Completed Location Class Module (database/location.py)

请注意,完整的代码略有不同,我们添加了注释,存储了MyGarage实例,检索了模式(数据库),并将两者都存储在类变量中。也就是说,表的检索分两步完成,而不是链接get_schema()get_table()方法。这种简化有时会使代码更容易阅读。

现在我们已经编写了代码模块,让我们编写一个单元测试来测试这个类。

测试类

我们已经看到了如何测试清单 5-16 中的类的原型。因此,我们所要做的就是执行那些相同的行,只添加 Location 类的 import 语句。清单 5-18 显示了这个类的完整测试代码。注意,我们为行的 print 语句添加了[:5]。这将打印限制在列表中的前五个项目(行)。

from database.garage_v1 import MyGarage
from database.location import Location
mygarage = MyGarage(mysqlx)
mygarage.connect('root', 'SECRET', 'localhost', 33060)
location = Location(mygarage)
rows = location.read()
print(rows[:5])

Listing 5-18Test Code for the Location Class

我们可以将这段代码放在一个文件中并执行它,但是让我们使用 shell 来代替。清单 5-19 显示了代码在 shell 中的执行。

MySQL  Py > from database.garage_v1 import MyGarage
Running from MySQL Shell. Provide mysqlx in constructor.

MySQL  Py > from database.location import Location
MySQL  Py > mygarage = MyGarage(mysqlx)
MySQL  Py > mygarage.connect('root', 'SECRET', 'localhost', 33060)
MySQL  Py > location = Location(mygarage)
MySQL  Py > rows = location.read()
MySQL  Py > print(rows[:5])
[['Kobalt 3000 Steel Rolling Tool Cabinet (Black)', 'Drawer', 'Bottom'], ['Kobalt 3000 Steel Rolling Tool Cabinet (Black)', 'Drawer', 'Left 1'], ['Kobalt 3000 Steel Rolling Tool Cabinet (Black)', 'Drawer', 'Left 2'], ['Kobalt 3000 Steel Rolling Tool Cabinet (Black)', 'Drawer', 'Left 3'], ['Kobalt 3000 Steel Rolling Tool Cabinet (Black)', 'Drawer', 'Right 1']]

Listing 5-19Executing the Location Class Test Code

如果你认为我们可能想让测试代码更正式、更容易使用,那你就对了。我们将在后面的章节中探讨这一点。但是首先,让我们看看如何为 vendor 表创建类。

供应商类别

Vendor类负责封装vendor表上的创建、读取、更新和删除(CRUD)操作。因此,该类将按名称实现这些方法。事实上,其他类将实现相同的方法。这样,我们在示例应用数据库代码中就有了一致性。

在这一节中,我们将详细检查Vendor类代码,包括如何使用 shell 构建类,以及如何在 shell 中测试类。现在我们已经看到了一个更小的例子(只有 read 操作),这个类的代码至少在外观上是熟悉的,但是您将会看到更详细的内容。

我们将看到一个详细的演示,展示如何从第一个操作——create 开始,以增量方式(一次一个方法)为类编写代码。我们还将看到测试代码被添加到每个示例中,但是为了简洁起见,我们将只显示为我们关注的方法执行的代码(每个 CRUD 操作)。

创造

创建操作是我们在表中创建新行的地方。因此,我们需要提供该行的所有数据。在这种情况下,它包括名称、URL 和源字段。回想一下,这允许我们给供应商一个我们认识的名称(例如,Kobalt,Craftsman),一个到供应商网站的 URL,以及一个 sources 字段,它是一个描述我们可以为该供应商购买产品的商店的文本字段。

Location类一样,我们需要添加一些指令来开始包含MyGarage类的导入和一个包含列名的列表。该列表纯粹是一种簿记方法,它允许我们更改列或者为不同的 SQL 操作使用替代的列定义。我们将在数据库代码的其他类中更详细地使用这种技术。

我们还包括了类定义,就像我们之前做的那样,适当地命名类,并添加一个构造函数方法,该方法接受MyGarage实例,将其存储在类变量中以备后用,获取模式并将其保存到类变量中,最后获取表类实例。

create()方法只是以字典的形式接受来自调用者的值,其中列名是键,然后发出insert()方法,通过链接values()方法传入列名和列值的列表。清单 5-20 显示了Vendor类的初始版本。花点时间通读一下类定义。

注意在清单的底部是额外的代码来创建一个Vendor类的实例,创建一个测试值字典,然后调用create()方法。最后,我们使用MyGarage方法get_last_insert_id()检索最后一个自动增量值并打印出来。

from database.garage_v1 import MyGarage
VENDOR_COLS_CREATE = ['Name', 'URL', 'Sources']
class Vendor(object):
    def __init__(self, myg):
        self.mygarage = mygarage
        self.schema = mygarage.get_db()
        self.tbl = self.schema.get_table('vendor')
    def create(self, vendor_data):
        vendor_name = vendor_data.get("Name", None)
        link = vendor_data.get("URL", None)
        sources = vendor_data.get("Sources", None)
        self.tbl.insert(VENDOR_COLS_CREATE).values(
            vendor_name, link, sources).execute()

mygarage = MyGarage(mysqlx)
mygarage.connect('root', 'SECRET', 'localhost', 33060)
vendor = Vendor(mygarage)
vendor_data = {
    "Name": "ACME Bolt Company",
    "URL": "www.acme.org",
    "Sources": "looney toons"
}

vendor.create(vendor_data)
last_id = mygarage.get_last_insert_id()[0]

Listing 5-20Vendor Class create() Method

当我们在 shell 中执行这段代码时,我们将看到每一行代码都经过了验证。不要忘记使用一个上面有空格的空行来分隔类方法,并使用一个上面没有空格的空行来终止类和字典定义。请参阅前面的注释,了解我们为什么需要这样做。

让我们专注于测试代码。这里,我们简单地创建了一个新行,并检索了它的Id列的值,如下所示(为了简洁起见,省略了演示的其余部分)。

...
MySQL  Py > print("Last insert id = {0}".format(last_id))
Last insert id = 177

然而,这不是很多细节,是吗?我们实际上无法判断该行是否被插入——只知道我们获得了最后一个插入 Id。因此,让我们实现读操作并使用它来验证创建。

阅读

读取操作需要两件事:它需要能够读取表中的所有行并返回它们,就像我们对Location类所做的那样,但是它也需要用于读取单个行并返回该数据。这是因为我们要么读取列表视图的所有行,要么在查看单行时检索该行的值。

为此,我们将使用一个名为vendor_id的参数,默认设置为None。这允许我们测试这个参数,如果是None,检索所有行,或者如果有值,检索单个行。

读操作还有另一个方面。回想一下 create 操作使用了一个列表来包含列名。在这种情况下,我们不需要Id字段,因为创建操作(插入)将导致 MySQL 填充该值。然而,为了读取一行或所有行,我们需要获取Id列。因此,我们构建了另一个列表来为 select()方法调用添加 Id 列。

让我们也添加一些错误处理代码。在这种情况下,我们将使用一个try…except块来捕捉在insert()select()期间的任何错误。我们还添加了一项技术来返回一个布尔值,告诉调用者操作是否有效,如果无效,则向用户显示一条错误消息。我们通过返回一个元组来做到这一点,比如(True, None)表示成功,或者(False, <error>)表示错误。这将有助于我们以后如果有问题。清单 5-21 显示了添加了read()方法并更新了测试代码的类。

from database.garage_v1 import MyGarage
VENDOR_COLS_CREATE = ['Name', 'URL', 'Sources']
VENDOR_COLS = []
VENDOR_COLS.extend(VENDOR_COLS_CREATE)
VENDOR_COLS.insert(0, 'Id') # Add the Id to the list
class Vendor(object):
    def __init__(self, mygarage):
        self.mygarage = mygarage
        self.schema = mygarage.get_db()
        self.tbl = self.schema.get_table('vendor')

    def create(self, vendor_data):
        vendor_name = vendor_data.get("Name", None)
        link = vendor_data.get("URL", None)
        sources = vendor_data.get("Sources", None)
        assert vendor_name, "You must supply a name for the vendor."
        try:
            self.tbl.insert(VENDOR_COLS_CREATE).values(
                vendor_name, link, sources).execute()
        except Exception as err:
            print("ERROR: Cannot add vendor: {0}".format(err))
            return (False, err)
        return (True, None)

    def read(self, vendor_id=None):
        if not vendor_id:
            # return all vendors
            sql_res = self.tbl.select(VENDOR_COLS).order_by("Name").execute()
        else:
            # return specific vendor
            sql_res = self.tbl.select(VENDOR_COLS).where(
                "Id = '{0}'".format(vendor_id)).execute()
        return self.mygarage.make_rows(sql_res)

mygarage = MyGarage(mysqlx)
mygarage.connect('root', 'SECRET', 'localhost', 33060)
vendor = Vendor(mygarage)
vendor_data = {
    "Name": "ACME Bolt Company",
    "URL": "www.acme.org",
    "Sources": "looney toons"
}

vendor.create(vendor_data)
last_id = mygarage.get_last_insert_id()[0]
print("Last insert id = {0}".format(last_id))
rows = vendor.read(last_id)
print("{0}".format(", ".join(rows[0])))
rows = vendor.read()
print(rows[:5])

Listing 5-21Adding the read() Method

注意,为了简洁,最后一行代码只打印了返回的前五行。像以前一样,我们将省略该类的条目,并将重点放在测试该类的行上。下面显示了为测试create()read()方法而执行的代码行。我们通过使用在create()之后返回的最后一个 Id 来测试读取单个行,然后执行一个read()来获取所有行。

...
MySQL  Py > print("Last insert id = {0}".format(last_id))
Last insert id = 178
MySQL  Py > rows = vendor.read(last_id)
MySQL  Py > print("{0}".format(", ".join(rows[0])))
178, ACME Bolt Company, www.acme.org, looney toons
MySQL  Py > rows = vendor.read()
MySQL  Py > print(rows[:5])
[['178', 'ACME Bolt Company', 'www.acme.org', 'looney toons'], ['175', 'ACME Bolt Company', 'www.acme.org', 'looney toons'], ['172', 'ACME Bolt Company', 'www.acme.org', 'looney toons'], ['171', 'ACME Bolt Company', 'www.acme.org', 'looney toons'], ['170', 'ACME Bolt Company', 'www.acme.org', 'looney toons']]

同样,这些行的打印并不漂亮,但出于开发目的,它确实显示了create()read()正在工作。酷!现在,让我们添加更新操作。

更新

更新操作类似于创建操作,因为我们需要该行的所有数据。但是与创建操作不同,我们需要Id列,以便更新正确的行。精明的开发人员会在发布更新之前添加一个验证列的步骤,以便只更新那些发生变化的列,但是我们将采用一种更简单的方法,提供所有的列,并让数据库进行排序。

然而,由于更新操作必须有Id列,我们将添加一个断言来确保调用者为where()方法提供该信息。否则,更新就太危险了!

我们还在update()周围使用了一个try...except块来捕捉任何错误。清单 5-22 显示了添加了update()方法的类,为了简洁起见,省略了构造函数、create()read()方法。注意我们如何使用一个for循环来设置列的值。

from database.garage_v1 import MyGarage
VENDOR_COLS_CREATE = ['Name', 'URL', 'Sources']
VENDOR_COLS = []
VENDOR_COLS.extend(VENDOR_COLS_CREATE)
VENDOR_COLS.insert(0, 'Id') # Add the Id to the list
class Vendor(object):
...
    def update(self, vendor_data):
        vendor_id = vendor_data.get("VendorId", None)
        vendor_name = vendor_data.get("Name", None)
        link = vendor_data.get("URL", None)
        sources = vendor_data.get("Sources", None)
        assert vendor_id, "You must supply an Id to update the vendor."
        field_value_list = [('Name', vendor_name),
                            ('URL', link), ('Sources', sources)]
        try:
            tbl_update = self.tbl.update()
            for field_value in field_value_list:
                tbl_update.set(field_value[0], field_value[1])
            tbl_update.where("Id = '{0}'".format(vendor_id)).execute()
        except Exception as err:
            print("ERROR: Cannot update vendor: {0}".format(err))
            return (False, err)
        return (True, None)

mygarage = MyGarage(mysqlx)
mygarage.connect('root', 'SECRET', 'localhost', 33060)
vendor = Vendor(mygarage)
vendor_data = {
    "Name": "ACME Bolt Company",
    "URL": "www.acme.org",
    "Sources": "looney toons"
}
vendor.create(vendor_data)
last_id = mygarage.get_last_insert_id()[0]
print("Last insert id = {0}".format(last_id))
rows = vendor.read(last_id)
print("{0}".format(", ".join(rows[0])))
rows = vendor.read()
print(rows[:5])
vendor_data = {
    "VendorId": last_id,
    "Name": "ACME Nut Company",
    "URL": "www.acme.org",
    "Sources": "looney toons"
}
vendor.update(vendor_data)
rows = vendor.read(last_id)
print("{0}".format(", ".join(rows[0])))

Listing 5-22Adding the update() Method

更新操作的测试代码简单地使用与创建操作相同的字典,只是我们更改了一些值来测试更新。下面显示了从创建操作后的读取开始的测试执行的输出。注意 update()确实改变了我们之前创建的那一行的值。

...
MySQL  Py > rows = vendor.read(last_id)
MySQL  Py > print("{0}".format(", ".join(rows[0])))
179, ACME Bolt Company, www.acme.org, looney toons
MySQL  Py > rows = vendor.read()
...
MySQL  Py > vendor_data = {
          ->     "VendorId": last_id,
          ->     "Name": "ACME Nut Company",
          ->     "URL": "www.acme.org",
          ->     "Sources": "looney toons"
          -> }
MySQL  Py > vendor.update(vendor_data)
MySQL  Py > rows = vendor.read(last_id)
MySQL  Py > print("{0}".format(", ".join(rows[0])))
179, ACME Nut Company, www.acme.org, looney toons

现在,让我们添加最后一个操作—删除。

删除

删除操作只是删除表中的一行。我们需要做的就是 Id 列。因此,delete()方法被编写为使用vendor_id作为参数测试,以确保提供了一个参数,然后在表上发出删除操作。

像其他方法一样,我们使用try…except块并返回一个元组来报告操作是否成功。清单 5-23 显示了添加了delete()方法的类,为了简洁起见,省略了构造函数、create()read()update()方法。

from database.garage_v1 import MyGarage
VENDOR_COLS_CREATE = ['Name', 'URL', 'Sources']
VENDOR_COLS = []
VENDOR_COLS.extend(VENDOR_COLS_CREATE)
VENDOR_COLS.insert(0, 'Id') # Add the Id to the list
class Vendor(object):
...
    def delete(self, vendor_id=None):
        """Delete a row from the table"""
        assert vendor_id, "You must supply an Id to delete the vendor."
        try:
            self.tbl.delete().where("Id = '{0}'".format(vendor_id)).execute()
        except Exception as err:
            print("ERROR: Cannot delete vendor: {0}".format(err))
            return (False, err)
        return (True, None)

mygarage = MyGarage(mysqlx)
mygarage.connect('root', 'secret', 'localhost', 33060)
vendor = Vendor(mygarage)
vendor_data = {
    "Name": "ACME Bolt Company",
    "URL": "www.acme.org",
    "Sources": "looney toons"
}
vendor.create(vendor_data)
last_id = mygarage.get_last_insert_id()[0]
print("Last insert id = {0}".format(last_id))
rows = vendor.read(last_id)
print("{0}".format(", ".join(rows[0])))
rows = vendor.read()
print(rows[:5])
vendor_data = {
    "VendorId": last_id,
    "Name": "ACME Nut Company",
    "URL": "www.acme.org",
    "Sources": "looney toons"
}
vendor.update(vendor_data)
rows = vendor.read(last_id)
print("{0}".format(", ".join(rows[0])))
vendor.delete(last_id)
rows = vendor.read(last_id)
if not rows:
    print("Record not found.")

Listing 5-23Adding the delete() Method

好了,这就是这个类的完整代码。下面显示了调用update()后的执行。在这里,我们尝试删除我们插入并稍后更新的行,然后尝试从表中读取它。如果没有返回行,则没有找到该行,因此我们显示删除操作成功。

...
MySQL  Py > rows = vendor.read(last_id)
MySQL  Py > print("{0}".format(", ".join(rows[0])))
180, ACME Nut Company, www.acme.org, looney toons
MySQL  Py > vendor.delete(last_id)
MySQL  Py > rows = vendor.read(last_id)
MySQL  Py > if not rows:
          ->     print("Record not found.")
Record not found.

既然我们已经详细了解了如何使用 shell 来创建数据库表(和视图)的类,那么让我们来看看其余每个类的概述。每个类都以与Vendor类相同的方式编写,并注明了该类的具体实现细节。正如你将看到的,一致性是我们的朋友。

手工工具类

Handtool类封装了handtool表的 CRUD 操作。它使用与其他类相同的类结构和方法。它在复杂性上与Vendor类有三个主要的不同。

首先,handtool表有几个不能为空的字段,所以插入和更新操作有一些额外的断言,如下所示。

assert tool_size, "You must specify a toolsize for the handtool."
assert handtool_type, "You must specify a type for the handtool."
assert description, "You must supply a description for the handtool."
assert place_id, "You must supply an Id for the handtool."

其次,该表有一个枚举字段,我们用代码中的另一个列表来表示它,如下所示。这允许我们将名称映射到枚举值。这可能看起来很奇怪,因为这是一个值重复的元组列表。那是故意的。映射可以这样定义,这样键(元组中的第一个值)就被用来“映射”到第二个值(或者只是值)。因为我们是优秀的数据库开发人员,我们不为枚举的字段值编码数值,我们必须重复该值。

HANDTOOL_TYPES = [
    ('Adjustable Wrench', 'Adjustable Wrench'), ('Awl', 'Awl'),
    ('Clamp', 'Clamp'), ('Crowbar', 'Crowbar'), ('Drill Bit', 'Drill Bit'),
    ('File', 'File'), ('Hammer', 'Hammer'), ('Knife', 'Knife'), ('Level', 'Level'),
    ('Nutdriver', 'Nutdriver'), ('Pliers', 'Pliers'), ('Prybar', 'Prybar'),
    ('Router Bit', 'Router Bit'), ('Ruler', 'Ruler'), ('Saw', 'Saw'),
    ('Screwdriver', 'Screwdriver'), ('Socket', 'Socket'),
    ('Socket Wrench', 'Socket Wrench'), ('Wrench', 'Wrench'),
]

第三,因为handtool表有几个字段,所以用于查找或浏览操作的表中的行列表不需要所有的字段。我们也希望看到外键所指向的值。因此,我们使用如下所示的 SQL SELECT查询来代替read()方法的 read all rows 特性。我们将它存储在一个常量中,以便于阅读和修改。

HANDTOOL_READ_LIST = (
    "SELECT handtool.Id, handtool.type, handtool.description, "
    "handtool.toolsize, storage.description as StorageEquipment, "
    "place.type as locationtype, place.description as location FROM garage_v1.handtool "
    "JOIN garage_v1.place ON "
    "handtool.placeid = place.id JOIN garage_v1.storage ON place.storageid = storage.id "
    "ORDER BY handtool.type, handtool.description"
)

该查询用于读取操作,如下所示。注意,我们使用会话对象的 sql()方法,而不是 select()方法来发出查询。因此,我们还在构造函数中捕获会话对象实例。

if not handtool_id:
    # return all handtools - uses a JOIN so we have to use the sql()
    # method instead of select, but we arrive at the same results
    sql_res = self.session.sql(HANDTOOL_READ_LIST).execute()
    return self.mygarage.make_rows_sql(sql_res, len(HANDTOOL_READ_COLS))
else:
    # return specific handtool
    sql_res = self.tbl.select(HANDTOOL_COLS).where(
        "Id = '{0}'".format(handtool_id)).execute()
return self.mygarage.make_rows(sql_res)

你可以在database/handtool.py代码模块中找到这个代码。花一些时间来研究这些变化,亲自看看它们是如何结合在一起的。

组织者类

Organizer类封装了organizer表的 CRUD 操作。它使用与其他类相同的类结构和方法。它在复杂程度上不同于Vendor级,比如Handtool级;它需要枚举列的映射和读取操作的 SQL SELECT语句,如下所示。否则,代码与Vendor类的模式相同。

ORGANIZER_TYPES = [('Bin', 'Bin'), ('Box', 'Box'), ('Case', 'Case')]
...
ORGANIZER_READ_LIST = (
    "SELECT organizer.Id, organizer.Type, organizer.Description, "
    "storage.description as StorageEquipment, place.type as LocationType, "
    "place.description as Location FROM  garage_v1.organizer JOIN "
    "garage_v1.place ON organizer.placeid = place.ID  JOIN "
    "garage_v1.storage ON place.storageid = storage.id "
    "ORDER BY Type, organizer.description"
)

处级

Place类封装了place表的 CRUD 操作。它使用与其他类相同的类结构和方法。它在复杂程度上不同于Vendor级,比如Handtool级;它需要枚举列的映射和读取操作的 SQL SELECT语句,如下所示。否则,代码与Vendor类的模式相同。

PLACE_TYPES = [('Drawer', 'Drawer'), ('Shelf', 'Shelf')]
...
PLACE_READ_LIST = (
    "SELECT place.Id, storage.description as StorageEquipment, place.Type as LocationType, "
    "place.Description as Location FROM garage_v1.place JOIN "
    "garage_v1.storage ON place.StorageId = storage.ID ORDER BY "
    "StorageEquipment, LocationType, Location"
)

电动工具类

Powertool类封装了powertool表的 CRUD 操作。它使用与其他类相同的类结构和方法。它在复杂程度上不同于Vendor级,比如Handtool级;它需要枚举列的映射和读取操作的 SQL SELECT语句,如下所示。否则,代码与Vendor类的模式相同。

POWERTOOL_TYPES = [('Corded', 'Corded'), ('Cordless', 'Cordless'), ('Air', 'Air')]
...
POWERTOOL_READ_LIST = (
    "SELECT powertool.Id, powertool.type, powertool.description, "
    "storage.description as StorageEquipment, place.type as locationtype, "
    "place.description as location FROM garage_v1.powertool JOIN garage_v1.place "
    "ON powertool.placeid = place.id JOIN garage_v1.storage ON "
    "place.storageid = storage.id ORDER BY powertool.type, powertool.description"
)

存储类

Storage类封装了storage表的 CRUD 操作。它使用与其他类相同的类结构和方法。它在复杂程度上不同于Vendor级,比如Handtool级;它需要枚举列的映射和读取操作的 SQL SELECT语句,如下所示。它在返回所有行的读操作上也有所不同,返回一个较小的列列表。这用于在用户界面中显示所有的存储设备。否则,代码与Vendor类的模式相同。

STORAGE_TYPES = [
    ('Cabinet', 'Cabinet'), ('Shelving', 'Shelving'),
    ('Toolchest', 'Toolchest'), ('Workbench', 'Workbench')
]
STORAGE_COLS_BRIEF = [
    'storage.Id', 'Type', 'Description', 'Location'
]
...
STORAGE_READ_LIST = (
   "SELECT storage.Id, vendor.name, Type, description, Location FROM "
   "garage_v1.storage JOIN garage_v1.vendor ON storage.VendorId = vendor.Id "
   "ORDER BY Type, Location"
)

测试类模块

开发人员工具箱中的一个工具是一组强大的测试。既然我们已经看到了如何在 shell 中创建数据库类,那么现在让我们看看如何开发一个测试框架来测试数据库类。

回想一下,这些类的测试使用了非常相似的机制,并且实际上遵循了相同的步骤序列。每当开发人员看到这一点,他们就会想到“自动化”和“类”也就是说,很容易创建一个包含所有步骤和子类的基类,这些步骤和子类实现了特定于被测试类的类(test)。这是实现可重复测试的一种非常常见的方式。

在这种情况下,我们在unittests/crud_test.py代码模块中创建了一个名为CRUDTest的基类,它实现了启动(或设置)测试的方法,一个显示返回行的通用方法,以及一个我们想要运行的测试用例。表 5-3 显示了类中实现的方法。

表 5-3

CRUDTest 类方法

|

方法

|

因素

|

描述

|
| --- | --- | --- |
| __init__() |   | 构造器 |
| begin() | mysqlx 实例,类名,用户名,密码 | 连接到 MySQL 服务器并设置 MyGarage 类。由 setup()方法调用 |
| show_rows() | 行数(列表),要显示的行数 | 打印列表中的行,直到指定的数目 |
| set_up() |   | 设置测试并初始化测试用例。每个类的覆盖 |
| create() |   | 运行创建测试用例。每个类的覆盖 |
| read_all() |   | 运行读取测试用例以返回所有行。每个类的覆盖 |
| read_one() |   | 运行读取测试用例以返回特定的行。每个类的覆盖 |
| update() |   | 运行更新测试用例。每个类的覆盖 |
| delete() |   | 运行删除测试用例。每个类的覆盖 |
| tear_down() |   | 关闭测试并断开与服务器的连接 |

这看起来工作量很大,但是让我们先看看这个类,然后看一个例子,看看我们如何从它派生出一个数据库类来创建一个测试。清单 5-24 显示了CRUDTest类的代码。

from __future__ import print_function
from getpass import getpass
from database.garage_v1 import MyGarage
class CRUDTest(object):
    """Base class for Unit testing table/view classes."""

    def __init__(self):
        """Constructor"""
        self.mygarage = None

    def __begin(self, mysql_x, class_name, user=None, passwd=None):
        """Start the tests"""
        print("\n∗∗∗ {0} Class Unit test ∗∗∗\n".format(class_name))
        self.mygarage = MyGarage(mysql_x)
        if not user:
            user = raw_input("User: ")
        if not passwd:
            passwd = getpass("Password: ")
        print("Connecting...")
        self.mygarage.connect(user, passwd, 'localhost', 33060)
        return self.mygarage

    @staticmethod
    def show_rows(rows, num_rows):
        """Display N rows from row result"""
        print("\n\tFirst {0} rows:".format(num_rows))
        print("\t--------------------------")
        for item in range(0, num_rows):
            print("\t{0}".format(", ".join(rows[item])))

    def set_up(self, mysql_x, user=None, passwd=None):
        """Setup functions"""
        pass

    def create(self):
        """Run Create test case"""
        pass

    def read_all(self):
        """Run Read(all) test case"""
        pass

    def read_one(self):
        """Run Read(record) test case"""
        pass

    def udpate(self):
        """Run Update test case"""
        pass

    def delete(self):
        """Run Delete test case"""
        pass

    def tear_down(self):
        """Tear down functions"""
        print("\nDisconnecting...")
        self.mygarage.disconnect()

Listing 5-24Code for the CRUDTest Class

注意,我们在基类中执行初始化、设置和拆卸步骤。这样,我们可以确保对每个类都以相同的方式执行这些步骤。

还要注意,我们要覆盖的方法是以“pass”作为主体列出的。这本质上是一个“什么都不做”但合法的方法体。我们将在用于为数据库类创建测试的类中编写每个方法的细节。

例如,我们通过创建一个名为VendorTests的新类来迁移我们对 Vendor 类的测试,这个新类是从CRUDTest派生的,并存储在文件unittests/vendor_test.py中。清单 5-25 显示了新类的代码。

from __future__ import print_function

from unittests.crud_test import CRUDTest
from database.vendor import Vendor

class VendorTests(CRUDTest):
    """Test cases for the Vendor class"""

    def __init__(self):
        """Constructor"""
        CRUDTest.__init__(self)
        self.vendor = None
        self.last_id = None

    def set_up(self, mysql_x, user=None, passwd=None):
        """Setup the test cases"""
        self.mygarage = self.begin(mysql_x, "Vendor", user, passwd)
        self.vendor = Vendor(self.mygarage)

    def create(self):
        """Run Create test case"""
        print("\nCRUD: Create test case")
        vendor_data = {
            "Name": "ACME Bolt Company",
            "URL": "www.acme.org",
            "Sources": "looney toons"
        }
        self.vendor.create(vendor_data)
        self.last_id = self.mygarage.get_last_insert_id()[0]
        print("\tLast insert id = {0}".format(self.last_id))

    def read_all(self):
        """Run Read(all) test case"""
        print("\nCRUD: Read (all) test case")
        rows = self.vendor.read()
        self.show_rows(rows, 5)

    def read_one(self):
        """Run Read(record) test case"""
        print("\nCRUD: Read (row) test case")
        rows = self.vendor.read(self.last_id)
        print("\t{0}".format(", ".join(rows[0])))

    def update(self):
        """Run Update test case"""
        print("\nCRUD: Update test case")
        vendor_data = {
            "VendorId": self.last_id,
            "Name": "ACME Nut Company",
            "URL": "www.acme.org",
            "Sources": "looney toons"
        }
        self.vendor.update(vendor_data)

    def delete(self):
        """Run Delete test case"""
        print("\nCRUD: Delete test case")
        self.vendor.delete(self.last_id)
        rows = self.vendor.read(self.last_id)
        if not rows:
            print("\tNot found (deleted).")

Listing 5-25Code for the VendorTests Class

这项技术的强大之处在于,我们可以继续为每个以类命名的数据库类创建新的测试,并将它们存储在同一个unittests文件夹中。然后我们可以编写一个驱动脚本,在一个循环中运行所有的测试。由于 Location 类只有一个 read all 操作,我们可以为其他操作编写“no operation ”,这允许我们在循环中包含LocationTests。清单 5-26 显示了名为run_all.py的驱动脚本的代码,也存储在unittests文件夹中。

from __future__ import print_function
from getpass import getpass
from unittests.handtool_test import HandtoolTests
from unittests.location_test import LocationTests
from unittests.organizer_test import OrganizerTests
from unittests.place_test import PlaceTests
from unittests.powertool_test import PowertoolTests
from unittests.storage_test import StorageTests
from unittests.vendor_test import VendorTests
print("CRUD Tests for all classes...")
crud_tests = []
handtool = HandtoolTests()
crud_tests.append(handtool)
location = LocationTests()
crud_tests.append(location)
organizer = OrganizerTests()
crud_tests.append(organizer)
place = PlaceTests()
crud_tests.append(place)
powertool = PowertoolTests()
crud_tests.append(powertool)
storage = StorageTests()
crud_tests.append(storage)
vendor = VendorTests()
crud_tests.append(vendor)
# Get user, passwd
user = raw_input("User: ")
passwd = getpass("Password: ")
# Run the CRUD operations for all classes that support them
for test in crud_tests:
    test.set_up(mysqlx, user, passwd)
    test.create()
    test.read_one()
    test.read_all()
    test.update()
    test.read_one()
    test.delete()
    test.tear_down()

Listing 5-26Test Driver run_all.py

要执行这个测试,您可以使用清单 5-27 中所示的命令以及预期的输出。这里,为了简洁起见,我们只看到输出的一部分。

C:\Users\cbell\Documents\mygarage_v1>mysqlsh --py -f unittests/run_all.py
Running from MySQL Shell. Provide mysqlx in constructor.
CRUD Tests for all classes...
User: root
Password:
∗∗∗ Handtool Class Unit test ∗∗∗
Connecting...
CRUD: Create test case
        Last insert id = 2267
CRUD: Read (row) test case
        2267, 101, Plumpbus, Hammer, medium, 1001
CRUD: Read (all) test case
        First 5 rows:
        --------------------------
        2050, Awl, Alloy Steel Scratch, 6-in, Kobalt 3000 Steel Rolling Tool Cabinet (Black), Drawer, Left 3
        2048, Awl, Complex Hook, 3-in, Kobalt 3000 Steel Rolling Tool Cabinet (Black), Drawer, Left 3
        2049, Awl, Curved Hook, 3-in, Kobalt 3000 Steel Rolling Tool Cabinet (Black), Drawer, Left 3
        2047, Awl, Hook, 3-in, Kobalt 3000 Steel Rolling Tool Cabinet (Black), Drawer, Left 3
        2046, Awl, Scratch, 3-in, Kobalt 3000 Steel Rolling Tool Cabinet (Black), Drawer, Left 3
CRUD: Update test case
CRUD: Read (row) test case
        2267, 101, Plumpbus Pro, Screwdriver, grande, 1001
CRUD: Delete test case
        Not found (deleted).
Disconnecting...
∗∗∗ Location Class Unit test ∗∗∗
Connecting...
CRUD: Create test case (SKIPPED)
CRUD: Read (row) test case (SKIPPED)
CRUD: Read (all) test case
        First 5 rows:
        --------------------------
        1007, Kobalt 3000 Steel Rolling Tool Cabinet (Black), Drawer, Bottom
        1001, Kobalt 3000 Steel Rolling Tool Cabinet (Black), Drawer, Left 1
        1002, Kobalt 3000 Steel Rolling Tool Cabinet (Black), Drawer, Left 2
        1003, Kobalt 3000 Steel Rolling Tool Cabinet (Black), Drawer, Left 3
        1004, Kobalt 3000 Steel Rolling Tool Cabinet (Black), Drawer, Right 1
CRUD: Update test case (SKIPPED)
CRUD: Read (row) test case (SKIPPED)
CRUD: Delete test case (SKIPPED)
...

Listing 5-27Executing the Test Driver

花些时间从书籍网站下载代码,并自己测试单元测试。您应该注意到使用这个概念非常容易,并且您可以开发其他类似的概念来测试您的数据库代码。只是觉得;我们无需编写任何用户界面代码就可以做到这一点,这允许在编写第一行用户界面代码之前验证我们的数据库代码。很好!

摘要

起初,有些人可能会怀疑 MySQL Shell 可以用作开发工具的说法。这可能部分是因为它是新的,部分是因为它不是一个典型的代码编辑器;相反,它更像是 Python 解释器。

但是,您已经亲眼看到了如何使用 shell 来测试代码,以了解哪些方法有效以及如何使用 X DevAPI,而且您还看到了在 shell 中编写代码并执行代码是多么容易。

事实上,我们通过演示如何在没有用户界面支持的情况下用 Python 开发和测试数据库代码模块,进一步提升了这一点。这对开发人员来说是一个巨大的好处,因为它经常被留到最后来测试像数据库类这样的模块。这样,我们在编写用户界面之前测试数据库代码,从而允许我们一次只关注应用的一部分。

在下一章中,我们将继续我们在 X DevAPI 中使用 shell 的旅程,深入了解如何使用 MySQL 文档存储——一种全新的、非 SQL (NOSQL)的数据处理方式。

六、将 Shell 与文档存储一起使用

到目前为止,在本书中,我们已经发现了 MySQL Shell,并了解了如何使用它来取代旧的 MySQL 客户端(MySQL ),以及如何使用 Shell 通过传统的 SQL 命令来管理我们的数据库。我们还学习了如何使用 X DevAPI 使用 shell 来开发我们的关系数据库代码,甚至进行测试!

现在是时候了解什么是文档存储以及我们如何开始使用它了。是的,你也可以用贝壳做同样的事情。核心概念是 JavaScript 对象符号(JSON)文档。我们将了解更多关于 JSON 是什么以及 MySQL 文档存储是如何工作的。我们还将看到几个如何将 JSON 与关系数据库结合的例子。在下一章,我们将着手用 JSON 文档构建一个 NoSQL 解决方案。

让我们简短地了解一下术语和技术概述。

概观

MySQL 文档存储库的起源在于几种技术,它们被结合在一起形成了文档存储库。具体来说,Oracle 将键、值机制与新的数据类型、新的编程库和新的访问机制结合起来,创建了现在的文档存储。这不仅允许我们使用带有 NoSQL 接口的 MySQL,还允许我们构建混合解决方案,利用关系数据的稳定性和结构,同时增加 JSON 文档的灵活性。

在本章中,我们将了解 MySQL 如何支持 JSON 文档,包括如何添加、查找、更新和删除数据(通常分别称为创建、读取、更新和删除或简称为 CRUD)。我们从本章和下一章中你将会遇到的概念和技术的更多信息开始。然后我们将继续学习更多关于 MySQL 服务器中 JSON 数据类型和 JSON 函数的知识。虽然这一章的重点是在关系数据中使用 JSON,但是掌握 MySQL 文档存储 NoSQL 接口(X DevAPI)需要一个关于如何使用 JSON 的坚实基础。

在 MySQL 中使用文档存储和 JSON 时,我们会遇到一些新概念、新技术和相关术语。在这一节中,我们将看到这些概念和技术如何解释 JSON 数据类型和文档存储接口的组成。让我们从 JSON 使用的最基本的概念开始:键、价值机制。

起源:关键,价值机制

像这个世界上的大多数东西一样,没有什么东西是真正新的,因为它是完全原创的,没有以前出现过的某种形式,因此通常是以新的方式应用现有技术构建的。键、值机制是基础技术的一个典型例子。我们使用术语“机制”,因为键的使用允许您访问值。

当我们说 key,value 时,我们的意思是存在一些标签(通常是一个字符串)构成了键,并且每个键都与一个值相关联。例如,"name":"Charlie"是一个示例,其中 key ( name)具有一个值(Charlie)。虽然键值存储中的值通常是短字符串,但是值可以是复杂的;数字、字母数字、列表,甚至嵌套键、值集。

Key,value 机制最出名的是易于编程使用,同时仍然保持可读性。也就是说,通过大量使用空格,一个复杂的嵌套键,值数据结构可以被人类读取。下面显示了一个示例,其格式类似于一些开发人员格式化代码的方式。 1 可以看出,很容易看出这一组键、值都存储了什么;姓名、地址和电话号码。

{ "name": {
    "first":"Charlie",
    "last":"Harrington"
  },
  "address": {
    "street":"123 Main Street",
    "city":"melborne",
    "state":"California",
    "zip":"90125"
  }
  "phone_numbers": [
      "800-555-1212",
      "888-212-1234"
    ]
  }
}

键、值机制(或存储)的一个例子是可扩展标记语言(XML),它已经存在了一段时间。下面是一个使用上述数据的简单 XML 示例。它是 SQL SELECT查询的结果,输出(行)以 XML 格式显示。注意 XML 是如何使用 HTML 这样的标签(因为它是从 HTML 派生出来的)以及数据的键、值存储的。这里,键是<row><field>,值是开始和结束标记符号之间的内容(<field> </field>)。

<?xml version="1.0"?>
<resultset statement="select ∗ from thermostat_model limit 1;" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <row>
    <field name="model_id">acme123</field>
    <field name="brand">WeMakeItSoCo</field>
  </row>
</resultset>

有些系统是围绕键、值机制(称为键、值或关系存储)设计的,比如语义网。简而言之,语义网试图利用数据的关联来描述事物、事件等。有时,术语“关系存储”或“三重存储”被用来描述所采用的存储系统的类型。语义 Web 中使用了几种形式的关键值机制,包括资源描述框架(RDF)、Web 本体语言(OWL)和可扩展标记语言(XML)。还有其他一些关键值机制的例子,但是与文档存储最相关的是 JSON。

现在让我们看看文档库的另一个关键组件——从编程库开始的 NoSQL 接口。

应用界面

回想一下,应用编程接口(API),有时简称为库或编程库,是一组支持一个或多个功能的操作的类和方法。通过这些类和方法,这些功能允许程序员使用这些类和方法来执行各种任务。

在 MySQL 文档存储的情况下,我们使用 X Developer API (X DevAPI)通过一组类和方法来访问服务器,这些类和方法提供了到服务器的连接,以及集合、表、SQL 操作等概念的抽象。这些结合起来允许一个 NoSQL 接口到 MySQL 服务器。

NoSQL 接口

关于 NoSQL,有几个有时相互矛盾的定义(如果不是例子的话)。出于本书和 MySQL 的目的,NoSQL 接口是一种不需要使用 SQL 语句来访问数据的 API。API 本身提供了到服务器的连接,以及创建、检索、更新和删除数据的类和方法。在上一章中,我们已经看到了这个范例的作用。

此时,您可能想知道 MySQL 如何处理将 JSON 文档与关系数据结合使用的混合选项。简单地说,MySQL 被设计成允许在关系数据中存储和检索 JSON 文档(通过 SQL 接口)。也就是说,服务器已经被修改来处理 JSON 文档。还有一组函数允许您对 JSON 数据做各种各样的事情,使得通过 SQL 接口使用 JSON 变得很容易。

然而,您也可以通过 NoSQL X DevAPI 使用 JSON 文档,或者通过 SQL 命令,或者使用 X DevAPI 的特殊类和方法作为纯文档存储。我们将在本章中看到使用 JSON 的两种方式的概述,并在下一章深入探讨通过 NoSQL 接口使用 JSON 文档。

文档存储

文档存储(也称为面向文档的数据库)是一个用于管理半结构化数据(即文档)的存储和检索系统。现代文档存储系统支持 XML 和 JSON 中的键、值结构。因此,文档存储系统有时被认为是关键值存储系统的一个子类。

文档存储系统也通常由实现为编程接口(API)的 NoSQL 接口来访问,该 API 允许开发者将文档的存储和检索合并到他们的程序中,而不需要第三方访问机制(API 实现访问机制)。

事实上,描述数据的元数据嵌入在数据本身中。粗略地说,这意味着键和键的布局(排列或嵌套)形成元数据,并且元数据对于存储机制变得不透明。更具体地说,数据如何排列(文档如何形成或描述数据)不反映在存储机制中,也不由存储机制管理。对半结构化数据的访问需要使用 NoSQL 接口访问为处理文档本身而设计的机制。

这两种特性,半结构化数据和 NoSQL 接口,是文档存储与关系数据的区别。关系数据需要不灵活的结构,迫使所有数据符合特定的结构。数据也以相同的结构分组在一起,通常很少考虑内容可能不同的数据。因此,我们通常不会看到通过传统关系数据机制访问文档存储。也就是说,直到现在。

使用文档存储有趣的一点是,学习如何使用文档存储并不需要成为 JavaScript 或 Python 专家。事实上,你要做的大部分事情并不需要掌握任何编程语言。也就是说,有很多关于如何做事的例子,所以你不需要学习所有关于这门语言的知识。事实上,你可以很快找到你需要的东西,然后随着你的需求的成熟,学习更多的语言知识。

数据

JSON 是一种人类和机器可读的基于文本的数据交换格式。它也是独立于平台的,这意味着不存在禁止它在几乎任何编程语言中使用的格式概念。此外,JSON 是互联网上广泛使用的一种格式。

JSON 允许您在不违反任何结构的情况下以任何方式描述数据。事实上,您可以按照自己的意愿设置数据的格式(布局)。唯一真正的限制是描述符(花括号、方括号、引号、逗号等)的正确使用,这些描述符必须对齐,在某些情况下必须正确配对。下面是一个有效 JSON 字符串的示例。

{
    "address": {
        "street": "123 First Street",
        "city": "Oxnard",
        "state": "CA",
        "zip": "90122"
    }
}

如果您认为这看起来很像之前的关键价值示例,那么您是对的,就是它!鉴于 JSON 是如何形成的,这并不是错误。然而,我们经常使用术语 string 来谈论 JSON,实际上有时我们看到 JSON 没有空格和换行符,如下所示。事实证明,大多数编程语言 JSON 机制可以正确解释空格和换行符。我们将在后面的章节中看到更多相关内容。

{"address": {"street": "123 First Street","city": "Oxnard","state": "CA","zip": "90122"}}

当编程语言支持时,开发人员可以通过键访问数据来轻松读取数据。更好的是,开发人员不需要知道键是什么(但这很有帮助!)因为它们可以使用语言支持机制来获取键并对它们进行迭代。这样,像 XML 一样,数据是自描述的。

现在,让我们深入了解什么是 JSON 文档,以及如何在 MySQL 中使用它们。

MySQL 中的 JSON 文档介绍

在 MySQL 5.7.8 和更高版本中,我们可以使用 JSON 数据类型将 JSON 文档存储在传统关系数据库表中存储的行的字段(列)中。有些人可能尝试(并成功)在 blob 或文本字段中存储 JSON。虽然这是可能的,但有几个很好的理由不这样做。最令人信服的原因是,它要求应用完成所有读取和写入 JSON 文档的繁重工作,从而使它变得更加复杂,并且可能容易出错。JSON 数据类型以两种方式克服了这个问题。

  • 验证:JSON 数据类型提供文档验证。也就是说,只有有效的 JSON 才能存储在 JSON 列中。

  • 高效访问:当一个 JSON 文档存储在一个表中时,存储引擎将数据打包成一种特殊的优化二进制格式,允许服务器快速访问数据元素,而不是每次访问数据时都解析数据。

这为以结构化形式(关系数据)存储非结构化数据开辟了一条全新的途径。然而,Oracle 并没有止步于简单地向 MySQL 添加 JSON 数据类型。Oracle 还增加了一个复杂的编程接口,以及将文档作为集合存储在数据库中的概念。我们将在下一章更多地讨论这些方面。在这一章中,我们将看到如何将 JSON 用于关系数据。

快速启动

如果您以前从未使用过 JSON,本节将帮助您入门。关于 JSON 及其在 MySQL 中的使用,您只需要了解一些东西,但最重要的是 JSON 格式化规则。

JSON 是由用括号括起来的字符串组成的,或者是用某些符号组织起来的。虽然我们已经讨论了与 JSON 相关的键、值机制,但是 JSON 属性有两种类型:由逗号分隔的列表形成的数组和由一组键、值对形成的对象。也可以嵌套 JSON 属性。例如,数组可以包含对象,对象键中的值可以包含数组或其他对象。JSON 数组和对象的组合称为 JSON 文档。

JSON 数组包含一个由逗号分隔并括在方括号([ ])中的值列表。例如,以下是有效的 JSON 数组。

["Cub Cadet", "Troy-Bilt", "John Deere", "Craftsman"]
[33,67,1,55,909]
[True, True, False, False]

注意,我们用方括号开始和结束数组,并用逗号分隔值。虽然我们没有使用空白,但是您可以使用空白,并且根据您的编程语言,您还可以使用换行符、制表符和回车符。例如,下面仍然是一个有效的 JSON 数组。

[
True,
12,
False,
33
]

JSON 对象是一组键/值对,其中每个键/值对都包含在左花括号和右花括号({ })中,并用逗号分隔。例如,以下是有效的 JSON 对象。注意,键地址有一个 JSON 对象作为它的值。

{"address": {
   "street": "123 First Street",
   "city": "Oxnard",
   "state": "CA",
   "zip": "90122"
}}

{"address": {
    "street":"4 Main Street",
    "city":"Melborne",
    "state":"California",
    "zip":"90125"
}}

JSON 数组通常用于包含相关(嗯,有时)事物的列表,JSON 对象用于描述复杂的数据。JSON 数组和对象可以包含标量值,比如字符串或数字、null文字(就像在关系数据中一样),或者布尔文字truefalse。请记住,键必须始终是字符串,并且通常用引号括起来。最后,JSON 值还可以包含时间信息(日期、时间或日期时间)。例如,下面显示了一个带有时间值的 JSON 数组。

["15:10:22.021100", "2019-03-23", "2019-03-23 08:51:29.012310"]

下一节将描述我们如何在 MySQL 中使用 JSON。在这种情况下,我们指的是关系数据,但是 JSON 文档的格式在文档存储中是相同的。

结合 SQL 和 JSON

对关系数据使用 JSON 可能看起来有点不寻常或反直觉。也就是说,为什么要在列中使用非结构化数据?这难道不违反一些关系数据库理论定律之类的吗? 2 虽然在某种程度上这可能是真的,但在我们的关系数据中添加非结构化数据的能力打开了以前关闭的几扇门。

例如,假设您需要为已经部署了一段时间的应用向现有表中添加更多数据。如果添加一个新列,就有可能需要修改所有使用该数据的应用。 3 目前为止没什么大不了的,对吧?但是,如果这些数据在类型和范围上都不同,也就是说,对于任何给定的一组行,添加的数据不能以相同的方式描述,或者每一行的数据都不同,该怎么办呢?这是非结构化数据的本质,它没有预定义的结构。因此,您不能轻松地扩展表,甚至不能创建新的引用(子)表。

这就是拥有 JSON 专栏有所帮助的地方。您只需添加一个新列,并将非结构化数据存储为 JSON。当然,这不会改变您必须更改应用的可能性,但是这确实意味着您不必重新调整数据库本身(除了添加 JSON 列之外)或者将数据强行放入一组类型化的列中。

在本节中,我们将了解如何在 MySQL 中使用 JSON,包括在 SQL 语句中包含 JSON 字符串的机制、MySQL 中可用于 JSON 的一些特殊函数、如何在 SQL 语句中访问 JSON 文档的部分内容,以及如何在关系数据中使用 JSON 列。我们将使用 shell 来演示这些主题。我们把使用纯 JSON 文档的工作留到下一章。

小费

MySQL Shell 在使用 JSON 方面有一些增强,包括以人类可读的形式显示 JSON 的能力。我们可以看到原始 JSON (json/raw)或打印精美的 JSON (json)的结果。对漂亮打印的 JSON 使用--result-format=json命令行选项,或者对无格式的 JSON 输出使用--result-format=json/raw命令行选项。

在 MySQL 中格式化 JSON 字符串

在 MySQL 中使用时,JSON 文档被写成字符串。MySQL 解析 JSON 数据类型中使用的任何字符串来验证文档。如果文档无效(不是格式正确的 JSON 文档),您将得到一个错误。您可以在任何合适的 SQL 语句中使用 JSON 文档,比如INSERTUPDATE语句以及像WHERE子句这样的子句。

小费

正确格式化 JSON 文档可能有点困难。最需要记住的是平衡你的引号,正确使用逗号,平衡所有的花括号和方括号。

当您将键和值指定为字符串时,必须使用双引号字符("),而不是单引号(')。因为 MySQL 期望 JSON 文档是字符串,所以您可以在整个 JSON 文档中使用单引号,而不是在文档中使用单引号。幸运的是,MySQL 提供了许多特殊的函数,您可以在 JSON 文档中使用,其中一个是JSON_VALID()函数,它允许您检查 JSON 文档的有效性。如果文档有效,则返回 1,否则返回 0。下面显示了使用单引号验证键和值的 JSON 文档与使用双引号验证格式正确的 JSON 文档的结果。

注意

此后,为了简洁起见,我们将省略 shell SQL 提示符。

> SELECT JSON_VALID("{'vendor': {'name': 'Craftsman','URL': 'http://www.craftsman.com','sources': 'Lowes'}}") AS IS_VALID \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
IS_VALID: 0
1 row in set (0.0005 sec)

> SELECT JSON_VALID('{"vendor": {"name": "Craftsman","URL": "http://www.craftsman.com","sources": "Lowes"}}') AS IS_VALID \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
IS_VALID: 1
1 row in set (0.0040 sec)

请注意,带双引号的字符串是有效的(函数返回 1),而带单引号的字符串是无效的(函数返回 0)。这是大多数人第一次使用 JSON 时最容易犯的错误。

在 SQL 语句中使用 JSON 字符串

让我们看看如何在 SQL 语句中使用 JSON 文档。假设我们想在一个表中存储地址。对于这个例子,我们将保持简单,将数据插入一个非常简单的表中。清单 6-1 显示了从创建一个测试表开始,然后插入前两个地址的练习的抄本。

C:\Users\cbell> mysqlsh --uri root@localhost:33060 --sql
MySQL Shell 8.0.16
...
> CREATE DATABASE `testdb_6`;
Query OK, 1 row affected (0.0098 sec)

> USE `testdb_6`;
Query OK, 0 rows affected (0.0010 sec)

> CREATE TABLE `testdb_6`.`addresses` (`id` int(11) NOT NULL AUTO_INCREMENT, `address` json DEFAULT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=latin1;
Query OK, 0 rows affected (0.0310 sec)

> INSERT INTO `testdb_6`.`addresses` VALUES (NULL, '{"address": {"street": "123 Second St","city": "Maynard","state": "CT","zip": "19023"}}');
Query OK, 1 row affected (0.0042 sec)

> INSERT INTO `testdb_6`.`addresses` VALUES (NULL, '{"address": {"street":"41 West Hanover","city":"Frederick","state":"Maryland","zip":"20445"}}');
Query OK, 1 row affected (0.0030 sec)

> SELECT ∗ FROM `testdb_6`.`addresses` \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
     id: 1
address: {"address": {"zip": "19023", "city": "Maynard", "state": "CT", "street": "123 Second St"}}
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 2\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
     id: 2
address: {"address": {"zip": "20445", "city": "Frederick", "state": "Maryland", "street": "41 West Hanover"}}
2 rows in set (0.0005 sec)

> DROP DATABASE `testdb_6`;
Query OK, 1 row affected (0.0132 sec)

Listing 6-1Using JSON with SQL Statements

注意,在CREATE语句中,我们使用了数据类型JSON。这通知 MySQL 在存储引擎中分配特殊的存储机制来处理 JSON。与一些报告相反,JSON 数据类型不仅仅是字符串的直接存储。相反,它是在内部组织的,以优化元素的检索。因此,正确格式化 JSON 非常重要。一个表中可以有多个 JSON 列。然而,一个表行中 JSON 文档的总和被限制为变量max_allowed_packet的值。

注意

JSON 列不能像其他列一样有默认值。

现在,让我们看看如果在 SQL 语句中使用无效的 JSON 文档(字符串)会发生什么。下面显示了插入上一个示例中的最后一个地址的尝试,只是没有在关键字周围加上正确的引号。请注意抛出的错误。

> INSERT INTO testdb_6.addresses VALUES (NULL, '{"address": {street:"173 Caroline Ave",city:"Monstrose",state:"Georgia",zip:31505}}');
ERROR: 3140: Invalid JSON text: "Missing a name for object member." at position 13 in value for column 'addresses.address'.

对于任何格式不正确的 JSON 文档,您都可能会看到这样或那样的错误。如果你想先测试你的 JSON,使用JSON_VALID()函数。然而,在构建 JSON 文档时,还有另外两个函数可能会有所帮助;JSON_ARRAY()JSON_OBJECT()

JSON_ARRAY()函数接受一个值列表,并返回一个有效的格式化 JSON 数组。下面显示了一个示例。注意,它返回了一个格式正确的 JSON 数组,带有正确的引号(双引号而不是单引号)和方括号。

> SELECT JSON_ARRAY(1, true, 'test', 2.4);
+----------------------------------------+
| JSON_ARRAY(1, true, 'test', 2.4) ------|
+----------------------------------------+
| [1, true, "test", 2.4]    ------       |
+----------------------------------------+
1 row in set (0.00 sec)

JSON_OBJECT()函数接受一个键、值对列表,并返回一个有效的 JSON 对象。下面显示了一个示例。注意这里我在调用函数时使用了单引号。这只是一个例子,我们可能会对使用哪种引语感到困惑。在这种情况下,函数的参数不是 JSON 文档;它们是普通的 SQL 字符串,可以使用单引号或双引号。

> SELECT JSON_OBJECT("street","4 Main Street","city","Melborne",'state','California','zip',90125) \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_OBJECT("street","4 Main Street","city","Melborne",'state','California','zip',90125): {"zip": 90125, "city": "Melborne", "state": "California", "street": "4 Main Street"}
1 row in set (0.0040 sec)

再次注意函数结果中引号的自动转换。如果您需要动态构建 JSON,这可能会很有帮助。

还有一个用于构造 JSON 文档的有用函数;JSON_TYPE()功能。这个函数获取一个 JSON 文档,并将其解析成一个 JSON 值。如果值有效,它将返回该值的 JSON 类型,如果无效,它将抛出一个错误。下面显示了该函数与前面语句的用法。

> SELECT JSON_TYPE('[1, true, "test", 2.4]');
+-------------------------------------------+
| JSON_TYPE('[1, true, "test", 2.4]')       |
+-------------------------------------------+
| ARRAY                                     |
+-------------------------------------------+
1 row in set (0.00 sec)

> SELECT JSON_TYPE('{"zip": 90125, "city": "Melborne", "state": "California", "street": "4 Main Street"}') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_TYPE('{"zip": 90125, "city": "Melborne", "state": "California", "street": "4 Main Street"}'): OBJECT
1 row in set (0.00 sec)

MySQL 提供了更多的函数来处理 JSON 数据类型。我们将在后面的章节中看到更多关于这些的内容。

本节只描述了在 SQL 语句中使用 JSON 和 MySQL 的基础知识。事实上,JSON 文档的格式化也适用于文档存储。然而,有一件事我们还没有谈到——如何访问 JSON 文档中的元素。

路径表达式

为了访问一个元素——通过它的键——我们使用一种叫做路径表达式的特殊符号。下面是一个简单的例子。请注意 WHERE 子句。这显示了一个路径表达式,其中我检查 address 列是否包含用特殊符号address->'$.address.city'引用的 JSON 键“city”。我们将在下一节看到更多关于路径表达式的细节。

> SELECT id, address->'$.address.city' FROM test.addresses
  WHERE address->'$.address.zip' = '90125';
+----+---------------------------+
| id | address->'$.address.city' |
+----+---------------------------+
|  2 | "Melborne"                |
+----+---------------------------+
1 row in set (0.00 sec)

如果您认为 JSON 文档可能是一组复杂的半结构化数据,并且在某些时候您需要访问文档中的某些元素,那么您可能还想知道如何从 JSON 文档中获得您想要的东西。幸运的是,有一种机制可以做到这一点,它被称为路径表达式。更具体地说,它是一种快捷表示法,您可以在 SQL 命令中使用它来获取元素,而无需额外的编程或脚本。

正如您将看到的,这是一种非常特殊的语法,虽然表达性不是很好(用英语读起来不是很好),但这种符号可以让您无需大量额外输入就能得到您需要的东西。路径表达式以包含在字符串中的美元符号($)开始。但是这个符号必须有一个上下文。在 SQL 语句中使用路径表达式时,必须使用JSON_EXTRACT()函数,它允许您使用路径表达式从 JSON 文档中提取数据。这是因为,与 X DevAPI 类和方法不同,并非所有 SQL 语句都直接支持路径表达式(但是我们将看到有些语句支持路径表达式)。例如,如果您想要数组中的第三个项目(在本例中是数字 3),您可以如下使用该函数。

> SELECT JSON_EXTRACT('[1,2,3,4,5,6]', '$[2]') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_EXTRACT('[1,2,3,4,5,6]', '$[2]'): 3
1 row in set (0.0049 sec)

注意这是访问 JSON 数组中的数据。在这里,我们使用一个数组下标,并在索引周围加上方括号(元素从 0 开始),就像在许多编程语言中使用数组一样。

小费

SQL 接口中路径表达式的使用仅限于其中一个 JSON 函数,或者仅用于已被修改为接受路径表达式的特定子句,如SELECT列列表或WHEREHAVINGORDER BYGROUP BY子句。

现在假设你想通过键访问一个元素。你也可以这样做。在这种情况下,我们使用美元符号后跟一个句点,然后是键名。下面显示了如何检索包含个人姓名和地址的 JSON 对象的姓氏。

> SELECT JSON_EXTRACT('{"name": {"first":"Billy-bob","last":"Throckmutton"},"address": {"street":"4 Main Street","city":"Melborne","state":"California","zip":"90125"}}', '$.name.first') AS Name \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
Name: "Billy-bob"
1 row in set (0.0008 sec)

请注意,我必须使用两个级别的访问权限。也就是说,我想要名为 name 的对象中名为 first 的键的值。于是,我用了“$.name.first”。这演示了如何使用路径表达式深入 JSON 文档。这也是为什么我们称之为路径表达式,因为我们形成表达式的方式给了我们到元素的“路径”。

现在我们已经看到了一些例子,让我们回顾一下路径表达式的完整语法;两者都在 SQL 和 NoSQL 接口中使用。除非另有说明,否则语法方面适用于两种接口。

再说一次,路径表达式以美元符号开始,后面可以有几种叫做选择器的语法形式,允许我们请求文档的一部分。这些选择器包括以下内容。

  • 一个句点后跟一个键名,引用该键的值。如果不带引号的名称无效(要求引号是有效的标识符,如带空格的键名),则必须在双引号内指定键名。

  • 使用带有整数索引([n])的方括号来选择数组中的元素。索引从 0 开始。

  • 路径可以包含通配符***,如下所示。

    • .[*]计算 JSON 对象中所有成员的值。

    • [*]计算 JSON 数组中所有元素的值。

    • 前缀∫后缀之类的序列计算以指定前缀开始并以指定后缀结束的所有路径。

  • 可以使用句点作为分隔符来嵌套路径。在这种情况下,句点之后的路径在父路径上下文的上下文中进行评估。例如,$.name.first将名为 first 的键的搜索限制在名称 JSON 对象。

如果路径表达式被评估为 false 或无法定位数据项,服务器将返回 null。例如,以下返回 null,因为数组中只有 6 项。你能看出为什么吗?记住,计数从 0 开始。对于那些不熟悉使用路径表达式(或者编程语言中的数组)的人来说,这是一个常见的错误。

> SELECT JSON_EXTRACT('[1,2,3,4,5,6]', '$[6]') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_EXTRACT('[1,2,3,4,5,6]', '$[6]'): NULL
1 row in set (0.0008 sec)

但是等等,路径表达式还有一个更好的选择。我们可以走捷径!也就是说,当按列访问 SQL 语句中的数据时,可以使用破折号和大于号(->)来代替JSON_EXTRACT()函数。多酷啊。使用->操作有时被称为“内嵌路径表达式”。例如,我们可以编写前面的示例,从一个表中查找 JSON 数组中的第三个项目,如下所示。

> CREATE TABLE testdb_6.ex1 (id int AUTO_INCREMENT PRIMARY KEY, recorded_data JSON);
Query OK, 0 rows affected (0.0405 sec)
> INSERT INTO testdb_6.ex1 VALUES (NULL, JSON_ARRAY(1,2,3,4,5,6));
Query OK, 1 row affected (0.0052 sec)
> INSERT INTO testdb_6.ex1 VALUES (NULL, JSON_ARRAY(7,8,9));
> SELECT ∗ FROM testdb_6.ex1 WHERE recorded_data->'$[2]' = 3 \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
           id: 1
recorded_data: [1, 2, 3, 4, 5, 6]
1 row in set (0.0045 sec)

注意,我只是使用了列名recorded_data,并在末尾添加了->,然后列出了路径表达式。太棒了!

但是等等,还有更多。这种捷径还有一种形式。如果->操作(JSON_EXTRACT)的结果是一个带引号的字符串,我们可以使用->>符号(称为内嵌路径操作符)来检索不带引号的值。这在处理数值时很有帮助。下面给出了两个例子。一个与->操作相同,一个与->>操作相同。

> INSERT INTO testdb_6.ex1 VALUES (NULL, '{"name":"will","age":"43"}');
Query OK, 1 row affected (0.00 sec)
> INSERT INTO testdb_6.ex1 VALUES (NULL, '{"name":"joseph","age":"11"}');
Query OK, 1 row affected (0.00 sec)
> SELECT ∗ FROM testdb_6.ex1 WHERE recorded_data->>'$.age' = 43 \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
           id: 3
recorded_data: {"age": "43", "name": "will"}
1 row in set (0.0014 sec)
> SELECT ∗ FROM testdb_6.ex1 WHERE recorded_data->'$.age' = '43' \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
           id: 3
recorded_data: {"age": "43", "name": "will"}
1 row in set (0.0009 sec)

注意,recorded_data值(年龄和姓名)被存储为一个字符串。但是如果数据存储为整数会怎么样呢?观察。

> INSERT INTO testdb_6.ex1 VALUES (NULL, '{"name":"amy","age":22}');
Query OK, 1 row affected (0.0075 sec)
> SELECT ∗ FROM testdb_6.ex1 WHERE recorded_data->'$.age' = 22 \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
           id: 5
recorded_data: {"age": 22, "name": "amy"}
1 row in set (0.0010 sec)
> SELECT ∗ FROM testdb_6.ex1 WHERE recorded_data->>'$.age' = 22 \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
           id: 5
recorded_data: {"age": 22, "name": "amy"}
1 row in set (0.0009 sec)

啊哈!因此,当值必须不加引号时,->>操作最有用。如果它们已经被取消引用(比如一个整数),那么->>操作将返回与->操作相同的结果。

请注意,快捷方式(内嵌路径表达式)的使用并不能直接替代JSON_EXTRACT()功能。下面总结了这些限制。

  • 数据源:在 SQL 语句中使用时,内嵌路径表达式只使用指定的字段(列)。该函数可以使用任何 JSON 类型的值。

  • 路径表达式字符串:内嵌路径表达式必须使用普通字符串。该函数可以使用任何字符串类型的值。

  • 表达式数量:内联路径表达式只能对单个字段(列)使用一个路径表达式。该函数可以对一个 JSON 文档使用多个路径表达式。

现在让我们看看可以用来处理 JSON 文档的各种 JSON 函数。

JSON 函数

在 MySQL 中有许多使用 JSON 的函数。我们不会列出所有的功能和过时的风险(似乎每个版本都会添加新的功能),而是列出一些最常用的功能,让您了解哪些功能可用。虽然我们不会探究每个函数的细微差别,但我们会在后面的章节中看到其中一些函数的使用。表 6-1 列出了 MySQL 8 中可用的 JSON 函数。

掌握这些函数对于使用文档存储来说并不重要,但是在开发混合解决方案(在 SQL 语句中使用 JSON)时会有很大帮助。

这些功能可以根据它们的使用方式进行分类。我们将看到对添加数据有用的函数,检索(搜索)数据的函数,等等。下面用简单的例子说明如何使用这些功能。

表 6-1

MySQL 中常用的 JSON 函数

|

功能

|

描述和使用

|
| --- | --- |
| JSON_ARRAY() | 评估值列表并返回包含这些值的 JSON 数组。 |
| JSON_ARRAYAGG() | 将结果集聚合为一个 JSON 数组,其元素由行组成。 |
| JSON_ARRAY_APPEND() | 将值追加到 JSON 文档中指定数组的末尾,并返回结果。 |
| JSON_ARRAY_INSERT() | 更新一个 JSON 文档,插入到文档内的一个数组中,并返回修改后的文档。 |
| JSON_CONTAINS() | 返回 0 或 1 以指示特定值是否包含在目标 JSON 文档中,或者,如果给定了路径参数,则返回目标文档中特定路径的值。 |
| JSON_CONTAINS_PATH() | 返回 0 或 1,以指示 JSON 文档是否包含给定路径中的数据。 |
| JSON_DEPTH() | 返回 JSON 文档的最大深度。 |
| JSON_EXTRACT() | 从 JSON 文档中返回数据,这些数据是从与路径参数匹配的文档部分中选择的。 |
| JSON_INSERT() | 将数据插入 JSON 文档并返回结果。 |
| JSON_KEYS() | 以 JSON 数组的形式返回 JSON 对象的顶级值的键,或者,如果给定了路径参数,则返回所选路径的顶级键。 |
| JSON_LENGTH() | 返回 JSON 文档的长度,或者,如果给定了路径参数,则返回由路径标识的文档中的值的长度。 |
| JSON_OBJECT() | 评估键/值对列表,并返回包含这些对的 JSON 对象。 |
| JSON_OBJECTAGG() | 接受两个列名或表达式作为参数,第一个用作键,第二个用作值,并返回包含键/值对的 JSON 对象。 |
| JSON_PRETTY() | 打印一个更好看的 JSON 文档布局。 |
| JSON_QUOTE() | 通过用双引号字符将字符串括起来并转义内部引号和其他字符,将字符串作为 JSON 值引用,然后将结果作为 utf8mb4 字符串返回。 |
| JSON_REMOVE() | 从 JSON 文档中移除数据并返回结果。 |
| JSON_REPLACE() | 替换 JSON 文档中的现有值并返回结果。 |
| JSON_SEARCH() | 返回 JSON 文档中给定字符串的路径。 |
| JSON_SET() | 在 JSON 文档中插入或更新数据,并返回结果。 |
| JSON_TABLE() | 从 JSON 文档中提取数据,并将其作为关系表返回。 |
| JSON_TYPE() | 返回一个 utf8mb4 字符串,指示 JSON 值的类型。 |
| JSON_VALID() | 返回 0 或 1 以指示值是否是有效的 JSON 文档。 |

创建 JSON 数据

创建 JSON 数据有几个有用的函数。我们已经看到了两个重要的函数:JSON_ARRAY()构建 JSON 数组类型,而JSON_OBJECT()构建 JSON 对象类型。本节讨论一些其他函数,这些函数可以用来帮助创建 JSON 文档,包括在 JSON 数组中聚合、追加和插入数据的函数。

JSON_ARRAYAGG()函数用于从几行中创建一个 JSON 文档数组。当您想要汇总数据或合并多行数据时,它会很有帮助。该函数接受一个列名,并将行中的 JSON 数据组合到一个新数组中。清单 6-2 展示了使用该函数的例子。这个示例获取表中的行,并将它们组合起来形成一个新的 JSON 对象数组。

> CREATE TABLE testdb_6.favorites (id int(11) NOT NULL AUTO_INCREMENT, preferences JSON, PRIMARY KEY (`id`));
> INSERT INTO testdb_6.favorites VALUES (NULL, '{"color": "red"}');
Query OK, 1 row affected (0.0077 sec)
> INSERT INTO testdb_6.favorites VALUES (NULL, '{"color": "blue"}');
Query OK, 1 row affected (0.0050 sec)
> INSERT INTO testdb_6.favorites VALUES (NULL, '{"color": "purple"}');
Query OK, 1 row affected (0.0034 sec)
> SELECT ∗ FROM testdb_6.favorites \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
         id: 1
preferences: {"color": "red"}
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 2\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
         id: 2
preferences: {"color": "blue"}
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 3\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
         id: 3
preferences: {"color": "purple"}
3 rows in set (0.0012 sec)
> SELECT JSON_ARRAYAGG(preferences) FROM testdb_6.favorites \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_ARRAYAGG(preferences): [{"color": "red"}, {"color": "blue"}, {"color": "purple"}]
1 row in set (0.0049 sec)

Listing 6-2Using the JSON_ARRAYARG Function

JSON_ARRAY_APPEND()是一个有趣的函数,它允许您将数据附加到 JSON 数组的末尾或紧接在给定路径表达式之后。该函数将 JSON 数组、路径表达式和要插入的值(包括 JSON 文档)作为参数。清单 6-3 展示了几个例子。

> SET @base = '["apple","pear",{"grape":"red"},"strawberry"]';
Query OK, 0 rows affected (0.0045 sec)
> SELECT JSON_ARRAY_APPEND(@base, '$', "banana") \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_ARRAY_APPEND(@base, '$', "banana"): ["apple", "pear", {"grape": "red"}, "strawberry", "banana"]
1 row in set (0.0009 sec)
> SELECT JSON_ARRAY_APPEND(@base, '$[2].grape', "green") \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_ARRAY_APPEND(@base, '$[2].grape', "green"): ["apple", "pear", {"grape": ["red", "green"]}, "strawberry"]
1 row in set (0.0012 sec)
> SET @base = '{"grape":"red"}';
Query OK, 0 rows affected (0.0004 sec)
> SELECT JSON_ARRAY_APPEND(@base, '$', '{"grape":"red"}') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_ARRAY_APPEND(@base, '$', '{"grape":"red"}'): [{"grape": "red"}, "{\"grape\":\"red\"}"]
1 row in set (0.0007 sec)

Listing 6-3Using the JSON_ARRAY_APPEND Function

请注意,第一个示例只是在数组末尾添加了一个新值。第二个示例将第三个索引中 JSON 对象的键值更改为一个数组,并添加一个新值。这是这个函数的一个有趣的副产品。在第三个例子中,我们再次看到了这一点,我们将一个基本的 JSON 对象更改为一个 JSON 对象的 JSON 数组。

JSON_ARRAY_INSERT()函数类似,只是它在路径表达式前插入值。该函数将 JSON 数组、路径表达式和要插入的值(包括 JSON 文档)作为参数。当包含多个路径表达式和值对时,当函数计算第一个路径表达式和值并将下一个对应用于结果时,效果是累积的,依此类推。清单 6-4 展示了一些使用新函数的例子,和前面的例子很相似。请注意,插入数据的位置在路径表达式之前。

> SET @base = '["apple","pear",{"grape":["red","green"]},"strawberry"]';
Query OK, 0 rows affected (0.0007 sec)
> SELECT JSON_ARRAY_INSERT(@base, '$[0]', "banana") \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_ARRAY_INSERT(@base, '$[0]', "banana"): ["banana", "apple", "pear", {"grape": ["red", "green"]}, "strawberry"]
1 row in set (0.0008 sec)
> SELECT JSON_ARRAY_INSERT(@base, '$[2].grape[0]', "white") \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_ARRAY_INSERT(@base, '$[2].grape[0]', "white"): ["apple", "pear", {"grape": ["white", "red", "green"]}, "strawberry"]
1 row in set (0.0009 sec)
> SET @base = '[{"grape":"red"}]';
Query OK, 0 rows affected (0.0004 sec)
> SELECT JSON_ARRAY_INSERT(@base, '$[0]', '{"grape":"red"}') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_ARRAY_INSERT(@base, '$[0]', '{"grape":"red"}'): ["{\"grape\":\"red\"}", {"grape": "red"}]
1 row in set (0.0007 sec)

Listing 6-4Using the JSON_ARRAY_INSERT Function

JSON_INSERT()函数被设计成获取一个 JSON 文档,并在指定的路径表达式中插入一个或多个值。也就是说,您可以一次传递成对的路径表达式和值。但是有一个问题。在这种情况下,路径表达式不能计算为文档中的元素。像上一个函数一样,当包含多个路径表达式时,效果是累积的,其中函数计算第一个路径表达式,将下一个路径表达式应用于结果,依此类推。清单 6-5 给出了一个例子。请注意,没有插入第三个路径表达式和值,因为路径表达式$[0]的计算结果是第一个元素 apple。

> SET @base = '["apple","pear",{"grape":["red","green"]},"strawberry"]';
Query OK, 0 rows affected (0.0007 sec)
> SELECT JSON_INSERT(@base, '$[9]', "banana", '$[2].grape[3]', "white", '$[0]', "orange") \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_INSERT(@base, '$[9]', "banana", '$[2].grape[3]', "white", '$[0]', "orange"): ["apple", "pear", {"grape": ["red", "green", "white"]}, "strawberry", "banana"]
1 row in set (0.0008 sec)

Listing 6-5Using the JSON_INSERT Function

JSON_MERGE_PATCH()JSON_MERGE_PRESERVE()函数被设计成获取两个或更多的 JSON 文档并将它们组合起来。JSON_MERGE_PATH()功能替换重复键的值,而JSON_MERGE_PRESERVE()保留重复键的值。像最后一个函数一样,您可以包含任意多的 JSON 文档。注意我是如何使用这个函数从前面的例子中构建示例 JSON 文档的。清单 6-6 展示了一个使用这些方法的例子。

> SELECT JSON_MERGE_PATCH('["apple","pear"]', '{"grape":["red","green"]}', '["strawberry"]') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_MERGE_PATCH('["apple","pear"]', '{"grape":["red","green"]}', '["strawberry"]'): ["strawberry"]
1 row in set (0.0041 sec)
> SELECT JSON_MERGE_PRESERVE('{"grape":["red","green"]}', '{"grape":["white"]}') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_MERGE_PRESERVE('{"grape":["red","green"]}', '{"grape":["white"]}'): {"grape": ["red", "green", "white"]}
1 row in set (0.0008 sec)

Listing 6-6Using the JSON_MERGE_PATCH and JSON_MERGE_PRESERVE Functions

如果向任何 JSON 函数传递了无效的参数、无效的 JSON 文档,或者路径表达式没有找到元素,则一些函数会返回 null,而其他函数可能会返回原始的 JSON 文档。清单 6-7 给出了一个例子。在这种情况下,位置 8 没有元素,因为数组只有 4 个元素。

> SET @base = '["apple","pear",{"grape":"red"},"strawberry"]' \G
Query OK, 0 rows affected (0.0007 sec)
> SELECT JSON_ARRAY_APPEND(@base, '$[7]', "flesh") \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_ARRAY_APPEND(@base, '$[7]', "flesh"): ["apple", "pear", {"grape": "red"}, "strawberry"]
1 row in set (0.0007 sec)

Listing 6-7Using the JSON_ARRAY_APPEND Function

现在让我们看看可以用来修改 JSON 数据的函数。

修改 JSON 数据

修改 JSON 数据有几个有用的函数。本节讨论了通过删除、替换和更新 JSON 文档中的元素来帮助修改 JSON 文档的函数。

JSON_REMOVE()函数用于删除匹配路径表达式的元素。您必须提供要操作的 JSON 文档以及一个或多个路径表达式,结果将是删除了元素的 JSON 文档。当包含多个路径表达式时,当函数计算第一个路径表达式并将下一个路径表达式应用于结果时,效果是累积的,依此类推。清单 6-8 给出了一个例子。注意,我必须想象中间结果是什么——也就是说,我使用了$[0]三次,因为该函数删除了第一个元素两次,留下 JSON 对象作为第一个元素。

 > SET @base = '["apple","pear",{"grape":["red","white"]},"strawberry"]';
Query OK, 0 rows affected (0.0008 sec)
> SELECT JSON_REMOVE(@base, '$[0]', '$[0]', '$[0].grape[1]') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_REMOVE(@base, '$[0]', '$[0]', '$[0].grape[1]'): [{"grape": ["red"]}, "strawberry"]
1 row in set (0.0009 sec)

Listing 6-8Using the JSON_REMOVE Function (single)

这可能需要一点时间来适应,但您可以多次使用该函数或嵌套使用,如清单 6-9 中的示例所示。

 > SET @base = '["apple","pear",{"grape":["red","white"]},"strawberry"]';
Query OK, 0 rows affected (0.0007 sec)
> SET @base = JSON_REMOVE(@base, '$[0]');
Query OK, 0 rows affected (0.0009 sec)
> SET @base = JSON_REMOVE(@base, '$[0]');
Query OK, 0 rows affected (0.0006 sec)
> SELECT JSON_REMOVE(@base, '$[0].grape[1]') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_REMOVE(@base, '$[0].grape[1]'): [{"grape": ["red"]}, "strawberry"]
1 row in set (0.0007 sec)
> SET @base = '["apple","pear",{"grape":["red","white"]},"strawberry"]';
Query OK, 0 rows affected (0.0004 sec)
> SELECT JSON_REMOVE(JSON_REMOVE(JSON_REMOVE(@base, '$[0]'), '$[0]'), '$[0].grape[1]') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_REMOVE(JSON_REMOVE(JSON_REMOVE(@base, '$[0]'), '$[0]'), '$[0].grape[1]'): [{"grape": ["red"]}, "strawberry"]
1 row in set (0.0005 sec)

Listing 6-9Using the JSON_REMOVE Function (nested)

JSON_REPLACE()函数接受一个 JSON 文档和一对路径表达式和值,用新值替换匹配路径表达式的元素。同样,结果是累积的,并且从左到右按顺序工作。这个函数也有一个问题。它会忽略任何新值或评估为新值的路径表达式。清单 6-10 给出了一个例子。请注意,第三对没有被删除,因为没有第十个元素。

 > SET @base = '["apple","pear",{"grape":["red","white"]},"strawberry"]';
Query OK, 0 rows affected (0.0008 sec)
> SELECT JSON_REPLACE(@base, '$[0]', "orange", '$[2].grape[0]', "green", '$[9]', "waffles") \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_REPLACE(@base, '$[0]', "orange", '$[2].grape[0]', "green", '$[9]', "waffles"): ["orange", "pear", {"grape": ["green", "white"]}, "strawberry"]
1 row in set (0.0040 sec)

Listing 6-10Using the JSON_REPLACE Function

JSON_SET()函数用于修改 JSON 文档元素。像其他函数一样,您传递一个 JSON 文档作为第一个参数,然后传递一对或多对要替换的路径表达式和值。但是,该函数还会插入文档中不存在的任何元素(找不到路径表达式)。清单 6-11 给出了一个例子。注意,最后一个元素并不存在,所以它将它添加到文档中。

 > SET @base = '["apple","pear",{"grape":["red","white"]},"strawberry"]';
Query OK, 0 rows affected (0.0007 sec)
> SELECT JSON_SET(@base, '$[0]', "orange", '$[2].grape[1]', "green", '$[9]', "123") \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_SET(@base, '$[0]', "orange", '$[2].grape[1]', "green", '$[9]', "123"): ["orange", "pear", {"grape": ["red", "green"]}, "strawberry", "123"]
1 row in set (0.0009 sec)

Listing 6-11Using the JSON_SET Function

现在让我们看看可以用来在文档中查找元素的 JSON 函数。

搜索 JSON 数据

处理 SQL 和 JSON 数据的另一个重要操作是在 JSON 文档中搜索数据。在本章的前面,我们发现了如何用特殊符号(路径表达式)引用文档中的数据,我们还了解到可以使用 JSON 函数来搜索数据。事实上,我们在上一节中看到了这两个概念一起使用。在这一节中,我们将回顾 JSON 数据搜索机制,因为您可能会比其他任何函数更多地使用这些函数,尤其是在查询中。

有四个 JSON 函数允许您搜索 JSON 文档。与前面的函数一样,这些函数使用一个或多个参数对 JSON 文档进行操作。我称它们为搜索函数,不是因为它们允许您在数据库或表格中搜索 JSON 数据,而是因为它们允许您在 JSON 文档中查找东西。这些函数包括检查文档中是否存在值或元素、路径表达式是否有效(使用它可以找到一些东西)以及从文档中检索信息的函数。

JSON_CONTAINS()函数有两个选项:您可以使用它来返回一个值是否存在于文档中的任何地方,或者是否存在使用路径表达式的值(路径表达式是一个可选参数)。该函数返回 0 或 1,其中 0 表示未找到该值。如果文档参数不是有效的 JSON 文档,或者路径参数不是有效的路径表达式,或者包含∗∗通配符,则会出现错误。还有一个问题。传入的值必须是有效的 JSON 字符串或文档。清单 6-12 展示了使用该函数搜索 JSON 文档的几个例子。

> SET @base = '{"grapes":["red","white","green"],"berries":["strawberry","raspberry","boysenberry","blackberry"]}';
Query OK, 0 rows affected (0.0007 sec)
> SELECT JSON_CONTAINS(@base,'["red","white","green"]') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_CONTAINS(@base,'["red","white","green"]'): 0
1 row in set (0.0010 sec)
> SELECT JSON_CONTAINS(@base,'{"grapes":["red","white","green"]}') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_CONTAINS(@base,'{"grapes":["red","white","green"]}'): 1
1 row in set (0.0006 sec)
> SELECT JSON_CONTAINS(@base,'["red","white","green"]','$.grapes') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_CONTAINS(@base,'["red","white","green"]','$.grapes'): 1
1 row in set (0.0004 sec)
> SELECT JSON_CONTAINS(@base,'"blackberry"','$.berries') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_CONTAINS(@base,'"blackberry"','$.berries'): 0
1 row in set (0.0004 sec)
> SELECT JSON_CONTAINS(@base,'blackberry','$.berries') \G
ERROR: 3141: Invalid JSON text in argument 2 to function json_contains: "Invalid value." at position 0.
> SELECT JSON_CONTAINS(@base,'"red"','$.grapes') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_CONTAINS(@base,'"red"','$.grapes'): 1
1 row in set (0.0004 sec)

Listing 6-12Using the JSON_CONTAINS Function

正如你所看到的,这是一个非常有用的函数,但是要正确使用它需要一点小心。也就是说,您必须确保该值是有效的字符串。在所有的例子中,除了一个例子,我使用路径表达式在 JSON 文档中搜索 JSON 文档(这样可以更容易地搜索嵌套数据)或单个值。记住,函数搜索的是值,而不是键。

注意倒数第二个例子。这将返回一个错误,因为该值不是有效的 JSON 字符串。您必须用双引号将它括起来,如下例所示。

JSON_CONTAINS_PATH()函数使用的参数策略略有不同。该函数搜索 JSON 文档以查看路径表达式是否存在,但它也允许您查找第一个或所有的匹配项。它还可以采用多个路径,并根据您作为第二个参数传递的值,将它们作为“或”或“与”条件进行计算,如下所示。

  • 如果您传递一个,如果至少找到一个路径表达式(或),函数将返回 1。

  • 如果传递 all,则只有找到所有路径表达式时,函数才会返回 1(和)。

该函数返回 0 或 1,以指示 JSON 文档在给定的一个或多个路径中是否包含数据。请注意,如果任何路径表达式或文档为 null,它可能会返回 null。如果 JSON 文档或任何路径表达式无效,或者第二个参数不是 one 或 all,则会发生错误。清单 6-13 展示了使用该函数的几个例子。

> SET @base = '{"grapes":["red","white","green"],"berries":["strawberry","raspberry","boysenberry","blackberry"],"numbers":["1","2","3","4","5"]}';
Query OK, 0 rows affected (0.0004 sec)
> SELECT JSON_CONTAINS_PATH(@base,'one','$') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_CONTAINS_PATH(@base,'one','$'): 1
1 row in set (0.0005 sec)
> SELECT JSON_CONTAINS_PATH(@base,'all','$') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_CONTAINS_PATH(@base,'all','$'): 1
1 row in set (0.0005 sec)
> SELECT JSON_CONTAINS_PATH(@base,'all','$.grapes','$.berries') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_CONTAINS_PATH(@base,'all','$.grapes','$.berries'): 1
1 row in set (0.0004 sec)
> SELECT JSON_CONTAINS_PATH(@base,'all','$.grapes','$.berries','$.numbers')\G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_CONTAINS_PATH(@base,'all','$.grapes','$.berries','$.numbers'): 1
1 row in set (0.0004 sec)
> SELECT JSON_CONTAINS_PATH(@base,'all','$.grapes','$.berries','$.num') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_CONTAINS_PATH(@base,'all','$.grapes','$.berries','$.num'): 0
1 row in set (0.0005 sec)
> SELECT JSON_CONTAINS_PATH(@base,'one','$.grapes','$.berries','$.num') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_CONTAINS_PATH(@base,'one','$.grapes','$.berries','$.num'): 1
1 row in set (0.0005 sec)
> SELECT JSON_CONTAINS_PATH(@base,'one','$.grapes') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_CONTAINS_PATH(@base,'one','$.grapes'): 1
1 row in set (0.0004 sec)
> SELECT JSON_CONTAINS_PATH(@base,'all','$.grape') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_CONTAINS_PATH(@base,'all','$.grape'): 0
1 row in set (0.0004 sec)
> SELECT JSON_CONTAINS_PATH(@base,'one','$.berries') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_CONTAINS_PATH(@base,'one','$.berries'): 1
1 row in set (0.0004 sec)
> SELECT JSON_CONTAINS_PATH(@base,'all','$.berries') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_CONTAINS_PATH(@base,'all','$.berries'): 1
1 row in set (0.0005 sec)

Listing 6-13Using the JSON_CONTAINS_PATH Function

花些时间浏览这些例子,这样你就能明白它们是如何工作的。注意在前两个例子中,我使用了一个美元符号的路径表达式。这只是整个文档的路径表达式,所以它自然存在。还要注意最后两个例子中使用 one 或 all 的区别。

JSON_EXTRACT()功能是最常用的功能之一。它允许你提取一个值或 JSON 数组或 JSON 对象等。使用一个或多个路径表达式从 JSON 文档中。我们已经看到了几个例子。Recall 函数返回 JSON 文档中与路径表达式匹配的部分。清单 6-14 展示了更多使用复杂路径表达式的例子。

> SET @base = '{"grapes":["red","white","green"],"berries":["strawberry","raspberry","boysenberry","blackberry"],"numbers":["1","2","3","4","5"]}';
Query OK, 0 rows affected (0.0004 sec)
> SELECT JSON_EXTRACT(@base,'$') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_EXTRACT(@base,'$'): {"grapes": ["red", "white", "green"], "berries": ["strawberry", "raspberry", "boysenberry", "blackberry"], "numbers": ["1", "2", "3", "4", "5"]}
1 row in set (0.0006 sec)
> SELECT JSON_EXTRACT(@base,'$.grapes') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_EXTRACT(@base,'$.grapes'): ["red", "white", "green"]
1 row in set (0.0005 sec)
> SELECT JSON_EXTRACT(@base,'$.grapes[∗]') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_EXTRACT(@base,'$.grapes[∗]'): ["red", "white", "green"]
1 row in set (0.0005 sec)
> SELECT JSON_EXTRACT(@base,'$.grapes[1]') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_EXTRACT(@base,'$.grapes[1]'): "white"
1 row in set (0.0005 sec)
> SELECT JSON_EXTRACT(@base,'$.grapes[4]') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_EXTRACT(@base,'$.grapes[4]'): NULL
1 row in set (0.0006 sec)
> SELECT JSON_EXTRACT(@base,'$.berries') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_EXTRACT(@base,'$.berries'): ["strawberry", "raspberry", "boysenberry", "blackberry"]
1 row in set (0.0009 sec)
> SELECT JSON_EXTRACT(@base,'$.berries[2]') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_EXTRACT(@base,'$.berries[2]'): "boysenberry"
1 row in set (0.0005 sec)
> SELECT JSON_EXTRACT(@base,'$.berries[2]','$.berries[3]') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_EXTRACT(@base,'$.berries[2]','$.berries[3]'): ["boysenberry", "blackberry"]
1 row in set (0.0006 sec)

Listing 6-14Using the JSON_EXTRACT Function

注意当我们使用一元符号时会发生什么。该函数返回整个文档。另外,注意当我们使用一个路径表达式时会发生什么,尽管它的语法是有效的,但它并不等于文档中的一个元素(参见第五个例子)。

还要注意最后一个例子,我们传入了两个路径表达式。注意它是如何返回一个 JSON 数组的,而之前只有一个路径表达式的例子返回一个 JSON 字符串值。这是该函数的一个更棘手的方面。只要您记得它返回一个有效的 JSON 字符串、数组或对象,您就可以毫无问题地使用该函数。

JSON_SEARCH()函数很有趣,因为它与JSON_EXTRACT()函数相反。更具体地说,它接受一个或多个值,如果在文档中找到这些值,则返回这些值的路径表达式。这使得验证路径表达式或动态构建路径表达式变得更加容易。

JSON_CONTAINS_PATH()函数一样,JSON_SEARCH()函数也允许您根据作为第二个参数传递的值来查找返回路径表达式的第一个或所有匹配项,如下所示。

  • 如果传递一个,函数将返回第一个匹配。

  • 如果通过 all,该函数将返回所有匹配项。

但是这里也有一个技巧。该函数接受第三个参数,该参数构成一个特殊的搜索字符串,其工作方式类似于 SQL 语句中的LIKE操作符。也就是说,搜索字符串参数可以像使用LIKE操作符一样使用%_字符。注意,要将%_用作文字,必须在它前面加上\(转义)字符。

该函数返回 0 或 1,以指示 JSON 文档是否包含这些值。请注意,如果任何路径表达式或文档为 null,它可能会返回 null。如果 JSON 文档或任何路径表达式无效,或者第二个参数不是 one 或 all,则会发生错误。清单 6-15 展示了使用该函数的几个例子。

> SET @base = '{"grapes":["red","white","green"],"berries":["strawberry","raspberry","boysenberry","blackberry"],"numbers":["1","2","3","4","5"]}';
Query OK, 0 rows affected (0.0011 sec)
> SELECT JSON_SEARCH(@base,'all','red') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_SEARCH(@base,'all','red'): "$.grapes[0]"
1 row in set (0.0006 sec)
> SELECT JSON_SEARCH(@base,'all','gr____') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_SEARCH(@base,'all','gr____'): NULL
1 row in set (0.0004 sec)
> SELECT JSON_SEARCH(@base,'one','%berry') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_SEARCH(@base,'one','%berry'): "$.berries[0]"
1 row in set (0.0005 sec)
> SELECT JSON_SEARCH(@base,'all','%berry') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_SEARCH(@base,'all','%berry'): ["$.berries[0]", "$.berries[1]", "$.berries[2]", "$.berries[3]"]
1 row in set (0.0006 sec)

Listing 6-15Using the JSON_SEARCH Function

现在我们来看最后一组 JSON 函数;这些工具本质上是实用的,允许您获得关于 JSON 文档的信息,并执行简单的操作来帮助处理 JSON 文档。

效用函数

最后,有几个函数可以返回关于 JSON 文档的信息,帮助添加或删除引号,甚至查找文档中的键。我们已经看到了几个实用程序JSON_TYPE()JSON_VALID()函数。以下是在使用 JSON 文档时可能会发现有用的其他实用函数。

JSON_DEPTH()函数返回 JSON 文档的最大深度。如果文档是空数组、对象或标量值,则该函数返回深度 1。仅包含深度为 1 的元素的数组或仅包含深度为 1 的成员值的非空对象返回深度为 2 的值。清单 6-16 展示了几个例子。

> SELECT JSON_DEPTH('8') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_DEPTH('8'): 1
1 row in set (0.0017 sec)
> SELECT JSON_DEPTH('[]') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_DEPTH('[]'): 1
1 row in set (0.0007 sec)
> SELECT JSON_DEPTH('{}') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_DEPTH('{}'): 1
1 row in set (0.0007 sec)
> SELECT JSON_DEPTH('[12,3,4,5,6]') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_DEPTH('[12,3,4,5,6]'): 2
1 row in set (0.0008 sec)
> SELECT JSON_DEPTH('[[], {}]') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_DEPTH('[[], {}]'): 2
1 row in set (0.0004 sec)
> SET @base = '{"grapes":["red","white","green"],"berries":["strawberry","raspberry","boysenberry","blackberry"],"numbers":["1","2","3","4","5"]}';
Query OK, 0 rows affected (0.0004 sec)
> SELECT JSON_DEPTH(@base) \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_DEPTH(@base): 3
1 row in set (0.0004 sec) 

> SELECT JSON_DEPTH(JSON_EXTRACT(@base, '$.grapes')) \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_DEPTH(JSON_EXTRACT(@base, '$.grapes')): 2
1 row in set (0.0005 sec)

Listing 6-16Using the JSON_DEPTH Function

JSON_KEYS()函数用于以 JSON 数组的形式从 JSON 对象的顶层值返回一个键列表。该函数还允许您传递路径表达式,这将产生所选路径表达式值的顶级键列表。如果json_doc参数不是有效的 JSON 文档,或者路径参数不是有效的路径表达式,或者包含∗∗通配符,则会出现错误。如果所选对象为空,则结果数组为空。

有一个限制。如果顶层值有嵌套的 JSON 对象,则返回的数组不包括这些嵌套对象的键。清单 6-17 显示了使用该功能的几个例子。

> SET @base = '{"grapes":["red","white","green"],"berries":["strawberry","raspberry","boysenberry","blackberry"],"numbers":["1","2","3","4","5"]}';
Query OK, 0 rows affected (0.0004 sec)
> SELECT JSON_KEYS(@base) \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_KEYS(@base): ["grapes", "berries", "numbers"]
1 row in set (0.0039 sec)
> SELECT JSON_KEYS(@base,'$') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_KEYS(@base,'$'): ["grapes", "berries", "numbers"]
1 row in set (0.0005 sec)
> SELECT JSON_KEYS('{"z":123,"x":{"albedo":50}}') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_KEYS('{"z":123,"x":{"albedo":50}}'): ["x", "z"]
1 row in set (0.0004 sec)
> SELECT JSON_KEYS('{"z":123,"x":{"albedo":50}}', '$.x') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_KEYS('{"z":123,"x":{"albedo":50}}', '$.x'): ["albedo"]
1 row in set (0.0004 sec)

Listing 6-17Using the JSON_KEYS Function

JSON_LENGTH()函数返回传递的 JSON 文档的长度。它还允许您传入路径表达式,如果提供了路径表达式,将返回与路径表达式匹配的值的长度。如果json_doc参数不是有效的 JSON 文档,或者路径参数不是有效的路径表达式,或者包含***通配符,则会出现错误。但是,返回值有如下几个约束。

  • 标量的长度为 1。

  • 数组的长度等于数组元素的数量。

  • 对象的长度等于对象成员的数量。

然而,有一个令人惊讶的限制:返回的长度不包括嵌套数组或对象的长度。因此,使用嵌套文档的路径表达式时,必须小心使用该函数。清单 6-18 显示了使用该函数的几个例子。

> SET @base = '{"grapes":["red","white","green"],"berries":["strawberry","raspberry","boysenberry","blackberry"],"numbers":["1","2","3","4","5"]}';
Query OK, 0 rows affected (0.0006 sec)
> SELECT JSON_LENGTH(@base,'$') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_LENGTH(@base,'$'): 3
1 row in set (0.0005 sec)
> SELECT JSON_LENGTH(@base,'$.grapes') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_LENGTH(@base,'$.grapes'): 3
1 row in set (0.0005 sec)
> SELECT JSON_LENGTH(@base,'$.grapes[1]') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_LENGTH(@base,'$.grapes[1]'): 1
1 row in set (0.0005 sec)
> SELECT JSON_LENGTH(@base,'$.grapes[4]') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_LENGTH(@base,'$.grapes[4]'): NULL
1 row in set (0.0004 sec)
> SELECT JSON_LENGTH(@base,'$.berries') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_LENGTH(@base,'$.berries'): 4
1 row in set (0.0004 sec)
> SELECT JSON_LENGTH(@base,'$.numbers') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_LENGTH(@base,'$.numbers'): 5
1 row in set (0.0005 sec)

Listing 6-18Using the JSON_LENGTH Function

请注意,第四个示例返回 null,因为路径表达式虽然是有效的语法,但并不等于值或嵌套的 JSON 数组或对象。

JSON_QUOTE()函数是一个方便的函数,可以帮助你在适当的地方添加引号。也就是说,该函数通过用双引号字符将字符串括起来并转义内部引号和其他字符来将字符串作为 JSON 字符串引用,并返回结果。注意,这个函数并不对 JSON 文档进行操作,而是只对一个字符串进行操作。

您可以使用这个函数生成一个有效的 JSON 字符串文字,以包含在 JSON 文档中。清单 6-19 展示了几个使用函数引用 JSON 字符串的简短例子。

> SELECT JSON_QUOTE("test") \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_QUOTE("test"): "test"
1 row in set (0.0012 sec)
> SELECT JSON_QUOTE('[true]') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_QUOTE('[true]'): "[true]"
1 row in set (0.0007 sec)
> SELECT JSON_QUOTE('90125') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_QUOTE('90125'): "90125"
1 row in set (0.0008 sec)
> SELECT JSON_QUOTE('["red","white","green"]') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_QUOTE('["red","white","green"]'): "[\"red\",\"white\",\"green\"]"
1 row in set (0.0007 sec)

Listing 6-19Using the JSON_QUOTE Function

注意最后一个例子。这里,函数添加了转义符(),因为传递的字符串包含引号。为什么会这样?记住,这个函数接受一个字符串,而不是一个 JSON 数组作为参数。

JSON_UNQUOTE()功能与JSON_QUOTE()功能相反。JSON_UNQUOTE()函数删除引号中的 JSON 值,并将结果作为 utf8mb4 字符串返回。该函数旨在识别而不是改变标记序列,如下所示。

  • \":双引号(")字符

  • \b:退格字符

  • \f:换页符

  • \n:换行符

  • \r:回车符

  • \t:制表符

  • \\:反斜杠(\)字符

清单 6-20 显示了使用该函数的例子。

> SELECT JSON_UNQUOTE("test 123") \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_UNQUOTE("test 123"): test 123
1 row in set (0.0005 sec)
> SELECT JSON_UNQUOTE('"true"') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_UNQUOTE('"true"'): true
1 row in set (0.0007 sec)
> SELECT JSON_UNQUOTE('\"true\"') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_UNQUOTE('\"true\"'): true
1 row in set (0.0007 sec)
> SELECT JSON_UNQUOTE('9\t0\t125\\') \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_UNQUOTE('9\t0\t125\\'): 9  0       125\
1 row in set (0.0006 sec)

Listing 6-20Using the JSON_UNQUOTE Function

JSON_PRETTY()函数格式化 JSON 文档以便于查看。您可以用它来生成一个输出发送给用户,或者让 JSON 在 shell 中看起来更好一些。清单 6-21 显示了一个不带功能和带功能的例子。注意当使用JSON_PRETTY()时,阅读变得容易多了。

> SET @base = '{"name": {"last": "Throckmutton", "first": "Billy-bob"}, "address": {"zip": "90125", "city": "Melborne", "state": "California", "street": "4 Main Street"}}';
Query OK, 0 rows affected (0.0005 sec)
> SELECT @base \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
@base: {"name": {"last": "Throckmutton", "first": "Billy-bob"}, "address": {"zip": "90125", "city": "Melborne", "state": "California", "street": "4 Main Street"}}
1 row in set (0.0005 sec)
> SELECT JSON_PRETTY(@base) \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
JSON_PRETTY(@base): {
  "name": {
    "last": "Throckmutton",
    "first": "Billy-bob"
  },
  "address": {
    "zip": "90125",
    "city": "Melborne",
    "state": "California",
    "street": "4 Main Street"
  }
}
1 row in set (0.0004 sec)

Listing 6-21Using the JSON_PRETTY Function

然而,有一件事这个例子没有涉及到。如果 JSON 数据元素是一个字符串,您必须使用JSON_UNQUOTE()函数从字符串中删除引号。假设我们想要为颜色数据元素添加一个生成的列。如果我们添加带有ALTER TABLE语句的列和索引而不删除引号,我们将得到一些不寻常的结果,如清单 6-22 所示。

> CREATE TABLE `testdb_6`.`thermostats` (`model_number` char(20) NOT NULL,`manufacturer` char(30) DEFAULT NULL,`capabilities` json DEFAULT NULL,PRIMARY KEY (`model_number`)) ENGINE=InnoDB DEFAULT CHARSET=latin1;
Query OK, 0 rows affected (0.0225 sec)
> INSERT INTO `testdb_6`.`thermostats` VALUES ('ODX-123','Genie','{"rpm": 3000, "color": "white", "modes": ["ac", "furnace"], "voltage": 220, "capability": "fan"}') \G
Query OK, 1 row affected (0.0037 sec)
> INSERT INTO `testdb_6`.`thermostats` VALUES ('AB-90125-C1', 'Jasper', '{"rpm": 1500, "color": "beige", "modes": ["ac"], "voltage": 110, "capability": "auto fan"}') \G
Query OK, 1 row affected (0.0041 sec)
> ALTER TABLE `testdb_6`.`thermostats` ADD COLUMN color char(20) GENERATED ALWAYS AS (capabilities->'$.color') VIRTUAL;
Query OK, 0 rows affected (0.0218 sec)
Records: 0  Duplicates: 0  Warnings: 0
> SELECT model_number, color FROM `testdb_6`.`thermostats` WHERE color = "beige" \G
Empty set (0.0006 sec)
> SELECT model_number, color FROM `testdb_6`.`thermostats` LIMIT 2 \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
model_number: AB-90125-C1
       color: "beige"
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 2\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
model_number: ODX-123
       color: "white"
2 rows in set (0.0006 sec)
> ALTER TABLE `testdb_6`.`thermostats` DROP COLUMN color;
Query OK, 0 rows affected (0.0206 sec)
Records: 0  Duplicates: 0  Warnings: 0
> ALTER TABLE `testdb_6`.`thermostats` ADD COLUMN color char(20) GENERATED ALWAYS AS (JSON_UNQUOTE(capabilities->'$.color')) VIRTUAL;
Query OK, 0 rows affected (0.0172 sec)
Records: 0  Duplicates: 0  Warnings: 0
> SELECT model_number, color FROM `testdb_6`.`thermostats` WHERE color = 'beige' LIMIT 1 \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
model_number: AB-90125-C1
       color: beige
1 row in set (0.0006 sec)

Listing 6-22Removing Quotes for Generated Columns on JSON Strings

注意,在第一个SELECT语句中,没有返回任何内容。这是因为虚拟生成的列使用了带引号的 JSON 字符串。当混合 SQL 和 JSON 数据时,这通常是混淆的来源。注意在第二个SELECT语句中,我们看到应该有几行被返回。还要注意,在我们删除该列并用JSON_UNQUOTE()函数再次添加它之后,SELECT返回正确的数据。

小费

有关使用 JSON 函数的更多信息,请参见在线 MySQL 参考手册( https://dev.mysql.com/doc/refman/8.0/en/json-functions.html )中的“JSON 函数”一节。

摘要

MySQL 增加了 JSON 数据类型,这为我们如何使用 MySQL 带来了一个范式转变。第一次,我们可以在关系数据(表)中存储半结构化数据。这不仅为我们提供了前所未有的灵活性,还意味着我们可以利用现代编程技术来访问我们应用中的数据,而无需付出巨大努力和增加复杂性。JSON 是一种众所周知的格式,在许多应用中广泛使用。

理解 JSON 数据类型是理解文档存储的关键。这是因为 JSON 数据类型虽然是为处理关系数据而设计的,但却形成了我们在文档存储中存储数据的模式——在 JSON 文档中!

在本章中,我们更详细地探讨了 JSON 数据类型。我们看到了如何通过 MySQL 中提供的大量内置 JSON 函数来处理关系表中的 JSON 数据的例子。

在下一章中,我们将通过 garage 示例应用并将其转换为文档存储来更详细地探索 MySQL 文档存储。也就是说,我们将看到如何通过使用 MySQL Shell 迁移数据和开发代码来构建带有 JSON 文档的 NoSQL 解决方案,从而将关系数据库解决方案迁移到文档存储中。

七、示例:文档存储开发

现在我们已经有了 JSON 的坚实基础,并且已经了解了如何使用 MySQL Shell 开发关系数据库代码,我们准备开始编写示例应用的 NoSQL 版本。

正如您将看到的,应用从纯关系模型到文档模型的演变演示了我们如何避免使用关系数据的一些混乱方面。可能会让您感到惊讶的一个因素是,文档存储(NoSQL)版本代码的复杂性降低,使代码更容易理解。还有什么更好的理由来考虑使用 MySQL 文档库编写未来的应用呢?

在上一章中,我们通过创建一个或多个 JSON 列,并使用 JSON 函数在 JSON 文档中创建、操作、搜索和访问数据,探索了如何在关系数据库中使用 JSON 数据。

在本章中,我们使用 MySQL 文档库将 garage 示例应用(关系数据库解决方案)迁移到一个纯 NoSQL 应用 1 。我们可以通过 Python 使用许多 X DevAPI 特性来做到这一点——所有这些都是在 MySQL Shell 的帮助下开发的!让我们开始吧。

入门指南

我们没有把文档存储的解释留在最后一章,而是用例子来支持它,解释使用文档存储通过代码处理数据的新方法的好处。我们将以互动的方式演示这些概念,以便通过示例来证明这些概念,而不是简单地展示它是如何实现的。在本节中,我们将了解一个致力于此的示例应用——证明如何使用 shell 来开发自己的文档存储。

然而,为了以这种方式展示 shell 的全部功能,样本必须足够复杂,具有足够的深度(和宽度)来完成它的角色。因此,对于这一章,我们将使用第五章中的 garage 示例应用作为起点,并将其迁移到文档存储中。

回想一下,示例应用是一个工具,您可以用它来组织车库或车间中的工具。然而,与关系数据库实现不同,我们将对数据采取稍微不同的观点。我们开始吧。

示例应用概念

就像我们在第五章看到的示例应用一样,这个示例应用完全是关于组织的。在第五章中,数据是以大多数关系数据库专家使用的典型方式组织的——他们构建表格来保存具有相同布局的东西,或者简单地说,表格代表数据的主要类别。回想一下,我们有放手动工具、电动工具、储物空间等的桌子。–所有你能在车库或车间找到的东西。

对于 garage 应用的这个版本,我们将把我们的关注点从将相似的东西组织成表,转变为通过集合来组织东西。例如,工具箱包含工具,存储单元包含箱子、盒子、箱子等。这似乎是一个非常细微的差别(也许确实如此),但它改变了数据的整个焦点。我们可以打开工具箱或查看搁架单元,看看那里存放了什么工具,而不是通过寻找工具来找到它的存放位置。通过这种方式,我们创造了一个更加用户友好的版本。现在,我们模仿大多数人寻找工具时的做法——一次打开一个抽屉(你知道你也会这么做)。

这是文档存储的优势之一——数据固有的灵活性允许您选择想要使用(或需要)的视图,并使数据及其访问层工作。这是一个挑战,一些关系数据库应用失败得很惨。 2

此外,与关系数据库版本不同,我们为每个主要存储设备创建集合,并在表示存储设备的文档中维护每个设备(例如,工具)的内容列表。我们将所有工具归入一个集合,并通过文档 id 在存储设备集合中引用它们。同样,这听起来可能很奇怪,但是随着您的阅读,您将会看到它是如何工作的。

在此之前,让我们花点时间来看看这个版本的示例应用的修改后的用户界面。我们将这个应用命名为mygarage_v2。由于我们将模式设计的重点放在了存储设备上,因此我们在用户界面中的视图将从这个角度来看。我们保留了列表视图的概念,只是我们为每个存储设备使用了主要部分。图 7-1 显示了一个显示工具箱细节视图的新界面示例。

img/478423_1_En_7_Fig1_HTML.jpg

图 7-1

工具箱详细视图

注意

当使用文档存储或 NoSQL 模型时,我们使用“模式”,对于关系数据库模型,我们使用“数据库”。

请注意,这里我们看到了工具箱的详细信息,以及每个存放位置(抽屉、架子)和其中的工具列表。虽然我们可以对关系数据库版本采用类似的视图, 3 但是文档存储使得编码变得相当容易。

小费

我们不会解释示例应用的每一个细微差别,而是将重点放在最适合用来证明使用 shell 开发代码的实用性的部分——模式集合代码模块。

与关系数据库版本不同,该版本背后的代码易于理解,并且在某些方面不太复杂。但是在我们看到模式设计之前,让我们在下面的列表中讨论一下这个版本的应用中使用的集合。这将帮助您理解与关系数据库版本(版本 1)的区别。

  • 橱柜:带门的储物设备,可能有一个或多个搁板,用于存放各种工具和收纳件

  • 工具箱:有零个或多个架子和一个或多个抽屉的存储设备——用于存储较小的工具

  • 工作台:有一个或多个架子和零个或多个抽屉的存储设备——用于存储较大的工具

  • 搁架单元:没有门和一个或多个搁架的存储设备——用于存储较大的箱子和类似的组织者

  • 组织者:可以容纳一个或多个工具但需要存放在存储设备中的容器

  • 工具:手动和电动工具

  • 供应商:工具和设备的制造商

注意这里有一点词汇上的变化。在应用的第一个版本中,我们使用单数形式的表名。文档存储通常使用复数,因为每个集合通常包含多个项目(文档)。此外,当使用文档存储时,我们应该始终使用术语“模式”,而不是“数据库”。虽然有些人认为它们是同义词,但是 X DevAPI 做出了明确的区分,所以我们将采用相同的方法,使用术语“模式”。

你可能想知道我们怎样才能得到工具箱和工具之间的联系,就像前面提到的那样。这是通过简单地将工具的文档 id 和每个存储位置(重命名为工具位置)存储在工具箱文档中来实现的。

清单 7-1 是一个样本清单,可以帮助形象化地展示如何为organizers系列完成这项工作。正如您将看到的,我们使用 shell 连接到模式,然后获取集合,并按照文档 id 键值获取工具列表(名为tool_ids的数组)。这类似于在关系数据库中查找东西,但是在这种情况下,我们不必编写特殊的 SQL 命令(或者更糟,邪恶的 SQL 连接)来获取数据。稍后我们将更多地讨论那些_id字段。

> garage_v2 = my_session.get_schema('garage_v2')
> tc = garage_v2.get_collection('toolchests')
> tc.find().execute().fetch_one()
{
    "_id": "00005cc33db4000000000000025f",
    "depth": 22,
    "description": "Kobalt 3000 Steel Rolling Tool Cabinet (Black)",
    "height": 54,
    "location": "Rear wall right of workbench",
    "tool_locations": [
        "00005cc33db40000000000000260",
        "00005cc33db40000000000000261",
        "00005cc33db40000000000000268"
    ],
    "vendorid": "00005cc33db40000000000000130",
    "width": 48
}
> locs = garage_v2.get_collection('locations')
> locs.find('_id = :param1').bind('param1', '00005cc33db40000000000000260').execute().fetch_one()
{
    "_id": "00005cc33db40000000000000260",
    "depth": 17,
    "description": "Left 1",
    "height": 2,
    "tool_ids": [
        "00005cc33db40000000000000146",
        "00005cc33db40000000000000147",
        "00005cc33db40000000000000148",
        "00005cc33db40000000000000149",
        "00005cc33db4000000000000014a",
        "00005cc33db4000000000000015a"
    ],
    "type": "Drawer",
    "width": 21
}

Listing 7-1Sample Toolchest JSON Document

此时,您可能想知道 JSON 文档的无忧无虑的无格式规则功能发生了什么变化。简而言之,它仍然存在,但是我们的代码要求每个文档都有一组特定的属性。正如您将看到的,我们定义的属性将在代码中直接用来访问文档中的代码。

这并不排除使用可以随时添加的附加属性,但是它要求您的代码支持这样的更改。这就是以代码为中心的无模式嵌入式数据设计(或简单的代码驱动数据)的含义。随着应用的发展,我们可以使用我们的代码来扩充我们的文档(和集合)。

例如,如果我们将来需要添加一个新属性,我们可以添加代码来处理呈现的新数据,这必须包括如何处理没有新属性的文档,但也可能包括根据需要将属性添加到旧文档的代码。与需要修改表的关系数据库应用不同,关系数据库应用会迫使代码发生变化(可能是以不愉快的方式),我们可以让文档存储的代码来实现这些变化。一切都归结于代码。

让我们进一步讨论模式设计以及如何将关系数据库迁移到文档存储中。

模式设计

您可能会认为我们可以使用 MySQL Workbench 这样的工具来创建模式和集合(您可以这样做),就像您创建关系数据库一样,但是您不需要这样做。您应该使用代码来执行创建事件。更具体地说,如果您要将文档存储的模式导入到 MySQL Workbench 这样的工具中,您不会看到太多感兴趣的内容。这是因为 SQL 模式下的 MySQL Shell(或 Workbench)将集合视为数据库。例如,清单 7-2 显示了这个版本的示例应用(garage_v2)的数据库(模式)的CREATE TABLE语句。

> EXPLAIN toolchests \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
  Field: doc
   Type: json
   Null: YES
    Key:
Default: NULL
  Extra:
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 2\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
  Field: _id
   Type: varbinary(32)
   Null: NO
    Key: PRI
Default: NULL
  Extra: STORED GENERATED
2 rows in set (0.0025 sec)

Listing 7-2Sample CREATE statement from SQL for garage_v2.

很明显,这是行不通的,它只不过是一种暂时的兴趣。请注意 _id 字段。这是文档 id。

当使用文档存储时,我们使用代码来创建模式和集合。回想一下,我们想要将模式命名为garage_v2并创建集合(cabinetstoolchestsworkbenchesshelving unitsorganizerstoolsvendors)。我们还有一个名为locations的集合,它将各种类型的存储位置(称为工具位置)存储为一个文档,如架子、抽屉等。locations集合不能从用户界面的主菜单访问,因为焦点在存储设备集合上。但是,每个存储设备详细视图允许您修改该存储设备的工具位置,以完成该集合的创建、读取、更新和删除(CRUD)操作。

我们将使用 shell 和 X DevAPI 来创建这些对象,如清单 7-3 所示。注意,我们首先连接到服务器,请求会话,然后创建模式和集合。就这样——我们的模式完成了,我们有了文档存储的基础!

from getpass import getpass
try:
    input = raw_input
except NameError:
    pass
# Get user id and password
userid = input("User Id: ")
passwd = getpass("Password: ")

user_info = {
    'host': 'localhost',
    'port': 33060,
    'user': userid,
    'password': passwd,
}
# Connect to the database garage_v1
my_session = mysqlx.get_session(user_info)
# Create the schema for garage_v2
my_session.drop_schema('garage_v2')
garage_v2 = my_session.create_schema('garage_v2')
# Create the collections
cabinets = garage_v2.create_collection('cabinets')
organizers = garage_v2.create_collection('organizers')
shelving_units = garage_v2.create_collection('shelving_units')
tools = garage_v2.create_collection('tools')
toolchests = garage_v2.create_collection('toolchests')
locations = garage_v2.create_collection('locations')
workbenches = garage_v2.create_collection('workbenches')
vendors = garage_v2.create_collection('vendors')
# Show the collections
print(garage_v2.get_collections())

Listing 7-3Creating the Document Store

当您运行代码时,您应该会看到如下所示的输出。

> mysqlsh --py -f listing7-3.py
User Id: root
Password: ∗∗∗∗∗∗∗∗∗∗∗
[<Collection:cabinets>, <Collection:locations>, <Collection:organizers>, <Collection:shelving_units>, <Collection:toolchests>, <Collection:tools>, <Collection:vendors>, <Collection:workbenches>]

这难道不比苦读一堆 SQL CREATE语句容易得多吗?如果你想从头开始创建模式和集合,你可以在你的机器上运行这个,但是如果你把我们在第五章中看到的数据从一个关系数据库转换到一个文档存储,你可能想按照设置和配置部分中的例子转换。

在此之前,让我们再来谈谈这些系列。这一次,我们将看到每个集合的样本文档。

注意

我们使用“工具位置”来代替版本 1 中“存储位置”的概念,以便区分和更好地描述 JSON 文档中的属性。

橱柜系列

cabinets集合存储描述橱柜的文档,橱柜可以包含一个或多个搁板和一个或多个门。因此,我们希望存储集合中每个橱柜的信息,包括它的物理大小和位置,以及它包含的工具位置(架子)。清单 7-4 展示了这个集合中的一个 JSON 文档示例。

> garage_v2 = my_session.get_schema('garage_v2')
> cabinets = garage_v2.get_collection('cabinets')
> cabinets.find().execute().fetch_one()
{
    "_id": "00005cae74150000000000000161",
    "depth": 24,
    "description": "Kobalt Steel Freestanding Garage Cabinet",
    "height": 72,
    "location": "Right wall",
    "numdoors": 2,
    "tool_locations": [
        "00005cae74150000000000000162",
        "00005cae74150000000000000163",
        "00005cae74150000000000000164",
        "00005cae74150000000000000165",
        "00005cae74150000000000000166"
    ],
    "vendorid": "00005cae74150000000000000001",
    "width": 48
}

Listing 7-4Cabinets Collection Example JSON Document

注意我们有一个名为_id的属性。当我们创建文档时,我们没有指定这个属性,如果您没有指定,MySQL 将为您创建一个唯一的值。这称为文档 id。如果需要,您可以为_id指定自己的值,但是通常不鼓励这样做,因为内部机制将确保文档是惟一的。把它想象成一个主键。 4 有关文档 id 的更多信息,请参见 https://dev.mysql.com/doc/x-devapi-userguide/en/working-with-document-ids.html

我们还为描述、大小(深度、高度、宽度)、物理位置、门的数量和工具位置的数组添加了属性。最后,注意我们有一个包含供应商文档 id 的属性。请注意,tool_locations数组没有施加任何限制,比如只允许使用盘架。这是因为在文档存储中,这些类型的约束被添加到代码中。文档存储只是存储文档。

注意

在 JSON 文档中,我们对集合和属性使用小写名称。这不是绝对必要的,但确实遵循一种熟悉的模式。

位置集合

locations集合存储描述工具存储位置的文档,如架子或抽屉。也就是说,我们希望存储关于集合中每个位置的信息,包括它的物理大小和位置,以及它包含的工具位置(架子或抽屉)。清单 7-5 展示了这个集合中的一个示例 JSON 文档。

> garage_v2 = my_session.get_schema('garage_v2')
> locations = garage_v2.get_collection('locations')
> locations.find().execute().fetch_one()
{
    "_id": "00005cae74150000000000000146",
    "depth": 17,
    "description": "Left 1",
    "height": 2,
    "tool_ids": [
        "00005cae74150000000000000141",
        "00005cae74150000000000000142",
        "00005cae74150000000000000139"
    ],
    "type": "Drawer",
    "width": 21
}

Listing 7-5Locations Collection Example JSON Document

注意,我们有一个工具 id 数组,它是存储在这个位置的每个工具的文档 id。还要注意,我们有一个 type 属性来存储刀具位置的类型。对这些值的控制也被转移到代码中,这允许您通过代码来更改允许的类型,而不是修改底层数据存储,这可能会在开发和发布期间造成额外的麻烦。

等等,为什么不把工具放在工具箱里?

一些文档存储开发人员可能会告诉您,使用单独的集合并通过 id 引用文档是不好的。他们会说,“只需将所有工具作为一个数组转储到存储设备集合中,不要弄乱位置集合。”这是一种有效的设计选择,但是数据中的一些严格性(例如以这种方式提取刀具位置到刀具的映射)同样有效,并且在某些情况下可能会使数据的转换更容易完成和可视化。我们将在本章后面看到这样一个例子。是的,JSON 可以让你把所有的东西放在一起,但是在数据组织上有一点严格性并不能验证 NoSQL 的目标。不要被愚弄,认为没有严谨就等同于更好的 NoSQL 设计。

组织者收藏

organizers集合存储描述组织者的文档,如箱子、盒子、盒子等。,它可以包含一个或多个工具。因此,我们希望存储集合中每个组织者的信息,包括其物理大小和位置,以及它包含的工具。清单 7-6 展示了这个集合中的一个示例 JSON 文档。

> garage_v2 = my_session.get_schema('garage_v2')
> organizers = garage_v2.get_collection('organizers')
> organizers.find().execute().fetch_one()
{
    "_id": "00005cae7415000000000000013b",
    "depth": 14,
    "description": "SAE/Metric Socket Set",
    "height": 4,
    "tool_ids": [
        "00005cae741500000000000000b2",
        "00005cae741500000000000000b9",
        "00005cae741500000000000000c7"
    ],
    "type": "Case",
    "width": 12
}

Listing 7-6Organizers Collection Example JSON Document

像 locations 集合一样,我们也有一个在代码中控制的 type 属性。除此之外,我们还添加了描述其物理大小和描述的属性。还有一个工具 id 数组。这就是我们所需要的。

对于示例应用,这里有一点需要注意。因为组织器可以很小,所以它们可以放置在工具位置。因此,应用需要额外的代码来处理查找,以区分组织者和工具。这是在应用中考虑灵活性的一个例子。关系数据库设计永远不会允许这样做,因为类型(组织者与工具位置)是不同的。因为我们用代码控制数据接口,所以我们需要做的就是编写代码来处理这种情况!我们将在下一节的代码设计中看到更多。

搁架单元系列

shelving_units集合存储描述搁架单元的文档,搁架单元可以包含一个或多个架子。因此,我们希望存储关于集合中每个搁架单元的信息,包括它的物理大小和位置,以及它包含的工具位置(搁架)。清单 7-7 展示了这个集合中的一个 JSON 文档示例。

> garage_v2 = my_session.get_schema('garage_v2')
> shelving_units = garage_v2.get_collection('shelving_units')
> shelving_units.find().execute().fetch_one()
{
    "_id": "00005cae7415000000000000014f",
    "depth": 24,
    "description": "Wire shelving #1",
    "height": 72,
    "location": "Right wall",
    "tool_locations": [
        "00005cae74150000000000000150",
        "00005cae74150000000000000152",
        "00005cae74150000000000000153"
    ],
    "vendorid": "00005cae74150000000000000015",
    "width": 48
}

Listing 7-7Shelving Units Collection Example JSON Document

你开始看到这里的趋势了吗?是的,存储设备系列具有非常相似的属性。这是有意的。为什么呢?因此,我们可以将文档隔离到离散的集合中,使集合变浅(更少的文档)。诚然,这将导致一个非常小的性能问题,但使用。随着应用的成熟或发展,它还允许您更改每个集合中的属性集。

例如,如果添加一种新型的搁架单元,它有门、抽屉、工具架等等,您可以更改代码来处理新添加的内容,而无需重做集合中的任何文档,也无需对其他集合强制进行更改。

回想一下,在关系数据库版本中,我们有一个用于存储门数量的字段,并不是所有存储设备类型都需要它。JSON 文档是这个 SQL 难题的答案。

工具箱系列

toolchests集合存储描述工具箱的文档,工具箱可以包含零个或多个架子和一个或多个抽屉。因此,我们希望存储集合中每个工具箱的信息,包括它的物理大小和位置,以及它包含的工具位置(架子和抽屉)。清单 7-8 展示了这个集合中的一个 JSON 文档示例。

> garage_v2 = my_session.get_schema('garage_v2')
> toolchests = garage_v2.get_collection('toolchests')
> toolchests.find().execute().fetch_one()
{
    "_id": "00005cae74150000000000000145",
    "depth": 22,
    "description": "Kobalt 3000 Steel Rolling Tool Cabinet (Black)",
    "height": 54,
    "location": "Rear wall right of workbench",
    "tool_locations": [
        "00005cae74150000000000000146",
        "00005cae74150000000000000147",
        "00005cae7415000000000000014e"
    ],
    "vendorid": "00005cae74150000000000000001",
    "width": 48
}

Listing 7-8Toolchests Collection Example JSON Document

工具集合

tools集合存储描述工具的文档。我们将包括所有工具;不仅仅是手动工具或电动工具。我们在文档中存储了每个工具的特性,如描述、类别、大小等。清单 7-9 展示了这个集合中的一个示例 JSON 文档。

> garage_v2 = my_session.get_schema('garage_v2')
> tools = garage_v2.get_collection('tools')
> tools.find().execute().fetch_one()
{
    "_id": "00005cae74150000000000000024",
    "category": "Handtool",
    "description": "1/8-in X 1-1/2-in",
    "size": "Slotted",
    "type": "Screwdriver",
    "vendorid": "00005cae74150000000000000002"
}

Listing 7-9Tools Collection Example JSON Document

关于这个系列有一点需要注意。回想一下,我们在版本 1 的关系数据库中有两个表,因为一些字段有惟一的枚举值。因为 JSON 允许我们存储我们想要的属性,所以这个集合中的文档可能有也可能没有一个或多个属性。例如,并非所有工具都有大小属性。像其他集合一样,类别和类型的值在代码中处理。

供应商集合

vendors集合存储描述供应商的文档。我们在版本 1 的关系数据库中存储了相同的特征,包括名称、来源和 URL。清单 7-10 展示了这个集合中的一个示例 JSON 文档。

> garage_v2 = my_session.get_schema('garage_v2')
> vendors = garage_v2.get_collection('vendors')
 MySQL  Py > vendors.find().execute().fetch_one()
{
    "_id": "00005cae74150000000000000001",
    "name": "Kobalt",
    "sources": "Lowes",
    "url": "https://www.kobalttools.com/"
}

Listing 7-10Vendors Collection Example JSON Document

工作台集合

workbenches集合存储描述工作台的文档,工作台可以包含零个或多个货架。因此,我们希望存储集合中每个工作台的信息,包括它的物理大小和位置,以及它包含的工具位置(架子)。清单 7-11 展示了这个集合中的一个 JSON 文档示例。

> garage_v2 = my_session.get_schema('garage_v2')
> workbenches = garage_v2.get_collection('workbenches')
> workbenches.find().execute().fetch_one()
{
    "_id": "00005cae74150000000000000167",
    "depth": 24,
    "description": "Large metal workbench",
    "height": 42,
    "location": "Rear wall",
    "tool_locations": [
        "00005cae74150000000000000168",
        "00005cae74150000000000000169",
        "00005cae7415000000000000016a"
    ],
    "vendorid": "00005cae74150000000000000002",
    "width": 72
}

Listing 7-11Workbenches Collection Example JSON Document

现在我们已经看到了集合是如何形成的,让我们看看如何使用 shell 来编写实现集合的 CRUD 操作所需的代码。

代码设计

示例应用的代码与版本 1 中的代码非常相似;这是一个 Python Flask 应用,具有组织代码模块的相同选择。与版本 1 一样,您可能会发现有些事情您会做得不同,但出于演示的目的,它仍然可以以当前的形式使用。为示例应用做出的代码设计选择包括以下内容。

  • 将 Flask 框架用于基于 web 的界面。

  • 使用一个类来表示数据库中的每个表。

  • 将单个类放在它自己的代码模块中。

  • 将所有模式代码模块放在它自己的文件夹(名为 schema)中。

  • 使用类封装到数据库服务器的连接。

  • 使用类模块测试每个表/视图类。

  • 使用从 shell 运行的代码模块来测试类模块。

我们将在演示中看到这些约束中的大部分。如前所述,用户界面的描述包含在附录中。

幸运的是,我们已经通过使用文档存储使编写代码变得稍微容易了一些。这是因为集合允许您指定属性,所以除了为有效值列表(关系数据库版本中的枚举列)添加约束和检查错误之外,创建、读取、更新和删除(CRUD)的代码是相同的。因此,我们将使用带有基类的对象层次结构,该基类包含 CRUD 操作的主要代码,并允许子类添加特定于每个集合的约束代码。

让我们花点时间列出我们将使用的代码模块。在这个示例应用中,我们将创建一个名为 schema 的文件夹,并将以下模块放在该文件夹中。然后,我们可以根据需要在应用代码中导入它们。表 7-1 显示了所需的代码模块。

表 7-1

模式代码模块

|

代码模块

|

类别名

|

描述

|
| --- | --- | --- |
| garage_v2 | MyGarage | 实现与服务器和通用服务器接口的连接 |
| garage_collection | GarageCollection | 所有集合类的基类 |
| cabinets.py | Cabinets | 为橱柜系列建模 |
| locations.py | Locations | 对位置集合建模 |
| organizers.py | Organizers | 模拟组织者集合 |
| shelving_units.py | ShelvingUnits | 对 shelving_units 集合进行建模 |
| toolchests.py | Toolchests | 为工具箱系列建模 |
| tools.py | Tools | 对工具集合建模 |
| vendors.py | Vendors | 对供应商集合建模 |
| workbenches.py | Workbenches | 对工作台集合建模 |

当我们为示例应用编写代码以使用这些代码模块时,我们将使用MyGarage类连接到数据库服务器,并且当被请求时,使用与每个集合相关联的类来调用每个集合上的 CRUD 操作。

现在,我们已经了解了示例应用及其设计的目标,让我们开始编写示例应用的模式代码。

设置和配置

以下演示的设置不需要安装任何东西,甚至不需要使用示例应用,相反,我们只需要加载示例模式,因为我们将只使用模式代码模块。虽然图像用于描述示例应用的某些方面,但在本章中并不一定需要它。同样,关于如何设置和使用完整的示例应用,请参见附录。

由于我们在第五章中使用了来自示例应用的相同数据,我们将看看如何将关系数据库数据转换成文档存储。之后,我们将看到如何为基类和集合类编写代码。

将关系数据转换为文档存储

我们可以将关系数据库转换成文档存储,而不是从头开始。如果您还没有运行第五章中的示例应用,并且您想了解转换是如何工作的,那么您可以首先运行本书源代码中的Ch05/mygarage/database/garage_v1.sql文件,然后跟随我们了解如何将数据库表转换为集合以及将数据转换为文档。即使您不打算使用示例应用,本节也可以帮助您将来将其他数据库转换为文档存储。

我们将采取逐步的方法来形成一个脚本来转换数据。这是因为有几个挑战会使转换代码变得有点棘手。幸运的是,当我们检查转换数据所必需的代码时,我们将遍历每一个。从关系数据库转换到文档存储的挑战在于如何将数据传递到新的集合中。这意味着在尝试转换之前,您必须计划您的收藏以及您希望如何使用它们。否则,随着模式设计的成熟,您可能会发现自己需要重新编写转换代码。 5

将示例应用(版本 1)转换为文档存储的挑战包括以下内容。

  • 我们不能使用自动增量值。因此,我们需要创建一个从旧的自动增量值到新的文档 id 的映射。

  • 我们将手动工具和电动工具合并为一个系列。

  • 我们将存储表分割成单独的集合。

  • 我们必须遍历位置表,并在新的集合中保留工具位置。

让我们从第一步开始,即转换代码的前同步码或设置。如果您计划将您自己的关系数据库转换为文档存储,您可以使用一组类似的步骤来编写您自己的转换代码,但是您可能不需要所有的步骤。

步骤 1:转换设置代码

这一步很简单。我们只需导入我们需要的模块,然后连接到服务器,获得一个garage_v1数据库的实例,并创建garage_v2模式和集合,就像我们之前在清单 7-3 中所做的那样。清单 7-12 显示了设置转换脚本的代码。

import json
from getpass import getpass
try:
    input = raw_input
except NameError:
    pass

try:
    import mysqlx
except Exception:
    from mysqlsh import mysqlx

# Get user id and password
userid = input("User Id: ")
passwd = getpass("Password: ")

user_info = {
    'host': 'localhost',
    'port': 33060,
    'user': userid,
    'password': passwd,
}

# Connect to the database garage_v1
my_session = mysqlx.get_session(user_info)
garage_v1 = my_session.get_schema('garage_v1')
# Get the tables
handtool_tbl = garage_v1.get_table('handtool')
organizer_tbl = garage_v1.get_table('organizer')
place_tbl = garage_v1.get_table('place')
powertool_tbl = garage_v1.get_table('powertool')
storage_tbl = garage_v1.get_table('storage')
vendor_tbl = garage_v1.get_table('vendor')

# Create the schema for garage_v2
my_session.drop_schema('garage_v2')
garage_v2 = my_session.create_schema('garage_v2')
# Create the collections
cabinets = garage_v2.create_collection('cabinets')
organizers = garage_v2.create_collection('organizers')
shelving_units = garage_v2.create_collection('shelving_units')
tools = garage_v2.create_collection('tools')
toolchests = garage_v2.create_collection('toolchests')
locations = garage_v2.create_collection('locations')
workbenches = garage_v2.create_collection('workbenches')
vendors = garage_v2.create_collection('vendors')

Listing 7-12Conversion Setup Code

步骤 2:助手函数

下一步需要一些解释。它接下来出现在脚本中(但也可以放在代码的前面)。在这一步中,我们创建了几个助手函数,用于处理数据库表,并重建原始表和新文档 id 映射之间的链接。表 7-2 列出了新的助手功能及其描述。我们也将看到函数的代码。

表 7-2

转换脚本的帮助函数

|

名字

|

因素

|

描述

|
| --- | --- | --- |
| show_collection(col_object) | 集合对象 | 打印集合的内容(用于调试)。 |
| get_places(storage_id) | 自动递增 id | 获取与此存储 id 匹配的存储位置。 |
| get_organizer_ids(place_id) | 自动递增 id | 在存放处获取组织者 id 列表。 |
| get_handtool_ids(place_id) | 自动递增 id | 在存放处获取手工工具 id 清单 |
| get_powertool_ids(place_id) | 自动递增 id | 在存放处获取电动工具 id 列表。 |
| get_mapping(old_id, mapping) | 自动递增 id,映射(数组) | 获取旧供应商 id 的新文档 id。 |
| find_tool_in_organizers(tool_id) | 工具的 id(_ I) | 在组织者集合中搜索该工具。 |
| find_tool(collection_name, tool_id) | 工具的集合名称,id | 在给定集合中搜索工具。 |
| get_tool_location(tool_id) | 工具的 id(
I) | 查找工具的 location _id。 |

get_∗函数全部用于查询关系数据库表,以找到匹配自动增量值的行,并获得电动工具和手工工具的自动增量 id 列表。这些用来获取一行,这样就可以将它们转换成 JSON 文档。find_∗函数用于在集合中搜索与工具 id 匹配的 JSON 文档,这样我们就可以填充 locations 集合。

此外,为了将工具存储位置与工具相关联,我们需要一种收集工具的方法。我们可以在文档中存储一个工具 id 数组,而不是创建一个引用、连接或查找表。因此,我们以这种方式创建链接,使其更加直观——我们打开一个工具箱,想看看里面有什么。

现在让我们看看这些函数的代码。清单 7-13 显示了函数的代码。我们不解释它们各自做什么,而是给出代码,并在后面的上下文中讨论它们。如果你看不到它们是如何工作的或者为什么要写它们,不要担心;当你看到它们在上下文中使用时,它们会更有意义。

# Display the documents in a collection
def show_collection(col_object):
    print("\nCOLLECTION: {0}".format(col_object.name))
    results = col_object.find().execute().fetch_all()
    for document in results:
        print(json.dumps(json.loads(str(document)),
              indent=4, sort_keys=True))

# Get the storage places that match this storage id
def get_places(storage_id):
    return place_tbl.select('Type', 'Description', 'Width', 'Depth',
                            'Height', 'Id')\
                    .where("StorageId = {0}".format(storage_id)).execute()

# Get the list of organizer ids at the storage place
def get_organizer_ids(place_id):
    organizer_ids = []
    org_results = organizer_tbl.select('Id')\
                     .where("PlaceId = {0}".format(place_id)).execute()
    for org in org_results.fetch_all():
        organizer_ids.append(get_mapping(org[0], organizer_place_map)[0])
    return organizer_ids

# Get the list of handtool ids at the storage place
def get_handtool_ids(place_id):
    handtool_ids = []
    ht_results = handtool_tbl.select('Id')\
                 .where("PlaceId = {0}".format(place_id)).execute()
    for ht in ht_results.fetch_all():
        handtool_ids.append(ht[0])
    return handtool_ids

# Get the list of powertool ids at the storage place
def get_powertool_ids(place_id):
    powertool_ids = []
    pt_results = powertool_tbl.select('Id')\
                 .where("PlaceId = {0}".format(place_id)).execute()
    for pt in pt_results.fetch_all():
        powertool_ids.append(pt[0])
    return powertool_ids

# Get the new docid for the old vendor id
def get_mapping(old_id, mapping):
    for item in mapping:
        if item[0] == old_id:
            return item
    return None

# Search the organizers collection for the tool
def find_tool_in_organizers(tool_id):
    # organizers contain no shelves or drawers so fetch only the tool ids
    organizers = garage_v2.get_collection('organizers')
    results = organizers.find().fields("_id", "tool_ids", "type",
                                       "description").execute().fetch_all()
    for result in results:
        if (result["tool_ids"]) and (tool_id in result["tool_ids"]):
            return ("{0}, {1}".format(result["type"], result["description"]),
                                      'organizers', result["_id"])
    return None

# Search for a tool in a given collection
def find_tool(collection_name, tool_id):
    collection = garage_v2.get_collection(collection_name)
    storage_places = collection.find()\
      .fields("_id", "description", "tool_locations").execute().fetch_all()
    for storage_place in storage_places:
        if storage_place["tool_locations"]:
            for location in storage_place["tool_locations"]:
                loc_data = locations.find('_id = :param1') \
                                    .bind('param1',
                                          location).execute().fetch_all()
                if loc_data:
                    loc_dict = dict(loc_data[0])
                    tool_ids = loc_dict.get("tool_ids", [])
                    if tool_id in tool_ids:
                        return ("{0}, {1} - {2}"
                                "".format(storage_place["description"],
                                          loc_dict["description"],
                                          loc_dict["type"]),
                                          collection_name,
                                          storage_place["_id"])
    return None

# Find the location document id for a tool.
def get_tool_location(tool_id):
    loc_found = find_tool_in_organizers(tool_id)
    if loc_found:
        return loc_found
    storage_collections = [
        'toolchests', 'shelving_units', 'workbenches', 'cabinets'
    ]
    for storage_collection in storage_collections:
        loc_found = find_tool(storage_collection, tool_id)
        if loc_found:
            return loc_found
    return None

Listing 7-13
Helper Functions

步骤 3:填充集合

下一步是填充集合。我们可以用数据填充集合,但是必须按照特定的顺序。例如,代表存储设备和工具的集合中的每个文档都是供应商的文档 id。因此,我们需要首先执行vendors集合,从表中创建旧 id 列到 vendors 集合中新文档 id 的映射。让我们看看如何做到这一点。清单 7-14 显示了将vendor表转换为vendors集合的代码。我们将更详细地研究这段代码,因为它构成了处理其他表和集合的模板。

# Get the vendors
my_results = vendor_tbl.select('Id', 'Name', 'URL', 'Sources').execute()
vendor_id_map = []
for v_row in my_results.fetch_all():
    new_item = {
        'name': v_row[1],
        'url': v_row[2],
        'sources': v_row[3]
    }
    last_docid = vendors.add(new_item).execute().get_generated_ids()[0]
    vendor_id_map.append((v_row[0], last_docid))
show_collection(vendors)

Listing 7-14Populate the Vendors Collection

注意这里我们打开了vendor表并读取了所有数据。然后,我们创建一个空映射(数组),我们将使用它来记录从表到集合中新文档 id 的自动增量 id。这将允许我们将其他表中供应商列的自动增量 id 替换为集合中供应商的新文档 id。这是在转换过程中保留关系链接的一种很好的方式。

我们使用一个循环来读取查询的结果,并使用表行中的数据形成一个包含属性(小写)的字典。然后我们使用vendors集合来添加供应商文档。

我们可以将add()方法与get_generated_ids()调用链接起来,以获得生成的最后一个文档 id。然后我们将它添加到名为vendor_id_map的新映射中,稍后我们将使用它在其他集合的其他文档中插入供应商的正确文档 id。

为了帮助可视化结果,我们使用show_collection()函数来打印集合的内容。

我们转换的下一个集合是tools集合。回想一下,我们将把handtoolpowertool表合并到工具集合中。因此,我们必须读取这些表,并将它们插入到tools集合中。清单 7-15 显示了这个转换的代码。花点时间熟悉一下代码。

# Get the tools combining the handtool and powertool tables
ht_results = handtool_tbl.select('Id', 'VendorId', 'Description', 'Type', 'Toolsize', 'PlaceId').execute()
tool_place_map = []
for ht_row in ht_results.fetch_all():
    new_item = {
        'category': 'Handtool',
        'vendorid': get_mapping(ht_row[1], vendor_id_map)[1],
        'description': ht_row[2],
        'type': ht_row[3],
        'size': ht_row[4],
    }
    last_docid = tools.add(new_item).execute().get_generated_ids()[0]
    tool_place_map.append((ht_row[0], last_docid))
pt_results = powertool_tbl.select('Id', 'VendorId', 'Description', 'Type', 'PlaceId').execute()
for pt_row in pt_results.fetch_all():
    new_item = {
        'category': 'Powertool',
        'vendorid': get_mapping(pt_row[1], vendor_id_map)[1],
        'description': pt_row[2],
        'type': pt_row[3],
    }
    last_docid = tools.add(new_item).execute().get_generated_ids()[0]
    tool_place_map.append((pt_row[0], last_docid))
show_collection(tools)

Listing 7-15Populate the Tools Collection

正如您所看到的,这段代码遵循与前面的代码相同的模式,为工具的自动增量 id 到生成的新文档 id 创建一个映射。请注意,电动工具没有大小属性,但手动工具有。因此,我们为手动工具而不是电动工具添加了该属性。这以一种小的方式展示了我们如何在同一个集合中使用具有不同属性(键)的文档。

为了帮助可视化结果,我们使用show_collection()函数来打印集合的内容。

我们转换的下一个集合是organizers集合。像以前一样,我们只是读取表中的行,并将它们插入到集合中。清单 7-16 显示了将organizer表转换为organizers集合的代码。

# Get organizers
org_results = organizer_tbl.select('Id', 'Description', 'Type', 'Width', 'Depth', 'Height', 'PlaceId').execute()
organizer_place_map = []
for org_row in org_results.fetch_all():
    tool_ids = get_handtool_ids(org_row[0])
    tool_ids.extend(get_powertool_ids(org_row[0]))
    tool_docids = [get_mapping(item, tool_place_map)[1] for item in tool_ids]
    new_item = {
        'description': org_row[1],
        'type': org_row[2],
        'width': org_row[3],
        'depth': org_row[4],
        'height': org_row[5],
    }
    if tool_docids:
        new_item.update({'tool_ids': tool_docids})
    last_docid = organizers.add(new_item).execute().get_generated_ids()[0]
    # We also need to save the mapping of organizers to storage places
    organizer_place_map.append((org_row[0], last_docid))
show_collection(organizers)

Listing 7-16Populate the Organizers Collection

虽然这段代码也遵循与前面相同的模式,但是我们创建了组织者 id 到新文档 id 的映射。然而,由于数据库中的 organizer 表通过place表引用了工具,我们使用 helper 函数从表中检索与该 organizer 匹配的工具 id。然后,我们构建一个工具 id 数组,并将其存储在属性tool_ids中。花点时间看看这是如何工作的。

为了帮助可视化结果,我们使用show_collection()函数来打印集合的内容。

我们转换的下一个集合是toolchests集合。这是我们将从存储表中分离出来的第一个集合,为每个存储设备创建一个单独的集合。由于我们在storage表中有不止一个存储设备,我们将把结果限制在那些type设置为toolchest的设备上。像以前一样,我们只是读取表中的行,并将它们插入到集合中。清单 7-17 显示了将storage表转换成toolchests集合的代码。

# Get the toolchests
tc_results = storage_tbl.select('Id', 'VendorId', 'Description', 'Width', 'Depth', 'Height', 'Location').where("Type = 'Toolchest'").execute()
# For each toolbox, get its storage places and insert into the collection
for tc_row in tc_results.fetch_all():
    new_tc = {
        'vendorid': get_mapping(tc_row[1], vendor_id_map)[1],
        'description': tc_row[2],
        'width': tc_row[3],
        'depth': tc_row[4],
        'height': tc_row[5],
        'location': tc_row[6],
    }
    _id = toolchests.add(new_tc).execute().get_generated_ids()[0]
    # Now, generate the tool locations for this document
    tool_locations = []
    for pl_row in get_places(tc_row[0]).fetch_all():
        # Get all organizers and tools that are placed here
        tool_ids = get_handtool_ids(pl_row[5])
        tool_ids.extend(get_powertool_ids(pl_row[5]))
        tool_docids = []
        org_ids = get_organizer_ids(pl_row[5])
        if org_ids:
            for org_id in org_ids:
                map_found = get_mapping(org_id, organizer_place_map)
                if map_found:
                    tool_docids.append(map_found[1])
        for item in tool_ids:
            map_found = get_mapping(item, tool_place_map)
            if map_found:
                tool_docids.append(map_found[1])
        if pl_row[0] == 'Shelf':

            new_item = {
                'type': 'Shelf',
                'description': pl_row[1],
                'width': pl_row[2],
                'depth': pl_row[3],
                'height': pl_row[4],
            }
            if tool_docids:
                new_item.update({'tool_ids': tool_docids})
            loc_id = locations.add(new_item).execute().get_generated_ids()[0]
            tool_locations.append(loc_id)
        else: # drawer is the only other value for type
            new_item = {
                'type': 'Drawer',
                'description': pl_row[1],
                'width': pl_row[2],
                'depth': pl_row[3],
                'height': pl_row[4],
            }
            if tool_docids:
                new_item.update({'tool_ids': tool_docids})
            loc_id = locations.add(new_item).execute().get_generated_ids()[0]
            tool_locations.append(loc_id)
    if len(tool_locations) > 0:
        toolchests.modify('_id = :param1') \
                  .bind('param1', _id) \
                  .set('tool_locations', tool_locations).execute()
show_collection(toolchests)

Listing 7-17Populate the Toolchests Collection

这段代码和前面一样,从表中获取行,并为集合创建一个新文档。然而,这变得有点复杂,因为我们必须将places表条目转换成tool_locations数组。这需要使用助手函数来构建来自数据库handtoolpowertool表的 id 列表,以及来自organizer表的 id 列表,因为根据经验,我们知道可以将一个组织者放在工具箱中。

然而,我们还需要检查 places 表,从数据库中找到存储位置,并将它们转换成新的locations集合。我们使用找到的工具 id 用新的工具 id 列表更新集合中的文档。这听起来很复杂,但是如果你花点时间研究一下代码,你会发现我们用助手函数来做这件事更容易。

为了帮助可视化结果,我们使用show_collection()函数来打印集合的内容。

为了简洁起见,我们将省略其他集合(cabinetsshelving_unitsworkbenches)的代码,因为它们遵循与toolchests转换代码相同的模式。像以前一样,我们只是读取表中的行,并将它们插入到新的集合中。

步骤 4:添加位置

最后一步用于填充每个工具和管理器的位置。回想一下数据库表,我们使用了一个表引用来查找位置。然而,由于我们有一个文档存储,我们可以简单地使用代码中内置的字符串。这保存了一个我们不需要维护的引用,相反,我们在创建和更新操作中设置位置时设置它。

为此,我们使用另一个助手函数来构建位置字符串。我们更新了工具和组织者集合中的所有文档。清单 7-18 显示了构建位置字符串的代码。

# Add the location for each tool
tool_results = tools.find().execute().fetch_all()
for tool in tool_results:
    _id = tool["_id"]
    try:
        location = get_tool_location(_id)
        if location:
            r = tools.modify('_id = :param1').bind('param1', _id).set('location', location[0]).execute()
    except Exception as err:
        print(err)
        exit(1)
show_collection(tools)
# Add the location for each organizer
org_results = organizers.find().execute().fetch_all()
for org in org_results:
    _id = org["_id"]
    try:
        location = get_tool_location(_id)
        if location:
            r = organizers.modify('_id = :param1').bind('param1', _id).set('location', location[0]).execute()
    except Exception as err:
        print(err)
show_collection(organizers)
show_collection(locations)

Listing 7-18Build Location String and Update the Tools and Organizer Collections

注意,我们只是获取每个集合中的所有文档,并用新字符串更新文档。最后,我们打印集合中的文档(用于调试)。

现在我们已经看到了所有的步骤,我们可以执行代码了。由于这是一个很长的脚本,我们将使用 Python 来执行代码,但是您可以使用 shell 来一次单独执行一个步骤。事实上,如果您以前从未编写过这样的代码,这将是首选方法。

您可能认为这需要做大量的工作,但是在开发应用时,它会派上用场。尤其是当您要替换仍在使用的旧的关系数据库应用时。更具体地说,您可以在开发过程中多次运行这个脚本,以改进它和新的应用。更好的是,您可以在切换到新应用的过程中使用该脚本。

幸运的是,您可以在图书网站上名为convert_rdb.py的文件中找到完整的代码。清单 7-19 显示了运行脚本的摘录。

C:\Users\cbell\MySQL Shell\source\Ch07> mysqlsh --py -f convert_rdb.py
User Id: root
Password: ∗∗∗∗∗∗∗∗∗
COLLECTION: vendors
{
    "_id": "00005cae7415000000000000016e",
    "name": "Kobalt",
    "sources": "Lowes",
    "url": "https://www.kobalttools.com/"
}
{
    "_id": "00005cae7415000000000000016f",
    "name": "Craftsman",
    "sources": "Lowes, Ace",
    "url": "https://www.craftsman.com/"
}
{
    "_id": "00005cae74150000000000000170",
    "name": "Irwin",
    "sources": "Lowes",
    "url": "https://www.irwin.com/"
}
...
{
    "_id": "00005cae74150000000000000e41",
    "depth": 12,
    "description": "Top",
    "height": 24,
    "tool_ids": [
        "00005cae74150000000000000df5",
        "00005cae74150000000000000e0d"
    ],
    "type": "Shelf",
    "width": 96
}
{
    "_id": "00005cae74150000000000000e42",
    "depth": 48,
    "description": "Bottom",
    "height": 42,
    "tool_ids": [
        "00005cae74150000000000000e0a"
    ],
    "type": "Shelf",
    "width": 96
}

Listing 7-19Executing the Conversion Script

这将完全填充garage_v2模式和集合。但是,如果您想知道是否需要为每次转换或数据生成创建这样的脚本。答案是你可能不会。

将数据导入文档存储

MySQL Shell 中有一个漂亮的实用程序,可以帮助将 JSON 文档导入到您的集合中。shell 有一个名为 JSON import 的实用程序,它允许您将 JSON 文档直接导入到您的集合中。因此,如果您有 JSON 格式的数据,或者您可以编写一个脚本将其转换成 JSON 格式,那么您可以使用 JSON import 实用程序一次性导入文档。这有多酷?

例如,假设您有从文件或其他输入流中读取的数据,并生成了 JSON 文档。如果您将它们写到一个文件中(文档之间没有逗号),您可以使用该实用程序一次性导入所有文档。让我们看看如何使用前面文本中的供应商数据来实现这一点。

我们从一个文件开始,文件中的每个文档都用 JSON 字符串表示,如下所示。请注意,文档之间没有逗号。还要注意我们没有_id属性(但是如果您想自己生成文档 id,可以添加它)。

{
    "name": "Kobalt",
    "sources": "Lowes",
    "url": "https://www.kobalttools.com/"
}
{
    "name": "Craftsman",
    "sources": "Lowes, Ace",
    "url": "https://www.craftsman.com/"
}
{
    "name": "Irwin",
    "sources": "Lowes",
    "url": "https://www.irwin.com/"
}
...

要导入文档,可以使用 shell 连接到服务器,然后使用util内置类和import_json()方法指定要导入的文件的路径和一个包含模式和集合的选项字典。清单 7-20 演示了将一个带有 JSON 文档的文件导入到garage_v2模式中的vendors集合中。注意,对于导入大量(JSON)数据来说,import 是一个非常方便的实用工具。

> mysqlsh --py --uri root@localhost:33060
> options = {
          ->   'schema': 'garage_v2',
          ->   'collection': 'vendors',
          -> }
          ->
> util.import_json('vendors.json', options)
Importing from file "vendors.json" to collection `garage_v2`.`vendors` in MySQL Server at localhost:33060

.. 35
Processed 4.68 KB in 35 documents in 0.0161 sec (2.17K documents/s)
Total successfully imported documents 35 (2.17K documents/s)

Listing 7-20Running the JSON Import Utility in the Shell

如您所见,该实用程序将读取文档并将它们插入到您指定的模式和集合中。该实用程序还会在插入之前验证 JSON 文档;因此,如果有错误,您将看到它们被报告,导入将停止。

通过在命令行上指定导入参数和连接,该实用程序还允许在命令行模式下执行,如下文所示。还有一些其他选项可以使用,包括将数据导入到关系表中的 JSON 列,以及支持导入二进制 JSON (BSON)数据。很好!有关 JSON 导入实用程序的更多信息,请参见 https://dev.mysql.com/doc/mysql-shell/8.0/en/mysql-shell-utilities-json.html

> mysqlsh --py --uri root@localhost:33060 --schema=garage_v2 \
          --import vendors.json vendors
Please provide the password for 'root@localhost:33060': ∗∗∗∗
Importing from file "vendors.json" to collection `garage_v2`.`vendors` in MySQL Server at localhost:33060

.. 35
Processed 4.68 KB in 35 documents in 0.0097 sec (3.61K documents/s)
Total successfully imported documents 35 (3.61K documents/s)

好了,现在我们有了一个完整的文档存储,我们可以看看如何为模式类编写代码了。

示范

示例应用的执行与第五章中的示例应用相同。不同之处在于,我们将为 CRUD 操作使用一个基类,为每个集合使用一个子类来处理该集合特有的验证。我们可以这样做,因为我们将数据布局(属性集)从数据中提取出来并放入代码中。因此,CRUD 方法的基本操作对于所有集合都是相同的。我们将在本节的后面看到这是如何工作的。

更具体地说,我们将看到如何首先创建基类(GarageCollection)然后继续创建其他一些类的演示。正如你将看到的,它们遵循相同的设计模式/布局,所以一旦你看到一两个,其他的就很容易预测了。因此,我们将看到使用两个类的详细演练,为了简洁起见,其余的将被演示和呈现较少的细节。

如果您想跟进,请确保已经加载了示例模式,并且 MySQL Shell 已经准备就绪。您可能还想使用代码或文本编辑器来编写代码模块。更重要的是,您应该创建一个名为schema的文件夹,并从父文件夹启动 shell。

例如,您应该创建一个名为mygarage_v2的文件夹,并在该文件夹中创建schema文件夹。然后我们将执行来自mygarage_v2的 shell。为什么呢?因为会使用 Python 导入指令,并使用文件夹名命名代码模块的路径(如from schema import Locations)。我们还将创建单元测试,因此需要一个名为unittests的文件夹来存储测试文件。

让我们从MyGarage类开始。回想一下,这是一个实用程序类,它处理与服务器的连接并获取模式。

我的车库班

该类旨在通过提供登录(连接)到服务器的机制和封装一些常见操作(如获取会话、当前数据库、检查到 MySQL 的连接是否活动、断开连接等)来简化 MySQL 服务器的工作。我们还将包括将 SQL 结果或 select 结果转换为 Python 列表(数组)的方法,以便于处理。表 7-3 显示了我们将为这个类创建的方法的完整列表,包括所需的参数(一些方法不需要它们)。

表 7-3

我的车库类方法

|

方法

|

参数

|

描述

|
| --- | --- | --- |
| __init__() | mysqlx_sh(消歧义) | 构造函数-如果从 MySQL Shell 运行,请提供 mysqlx。 |
| connect() | 用户名,密码,主机,端口 | 连接到主机端口的 MySQL 服务器。 |
| get_session() |   | 返回会话供其他类使用。 |
| get_schema() |   | 返回在其他类中使用的模式。 |
| is_connected() |   | 检查是否连接到服务器。 |
| disconnect() |   | 断开与服务器的连接。 |
| get_locations() |   | 返回一个 Python 数组,其中包含可以放置工具或管理器的所有位置。 |
| build_storage_contents() | 工具 _ 位置 | 返回工具位置中工具的 Python 数组。 |
| vendor_in_use() | 供应商文档 id | 如果在任何集合中使用了供应商,则返回True。 |

编写源代码

这些方法中的大部分和我们在第五章中看到的一样。但是,后三种不一样。我们需要这些来管理选择放置工具的位置,获取特定工具位置中所有工具的列表,并实现对vendors集合的删除操作的引用完整性。也就是说,我们使用代码来确保没有人删除从另一个文档引用的供应商。

清单 7-21 显示了MyGarage类的代码。虽然这段代码看起来比版本 1 长(确实如此),但这是因为我们将位置字符串处理和供应商参照完整性移到了代码中。

注意

为简洁起见,本章源代码清单中的注释和非必要行已被删除。

class MyGarage(object):
    def __init__(self, mysqlx_sh=None):
        self.session = None
        if mysqlx_sh:
            self.mysqlx = mysqlx_sh
            self.using_shell = True
        else:
            self.mysqlx = mysqlx
            self.using_shell = False
        self.schema = None
    def connect(self, username, passwd, host, port):
        config = {
            'user': username,
            'password': passwd,
            'host': host,
            'port': port,
        }
        try:
            self.session = self.mysqlx.get_session(∗∗config)
        except Exception as err:
            print("CONNECTION ERROR:", err)
            self.session = None
            raise
        self.schema = self.session.get_schema('garage_v2')
    def get_session(self):
        return self.session
    def get_schema(self):
        return self.schema
    def is_connected(self):
        return self.session and (self.session.is_open())
    def disconnect(self):

        try:
            self.session.close()
        except Exception as err:
            print("WARNING: {0}".format(err))
    def get_locations(self, include_organizers=True):
        tool_locations = []
        if include_organizers:
            organizers = self.schema.get_collection('organizers').find().\
                fields("_id", "type", "description").execute().fetch_all()
            for organizer in organizers:
                list_item_str = "{0} - {1}"\
                  .format(organizer["type"], organizer["description"])
                tool_locations.append((list_item_str, list_item_str))
        storage_collections = ['toolchests', 'shelving_units',
                               'workbenches', 'cabinets']
        for storage_collection in storage_collections:
            collection = self.schema.get_collection(storage_collection)
            items = collection.find().fields("_id", "description",
                             "tool_locations").execute().fetch_all()
            for item in items:
                locations_found = item["tool_locations"]
                if locations_found:
                    for tool_loc_id in locations_found:
                        tool_location = self.schema\
                           .get_collection("locations").\
                            find('_id = :param1')\
                           .bind('param1', tool_loc_id).execute().fetch_all()
                        if tool_location:
                            list_item_str = "{0}, {1} - {2}"\
                                "".format(item["description"],
                                tool_location[0]["description"],
                                          tool_location[0]["type"])
                            tool_locations.append((list_item_str,
                                                   list_item_str))
        return tool_locations
    def build_storage_contents(self, tool_locations):
        storage_places = []
        tools = self.schema.get_collection('tools')
        organizers = self.schema.get_collection('organizers')
        locations = self.schema.get_collection('locations')
        if not tool_locations:
            return storage_places

        list_of_tools = []
        for loc_id in tool_locations:
            tool_location = locations.find("_id = :param1").\
                bind("param1", loc_id).execute().fetch_all()
            if not tool_location or tool_location == []:
                organizer = organizers.find("_id = :param1").\
                    bind("param1", loc_id).execute().fetch_all()
                if not organizer or organizer == []:
                    continue # This is an error!
                description = organizer[0]['description']
                loc_type = organizer[0]['type']
                list_of_tools.append(('organizers', loc_type,
                                     description, 'organizer', ' '))
                continue
            else:
                try:
                    tool_id_list = tool_location[0]['tool_ids']
                except KeyError:
                    tool_id_list = []
                description = tool_location[0]['description']
                loc_type = tool_location[0]['type']
            tool_list_str = '_id in [{0}]'.format(
                ', '.join(['"{0}"'.format(t_id) for t_id in tool_id_list]))
            found_tools = tools.find(tool_list_str).execute().fetch_all()
            for tool in found_tools:
                size = dict(tool).get('size', ' ')
                list_of_tools.append(('tools', tool['type'],
                                      tool['description'],
                                      tool['category'], size))
            storage_places.append((loc_type, description, list_of_tools))
            list_of_tools = []
        return storage_places
    def vendor_in_use(self, vendor_id):
        collections = ['cabinets', 'shelving_units', 'toolchests',
                       'tools', 'workbenches']
        for collection_name in collections:
            collection = self.schema.get_collection(collection_name)
            res = collection.find('vendorid = :param1').\
                bind('param1', vendor_id).execute().fetch_all()
            if res:
                return True
        return False

Listing 7-21MyGarage Class Code

这个代码模块garage_v2还包含一个名为make_list()的助手函数,我们可以用它从一个读操作中生成一个 Python 数组列表。清单 7-22 显示了这个函数的代码。花点时间通读它,你会发现它是转换结果的简单代码。我们可以在集合类中使用这个方法来帮助处理来自模式的结果。

def make_list(results, key_list):
    """Build list of Python arrays from results

    Return a Python array for the list of documents returned from a read
    operation.
    """
    result_list = []
    for result in results:
        item_values = []
        for key in key_list:
            try:
                item_values.append(result[key])
            except KeyError:
                # If key not found, create a placeholder
                item_values.append(“)
        result_list.append(item_values)
    return result_list

Listing 7-22
Helper Function

测试类

在我们开始测试这个类之前,我们必须设置 Python path 变量(PYTHONPATH)来包含我们想要运行测试的文件夹。这是因为我们使用的模块没有安装在系统级,而是位于与我们测试的代码相关的文件夹中。在 Windows 中,可以使用以下命令将执行路径添加到 Python 路径中。

C:\Users\cbell\Documents\my_garage_v1> set PYTHONPATH=%PYTHONPATH%;c:\users\cbell\Documents\mygarage_v1

小费

如果路径中有空格,请确保在路径两边使用引号。

或者,在 Linux 和 macOS 上,您可以使用这个命令来设置 Python 路径。

export PYTHONPATH=$(pwd);$PYTHONPATH

现在我们可以运行 shell 了。为此,我们将使用--py选项在 Python 模式下启动。让我们在课堂上练习一些方法。我们可以尝试所有的方法,除了make_rows()方法。我们稍后会看到这些。清单 7-23 展示了如何在 shell 中导入类,初始化(创建)一个名为mygarage的类实例,然后连接connect()并执行一些方法。最后,我们调用disconnect()来关闭与服务器的连接。

C:\Users\cbell\Documents\mygarage_v2>mysqlsh --py
> from schema.garage_v2 import MyGarage
Running from MySQL Shell. Provide mysqlx in constructor.
> myg = MyGarage(mysqlx)
> myg.connect('root', 'root', 'localhost', 33060)
> schema = myg.get_schema()
> s = myg.get_session()
> myg.is_connected()
true
> myg.disconnect()
> myg.is_connected()
false

Listing 7-23Testing MyGarage using MySQL Shell

接下来,我们将为这个类创建一个单元测试,方法与我们在第五章中所做的相似。事实上,我们将在unittests文件夹中创建一个名为garage_v2_test.py的测试,它将使用与第五章中几乎相同的代码,只改变导入语句中出现的database到 s chemav1v2。因此,我们在清单 7-24 中给出了代码,没有进一步的解释。

from __future__ import print_function

from getpass import getpass
from schema.garage_v2 import MyGarage

print("MyGarage Class Unit test")
mygarage = MyGarage(mysqlx)
user = raw_input("User: ")
passwd = getpass("Password: ")
print("Connecting...")
mygarage.connect(user, passwd, 'localhost', 33060)
print("Getting the schema...")
schema = mygarage.get_schema()
print(schema)
print("Getting the session...")
session = mygarage.get_session()
print(session)
print("Connected?")
print(mygarage.is_connected())
print("Disconnecting...")
mygarage.disconnect()
print("Connected?")
print(mygarage.is_connected())

Listing 7-24
garage_v2_test.py

执行单元测试也像我们在第五章中所做的那样。清单 7-25 展示了这个从 shell 运行的测试。

> mysqlsh --py -f unittests\garage_v2_test.py
Running from MySQL Shell. Provide mysqlx in constructor.
MyGarage Class Unit test
User: root
Password:
Connecting...
Getting the schema...
<Schema:garage_v2>
Getting the session...
<Session:root@localhost:33060>
Connected?
True
Disconnecting...
Connected?
False

Listing 7-25Running the garage_v1_test Unit Test

现在,让我们看看构成集合类基础的基类。

集合基类

如前所述,我们将创建一个包含集合的所有 CRUD 操作的基类。我们可以对所有集合使用相同方法的原因是,操作的格式、布局或简单的字段列表是由 JSON 文档本身控制的。因此,通过简单地使用集合类的方法,我们可以通过使用一个基类来简化我们的开发,这个基类完成所有的 CRUD 操作,并使用子类来处理数据上的任何约束。回想一下,我们将强加的约束与必填字段有关,在某些情况下与引用完整性有关。

基类中的方法应该是熟悉的,因为它们与我们在第五章中使用的方法相同,只是为了方便和验证增加了一些。表 7-4 显示了基类中的每个方法,包括每个方法的简短描述。

表 7-4

GarageCollection 基类的方法

|

方法

|

参数

|

描述

|
| --- | --- | --- |
| __init__() | 架构,集合名称 | 构造器 |
| check_create_prerequisites() | JSON 文档 | 创建前检查数据 |
| check_upate_prerequisites() | JSON 文档 | 更新前检查数据 |
| create() | JSON 文档 | 执行创建操作 |
| read() | 文档 Id | 执行读取操作 |
| update() | JSON 文档 | 执行更新操作 |
| delete() | 文档 Id | 执行删除操作 |
| get_last_docid() |   | 返回最后生成的文档 id。 |
| get_tool_locations() | 文档 Id | 返回工具位置列表。 |

注意,我们有预期的 CRUD 操作方法,但是我们也看到了检查创建和更新操作的先决条件的方法。默认情况下,这些方法被设置为返回 True,期望需要这些方法的子类将根据特定于集合的要求填充它们。

还要注意,我们有助手函数来检索生成的最后一个文档 id,这有助于创建操作和获取工具位置列表的方法。最后一个方法不是绝对必要的,因为您可以使用路径表达式来访问数组,但是它使类更加整洁,并且在代码中更容易使用(和阅读)。

编写源代码

基类的代码使用构造函数中传递的集合名称实现前面文本中的方法,以从架构中获取集合的实例。这允许我们对每个 CRUD 操作使用相同的 X DevAPI 调用,不管我们使用哪个集合。事实上,由于我们将 JSON 文档的结构移到了用户界面代码中,所以我们甚至不需要直接使用它,除非我们希望验证必填字段或引用完整性。

因此,我们将创建前面文本中描述的先决条件函数,默认情况下返回 True,这样,如果子类(集合)不需要它们,如果先决条件函数没有在子类中被覆盖,代码将不会停止。

例如,如果我们不需要在更新操作中进行验证,我们就不需要在子类定义中包含该函数,这意味着当从删除或更新操作中调用它时,它仍然可以工作。

此外,由于我们使用了基类,子类继承了基类的方法,这再次意味着我们只需要为 CRUD 操作编写一次代码。让我们看看代码,你可以看到这是如何工作的。清单 7-26 显示了新基类的代码。

class GarageCollection(object):
    def __init__(self, mygarage, collection_name):
        self.mygarage = mygarage
        self.schema = mygarage.get_schema()
        self.collection_name = collection_name
        self.col = self.schema.get_collection(collection_name)
        self.docid = None
    def check_create_prerequisites(self, doc_data):
        return True
    def check_update_prerequisites(self, doc_data):
        return True
    def create(self, doc_data):
        if not self.check_create_prerequisites(doc_data):
            return (False, "Required fields missing.")
        try:
            json_str = {}
            for key in doc_data.keys():
                json_str.update({key: doc_data[key]})
            self.docid = self.col.add(json_str).\
                         execute().get_generated_ids()[0]
        except Exception as err:
            print("ERROR: Cannot add {0}: {1}"
                  "".format(err, self.collection_name))
            return (False, err)
        return (True, None)
    def read(self, _id=None):
        if not _id:
            res = self.col.find().execute().fetch_all()
        else:
            res = self.col.find('_id = :param1').\
                  bind('param1', _id).execute().fetch_all()
        return res

    def update(self, doc_data):
        _id = doc_data.get("_id", None)
        assert _id, "You must supply an Id to update the {0}."\
            "".format(self.collection_name.rstrip('s'))
        if not self.check_update_prerequisites(doc_data):
            return (False, "Required fields missing.")
        try:
            for key in doc_data.keys():
                # Skip the _id key
                if key != '_id':
                    self.col.modify('_id = :param1') \
                        .bind('param1', _id) \
                        .set(key, doc_data[key]).execute()
        except Exception as err:
            print("ERROR: Cannot update {0}: {1}".format(
                self.collection_name.rstrip('s'), err))
            return (False, err)
        return (True, None)
    def delete(self, _id=None):
        assert _id, "You must supply an Id to delete the {0}."\
            "".format(self.collection_name.rstrip('s'))
        try:
            self.col.remove('_id = :param1').bind('param1', _id).execute()
        except Exception as err:
            print("ERROR: Cannot delete {0}: {1}"
                  "".format(self.collection_name.rstrip('s'), err))
            return (False, err)
        return (True, None)
    def get_last_docid(self):
        docid = self.docid
        self.docid = None # Clear it after it was read
        return docid
    def get_tool_locations(self, _id=None):
        assert _id, "You must supply an Id to get the tool locations."
        results = []
        if _id:
            places = self.col.find('_id = :param1').bind('param1', _id).\
                fields("tool_locations").execute().fetch_all()
            try:
                tool_locations = places[0]["tool_locations"]
                if tool_locations:
                    locations = self.mygarage.get_schema().\
                               get_collection("locations")
                    tool_ids = ', '.join(['"{0}"'
                     “.format(tool_id) for tool_id in tool_locations])
                    tool_loc_str = '_id in [{0}]'.format(tool_ids)
                    results = locations.find(tool_loc_str).\
                              execute().fetch_all()
            except KeyError:
                results = []
        return results

Listing 7-26The GarageCollection Base Class Code

让我们看看其中的一个子类。在这个例子中,我们将看到Vendors类,它在garage_v2模式中对 vendors 集合进行建模。我们将这段代码放在名为vendors.pyschema文件夹中的代码文件中。下面显示了Vendors类的代码。注意,它比我们在第五章的示例应用版本 1 中使用的Vendor类代码要少得多。

from schema.garage_collection import GarageCollection
class Vendors(GarageCollection):
    def __init__(self, mygarage):
        """Constructor - set collection name"""
        GarageCollection.__init__(self, mygarage, 'vendors')
    def check_create_prerequisites(self, doc_data):
        """Check prerequisites for the create operation."""
        vendor_name = doc_data.get("name", None)
        assert vendor_name, "You must supply a name for the vendor."
        return True

集合的其余类同样简短,只包含适用于集合的验证代码方法。清单 7-27 显示了集合代码模块的组成(每个模块保存在一个单独的代码模块中),为了简洁起见,去掉了注释。花一些时间来看看使用基类如何通过允许您将特定于集合的代码放在子类中而使编写集合类变得更容易。每个集合的部分以粗体突出显示。为了简洁起见,注释和多余的行再次被删除。

注意

我们将重用第五章中的技术,用单元测试来测试类模块,而不是带你测试每个类模块。

# Cabinets collection - cabinets.py

class Cabinets(GarageCollection):
...
def check_create_prerequisites(self, doc_data):
        vendor_id = doc_data.get("vendorid", None)
        description = doc_data.get("description", None)
        location = doc_data.get("location", None)
        numdoors = doc_data.get("numdoors", None)
        assert vendor_id, "You must supply a vendor id for the cabinet."
        assert description, "You must supply a description for the cabinet."
        assert numdoors, "You must supply the number of doors "\
                         "for the cabinet."
        assert location, "You must supply a location for the cabinet."
        return True

# Locations collection - locations.py

class Locations(GarageCollection):
...
    def check_create_prerequisites(self, doc_data):
        loc_type = doc_data.get("type", None)
        description = doc_data.get("description", None)
        assert loc_type, "You must supply a type for the location."

        assert description, "You must supply a description for the location."
        return True
    def remove_tool(self, tool_id):
        location = self.col.find(':param1 in $.tool_ids').\
            bind('param1', tool_id).execute().fetch_all()
        if location:
            tool_locations = location[0]['tool_ids']
            tool_locations.remove(tool_id)

# Organizers collection - organisers.py

ORGANIZER_TYPES = [
    ('Bag', 'Bag'), ('Basket', 'Basket'), ('Bin', 'Bin'),
    ('Box', 'Box'), ('Case', 'Case'), ('Crate', 'Crate')
]
class Organizers(GarageCollection):
...
    def check_create_prerequisites(self, doc_data):
        description = doc_data.get("description", None)
        org_type = doc_data.get("type", None)
        assert description, "You must supply a description for "\
                            "the organizer."
        assert org_type, "You must supply type for the organizer."
        return True
    def remove_tool(self, tool_id):
        location = self.col.find(':param1 in $.tool_ids').\
            bind('param1', tool_id).execute().fetch_all()
        if location:
            tool_locations = location[0]['tool_ids']
            tool_locations.remove(tool_id)

# Shelving Units collection - shelving_units.py

class ShelvingUnits(GarageCollection):
...
    def check_create_prerequisites(self, doc_data):
        vendor_id = doc_data.get("vendorid", None)
        description = doc_data.get("description", None)
        location = doc_data.get("location", None)
        assert vendor_id, "You must supply a vendor id for "\
                          "the shelving_unit."
        assert description, "You must supply a description for "\
                            "the shelving_unit."
        assert location, "You must supply a location for the shelving_unit."
        return True

# Toolchests collection - toolchests.py

class Toolchests(GarageCollection):
...
    def check_create_prerequisites(self, doc_data):
        vendor_id = doc_data.get("vendorid", None)
        description = doc_data.get("description", None)
        location = doc_data.get("location", None)
        assert vendor_id, "You must supply a vendor id for the toolchest."
        assert description, "You must supply a description for "\
                            "the toolchest."
        assert location, "You must supply a location for the toolchest."
        return True

# Tools collection - tools.py

TOOL_TYPES = [
    ('Adjustable Wrench', 'Adjustable Wrench'), ('Awl',  'Awl'),
    ('Clamp', 'Clamp'), ('Crowbar', 'Crowbar'), ('Drill Bit', 'Drill Bit'),
    ('File', 'File'), ('Hammer', 'Hammer'), ('Knife', 'Knife'),
    ('Level', 'Level'), ('Nutdriver', 'Nutdriver'), ('Pliers', 'Pliers'),
    ('Prybar', 'Prybar'), ('Router Bit', 'Router Bit'), ('Ruler', 'Ruler'),
    ('Saw', 'Saw'), ('Screwdriver', 'Screwdriver'), ('Socket', 'Socket'),
    ('Socket Wrench', 'Socket Wrench'), ('Wrench', 'Wrench'),
    ('Corded', 'Corded'), ('Cordless', 'Cordless'), ('Air', 'Air')
]

class Tools(GarageCollection):
...
    def check_create_prerequisites(self, doc_data):
        vendor_id = doc_data.get("vendorid", None)
        description = doc_data.get("description", None)
        tool_type = doc_data.get("type", None)
        category = doc_data.get("category", None)
        assert vendor_id, "You must supply a vendor id for the tool."
        assert description, "You must supply a description for the tool."
        assert category, "You must supply the category of tool "\
                         "(handtool or powertool) for the tool."
        assert tool_type, "You must supply category for the tool."
        return True

# Workbenches collection - workbenches.py

class Workbenches(GarageCollection):
...
    def check_create_prerequisites(self, doc_data):
        vendor_id = doc_data.get("vendorid", None)
        description = doc_data.get("description", None)
        location = doc_data.get("location", None)
        assert vendor_id, "You must supply a vendor id for the workbench."
        assert description, "You must supply a description for "\
                            "the workbench."
        assert location, "You must supply a location for the workbench."
        return True

Listing 7-27Collection Classes for MyGarage V2

请注意,每个集合都有定制,主要是针对从基类派生的验证方法。但是有些类还添加了额外的方法来允许特定于集合的选项。

例如,我们在 Locations 和 Organizers 类中看到了方法remove_tool()。此方法允许我们通过文档 id 从位置或组织者中删除工具。通过这种方式,我们可以确保在从集合中删除工具时将其删除。

还要注意,我们为集合中的属性添加了数组,这些属性具有一组已知的值(版本 1 中关系表中的枚举值)。在这种情况下,它们出现在organizerstools集合中。回想一下,我们提到过这些是用代码处理的。在本章的示例应用中,我们使用 Python 数组在用户界面的下拉列表中使用。因此,我们使用代码来建立一组有效值。图 7-2 就是这样一个例子。

img/478423_1_En_7_Fig2_HTML.jpg

图 7-2

工具类型的下拉列表

现在,让我们回顾一下在编写应用的其余部分之前如何测试类模块。

测试类模块

我们也将使用第五章中相同的单元测试机制。为了简洁,我们将只检查其中一个单元测试代码模块来提醒我们代码。然后我们使用我们在第五章中使用的相同的run_all.py代码模块机制来执行单元测试。

回想一下,我们在unittests/crud_test.py代码模块中创建了一个名为CRUDTest的基类,它实现了启动(或设置)测试的相同方法,一个显示返回行的通用方法,以及一个我们想要运行的测试用例。然后我们用一个类创建了一个代码模块来测试其中一个集合类(或者我们在第五章中称之为表类)。

例如,我们通过创建一个名为VendorTests的新类来为Vendors类创建一个测试,这个新类是从CRUDTest派生的,并存储在文件unittests/vendor_test.py中。清单 7-28 显示了新类的代码。正如你将看到的,它与我们在第五章中写的测试非常相似,这也展示了为 SQL 和 NoSQL 接口开发代码是多么容易——代码非常相似。

from __future__ import print_function

from unittests.crud_test import CRUDTest
from schema.vendors import Vendors

class VendorTests(CRUDTest):
    """Test cases for the Vendors class"""

    def __init__(self):
        """Constructor"""
        CRUDTest.__init__(self)
        self.vendors = None
        self.last_id = None
        self.vendors = None

    def set_up(self, mysql_x, user=None, passwd=None):
        """Setup the test cases"""
        self.mygarage = self.begin(mysql_x, "Vendors", user, passwd)
        self.vendors = Vendors(self.mygarage)

    def create(self):
        """Run Create test case"""
        print("\nCRUD: Create test case")
        vendor_data = {
            "name": "ACME Bolt Company",
            "url": "www.acme.org",
            "sources": "looney toons"
        }
        self.vendors.create(vendor_data)
        self.last_id = self.vendors.get_last_docid()
        print("\tLast insert id = {0}".format(self.last_id))

    def read_all(self):
        """Run Read(all) test case"""
        print("\nCRUD: Read (all) test case")
        docs = self.vendors.read()
        self.show_docs(docs, 5)

    def read_one(self):
        """Run Read(record) test case"""
        print("\nCRUD: Read (doc) test case")
        docs = self.vendors.read(self.last_id)
        self.show_docs(docs, 1)

    def update(self):
        """Run Update test case"""
        print("\nCRUD: Update test case")
        vendor_data = {
            "_id": self.last_id,
            "name": "ACME Nut Company",
            "url": "www.weesayso.co",
        }
        self.vendors.update(vendor_data)

    def delete(self):
        """Run Delete test case"""
        print("\nCRUD: Delete test case")
        self.vendors.delete(self.last_id)
        docs = self.vendors.read(self.last_id)
        if not docs:
            print("\tNot found (deleted).")

Listing 7-28Code for the VendorTests Class

这项技术的强大之处在于,我们可以继续为每个以类命名的数据库类创建新的测试,并将它们存储在同一个unittests文件夹中。由于测试使用了与第五章中相同的类签名(相同的方法),我们可以重用它们,用schema代替database,并在导入语句中使用复数形式的集合名称,然后根据需要对数据进行微小的修改,以匹配集合模块的新属性集。

一旦编写了所有的集合类测试代码模块,我们就可以编写一个驱动程序脚本,在一个循环中运行所有的测试。回想一下,驱动程序脚本名为run_all.py,也存储在unittests文件夹中。清单 7-29 显示了这个模块的代码。

from __future__ import print_function

from getpass import getpass
from unittests.cabinet_test import CabinetTests
from unittests.location_test import LocationTests
from unittests.organizer_test import OrganizerTests
from unittests.shelving_unit_test import ShelvingUnitTests
from unittests.toolchest_test import ToolchestTests
from unittests.tool_test import ToolTests
from unittests.vendor_test import VendorTests
from unittests.workbench_test import WorkbenchTests

print("CRUD Tests for all classes...")
crud_tests = []
cabinets = CabinetTests()
crud_tests.append(cabinets)
locations = LocationTests()
crud_tests.append(locations)
shelving_units = ShelvingUnitTests()
crud_tests.append(shelving_units)
toolchests = ToolchestTests()
crud_tests.append(toolchests)
tools = ToolTests()
crud_tests.append(tools)
organizers = OrganizerTests()
crud_tests.append(organizers)
vendors = VendorTests()
crud_tests.append(vendors)
workbenches = WorkbenchTests()
crud_tests.append(workbenches)
user = raw_input("User: ")
passwd = getpass("Password: ")
for test in crud_tests:
    test.set_up(mysqlx, user, passwd)
    test.create()
    test.read_one()
    test.read_all()
    test.update()
    test.read_one()
    test.delete()
    test.tear_down()

Listing 7-29
Test Driver run_all.py

要执行这个测试,您可以使用清单 7-30 中所示的命令以及预期的输出。这里,为了简洁起见,我们只看到输出的一部分。

C:\Users\cbell\Documents\mygarage_v2>mysqlsh --py -f unittests\run_all.py
Running from MySQL Shell. Provide mysqlx in constructor.
CRUD Tests for all classes...
User: root
Password: ∗∗∗∗∗∗∗∗∗∗∗∗

∗∗∗ Cabinets Class Unit test ∗∗∗

Connecting...

CRUD: Create test case
        Last insert id = 00005cc4bca00000000000000001

CRUD: Read (doc) test case

First 1 docs:
--------------------------
{
    "_id": "00005cc4bca00000000000000001",
    "depth": 11,
    "description": "Large freestanding cabinet",
    "height": 11,
    "location": "Read wall next to compressor",
    "numdoors": 2,
    "shelves": [
        {
            "depth": 20,
            "description": "Middle",
            "height": 18,
            "width": 48
        }
    ],
    "vendorid": "00005cae74150000000000000cd6",
    "width": 11
}
...
CRUD: Update test case

CRUD: Read (doc) test case

First 1 docs:
--------------------------
{

    "_id": "00005cc4bca00000000000000001",
    "depth": 11,
    "description": "Cold Storage",
    "height": 11,
    "location": "3rd floor basement",
    "numdoors": 2,
    "shelves": [
        {
            "depth": 20,
            "description": "Top",
            "height": 18,
            "width": 48
        },
        {
            "depth": 20,
            "description": "Bottom",
            "height": 18,
            "tool_ids": [
                "00005cafa3eb00000000000007c5",
                "00005cafa3eb00000000000007c6",
                "00005cafa3eb00000000000007c7"
            ],
            "width": 48
        }
    ],
    "vendorid": "00005cae74150000000000000cd6",
    "width": 11
}

CRUD: Delete test case
        Not found (deleted).

Disconnecting...
...

Listing 7-30Executing the test driver

花些时间从书籍网站下载代码,并自己测试单元测试。我们鼓励您下载这个示例应用的源代码,并自己进行测试。深入研究代码,看看它是如何工作的。使用这个示例应用作为指南,您可能会惊讶于创建自己的文档存储应用是多么容易。

您应该注意到使用这个概念非常容易,并且您可以开发其他概念,比如帮助测试您的数据库代码。只是觉得;我们无需编写任何用户界面代码就可以做到这一点,这允许在编写第一行用户界面代码之前验证我们的数据库代码。很好!

摘要

为文档存储(NoSQL)创建和编写应用可能看起来不如传统的关系数据库应用直观,但是现在您已经看到了将数据控制直接放入代码中的强大功能,您可以看到文档存储应用更容易编写。事实上,它们使用类似的 CRUD 操作,正如我们在第五章的关系数据库例子中看到的。

在本章中,我们看到了如何使用 shell 来开发一个 NoSQL 应用。我们发现了如何编写 CRUD 操作来处理文档存储,以及如何编写模块化模式类来管理操作。我们还看到了如何编写测试来测试模式类。这演示了 shell 在使用 X DevAPI 开发 Python 应用时的效用。

在下一章中,我们将看看 MySQL 中的另一个主要特性,您可以使用 shell 来管理它——MySQL 组复制。正如您将看到的,组复制是 MySQL 高可用性的一大进步。

八、将 Shell 用于组复制

MySQL 内置的最先进的特性之一是它能够将两台或多台服务器连接在一起,其中所有服务器都维护数据的副本(复制品)以实现冗余。管理基础设施的数据库管理员和系统架构师了解在尽量减少维护工作量的同时构建冗余的必要性。用于实现这一点的工具之一是一类使服务器或服务尽可能可用的特性。我们称之为高可用性(HA)。

高可用性不仅是建立健壮、随时可用的基础设施的关键因素,也是健壮的企业级数据库系统的一个品质。Oracle 一直在开发和改进 MySQL 中的高可用性特性。事实上,它们已经成熟,包括详细的管理和配置、状态报告,甚至主服务器的自动故障转移,以确保即使主服务器出现故障,您的数据仍然可用。最棒的是,Oracle 在 MySQL 的社区版中包含了这些特性,因此全世界都可以使用 xthem。

在这一章中,我们将大致了解如何在 MySQL 中设置、配置和维护高可用性。由于高可用性是一个很大的主题,需要考虑很多方面,我们将首先简要介绍一下高可用性——它是什么以及它是如何工作的,然后是 MySQL 的高可用性功能——我们将了解基本复制是如何工作的。然后,在下一章中,wxe 将看到如何在您自己的系统上用 MySQL Shell 设置和配置 MySQL 组复制的演示。

小费

本章通过学习经典复制为您开始使用 MySQL 组复制做准备。如果您熟悉 MySQL 复制,知道如何使用二进制日志文件设置经典复制,并且知道什么是全局事务标识符(GTIDs ),您可能希望浏览本章。

概观

MySQL 高可用性是建立在 MySQL 复制的长期稳定性之上的组件集合。这些组件包括对服务器的修改以及新的特性和组件,如全局事务标识符、对核心复制特性集的大量改进,以及 MySQL 组复制的添加,因此也是组复制。这些组件共同构成了 MySQL 高可用性的新范例。

正如您将看到的,在 MySQL 中使用高可用性有相当多的功能和几种选择。让我们从一个关于高可用性的简短教程开始。

什么是高可用性?

如果您认为高可用性大致等同于可靠性,那么高可用性就最容易理解了——使解决方案尽可能易于访问,并在约定的时间内容忍计划内或计划外的故障。也就是说,这是用户对系统可操作性的期望值。系统越可靠,运行时间越长,就相当于可用性水平越高。

高可用性可以通过多种方式实现,从而产生不同级别的可用性。这些级别可以表示为达到某种更高可靠性状态的目标。本质上,您使用技术和工具来提高可靠性,并使解决方案尽可能长时间地保持运行和数据可用(也称为正常运行时间)。正常运行时间表示为解决方案运行时间的比率或百分比。

您可以通过实践以下工程原则来实现高可用性:

  • 消除单点故障:设计您的解决方案时,尽可能减少组件数量,以免在组件出现故障时导致解决方案无法使用。

  • 通过冗余增加恢复能力:设计您的解决方案,允许多个活动冗余机制,以便从故障中快速恢复。

  • 实施容错:设计您的解决方案,通过切换到冗余或替代机制,主动检测故障并自动恢复。

这些原则是实现更高级别的可靠性和高可用性的基础或步骤。即使您不需要实现最大的高可用性(解决方案几乎随时可用),通过实现这些原则,您至少会使您的解决方案更加可靠,这是一个很好的目标。

既然您已经理解了高可用性(HA)可以解决的目标或需求,现在让我们讨论一些在 MySQL 解决方案中实现 HA 的选项。实现高可用性目标有四个选项。通过实现所有这些,您将获得一定程度的高可用性。您的成就不仅取决于您如何实现这些选项,还取决于您满足可靠性目标的程度。

  • 恢复:最容易实现的可靠性是从故障中恢复的能力。这可能是组件、应用服务器、数据库服务器或解决方案的任何其他部分的故障。因此,恢复就是如何以尽可能少的时间和成本让解决方案恢复运行。

    • 逻辑备份:逻辑备份通过遍历数据,逐行复制数据,并通常将数据从二进制形式转换为 SQL 语句来复制数据。逻辑备份的优势在于数据是可读的,甚至可以在恢复数据之前对其进行修改或更正。不利的一面是,对于较大的数据量,逻辑备份往往较慢,并且可能比实际数据占用更多的存储空间(取决于数据类型、索引数量等)。

    • 物理备份:物理备份对磁盘存储层的数据进行二进制复制。备份通常是特定于应用的;您必须使用制作备份的同一应用来恢复它。优点是备份速度更快,大小更小。此外,执行物理备份的应用具有一些高级功能,如增量备份(仅备份自上次备份以来发生变化的数据)和其他高级功能。对于小型解决方案,逻辑备份可能已经足够,但是随着您的解决方案(您的数据)的增长,您将需要使用物理备份解决方案。

  • 冗余:可靠性的一个更具挑战性的实现是冗余——让两个或更多的组件在系统中扮演相同的角色。冗余的目标可能只是在需要替换主要组件的情况下准备一个组件。这可能是一个热备用,其中组件主动与主组件并行工作,当检测到故障时,系统会自动切换到冗余组件。冗余最常见的目标是数据库服务器。

  • 扩展:另一个可靠性实现与性能有关。在这种情况下,您希望最大限度地减少存储和检索数据的时间。您可以通过设计解决方案将数据写入(保存)到主设备(主设备)并从从设备(辅助设备)读取数据来实现这一点。随着应用的增长,您可以添加额外的从机来帮助最小化读取数据的时间。通过拆分写和读,可以减轻主机执行许多语句的负担。考虑到大多数应用的读操作比写操作多得多,使用不同的服务器(或几个服务器)来提供读操作的数据并将写操作留给一个主服务器是有意义的。

  • 容错:可靠性的最后一个实现,也是区分大多数高可用性解决方案正常运行时间的关键是容错,即检测故障并从事件中恢复的能力。容错是通过利用恢复和冗余以及添加检测机制和主动切换来实现的。也就是说,当一个组件出现故障时,另一个组件会取代它的位置。容错的最佳形式是实现自动切换(故障转移),即使单个组件出现故障,应用也能继续运行。

注意

横向扩展有两种形式:读取和写入。您可以使用冗余读取器(如 MySQL 复制中的多个从属读取器)实现读取扩展,但是实现写入扩展需要一个可以在两台或更多服务器上协商和处理更新的解决方案。幸运的是,我们有 MySQL 组复制。我们将在后面的章节中看到更多关于这个特性的内容。

MySQL 高可用性特性

MySQL 有几个高可用性特性,可以满足这些技术及其目标中更具挑战性的部分。下面总结了可以用于上述每种技术的功能。

  • 恢复 : MySQL 复制提供了一种通过副本来提供恢复的机制。如果主服务器关闭,可以提升一个从服务器来承担其角色。

    • 逻辑备份:客户端实用程序mysqlpump(或者更老的mysqldump)可以用来对你的数据进行逻辑备份,以便以后恢复。请注意,这些工具面临着逻辑备份的所有挑战。具体来说,它们将数据库转储到一个由 SQL CREATEINSERT语句构成的文件中,因此它们可能不是大型数据集的最佳选择。

    • 物理备份:MySQL 企业版提供了一个名为 MySQL 企业备份的工具,可以用来为你的数据创建物理备份,并进行恢复。

  • 冗余:通过使用多个从服务器,MySQL 复制可以通过某些技巧和窍门实现一定的冗余,例如使用一个与主服务器硬件相同的专用从服务器(称为热备用),或者使用多个从服务器来确保您始终有足够的数据读取权限。

  • 扩展:同样,MySQL 复制可以为您的应用提供读取扩展。使用 MySQL 组复制实现写入横向扩展。

  • 容错:可以使用 MySQL 复制来实现切换。也就是说,当主服务器关闭时,我们使用 MySQL 中的复制命令将主服务器的角色切换到一个从服务器。使用 MySQL 时,有两种类型的主角色变化:切换,即当主服务器仍在运行时,将主服务器的角色切换到从服务器;故障转移,即当主服务器不再运行时,选择从服务器来承担主服务器的角色。也就是说,切换是有意的,而故障转移是被动的。但是,您可以使用允许自动故障转移的组复制。

注意

还有 MySQL 路由器,它是 MySQL 的连接路由器,允许您设置路由器使用的一组特定服务器,以便路由器在当前服务器离线(变得不可访问)时自动切换到另一台服务器。幸运的是,组复制和路由器都是 InnoDB 集群的一部分,我们将在第十章中探讨。

现在我们知道了什么是高可用性,以及实现高可用性的一些技术和目标,让我们深入了解基本的高可用性特性——MySQL 复制。

什么是 MySQL 复制?

MySQL 复制是一个易于使用的特性,也是 MySQL 服务器的一个复杂和主要的组件。本节提供了复制的鸟瞰图,目的是解释其功能。我们将在下一节看到一个设置 MySQL 复制的例子。

MySQL 复制需要两台或更多服务器。必须将一台服务器指定为主服务器或主服务器。主角色意味着对数据的所有数据更改(写入)都发送到主服务器,并且只发送到主服务器。拓扑中的所有其他服务器维护主数据的副本,并且根据设计和要求,它们是只读(RO)服务器,称为辅助服务器或从属服务器。因此,当您的应用发送数据进行存储时,它们会将数据发送到主服务器。您编写的使用传感器数据的应用可以从从属服务器读取这些数据。

复制机制使用一种称为二进制日志的技术工作,该技术以特殊的格式存储更改,从而保留所有更改的记录。这些更改随后被发送到从机,在那里更改被存储在类似的日志文件(称为中继日志)中,然后在从机上读取并执行。因此,一旦从机执行了更改(称为事件),它就拥有了数据的精确副本。

在最低级别,主服务器和从服务器之间的二进制日志交换支持三种格式:基于语句的复制(SBR),它复制整个 SQL 语句;基于行的复制(RBR),仅复制已更改的行;对于某些场景,混合式复制(MBR)是 RBR 与使用 SQL 语句记录的一些事件的混合。

正如您所想象的,从主服务器上发生更改到从服务器上发生更改之间会有很小的延迟。幸运的是,除了在高流量(大量变化)的拓扑中,这种延迟几乎是不明显的。出于您的目的,当您从从属服务器读取数据时,它可能是最新的。您可以使用命令SHOW SLAVE STATUS检查从设备的进度;在许多其他事情中,它向你展示了奴隶已经落后于主人有多远。您将在后面的小节中看到这个命令的运行。

MySQL 复制支持两种复制方法。最初的方法(有时称为经典复制、二进制日志文件和位置复制,或简称为日志文件和位置复制)涉及使用二进制日志文件名和位置来跟踪和执行事件,或应用更改以在主设备和从设备之间同步数据。较新的事务性方法使用全局事务标识符(GTIDs ),因此不需要处理日志文件或位置(但它仍然使用相同的二进制日志和中继日志文件),这大大简化了许多常见的复制任务。最重要的是,使用 gtid 的复制保证了主设备和从设备之间的一致性,因为当从设备连接到主设备时,新协议允许从设备请求丢失的 gtid。因此,每个从设备可以与主设备协商接收丢失的事件。

什么是 GTIDs?

GTIDs 使服务器能够为每个事件集或组分配一个唯一的标识符,从而可以知道每个从服务器上应用了哪些事件。要使用 GTIDs 执行故障转移,可以选择最好的从设备(丢失事件最少且硬件与主设备最匹配的设备),并使其成为所有其他从设备的从设备。我们称这个从设备为候选从设备。GTID 机制将确保只应用那些没有在候选从设备上执行的事件。以这种方式,候选从设备成为最新的,并因此成为主设备的替代。

MySQL 复制还支持两种类型的同步。原始类型(异步)是单向的,其中在主服务器上执行的事件被传输到从服务器,并在事件到达时执行(或应用),而不进行检查以确保从服务器与主服务器处于相同的同步点(当有许多事务时,从服务器更新可能会延迟)。另一种类型是半同步的,其中在主模块上执行的提交在返回到执行事务的会话之前被阻塞,直到至少一个从模块确认它已经接收并记录了事务的事件。

MySQL NDB 集群支持同步复制,即在全有或全无提交的情况下,保证所有节点拥有相同的数据。有关同步复制的信息,请参见在线参考手册中的 MySQL NDB 集群部分。

小费

有关复制的更多信息,请参见在线 MySQL 参考手册( https://dev.mysql.com/doc/refman/8.0/en/replication.html )。

什么是组复制?

MySQL 组复制是 MySQL 复制的一种高级形式,用于实现容错系统。复制组(拓扑)是一组通过消息传递相互交互的服务器。通信层提供了一组保证,如原子消息和全序消息传递。这些强大的属性转化为非常有用的抽象,支持高级数据库复制解决方案。

组复制建立在现有复制属性和抽象的基础上,并实现多主机、随处更新的复制协议。使组复制成为可能的技术之一是 GTIDs。因此,参与组复制的服务器必须启用 GTIDs。

本质上,一个组由多个服务器组成,组中的每个服务器可以独立执行事务。但是所有读写(RW)事务只有在得到组的批准后才会提交。只读(RO)事务不需要在组内协调,因此可以立即提交。换句话说,对于任何 RW 事务,组需要决定它是否提交,因此提交操作不是来自发起服务器的单方面决定。

准确地说,当一个事务准备在原始服务器上提交时,服务器自动广播写值(已更改的行)和相应的写集(已更新的行的唯一标识符)。然后为该交易建立全局总订单。最终,这意味着所有服务器以相同的顺序接收相同的事务集。由于所有服务器都以相同的顺序应用相同的更改集,因此它们在组内保持一致。

组复制通过在复制组之间复制系统状态来提供冗余。如果一台(或多台)服务器出现故障,系统仍然可用。虽然如果有足够多的服务器发生故障,可能会影响某些性能或可伸缩性,但系统仍将保持可用。

这是通过组成员服务实现的,该服务依赖于分布式故障检测器,当任何服务器通过有意的交互或由于故障而离开组时,该检测器可以发出信号。有一个分布式恢复过程来确保当服务器加入组时,它们会自动更新。不需要服务器故障转移,多主更新无处不在的特性确保了即使在单个服务器出现故障的情况下更新也不会被阻止。因此,MySQL 组复制保证了数据库服务的持续可用性。

组复制使得拓扑最终同步复制(在属于同一个组的节点之间)成为现实,而现有的 MySQL 复制特性是异步的(或者最多是半同步的)。因此,可以提供更好的高可用性保证,因为事务以相同的顺序交付给所有成员(尽管在被接受后在每个成员中以自己的速度应用)。

组复制是通过分布式状态机来实现的,在分配给组的服务器之间有很强的协调性。这种通信允许服务器在组内自动协调复制。更具体地说,组维护成员关系,以便服务器之间的数据复制在任何时间点都是一致的。即使从组中删除了服务器,当添加它们时,一致性也会自动启动。此外,对于离线或变得不可达的服务器,还有一个故障检测机制。

组复制还可以与 MySQL 路由器一起使用,以实现应用级路由和操作容错(在应用级)。图 8-1 显示了您将如何在我们的应用中使用组复制来实现高可用性。

img/478423_1_En_8_Fig1_HTML.jpg

图 8-1

对应用使用组复制以实现高可用性(由 Oracle 提供)

请注意,组复制可以与 MySQL 路由器一起使用,以允许您的应用拥有一个与集群隔离的层。当我们在第 10 和 11 章中研究 InnoDB 集群时,我们会看到一些关于路由器的内容。

组复制和标准复制的另一个重要区别是,组中的所有服务器都可以参与更新数据,并自动解决冲突。是的,您不再需要精心设计您的应用来发送写入(更新)到特定的服务器!但是,您可以将组复制配置为只允许一台服务器(称为主服务器)进行更新,其他服务器充当辅助服务器或备份服务器(用于故障转移)。

使用组复制中内置的三种特定技术,可以实现这些功能以及更多功能:组成员资格、故障检测和容错。下面列出了这些技术,并对每种技术进行了简要概述。

  • 组成员身份:管理服务器是否活动(在线)并加入组。此外,确保组中的每台服务器都有一致的成员集视图,也就是说,每台服务器都知道组中服务器的完整列表。当服务器添加到组中时,组成员资格服务会自动重新配置成员资格。

  • 故障检测:一种机制,可以发现并报告哪些服务器离线(不可达)并被认为是死的。故障检测器是一种分布式服务,它允许组中的所有服务器测试假定失效服务器的状况,这样,组就可以确定服务器是否不可达(失效)。这允许该组通过协调排除故障服务器的过程来自动重新配置。

  • 容错:该服务使用 Paxos 分布式算法的实现来提供服务器之间的分布式协调。简而言之,该算法允许在组内自动提升角色,以确保即使一个(或几个)服务器出现故障或离开组,组也保持一致(数据一致且可用)。像类似的容错机制一样,失败(失败的服务器)的数量是有限的。目前,组复制容错能力可以定义为n = 2f + 1,其中 n 是容忍f故障所需的服务器数量。例如,如果您希望容忍多达 5 台服务器出现故障,则该组中至少需要 11 台服务器。

小费

有关组复制的更多信息,请参见位于 https://dev.mysql.com/doc/refman/8.0/en/group-replication.html 的在线参考手册

现在我们对 MySQL 复制和组复制有了更多的了解,让我们来看一个关于如何设置和配置 MySQL 复制的简短入门。我们将把它作为下一章的跳板,在下一章中,我们将使用 MySQL Shell 配置组复制。

设置和配置

现在您已经对复制及其工作原理有了一些了解,让我们来看看如何设置它。下一节讨论如何设置复制,将一台服务器作为主服务器,另一台作为从服务器。我们将看到两种类型的复制都被使用。正如您将看到的,在如何配置服务器和启动复制方面只有一些差异。

注意

术语“主服务器”和“从服务器”专门用于 MySQL 复制,表示只有一个服务器可以写入,因此拥有“主”副本。其余的服务器是只读的,包含数据的副本。在组复制中,这些术语被更改为“主”和“次”,以便更好地描述新功能中的角色。

但是首先,您需要在使用不同端口的系统上、在另外两个系统上或者在两个虚拟机上运行两个 MySQL 实例。对于大多数情况下的探索,在本地计算机上运行多个 MySQL 实例会很好。

启动实例时,必须设置一些复制的先决条件。建议您为每个实例创建一个单独的配置文件(在 Windows 中为my.cnfmy.ini),这样您就不会冒险为两个实例使用相同的目录。为此,我们应该已经在系统上安装了 MySQL。

启动一个新的 MySQL 实例很容易,只需要几个管理任务。以下是这些任务的概要。

  • 数据目录:您必须创建一个文件夹来包含数据目录。

  • 端口:您必须为每个实例选择一个端口。

  • 配置文件:您必须为每台服务器创建一个单独的配置文件。

  • 手动启动实例:要运行实例,您将从命令行(或通过批处理文件)启动 MySQL (mysqld ),指定正确的配置文件。

我们将在下一节中看到这些步骤以及更多演示,下一节将介绍如何在本地计算机上设置和运行 MySQL 复制的教程。

辅导的

本节演示如何设置从一台服务器(主服务器)到另一台服务器(从服务器)的复制。这些步骤包括通过启用二进制日志记录和创建用于读取二进制日志的用户帐户来准备主服务器,通过将从服务器连接到主服务器来准备从服务器,以及启动从服务器进程。这一部分以对复制系统的测试结束。

注意

使用二进制日志文件和位置设置复制的步骤与使用 GTIDs 的步骤相同,但是在某些步骤中命令略有不同。本教程将展示这两种方法。

设置和配置 MySQL 复制的步骤如下。可能还有其他同样可行的步骤来设置复制,但是这些可以在任何机器上完成,并且不会影响 MySQL 的任何现有安装。也就是说,建议在开发机器上执行这些步骤,以消除中断生产系统的风险。

  • 初始化数据目录:创建文件夹存储数据。

  • 配置主机:用二进制日志,新配置文件配置主机。

  • 配置从机:用二进制日志,新配置文件配置从机。

  • 启动 MySQL 实例:启动 MySQL 服务器实例。

  • 创建复制用户帐号:在所有服务器上创建复制用户。

  • 将从机连接到主机:将每个从机连接到主机。

  • 开始复制:发起复制。

  • 验证复制状态:执行一个简短的测试以确保数据正在被复制。

以下部分将更详细地演示这些步骤。虽然本教程使用多个本地实例来演示如何使用复制,但是在使用离散机器(或虚拟机)的开发或生产环境中设置复制的过程是相同的。使用特定主机、驱动器、文件夹、端口等的各个命令的详细信息。是在生产中使用该程序唯一会改变的东西。

注意

本教程中显示的步骤是在 macOS 平台上运行的。虽然有特定于平台的命令和一些特定于平台的选项,但本教程只需稍加修改就可以在 Linux 和 Windows 平台上运行。

初始化数据目录

第一步是为使用的每台机器初始化一个数据目录。在这种情况下,我们将在本地计算机上创建一个文件夹来包含所有数据目录。我们将使用 MySQL 的两个实例来表示一个主服务器和一个从服务器。下面演示了如何创建所需的文件夹。请注意,我在我使用的用户帐户可以访问的本地文件夹中创建这些文件,而不是系统或管理帐户。这是因为我们将在本地运行实例,不需要额外的权限或访问此类帐户的许可。如果您要使用管理帐户创建文件夹,您必须以该管理员的身份运行服务器(mysqld),这是我们不想做的。

$ mkdir rpl
$ cd rpl
$ mkdir data

现在我们有了一个文件夹<user_home>/rpl/data,我们可以使用 MySQL 服务器的初始化选项来设置我们的数据目录。我们使用服务器可执行文件的特殊选项--initialize-insecure--datadir来实现这一点。--initialize-insecure选项告诉服务器创建数据目录并用系统数据填充它,但是跳过任何身份验证的使用。这是安全的,因为还没有创建用户(没有数据目录!).

--datadir选项指定数据目录主文件夹的位置。因为我们是作为本地用户运行的,所以我们还需要--user选项。

小费

如果您正在复制本教程中的命令,请确保使用您自己的用户名作为--user选项和所有路径。

我们还需要知道安装在本地机器上的 MySQL 服务器的基目录(名为basedir)。您可以从服务器配置文件中获取这些信息,或者使用 shell 并发出SHOW SQL 命令。下面演示了如何做到这一点。在这里,我们看到的基本目录是/usr/local/mysql-8.0.16-macos10.14-x86_64。我们将使用这个值,以便mysqld可执行文件可以找到它的依赖库和文件。

$ mysqlsh --uri root@localhost:33060 --sql -e "SHOW VARIABLES LIKE 'basedir'"
Variable_name      Value
basedir      /usr/local/mysql-8.0.16-macos10.14-x86_64/

下面显示了初始化主机和从机的数据目录所需的命令。注意我用“slave1”来表示奴隶。这样,如果您想尝试添加额外的从属对象,就可以将教程扩展到多个从属对象。

$ mysqld --user=cbell --initialize-insecure --basedir=/usr/local/mysql-8.0.16-macos10.14-x86_64/ --datadir=/Users/cbell/rpl/data/master
$ mysqld --user=cbell --initialize-insecure --basedir=/usr/local/mysql-8.0.16-macos10.14-x86_64/ --datadir=/Users/cbell/rpl/data/master

当您运行这些命令时,您会看到打印出几条消息。您可以放心地忽略这些警告,但是请注意,最后一个警告告诉我们没有为 root 用户分配密码。这对于我们的教程来说是没问题的,但是对于生产安装来说,您绝对不希望这样做。幸运的是,一旦我们启动了实例,就可以很容易地解决这个问题。

现在我们已经创建并填充了数据目录,我们可以配置主服务器和从服务器了。

配置主服务器

复制要求主服务器启用二进制日志记录。默认情况下它是不打开的,因此您必须在配置文件中添加此选项。事实上,我们需要为每个想要启动的实例配置一个配置文件。在这一节中,我们集中讨论主设备,在下一节中,我们将看到从设备的配置文件。

我们还需要为实例选择端口。对于本教程,我们将使用从 13001 开始的主端口号和 13002+开始的从端口号。此外,我们需要选择唯一的服务器标识号。我们将用 1 代表主设备,用 2+代表从设备。

我们还需要做一些其他的设置。与其列出它们,不如让我们来看一个典型的主服务器的基本配置文件,它使用带有二进制日志和文件位置的复制。清单 8-1 显示了我们将在本教程中用于主服务器的配置文件。

[mysqld]
datadir="/Users/cbell/rpl/data/master"
basedir="/usr/local/mysql-8.0.16-macos10.14-x86_64/"
port=13001
socket="/Users/cbell/rpl/master.sock"
server_id=1
master_info_repository=TABLE
relay_log_info_repository=TABLE
log_bin=master_binlog
binlog_format=row

Listing 8-1Master Configuration File (Log File and Position)

注意,配置文件有一个名为mysqld的部分,它只适用于 MySQL 服务器的可执行文件。也就是说,只有 mysqld 和相关的可执行文件会读取这个部分的值。这些值包括 datadir、basedir、port 和 socket(对于 nix 风格的平台)的常见必需设置。请注意,这些值与我们之前讨论过的设置相匹配。

下一节设置服务器 id,打开用于存储复制信息的TABLE选项,这使得复制可以从崩溃中恢复,并打开二进制日志并设置其位置。最后,我们对二进制日志使用ROW格式,这是一种二进制格式,是 MySQL 复制最新版本的默认格式。

如果我们想要使用基于 GTID 的复制,那么必须设置一些额外的选项。对于大师来说,只有三个;打开 GTIDs,设置一致性强制,并记录从属更新。因此,启用 GTID 的主服务器的配置文件如清单 8-2 所示。请注意,文件的第一部分与前面的示例相同。只添加最后几行来启用 GTIDs。

[mysqld]
datadir="/Users/cbell/rpl/data/master"
basedir="/usr/local/mysql-8.0.16-macos10.14-x86_64/"
port=13001
socket="/Users/cbell/rpl/master.sock"
server_id=1
master_info_repository=TABLE
relay_log_info_repository=TABLE
log_bin=master_binlog
binlog_format=row

# GTID VARIABLES
gtid_mode=on
enforce_gtid_consistency=on
log_slave_updates=on

Listing 8-2Master Configuration File (GTIDs)

对于本教程,我们将使用支持 GTID 的复制,所以您应该在我们之前创建的文件夹中创建一个名为master.cnf的文件;比如/Users/cbell/rpl/master.cnf。在后面的步骤中,我们将使用该文件启动主服务器的实例。

小费

如果配置文件是全球可读的,一些平台可能无法启动 MySQL。如果您的服务器没有启动,请检查日志中有关文件权限的消息。

现在,让我们来看看从属服务器的配置文件。

配置从机

虽然日志文件和位置复制要求主服务器启用二进制日志记录,但从服务器不需要。但是,如果您想使用从属服务器来生成备份或进行崩溃恢复,那么打开从属服务器的二进制日志是一个好主意。但是,如果您想使用支持 GTID 的复制,也需要二进制日志记录。在本节中,我们将在从属服务器上使用二进制日志记录。

像 master 一样,我们需要设置几个变量,包括 datadir、basedir、port 和 socket(对于 nix 风格的平台)。清单 8-3 显示了第一个从机(名为 slave1)的配置文件。

[mysqld]
datadir="/Users/cbell/rpl/data/slave1"
basedir="/usr/local/mysql-8.0.16-macos10.14-x86_64/"
port=13002
socket="/Users/cbell/rpl/slave1.sock"
server_id=2
master_info_repository=TABLE
relay_log_info_repository=TABLE
log_bin=slave1_binlog
binlog_format=row
report-port=13002
report-host=localhost

Listing 8-3Slave Configuration File (Log File and Position)

注意,这里设置了两个额外的变量;报告端口和报告主机。这些对于确保像SHOW SLAVE HOSTS这样的命令报告正确的信息是必要的。也就是说,该视图的信息来自这些变量。因此,正确设置它们总是一个好主意。

还要注意,我们将数据目录设置为一个为这个从服务器留出的目录,服务器 id 也发生了变化。最后,我们还更改了二进制日志的名称,以确保我们知道日志来自哪个服务器(如果将来需要的话)。

如果我们想要使用基于 GTID 的复制,我们将添加与主服务器相同的一组变量,如清单 8-4 所示。

[mysqld]
datadir="/Users/cbell/rpl/data/slave1"
basedir="/usr/local/mysql-8.0.16-macos10.14-x86_64/"
port=13002
socket="/Users/cbell/rpl/slave1.sock"
server_id=2
master_info_repository=TABLE
relay_log_info_repository=TABLE
log_bin=slave1_binlog
binlog_format=row
report-port=13002
report-host=localhost

# GTID VARIABLES
gtid_mode=on
enforce_gtid_consistency=on
log_slave_updates=on

Listing 8-4Slave Configuration File (GTIDs)

对于本教程,我们将使用支持 GTID 的复制,所以您应该在我们之前创建的文件夹中创建一个名为slave1.cnf的文件;比如/Users/cbell/rpl/slave1.cnf。如果您想添加更多的从服务器,可以使用相同的数据创建额外的配置文件,只更改数据目录、套接字、端口、服务器 id 和二进制日志文件。

启动 MySQL 实例

现在我们已经准备好启动 MySQL 实例了。这很容易做到,因为我们已经用我们需要的所有参数创建了配置文件。我们只需要提供带有--defaults-file选项的配置文件。下面显示了启动这两个服务器实例的命令。

$ mysqld --defaults-file=master.cnf
$ mysqld --defaults-file=slave1.cnf

运行这些命令时,应该从包含配置文件的文件夹中运行它们。否则,您必须提供配置文件的完整路径。使用单独的终端窗口启动每个实例或者将输出(消息记录)重定向到一个文件也是一个好主意,如清单 8-5 所示。但是,您可能希望在第一次启动服务器时使用单独的终端,以确保没有错误。清单 8-5 显示了启动主程序时打印的消息摘录(没有返回&)。

297Z 0 [System] [MY-010116] [Server] /usr/local/mysql-8.0.16-macos10.14-x86_64/bin/mysqld (mysqld 8.0.16) starting as process 2615
2019-05-03T23:14:46.081413Z 0 [Warning] [MY-010159] [Server] Setting lower_case_table_names=2 because file system for /Users/cbell/rpl/data/master/ is case insensitive
2019-05-03T23:14:46.341919Z 0 [Warning] [MY-010068] [Server] CA certificate ca.pem is self signed.
2019-05-03T23:14:46.342661Z 0 [Warning] [MY-011810] [Server] Insecure configuration for --pid-file: Location '/Users/cbell/rpl/data' in the path is accessible to all OS users. Consider choosing a different directory.
2019-05-03T23:14:46.355855Z 0 [System] [MY-010931] [Server] /usr/local/mysql-8.0.16-macos10.14-x86_64/bin/mysqld: ready for connections. Version: '8.0.16'  socket: '/Users/cbell/rpl/master.sock'  port: 13001  MySQL Community Server - GPL.
2019-05-03T23:14:46.569522Z 0 [System] [MY-011323] [Server] X Plugin ready for connections. Socket: '/Users/cbell/rpl/masterx.sock' bind-address: '::' port: 33061

Listing 8-5Starting the Master Instance

如果您计划使用单个终端,建议将输出重定向到一个名为 master_log.txt 的文件,并使用选项在另一个进程中启动应用(例如,&符号)。服务器生成消息时会更新日志文件,因此如果遇到问题,您可以参考这些日志文件。这也有助于保持您的终端会话清除额外的消息。下面显示了如何构建前面的命令,使其作为单独的进程启动,并将消息记录到文件中。

$ mysqld --defaults-file=master.cnf > master_output.txt 2>&1 &

如果您还没有这样做,请继续启动从属服务器。下面是我用来启动从机(slave1)的命令。

$ mysqld --defaults-file=slave1.cnf > slave1_output.txt 2>&1 &

创建复制用户帐户

MySQL 实例启动后,您必须创建一个用户,供从服务器连接到主服务器并读取二进制日志,然后才能设置复制。这个有一个特殊的特权叫做REPLICATION SLAVE。下面显示了创建用户和添加权限的正确的GRANT语句。记住您在这里使用的用户名和密码,因为您将需要它来连接从设备。

下面显示了创建复制用户所需的命令。在所有服务器上执行这些命令。虽然从属服务器不需要它,但现在创建用户将允许您使用从属服务器进行恢复、切换或故障转移,而无需创建用户。事实上,这一步是允许自动故障转移所必需的。

SET SQL_LOG_BIN=0;
CREATE USER rpl_user@'localhost' IDENTIFIED BY 'rpl_pass';
GRANT REPLICATION SLAVE ON ∗.∗ TO rpl_user@'localhost';
FLUSH PRIVILEGES;
SET SQL_LOG_BIN=1;

请注意第一个和最后一个命令。这些命令告诉服务器暂时禁止记录二进制日志的更改。每当我们不想在拓扑中的其他机器上复制命令时,我们就这样做。具体来说,不应该复制像创建用户这样的维护和管理命令。关闭二进制日志是确保您不会意外发出无法在其他机器上执行的事务的好方法。

执行这些命令的最佳方式是将它们保存到名为create_rpl_user.sql的文件中,并使用 mysql 客户端的源命令从文件中读取命令并执行它们。因此,您可以使用以下命令在所有实例上快速创建复制用户。

$ mysqlsh --uri root@localhost:33061 --sql -f "create_rpl_user.sql"
$ mysqlsh --uri root@localhost:33062 --sql -f "create_rpl_user.sql"

现在,我们已经准备好将从设备连接到主设备,并开始复制数据。

将从设备连接到主设备

下一步是将从设备连接到主设备。根据您使用的复制形式,有不同的方法可以做到这一点。具体来说,当使用日志文件和位置与 GTID 复制时,将从设备连接到主设备的命令是不同的。还有两个步骤:配置从机进行连接和开始复制。让我们先来看看用日志文件和位置配置从属服务器。

连接日志文件并定位

要使用日志文件和位置将从设备连接到主设备,我们需要一些信息。完成指示从机与主机建立连接的CHANGE MASTER命令需要这些信息。表 8-1 显示了所需信息的完整列表。该表包括信息的来源之一,以及本教程中使用的值的示例。

表 8-1

连接从属设备所需的信息(日志文件和位置)

|

主文件中的项目

|

来源

|

例子

|
| --- | --- | --- |
| IP 地址或主机名 | master.cnf | 本地主机 |
| 港口 | master.cnf | Thirteen thousand and one |
| 二进制日志文件 | SHOW MASTER STATUS | master_binlog.000005 |
| 二进制日志文件位置 | SHOW MASTER STATUS | One hundred and fifty-four |
| 复制用户 ID | create_rpl_user.sql | rpl _ 用户 |
| 复制用户密码 | create_rpl_user.sql | rpl_pass |

主二进制日志文件的信息可以通过SHOW MASTER STATUS命令找到。下面展示了如何使用 mysql 客户端执行命令并返回。

$ mysqlsh --uri root@localhost:33061 --sql -e "SHOW MASTER STATUS"
File    Position    Binlog_Do_DB    Binlog_Ignore_DB    Executed_Gtid_Set
master_binlog.000005    155

请注意,该命令还显示任何活动的过滤器,以及最新 GTID 执行集的特定于 GTID 的值。在本教程中我们不需要它,但是如果您需要恢复启用了 GTID 的拓扑,将它归档是一个好主意。

现在您已经有了主服务器的二进制日志文件名和位置以及复制用户和密码,您可以访问您的从服务器并使用CHANGE MASTER命令将其连接到主服务器。该命令可以根据表 8-1 中的信息构建,如下所示(格式化以使其更容易阅读——如果您正在学习本教程,请删除“”。

CHANGE MASTER TO MASTER_USER="rpl_user", MASTER_PASSWORD="rpl_pass", \
    MASTER_HOST='localhost', MASTER_PORT=13001, \
    MASTER_LOG_FILE='master_binlog.000005', MASTER_LOG_POS=155;

您必须在所有从属服务器上运行该命令。将它保存到一个文件并使用 mysql 客户端执行它可能更容易,就像我们为复制用户所做的那样。例如,将它保存到一个名为change_master.sql的文件中,并如下执行。

$ mysqlsh --uri root@localhost:33062 --sql -f "change_master.sql"

启动从服务器还需要一个步骤,但是让我们先来看看如何为启用 GTID 的复制配置CHANGE MASTER命令。

用 GTIDs 连接

要使用 GTIDs 将从设备连接到主设备,我们需要一些信息。完成指示从机与主机建立连接的CHANGE MASTER命令需要这些信息。表 8-2 显示了所需信息的完整列表。该表包括信息的来源之一,以及本教程中使用的值的示例。

表 8-2

连接从机所需的信息(GTIDs)

|

主文件中的项目

|

来源

|

例子

|
| --- | --- | --- |
| IP 地址或主机名 | master.cnf | 本地主机 |
| 港口 | master.cnf | Thirteen thousand and one |
| 复制用户 ID | create_rpl_user.sql | rpl _ 用户 |
| 复制用户密码 | create_rpl_user.sql | rpl_pass |

请注意,我们需要的信息比日志文件和位置复制少。我们不需要知道主二进制日志文件或位置,因为 GTID 握手过程将为我们解析该信息。我们需要的只是主服务器和复制服务器的主机连接信息以及密码。对于支持 GTID 的复制,我们使用一个特殊的参数MASTER_AUTO_POSITION来指示复制自动协商连接信息。可以根据表 8-2 中的信息构造CHANGE MASTER命令,如下所示(格式化以便于阅读——如果您按照本教程进行操作,则不需要删除\)。

CHANGE MASTER TO MASTER_USER="rpl_user", MASTER_PASSWORD="rpl_pass", \
    MASTER_HOST='localhost', MASTER_PORT=13001, MASTER_AUTO_POSITION = 1;

您必须在所有从属服务器上运行该命令。将它保存到一个文件并使用 mysql 客户端执行它可能更容易,就像我们为复制用户所做的那样。例如,将它保存到一个名为change_master.sql的文件中,并如下执行。

mysqlsh --uri root@localhost:33062 --sql -f "change_master.sql"

如果您希望能够将该文件用于任何一种形式的复制,您可以简单地将这两个命令放在文件中,并注释掉一个您不需要的命令。例如,下面显示了带有两个CHANGE MASTER命令的示例文件。请注意,GTID 变体是用#符号注释掉的。

CHANGE MASTER TO MASTER_USER="rpl_user", MASTER_PASSWORD="rpl_pass", MASTER_HOST="localhost", MASTER_PORT=13001, MASTER_LOG_FILE='master_binlog.000005', MASTER_LOG_POS=155;
# GTID option:
# CHANGE MASTER TO MASTER_USER="rpl_user", MASTER_PASSWORD="rpl_pass", MASTER_HOST="localhost", MASTER_PORT=13001, MASTER_AUTO_POSITION = 1;

既然我们已经配置了从属设备进行连接,我们必须通过告诉从属设备启动连接并开始复制来完成这个过程。

开始复制

下一步是启动从属进程。这个命令简单来说就是START SLAVE。我们将在所有从机上运行这个命令,就像我们对CHANGE MASTER命令所做的那样。下面显示了启动从机的命令。

$ mysqlsh --uri root@localhost:33062 --sql -e "START SLAVE"

START SLAVE命令通常不报告任何错误;您必须使用SHOW SLAVE STATUS才能看到它们。清单 8-6 显示了运行中的命令。为了安全和放心,您可能希望在您启动的任何从属服务器上运行这个命令。

$ mysqlsh --uri root@localhost:33062 --sql -e "SHOW SLAVE STATUS\G"
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
               Slave_IO_State: Waiting for master to send event
                  Master_Host: localhost
                  Master_User: rpl_user
                  Master_Port: 13001
                Connect_Retry: 60
              Master_Log_File: master_binlog.000005
          Read_Master_Log_Pos: 155
               Relay_Log_File: MacBook-Pro-2-relay-bin.000002
                Relay_Log_Pos: 377
        Relay_Master_Log_File: master_binlog.000005
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
              Replicate_Do_DB:
          Replicate_Ignore_DB:
           Replicate_Do_Table:
       Replicate_Ignore_Table:
      Replicate_Wild_Do_Table:
  Replicate_Wild_Ignore_Table:
                   Last_Errno: 0
                   Last_Error:
                 Skip_Counter: 0
          Exec_Master_Log_Pos: 155
              Relay_Log_Space: 593
              Until_Condition: None
               Until_Log_File:
                Until_Log_Pos: 0
           Master_SSL_Allowed: No
           Master_SSL_CA_File:
           Master_SSL_CA_Path:
              Master_SSL_Cert:
            Master_SSL_Cipher:
               Master_SSL_Key:
        Seconds_Behind_Master: 0
Master_SSL_Verify_Server_Cert: No
                Last_IO_Errno: 0
                Last_IO_Error:
               Last_SQL_Errno: 0
               Last_SQL_Error:
  Replicate_Ignore_Server_Ids:
             Master_Server_Id: 1
                  Master_UUID: b6632bf2-6df6-11e9-8bf5-cc9584d9b3f3
             Master_Info_File: mysql.slave_master_info
                    SQL_Delay: 0
          SQL_Remaining_Delay: NULL
      Slave_SQL_Running_State: Slave has read all relay log; waiting for more updates
           Master_Retry_Count: 86400
                  Master_Bind:
      Last_IO_Error_Timestamp:
     Last_SQL_Error_Timestamp:
               Master_SSL_Crl:
           Master_SSL_Crlpath:
           Retrieved_Gtid_Set:
            Executed_Gtid_Set: ccc5263e-6df6-11e9-88d5-910b8477c67b:1
                Auto_Position: 1
         Replicate_Rewrite_DB:
                 Channel_Name:
           Master_TLS_Version:
       Master_public_key_path:
        Get_master_public_key: 0
            Network_Namespace:

Listing 8-6Checking SLAVE STATUS

花点时间费力地读完所有这些行。有几个关键字段你需要注意。这些包括名称中带有错误的任何内容,以及状态列。例如,第一行(Slave_IO_State)显示了指示从机 I/O 线程状态的文本消息。I/O 线程负责从主服务器的二进制日志中读取事件。还有一个 SQL 线程负责从中继日志中读取事件并执行它们。

对于这个例子,您只需要确保两个线程都在运行(YES)并且没有错误。关于SHOW SLAVE STATUS命令中所有字段的详细解释,请参见在线 MySQL 参考手册中的“用于控制从属服务器的 SQL 语句”一节1

既然从服务器已经连接并正在运行,让我们通过检查主服务器并创建一些数据来检查复制。

验证复制状态

使用SHOW SLAVE STATUS命令检查从属状态是验证复制健康的第一步。下一步是使用SHOW SLAVE HOSTS命令检查主机。清单 8-7 显示了本教程中拓扑设置的SHOW SLAVE HOSTS的输出。该命令显示连接到主设备的从设备及其 UUIDs。需要注意的是,这些信息是一个视图,不是实时的。也就是说,从属连接可能会失败,但仍会显示在报告中,直到进程超时,服务器终止它们。因此,这个命令最好用作健全性检查。

mysqlsh --uri root@localhost:33061 --sql -e "SHOW SLAVE HOSTS\G"
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
 Server_id: 2
      Host: localhost
      Port: 13002
 Master_id: 1
Slave_UUID: ccc5263e-6df6-11e9-88d5-910b8477c67b

Listing 8-7SHOW SLAVE HOSTS Command (Master)

在这里,我们看到所有从机都已连接,从最后一部分我们知道从机状态良好。

接下来,让我们在主服务器上创建一些简单的数据,然后看看这些数据是否被复制到了从服务器上。在这种情况下,我们将简单地创建一个数据库、一个表和一个单独的行,然后在主服务器上运行它。清单 8-8 显示了在主机上执行的样本数据。

$ mysqlsh --uri root@localhost:33061 --sql
MySQL Shell 8.0.16

Copyright (c) 2016, 2019, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its affiliates.
Other names may be trademarks of their respective owners.

Type '\help' or '\?' for help; '\quit' to exit.
Creating a session to 'root@localhost:33061'
Fetching schema names for autocompletion... Press ^C to stop.
Your MySQL connection id is 18 (X protocol)
Server version: 8.0.16 MySQL Community Server - GPL
No default schema selected; type \use <schema> to set one.

 MySQL  localhost:33061+ ssl  SQL > CREATE DATABASE test;
Query OK, 1 row affected (0.0029 sec)

 MySQL  localhost:33061+ ssl  SQL > USE test;
Query OK, 0 rows affected (0.0002 sec)

 MySQL  localhost:33061+ ssl  SQL > CREATE TABLE test.t1 (c1 INT PRIMARY KEY, c2 TEXT NOT NULL);
Query OK, 0 rows affected (0.0054 sec)

 MySQL  localhost:33061+ ssl  SQL > INSERT INTO test.t1 VALUES (1, 'Dr. Charles Bell');
Query OK, 1 row affected (0.0092 sec)

Listing 8-8Creating Sample Data for Testing Replication (Master)

为了验证数据是否被复制,我们需要做的就是在一个从服务器(或者所有从服务器,如果您愿意的话)的表上发出一个SELECT SQL 命令。下面显示了我们期望在每个从机上看到的一个例子。

$ mysqlsh --uri root@localhost:33062 --sql -e "SELECT ∗ FROM test.t1"
c1    c2
1     Dr. Charles Bell

关于设置 MySQL 复制的简短教程到此结束。本节用最简洁的术语简要介绍了 MySQL 复制。现在,让我们看看如何编写 MySQL 复制的示例设置。然而,还有一步。

关闭复制

如果您在自己的机器上尝试本教程,请记住按照精确的顺序关闭实例。在每个从属服务器上,首先要用下面的命令停止从属服务器。

$ mysqlsh --uri root@localhost:33062 --sql -e "STOP SLAVE"

一旦所有从属服务器都停止了,您可以使用以下命令首先关闭从属服务器,然后关闭主服务器。注意,我们对主设备和从设备使用了旧的协议端口(13001、13002)。这是因为 X 协议不支持 shutdown 命令。如果在使用 MySQL X 端口时遇到错误,请尝试旧的协议并重新运行该命令。

$ mysqlsh --uri root@localhost:13002 --sql -e "SHUTDOWN"
$ mysqlsh --uri root@localhost:13001 --sql -e "SHUTDOWN"

MySQL 高可用性参考资料

如果您仔细阅读了在 InnoDB Cluster 之前关于 MySQL 高可用性的一些较长的作品,您可能会发现一些针对特定用例的额外的、更复杂的挑战。如果您想在使用 InnoDB Cluster 之前了解更多关于 MySQL 复制和 MySQL 高可用性特性的信息,以下是一些优秀的资源。

  • Bell,Kindahl,Thalmann, MySQL 高可用性:构建健壮数据中心的工具,第二版(O'Reilly,2014)

  • Das, MySQL 复制简化版:建立、故障排除和监控复制的简单分步示例 (Business Compass,LLC,2014 年)

  • 特卡琴科扎依采夫施瓦茨高性能 MySQL:优化、备份、复制等 (O'Reilly,2012)

  • Davies, MySQL 高可用性食谱 (Packt,2010 年)

在线参考手册包含大量文档,应该是您的主要来源( https://dev.mysql.com/doc/refman/8.0/en/replication.html )。

摘要

使用 MySQL 复制可以在 MySQL 中实现高可用性。事实上,您可以通过复制创建健壮的数据中心。更好的是,复制已经存在了很长时间,并且被认为非常稳定。从小型安装到大规模安装,许多组织已经并将继续在生产中成功使用复制。

即便如此,使用 MySQL 复制也有一些限制,例如,如果主服务器出现故障,如何将主服务器角色切换到另一台机器(从服务器),如何自动执行这一操作,如何处理多个写入场景,以及一般的故障排除和维护。其中许多在组复制中得到了改进。然而,正如我们所看到的,复制的设置需要一些工作和维护,这可能是规划者和管理员都关心的问题。

在这一章中,我们了解了什么是高可用性,MySQL 中一些高可用性特性的概述,以及如何用一个主服务器和一个从服务器实现 MySQL 复制。尽管本教程仅限于两台机器,但是您可以轻松地将其扩展到多台机器。

您可能认为 MySQL 复制将满足您的高可用性需求(这很好),但 MySQL Group Replication 通过引入容错和一系列功能将高可用性提升到了另一个水平,使其更加通用,可以满足更苛刻的高可用性需求。

在下一章,我们将看到如何使用 MySQL Shell 设置组复制的完整演练。

九、示例:组复制设置和管理

正如我们在上一章中了解到的,组复制是 MySQL 复制的一种发展,旨在使数据复制更加健壮和可靠。与对 InnoDB 存储引擎的修改(全部隐藏在罩下)一起,组复制实现了高可用性功能,这在过去需要专门的、有时是定制的产品、中间件和定制的应用才能实现。

在本章中,我们将更深入地了解组复制的组成部分。之后,我们将在动手演练中对组复制进行指导。

入门指南

设置 MySQL 组复制类似于设置 MySQL 复制的过程,这可能不会让您感到惊讶。毕竟,组复制建立在 MySQL 复制的基础上。在下一节中,我们将看到组复制的演示,但我们不会集中在 MySQL 复制教程中的相同步骤上,我们将简要介绍相同的主题,并深入探讨特定于组复制的细微差别。

但是首先,让我们从组成描述组复制的语言的概念和术语的列表开始。

概念、术语和行话

很可能除了最精通或者掌握 MySQL 最新知识的人之外,所有人都会完全理解描述组复制的所有术语和概念。在这一节中,我们暂时后退一步,讨论一些您将在本章和接下来的两章(或任何一本关于 MySQL 高可用性的书)中遇到的术语和概念。因此,本节是与组复制相关的术语表。请随时参考这一部分。

  • 二进制日志:服务器产生的文件,包含所有执行的事务的二进制形式。二进制日志文件也用于复制,以便在两台服务器之间交换事务。当在主服务器(主服务器)上使用时,它会形成所有更改的记录,这些记录可以发送到辅助服务器(从服务器)上执行以创建副本。

  • 多主节点:一个组,其中的写入可以发送到多个主节点,并在组内复制。

  • 故障转移:允许组从主服务器上的故障中恢复并自动选择新的主服务器的事件。

  • 容错:从组中检测到的故障或错误中恢复而不丢失数据或功能的能力。请注意,组复制中的容错能力受到组中服务器数量的限制。有关如何计算服务器数量以及组可以容忍的故障数量,请参见联机参考手册中的“容错”一节。 1

  • Group :参与同一个组复制通信设置的一组 MySQL 服务器。

  • 组通信:一种特殊的机制,使用状态机和通信协议来保持组中服务器的协调,包括事务执行的同步和角色的选择/选举。

  • 实例:正在运行的 MySQL 服务器。通常用于指在同一台机器上运行的一个或多个 MySQL 服务器。这和“MySQL 服务器”不一样,后者往往指的是硬件和 MySQL 执行的集合。

  • Primary :组中的服务器,被分配收集所有数据写入(更新)的角色。

  • 中继日志:二进制日志文件,在辅助(从)上使用,记录从主(主)二进制日志中读取并保存以供执行的事务。它的格式与二进制日志相同。

  • 辅助服务器:组中被分配了 reader 角色的服务器,这意味着应用可以从辅助服务器读取数据,但不能写入辅助服务器。

  • 单主:配置有一台主服务器和一台或多台从服务器的组。这类似于旧的 MySQL 复制特性中的主/从配置。

  • 切换:一种受控的维护事件,管理员主动更改主服务器角色,将其从一台服务器上移除并分配给另一台服务器(使新服务器成为主服务器)。这不会自动发生,通常与故障无关。

  • 事务:一组数据更改,在将该组数据应用到数据之前,必须全部成功。失败的事务不会写入数据库。

  • 拓扑:描述复制组中服务器逻辑布局的术语。示例包括单主服务器,表示为单个服务器与每个从服务器之间的放射状连接;分层服务器,表示为单主服务器组的连接,其中每个从服务器都是另一组从服务器的主服务器;多主服务器,通常表示为每个主服务器连接到组中的每个其他主服务器以及组中的从服务器。

您应该了解组复制的另一个方面;通过计算故障数量,一个组可以恢复并继续提供高可用性。

组复制容错

回想一下第八章,我们了解到当服务器离线(离开组)时,组复制可以成功容忍一定数量的服务器故障。该组可以容忍的故障数量取决于该组中的服务器数量。用于确定一组服务器 fS 能够容忍多少故障同时或连续、未恢复故障的公式如下。

S = 2f + 1

例如,一组 11 台服务器最多可以容忍 5 次故障。

11 = 2f + 1
10 = 2f
2f = 10
f = 10/2
f = 5

如果你想知道有多少个服务器, s ,容忍已知数量的故障, F ,一个小小的数学应用揭示了以下内容。请注意,您必须向下舍入任何分数。你不能让 1.5 服务器失败。

s = 2F + 1
(s - 1) = 2F
2F = (s - 1)
F = (s - 1)/2

例如,一组 5 台服务器可以容忍 2 次故障。

F = (5 - 1)/2
F = 4/2
F = 2

类似地,一组 7 台服务器可以容忍 3 次故障。

F = (7 - 1)/2
F = 6/2
F = 3

设置和配置

现在您已经对组复制及其工作原理有了一些了解,让我们来看看如何设置它。我们将了解如何使用四台服务器设置组复制。因此,您需要在使用不同端口的系统上、在另外四个系统上或者在四个虚拟机上运行四个 MySQL 实例。对于大多数情况下的探索,在本地计算机上运行多个 MySQL 实例会很好。

启动实例时,必须设置组复制的一些先决条件。建议您为每个实例创建一个单独的配置文件(my.cfg),这样就不会冒险为两个实例使用相同的目录。为此,我们应该已经在系统上安装了 MySQL。

启动一个新的 MySQL 实例很容易,只需要几个管理任务。下面概述了这些任务。

  • 数据目录:您必须创建一个文件夹来包含数据目录。

  • 端口:您必须为每个实例选择一个端口。

  • 配置文件:您必须为每台服务器创建一个单独的配置文件。

  • 手动启动实例:要运行实例,您将从命令行(或通过批处理文件)启动 MySQL (mysqld ),指定正确的配置文件。

我们将在下一节中看到这些步骤以及更多演示,下一节将介绍如何在本地计算机上设置和运行 MySQL 组复制的教程。

辅导的

本节演示如何在本地计算机上运行的一组四个 MySQL 实例之间设置组复制。如前所述,组复制对组中的角色使用不同的术语。具体来说,有一个主要角色和一个次要角色。与将一台服务器指定为主服务器的 MySQL 复制不同,组复制可以根据需要自动更改组中服务器的角色。因此,虽然我们将设置组复制,将其中一台服务器标识为主服务器,但是随着时间的推移,组的最终状态可能会导致其他服务器之一成为主服务器。

如果您想自己体验本教程,您应该准备四台服务器。像上一个教程一样,我们将使用在当前机器上运行的几个实例。我们需要几个实例来确保该组有一个可行的集合来启用冗余和故障转移。在这种情况下,该组最多可以容忍 1 次故障;(4-1)/2 = 1.5 或 1 向下舍入。

设置和配置组复制的步骤包括以下内容。可能还有其他同样可行的步骤来设置组复制,但是这些可以在任何机器上完成,并且不会影响 MySQL 的任何现有安装。也就是说,建议在开发机器上执行这些步骤,以消除中断生产系统的风险。

注意

用于设置组复制的步骤与 MySQL 复制非常相似。事实上,除了术语(例如,从属与辅助)、配置文件以及首次在主服务器上启动组复制之外,过程是相同的。

  • 初始化数据目录:创建文件夹存储数据。

  • 配置主节点:用新的配置文件配置主节点。

  • 配置从站:用新的配置文件配置从站。

  • 启动 MySQL 实例:启动 MySQL 服务器实例。

  • 创建复制用户帐号:在所有服务器上创建复制用户。

  • 在主节点上启动组复制:启动主节点,建立组。

  • 将辅助节点连接到主节点:启动复制。

  • 在辅助节点上启动组复制:将辅助节点添加到组成员中。

  • 验证组复制状态:执行一个简短的测试以确保正在复制数据。

下面几节将详细演示在安装了 MySQL 的 macOS 系统上运行的每一个步骤。对于其他平台,步骤是相同的,但路径可能略有不同。虽然本教程使用多个本地实例来演示如何使用复制,但是在生产环境中设置复制的过程是相同的。使用特定主机、驱动器、文件夹、端口等的各个命令的详细信息。是在生产中使用该程序唯一会改变的东西。

初始化数据目录

第一步是为使用的每台机器初始化一个数据目录。在这种情况下,我们将在本地计算机上创建一个文件夹来包含所有数据目录。我们将使用四个 MySQL 实例来代表一个主服务器和三个辅助服务器。下面演示了如何创建所需的文件夹。请注意,我们在我们使用的用户帐户可以访问的本地文件夹中创建这些文件,而不是系统或管理帐户。这是因为我们将在本地运行实例,不需要额外的权限或访问此类帐户的许可。

$ mkdir gr
$ cd gr
$ mkdir data

现在我们有了一个文件夹<user_home>/gr/data,我们可以使用 MySQL 服务器的初始化选项来设置我们的数据目录。我们使用服务器可执行文件的特殊选项--initialize-insecure--datadir来实现这一点。--initialize-insecure选项告诉服务器创建数据目录并用系统数据填充它,但是跳过任何身份验证的使用。这是安全的,因为还没有创建用户(没有数据目录!).

--datadir选项指定数据目录主文件夹的位置。因为我们是作为本地用户运行的,所以我们还需要--user选项。

小费

如果您正在复制本教程中的命令,请确保使用您自己的用户名作为--user选项和所有路径。

我们还需要知道安装在本地机器上的 MySQL 服务器的基目录(名为basedir)。您可以从服务器配置文件中获取这些信息,或者使用 shell 并发出SHOW SQL 命令。下面演示了如何做到这一点。在这里,我们看到的基本目录是/usr/local/mysql-8.0.16-macos10.14-x86_64。我们将使用这个值,以便mysqld可执行文件可以找到它的依赖库和文件。

$ mysqlsh --uri root@localhost:33060 --sql -e "SHOW VARIABLES LIKE 'basedir'"
Variable_name  Value
basedir        /usr/local/mysql-8.0.16-macos10.14-x86_64/

下面显示了初始化主节点和辅助节点的数据目录所需的命令。

$ mysqld --user=cbell --initialize-insecure \
  --basedir=/usr/local/mysql-8.0.16-macos10.14-x86_64/ \
  --datadir=/Users/cbell/gr/data/primary
$ mysqld --user=cbell --initialize-insecure \
  --basedir=/usr/local/mysql-8.0.16-macos10.14-x86_64/ \
  --datadir=/Users/cbell/gr/data/secondary1
$ mysqld --user=cbell --initialize-insecure \
  --basedir=/usr/local/mysql-8.0.16-macos10.14-x86_64/ \
  --datadir=/Users/cbell/gr/data/secondary2
$ mysqld --user=cbell --initialize-insecure \
  --basedir=/usr/local/mysql-8.0.16-macos10.14-x86_64/ \
  --datadir=/Users/cbell/gr/data/secondary3

现在我们已经创建并填充了数据目录,我们可以配置主服务器和从服务器了。

配置主服务器

这一步与 MySQL 复制最不同。事实上,配置文件有很大的不同。具体来说,除了必须设置的几个更常见的组复制变量之外,我们还使用启用 GTID 的复制中的相同变量。下面列出了与组复制相关的变量及其用途。控制组复制还有其他变量。有关完整列表,请参见在线参考手册中的 https://dev.mysql.com/doc/refman/8.0/en/group-replication-options.html

  • transaction_write_set_extraction:定义用于散列在事务期间提取的写入的算法。组复制必须设置为XXHASH64

  • group_replication_recovery_use_ssl:决定组复制恢复连接是否使用 SSL。通常设置为ON。默认是OFF

  • group_replication_group_name:该服务器实例所属的组的名称。必须是有效的、唯一的 UUID。

  • group_replication_start_on_boot:决定服务器启动时是否启动组复制。

  • group_replication_local_address:成员为来自其他成员的连接提供的网络地址,指定为 host:port 格式的字符串。请注意,此连接用于组复制成员之间的通信,而不是供客户端使用。

  • group_replication_group_seeds:群组成员列表,用于建立新成员到群组的连接。该列表由种子成员的group_replication_local_address网络地址组成,指定为逗号分隔列表,例如 host1:port1,host2:port2。

  • group_replication_bootstrap_group:配置此服务器以引导组。此选项只能在一台服务器上设置,并且只能在第一次启动组或重新启动整个组时设置。组启动后,将该选项设置为OFF

注意最后一个变量,group_replication_bootstrap_group。这个变量是我们将在配置文件中设置为OFF的东西,但是只有在我们第一次引导组之后。这是初始主节点的用途之一,即启动组。我们将看到一个特殊的步骤,您必须在第一次启动主服务器时执行该步骤,以启动该组。之后,该变量必须设置为OFF

主要配置文件

为了构造主服务器的配置文件,我们需要几样东西:数据目录、基目录、端口等的常用变量。以及 GTID 变量和组复制变量。添加插件目录以确保服务器可以找到组复制插件(我们将在后面的步骤中看到这一点)并打开二进制日志校验和也是一个好主意。

由于group_replication_group_seeds变量需要最初加入组的服务器列表,我们必须决定每个服务器将使用的端口。组复制设置要求每台服务器有两个端口:一个用于正常连接,另一个用于组复制。在本教程中,我们将端口 24801+用于服务器连接,端口 24901+用于组复制端口。此外,由于我们使用本地实例,组中所有成员的主机名都将使用环回地址(127.0.0.1),但这通常是运行它的服务器的主机名。最后,我们还需要选择服务器 id,因此我们将使用从 1 开始的顺序值。清单 9-1 显示了我们将在本教程中用于主服务器的配置文件。

[mysqld]
datadir="/Users/cbell/gr/data/primary"
basedir="/usr/local/mysql-8.0.16-macos10.14-x86_64/"
plugin_dir=/usr/local/mysql/lib/plugin/
plugin-load=group_replication.so
port=24801
mysqlx-port=33061
mysqlx-socket="/Users/cbell/rpl/primaryx.sock"
socket="/Users/cbell/rpl/primary.sock"
server_id=101
master_info_repository=TABLE
relay_log_info_repository=TABLE
log_bin=master_binlog
binlog_format=row

# GTID VARIABLES
gtid_mode=ON
enforce_gtid_consistency=ON
log_slave_updates=ON
binlog_checksum=NONE

# GR VARIABLES
transaction_write_set_extraction=XXHASH64
group_replication_recovery_use_ssl=ON
group_replication_group_name="bbbbbbbb-bbbb-cccc-dddd-eeeeeeeeeeee"
group_replication_start_on_boot=OFF
group_replication_local_address="127.0.0.1:24801"
group_replication_group_seeds="127.0.0.1:24901,127.0.0.1:24902,127.0.0.1:24903,127.0.0.1:24904"
group_replication_bootstrap_group=OFF

Listing 9-1Primary Configuration File (Group Replication)

您可能会注意到没有设置log-bin变量。当服务器遇到组复制所需的变量时,它会自动启用二进制日志。但是,如果您想要命名二进制日志文件或者将它们放在另一个文件夹中,您可以包含变量,但是这是一个高级配置选项,对于教程甚至开发安装来说都不是必需的。

注意

如果您在 Windows 上运行本教程,并且没有安装 SSL 和配置 MySQL 来使用 SSL 连接,您必须删除group_replication_recovery_use_ssl选项。

对于本教程,您应该在我们之前创建的文件夹中创建一个名为primary.cnf的文件;比如/Users/cbell/gr/primary.cnf。在后面的步骤中,我们将使用该文件启动主实例。

现在,让我们看看辅助节点的配置文件。

辅助配置文件

辅助节点的配置文件与主节点的配置文件非常相似,唯一的变化是为特定于实例的变量(如端口、数据目录、套接字和服务器 id)使用了正确的值。然而,除了这些设置之外,还有一些不同之处。transaction_write_set_extraction变量在初始初级上设置。

对于辅助节点,我们添加group_replication_recovery_get_public_key并将其设置为ON。此变量确定辅助节点是否向主节点请求基于 RSA 密钥对的密码交换所需的公钥。该变量适用于通过caching_sha2_password认证插件进行认证的辅助节点。

我们还包含了插件可执行文件路径的plugin_dir,您可以使用SHOW VARIABLES LIKE '%plugin%'命令。我们包含的另一个变量是值为group_replication.soplugin-load变量。注意,.so文件扩展名指的是 nix 计算机上的共享对象库。在 Windows 上,你可以使用.dll

清单 9-2 显示了第一个辅助节点(名为secondary1.cnf)的配置文件。

[mysqld]
datadir="/Users/cbell/gr/data/secondary1"
basedir="/usr/local/mysql-8.0.16-macos10.14-x86_64/"
plugin_dir=/usr/local/mysql/lib/plugin/
plugin-load=group_replication.so
port=24901
mysqlx-port=33062
mysqlx-socket="/Users/cbell/rpl/secondary1x.sock"
socket="/Users/cbell/rpl/data/secondary1.sock"
server_id=102
master_info_repository=TABLE
relay_log_info_repository=TABLE
log_bin=slave1_binlog
binlog_format=row
report-port=24901
report-host=localhost

# GTID VARIABLES
gtid_mode=ON
enforce_gtid_consistency=ON
log_slave_updates=ON
binlog_checksum=NONE

# GR VARIABLES
group_replication_recovery_get_public_key=ON
group_replication_recovery_use_ssl=ON
group_replication_group_name="bbbbbbbb-bbbb-cccc-dddd-eeeeeeeeeeee"
group_replication_start_on_boot=OFF
group_replication_local_address="127.0.0.1:24901"
group_replication_group_seeds="127.0.0.1:24901,127.0.0.1:24902,127.0.0.1:24903,127.0.0.1:24904"
group_replication_bootstrap_group=OFF

Listing 9-2Secondary Configuration File (Group Replication)

在本教程中,我们将使用三个辅助节点,因此您应该在我们之前创建的文件夹中为每个节点创建一个文件,并将它们命名为secondary1.cnfsecondary2.cnfsecondary3.cnf。确保更改特定于实例的变量,如数据目录、套接字、端口、服务器 id 等。您必须更改两个端口;服务器端口和组复制端口。

启动 MySQL 实例

现在我们已经准备好启动 MySQL 实例了。这很容易做到,因为我们已经用我们需要的所有参数创建了配置文件。我们只需要提供带有--defaults-file选项的配置文件。下面显示了启动本教程中使用的服务器实例的命令。请注意,添加了一个重定向,将来自服务器的消息放在日志文件中。

$ mysqld --defaults-file=primary.cnf > primary_output.txt 2>&1 &
$ mysqld --defaults-file=secondary1.cnf > secondary1_output.txt 2>&1 &
$ mysqld --defaults-file=secondary2.cnf > secondary2_output.txt 2>&1 &
$ mysqld --defaults-file=secondary3.cnf > secondary3_output.txt 2>&1 &

运行这些命令时,应该从包含配置文件的文件夹中运行它们。否则,您必须提供配置文件的完整路径。虽然命令中包含重定向,但您可能希望在第一次启动服务器时使用单独的终端,以确保没有错误。清单 9-3 显示了启动主服务器时打印的消息摘录。

$ mysqld --defaults-file=primary.cnf
MacBook-Pro-2:gr cbell$ 2019-05-05T21:00:42.388965Z 0 [System] [MY-010116] [Server] /usr/local/mysql-8.0.16-macos10.14-x86_64/bin/mysqld (mysqld 8.0.16) starting as process 935
2019-05-05T21:00:42.392064Z 0 [Warning] [MY-010159] [Server] Setting lower_case_table_names=2 because file system for /Users/cbell/gr/data/primary/ is case insensitive
2019-05-05T21:00:42.662123Z 0 [Warning] [MY-010068] [Server] CA certificate ca.pem is self signed.
2019-05-05T21:00:42.676713Z 0 [System] [MY-010931] [Server] /usr/local/mysql-8.0.16-macos10.14-x86_64/bin/mysqld: ready for connections. Version: '8.0.16'  socket: '/Users/cbell/rpl/primary.sock'  port: 24801  MySQL Community Server - GPL.
2019-05-05T21:00:42.895674Z 0 [System] [MY-011323] [Server] X Plugin ready for connections. Socket: '/Users/cbell/rpl/primaryx.sock' bind-address: '::' port: 33061

Listing 9-3Starting the Primary Instance

同样,如果您计划使用单个终端,建议将输出重定向到一个文件,并使用选项在另一个进程中启动应用(例如,&符号)。

如果您正在按照本教程进行操作,并且还没有这样做,请继续操作并启动辅助服务器。使用前面显示的命令重定向到一个文件。一旦启动了所有的服务器实例,我们就可以进入下一步了。

安装组复制插件

如果您不想在配置文件中包含组复制插件,您可以使用INSTALL PLUGIN命令手动安装。完成后,您不必再次运行该命令——服务器将重新启动并启用插件。该命令需要插件的名称以及可动态加载的可执行文件的名称。在这种情况下,插件的名字是group_replication,可加载的可执行文件的名字是group_replication.so。因此,我们将使用的命令是INSTALL PLUGIN group_replication SONAME 'group_replication.so'

注意文件名中的.so。这是您将用于*nix 平台的扩展。在 Windows 上,文件扩展名是.dll。你可以用SHOW PLUGINS命令检查插件的状态。

您必须在所有实例上运行该命令。一旦插件加载到所有实例上,我们就可以继续在所有实例上创建复制用户。

创建复制用户帐户

MySQL 实例启动后,您必须创建一个用户,供服务器相互连接使用。回想一下,在组复制中,服务器都相互“对话”。幸运的是,这些命令与我们在 MySQL 复制中使用的命令相同。我们需要在所有服务器实例上创建这个用户。下面显示了创建复制用户所需的命令。在所有服务器上执行这些命令。

SET SQL_LOG_BIN=0;
CREATE USER rpl_user@'%' IDENTIFIED BY 'rpl_pass';
GRANT REPLICATION SLAVE ON ∗.∗ TO rpl_user@'%';
FLUSH PRIVILEGES;
SET SQL_LOG_BIN=1;

注意主机名中使用了%。这样做是为了确保复制用户可以从任何服务器连接。对于生产环境,您通常不会这样做,但是对于教程或开发测试,这使事情变得简单了一些。

回想一下,这些命令告诉服务器暂时禁止记录对二进制日志的更改。每当我们不想在拓扑中的其他机器上复制命令时,我们就这样做。具体来说,不应该复制像创建用户这样的维护和管理命令。关闭二进制日志是确保您不会意外发出无法在其他机器上执行的事务的好方法。

执行这些命令的最佳方式是将它们保存到名为create_rpl_user.sql的文件中,并使用 mysql 客户端的源命令从文件中读取命令并执行它们。因此,您可以使用以下命令在所有实例上快速创建复制用户。

$ mysqlsh --uri root@localhost:24801 --sql -f "create_rpl_user.sql"
$ mysqlsh --uri root@localhost:24802 --sql -f "create_rpl_user.sql"
$ mysqlsh --uri root@localhost:24803 --sql -f "create_rpl_user.sql"
$ mysqlsh --uri root@localhost:24804 --sql -f "create_rpl_user.sql"

现在,我们准备在主节点上启动组复制。

在主节点上启动组复制

下一步是首次在主节点上启动组复制。回想一下我们对组复制变量的讨论,变量group_replication_bootstrap_group通常被设置为OFF,除非在组的第一次启动时。由于该组从未启动过,因此我们必须在主服务器上启动。

幸运的是,变量group_replication_bootstrap_group是动态的,我们可以动态地打开和关闭它。因此,我们可以在主节点上运行以下命令来首次启动组复制。

$ mysqlsh --uri root@localhost:24801 --sql
...
SET GLOBAL group_replication_bootstrap_group=ON;
START GROUP_REPLICATION;
SET GLOBAL group_replication_bootstrap_group=OFF;

如果您还记得,我们在主配置文件中将group_replication_bootstrap_group设置为OFF。这样,如果我们重新启动主服务器,设置将是正确的。如果您愿意,您可以设置它ON,但是您必须在重新启动主服务器之前在配置文件中更改它。设置为OFF安全多了,工作量也少了。

如果您正在学习本教程,那么现在就在主服务器上运行这些命令。完成后,您就可以将辅助节点连接到主节点了。

将辅助节点连接到主节点

下一步是将辅助节点连接到主节点。我们使用与上一教程中相同的CHANGE MASTER命令,但是,我们只需要复制用户和密码。我们告诉服务器连接到名为group_replication_recovery的特殊复制通道。下面显示了用于将每个辅助节点连接到主节点的命令。

CHANGE MASTER TO MASTER_USER="rpl_user", MASTER_PASSWORD="rpl_pass" FOR CHANNEL 'group_replication_recovery';

注意,我们需要的信息甚至比启用了 GTID 的复制还要少。酷!您必须在所有辅助节点上运行该命令。将它保存到一个文件并使用 mysql 客户端执行它可能更容易,就像我们为复制用户所做的那样。例如,将它保存到一个名为change_master.sql的文件中,并按如下方式执行。

$ mysqlsh --uri root@localhost:24802 --sql -f "change_master.sql"
$ mysqlsh --uri root@localhost:24803 --sql -f "change_master.sql"
$ mysqlsh --uri root@localhost:24804 --sql -f "change_master.sql"

现在,我们已经将辅助节点配置为连接主节点,我们必须通过启动组复制来完成该过程。

在辅助节点上启动组复制

下一步是在辅助节点上启动组复制。组复制不像 MySQL 复制那样使用START SLAVE命令,而是使用START GROUP_REPLICATION命令。如下所示,在每个辅助节点上运行此命令。

$ mysqlsh --uri root@localhost:24802 --sql -e "START GROUP_REPLICATION"
$ mysqlsh --uri root@localhost:24803 --sql -e "START GROUP_REPLICATION"
$ mysqlsh --uri root@localhost:24804 --sql -e "START GROUP_REPLICATION"

START GROUP_REPLICATION命令通常不会报告任何错误,可能需要更长时间才能返回。这是因为当辅助节点连接到主节点并开始与主节点协商时,有许多事情在后台进行。然而,与 MySQL 复制不同,您不能使用SHOW SLAVE STATUS来检查状态。事实上,发出那个命令不会得到任何结果。那你是做什么的?

验证组复制状态

组复制重新设计了我们监视复制服务的方式。组复制向performance_schema数据库添加了几个视图,您可以使用这些视图来监控组复制。那里有很多信息,如果你感兴趣,你可以查看 https://dev.mysql.com/doc/refman/8.0/en/group-replication-monitoring.html 来了解更多关于视图和它们包含的内容。

检查组复制状态需要针对performance_schema视图发出查询。replication_group_members视图(表)用于监视不同服务器实例的状态,这些实例在当前视图中被跟踪,或者换句话说,它们是组的一部分,因此由成员资格服务跟踪。该信息在作为复制组成员的所有服务器实例之间共享,因此可以从任何成员处查询所有组成员的信息。清单 9-4 展示了运行中的命令。您可以将命令保存在一个名为check_gr.sql的文件中,以备后用。

$ mysqlsh --uri root@localhost:24802 --sql
MySQL Shell 8.0.16

Copyright (c) 2016, 2019, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its affiliates.
Other names may be trademarks of their respective owners.

Type '\help' or '\?' for help; '\quit' to exit.
Creating a session to 'root@localhost:24802'
Fetching schema names for autocompletion... Press ^C to stop.
Your MySQL connection id is 28
Server version: 8.0.16 MySQL Community Server - GPL
No default schema selected; type \use <schema> to set one.

> SHOW SLAVE STATUS\G

Empty set (0.0003 sec)

> SELECT ∗ FROM performance_schema.replication_group_members \G

∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
  CHANNEL_NAME: group_replication_applier
     MEMBER_ID: e0b8aeca-6f7e-11e9-bd22-533c3552fe03
   MEMBER_HOST: MacBook-Pro-2.local
   MEMBER_PORT: 24801
  MEMBER_STATE: ONLINE
   MEMBER_ROLE: PRIMARY
MEMBER_VERSION: 8.0.16
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 2\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
  CHANNEL_NAME: group_replication_applier
     MEMBER_ID: e32085ac-6f7e-11e9-a081-cc47fe26c35d
   MEMBER_HOST: localhost
   MEMBER_PORT: 24901
  MEMBER_STATE: ONLINE
   MEMBER_ROLE: SECONDARY
MEMBER_VERSION: 8.0.16
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 3\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
  CHANNEL_NAME: group_replication_applier
     MEMBER_ID: e5818c4c-6f7e-11e9-a41a-c85d7844eaca
   MEMBER_HOST: localhost
   MEMBER_PORT: 24902
  MEMBER_STATE: ONLINE
   MEMBER_ROLE: SECONDARY
MEMBER_VERSION: 8.0.16
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 4\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
  CHANNEL_NAME: group_replication_applier
     MEMBER_ID: e7f23058-6f7e-11e9-a7c3-c576fe9a0754
   MEMBER_HOST: localhost
   MEMBER_PORT: 24903
  MEMBER_STATE: RECOVERING
   MEMBER_ROLE: SECONDARY
MEMBER_VERSION: 8.0.16
4 rows in set (0.0013 sec)

Listing 9-4Checking Group Replication Status

注意,我们运行了SHOW SLAVE STATUS命令,但没有得到任何回报。Drat。然而,当我们查询视图时,我们得到一个简短的信息列表,包括组中每个成员的当前状态。有趣的是,您可以对组中的任何成员运行这个查询。这显示了组复制如何将元数据传播到组中的所有成员。

您还可以缩小输出范围,以获得更令人满意的视图,其中仅包括此处显示的成员主机、端口、状态和角色。

> SELECT MEMBER_HOST, MEMBER_PORT, MEMBER_STATE, MEMBER_ROLE FROM performance_schema.replication_group_members;
+---------------------+-------------+--------------+-------------+
| MEMBER_HOST         | MEMBER_PORT | MEMBER_STATE | MEMBER_ROLE |
+---------------------+-------------+--------------+-------------+
| MacBook-Pro-2.local |       24801 | ONLINE       | PRIMARY     |
| localhost           |       24901 | ONLINE       | SECONDARY   |
| localhost           |       24902 | ONLINE       | SECONDARY   |
| localhost           |       24903 | ONLINE       | SECONDARY   |
+---------------------+-------------+--------------+-------------+
4 rows in set (0.0006 sec)

如果您只想定位主服务器,可以对任何组成员使用以下查询。当您对组中的任何一个成员执行此查询时,您将看到主服务器的 UUID 如下所示。

> SELECT member_id, member_host, member_port FROM performance_schema.global_status JOIN performance_schema.replication_group_members ON VARIABLE_VALUE=member_id WHERE VARIABLE_NAME="group_replication_primary_member";
+--------------------------------------+---------------------+-------------+
| member_id                            | member_host         | member_port |
+--------------------------------------+---------------------+-------------+
| e0b8aeca-6f7e-11e9-bd22-533c3552fe03 | MacBook-Pro-2.local |       24801 |
+--------------------------------------+---------------------+-------------+
1 row in set (0.0014 sec)

现在我们已经运行了组复制,让我们创建一些数据。我们将使用与上一教程中相同的样本数据。但是,这一次,我们将在其中一个辅助节点上执行查询。你预计会发生什么?如果您从 MySQL 复制的角度考虑,您可能希望数据只出现在其中一个辅助服务器上。让我们看看会发生什么。下面显示了在其中一个辅助节点上执行数据查询。

> CREATE DATABASE test;
ERROR: 1290 (HY000): The MySQL server is running with the --super-read-only option so it cannot execute this statement

为什么会出现这个错误?事实证明,每个辅助节点都是以超级只读启动的,这意味着任何人都不能提交写操作,即使是那些拥有“超级”权限的人。因此(从 MySQL 复制)向从服务器发送写操作的常见问题得到了解决。万岁。使用超级只读还表明我们正在单主模式下运行组复制(这是默认模式)。当我们在后面的章节中探索 InnoDB 集群的细微差别时,我们将看到其他模式。

回到我们创建一些数据的测试,让我们在主服务器上运行相同的命令。下面显示了预期的结果。

$ mysqlsh --uri root@localhost:24801 --sql
...

> CREATE DATABASE test;

Query OK, 1 row affected (0.0042 sec)

> USE test;

Query OK, 0 rows affected (0.0003 sec)

> CREATE TABLE test.t1 (id INT PRIMARY KEY, message TEXT NOT NULL);

Query OK, 0 rows affected (0.0093 sec)

> INSERT INTO test.t1 VALUES (1, 'Chuck');

Query OK, 1 row affected (0.0103 sec)

在这里,我们看到数据是创建的。现在,检查第二个。下面显示了在辅助节点上运行查询的结果。如您所见,数据已经被复制。

$ mysqlsh --uri root@localhost:24803 --sql
...

> SELECT ∗ FROM test.t1;

+----+---------+
| id | message |
+----+---------+
|  1 | Chuck   |
+----+---------+
1 row in set (0.0006 sec)

关于设置 MySQL 组复制的简短教程到此结束。本节用最简洁的术语简要介绍了 MySQL 组复制。现在,让我们看看如何编写 MySQL 组复制的示例设置。然而,还有一步。

正在关闭组复制

如果您在自己的机器上尝试本教程,请记住按照精确的顺序关闭实例。在每个辅助节点上,您首先需要使用以下命令停止组复制。

$ mysqlsh --uri root@localhost:24802 --sql -e "STOP GROUP_REPLICATION"

一旦停止了所有从服务器,您可以使用以下命令先关闭从服务器,然后关闭主服务器。注意,我们对主设备和从设备使用了旧的协议端口(13001、13002)。这是因为 X 协议不支持 shutdown 命令。如果在使用 MySQL X 端口时遇到错误,请尝试旧的协议并重新运行该命令。

$ mysqlsh --uri root@localhost:24804 --sql -e "SHUTDOWN"
$ mysqlsh --uri root@localhost:24803 --sql -e "SHUTDOWN"
$ mysqlsh --uri root@localhost:24802 --sql -e "SHUTDOWN"
$ mysqlsh --uri root@localhost:24801 --sql -e "SHUTDOWN"

故障转移演示

现在我们有了一个工作组复制设置,让我们看看自动故障转移是如何工作的。如果您还没有运行前面的教程,并且想继续学习,请确保先运行前面的步骤。

自动故障切换是组复制的内置功能。通信机制确保主节点(在单主节点配置中)的活动受到监控,当它不再可用或出现严重问题时,该组可以决定终止主节点连接并选举新的主节点。

让我们看看这是如何工作的。回想一下前面的教程,我们在端口 24801 上运行初始主服务器。我们可以通过终止该服务器的 MySQL 进程来模拟失败。因为我们运行在 Linux 上,所以我们可以通过检查进程 id 文件来确定进程 id,这个文件是 MySQL 用机器名和数据目录中的文件扩展名.pid创建的。例如,教程中显示的主文件在data/primary/oracle-pc.pid中。下面演示了如何找到进程 id 并停止它。请注意,您可能需要超级用户权限来终止该进程。

$ more ./data/primary/oracle-pc.pid
18019
$ sudo kill -9 18019

小费

在 Windows 上,您可以使用任务管理器终止该进程。

既然主服务器已经关闭,我们可以使用前面的查询来查看组的运行状况。回想一下,我们使用包含查询的check_gr.sql文件(参见清单 9-4 )。清单 9-5 显示了查询的输出。

$ mysqlsh --uri root@localhost:24802 --sql -e "source check_gr.sql"
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
  CHANNEL_NAME: group_replication_applier
     MEMBER_ID: 2854aecd-4330-11e8-abb6-d4258b76e981
   MEMBER_HOST: oracle-pc
   MEMBER_PORT: 24802
  MEMBER_STATE: ONLINE
   MEMBER_ROLE: PRIMARY
MEMBER_VERSION: 8.0.16
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 2\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
  CHANNEL_NAME: group_replication_applier
     MEMBER_ID: 2ecd9f66-4330-11e8-90fe-d4258b76e981
   MEMBER_HOST: oracle-pc
   MEMBER_PORT: 24803
  MEMBER_STATE: ONLINE
   MEMBER_ROLE: SECONDARY
MEMBER_VERSION: 8.0.16
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 3\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
  CHANNEL_NAME: group_replication_applier
     MEMBER_ID: 3525b7be-4330-11e8-80b1-d4258b76e981
   MEMBER_HOST: oracle-pc
   MEMBER_PORT: 24804
  MEMBER_STATE: ONLINE
   MEMBER_ROLE: SECONDARY
MEMBER_VERSION: 8.0.16
+--------------------------------------+-------------+-------------+
| member_id                            | member_host | member_port |
+--------------------------------------+-------------+-------------+
| 2854aecd-4330-11e8-abb6-d4258b76e981 | oracle-pc   |       24802 |
+--------------------------------------+-------------+-------------+

Listing 9-5Checking Group Health After Primary Goes Down

请注意,我们看到该组已经自动选择了一个新的主服务器(在端口 24802 上),现在该组中只有三台服务器。因此没有写能力的损失。但是,回想一下前面的讨论,该组只能容忍这么多故障,一旦达到该限制,该组将无法成功进行故障切换,在这种情况下,该组可能无法容错。清单 9-6 显示了第二台和第三台主机停止后同一组的状态。请注意,最后一个主节点的状态未知。

$ mysqlsh --uri root@localhost:24804 --sql -e "source check_gr.sql"
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
  CHANNEL_NAME: group_replication_applier
     MEMBER_ID: 2ecd9f66-4330-11e8-90fe-d4258b76e981
   MEMBER_HOST: oracle-pc
   MEMBER_PORT: 24803
  MEMBER_STATE: UNREACHABLE
   MEMBER_ROLE: PRIMARY
MEMBER_VERSION: 8.0.16
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 2\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
  CHANNEL_NAME: group_replication_applier
     MEMBER_ID: 3525b7be-4330-11e8-80b1-d4258b76e981
   MEMBER_HOST: oracle-pc
   MEMBER_PORT: 24804
  MEMBER_STATE: ONLINE
   MEMBER_ROLE: SECONDARY
MEMBER_VERSION: 8.0.16
+--------------------------------------+-------------+-------------+
| member_id                            | member_host | member_port |
+--------------------------------------+-------------+-------------+
| 2ecd9f66-4330-11e8-90fe-d4258b76e981 | oracle-pc   |       24803 |
+--------------------------------------+-------------+-------------+

Listing 9-6State of the Group When No More Primary Servers Remain

在这里,我们可以看到其中一个辅助节点确实接管了主节点的角色,并且该组容忍了故障。但是,请注意,由于我们从四台服务器开始,我们只能容忍一台服务器出现故障。但是,我们可以随时添加新的辅助节点,以提高组可以容忍的故障数量。例如,我们可以修复出现故障的辅助节点,并将其添加回组中。

摘要

不可否认,组复制是 MySQL 高可用性的一次飞跃。然而,正如我们在本章的教程中所看到的,设置起来并不简单。虽然那些熟悉 MySQL 复制的人会看到类似的过程,但有一些额外的步骤和略有不同的命令,配置文件中的变化最多,但那些不熟悉 MySQL 和高可用性的人可能会觉得学习曲线相当陡峭。

在本章中,我们对组复制进行了初步了解,并体验了最初以及在一两次故障期间设置和维护组的过程。如果你是那些认为一定有更好的方法的人之一,那就有,我们就快到了!

在下一章中,我们将了解 MySQL 中最新、最好的高可用性特性——InnoDB Cluster,并使用 MySQL Shell 来管理它。

十、将 Shell 与 InnoDB 集群一起使用

到目前为止,我们已经了解了什么是高可用性,以及如何使用 MySQL 复制设置一个基本的高可用性安装。我们还学习了如何配置组复制以获得更好的高可用性。然而,我们在这个过程中了解到,完成所有这些工作的设置和命令是特定的,需要比大多数人想要的更多的手动设置和配置步骤。幸运的是,Oracle 听取了客户的意见,并一直致力于 MySQL 高可用性,不仅大幅改进了特性和功能,还提高了易用性(或管理)。这是 InnoDB 集群真正闪光的地方。

InnoDB Cluster 代表了 MySQL 在高可用性方面的一次巨大飞跃。最重要的是,它是 MySQL 8.0 所有版本的标准配置。在本章中,我们将发现是什么使 InnoDB Cluster 成为大大小小企业的重要特征。正如您将看到的,InnoDB Cluster 由几个组件组成,这些组件可以很好地协同工作,并且可以立即为 MySQL 实现最高形式的高可用性!

概观

MySQL 8.0 中最令人兴奋的新特性之一是 InnoDB 集群。它旨在使高可用性更易于设置、使用和维护。InnoDB Cluster 通过 MySQL Shell 和 AdminAPI、组复制和 MySQL 路由器与 X DevAPI 一起工作,将高可用性和读取可伸缩性提升到一个新的水平。

也就是说,它将 InnoDB 存储引擎(MySQL 的内部)中用于克隆数据的新功能与组复制和 MySQL 路由器相结合,提供了一种在应用层设置和管理高可用性的新方法。下面的列表描述了构成 InnoDB 集群的组件。我们之前已经看到了其中的大部分,我们将在接下来的章节中了解更多,并在下一章中详细介绍如何使用它们。

  • 组复制:一种新的复制形式,建立在 MySQL 复制的基础上,增加了一个活动的通信协议(组成员),允许更高级别的可用性,包括自动故障转移的容错能力。

  • MySQL Shell:JavaScript 和 Python 的新 MySQL 客户端和编程环境,以及我们将看到的 InnoDB 集群的管理控制台。

  • X DevAPI :一个特殊的应用编程接口,供应用以编程方式与数据进行交互。

  • AdminAPI :是一个特殊的 API,可以通过 MySQL Shell 配置 InnoDB 集群并与之交互。因此,AdminAPI 具有旨在简化 InnoDB 集群工作的特性。

  • MySQL 路由器:轻量级的中间件,在你的应用和后端 MySQL 服务器之间提供透明的路由。我们将在后面的章节中学习更多关于 MySQL 路由器的知识。

您可能想知道所有这些产品和功能如何无缝地协同工作。正如您将看到的,通过 MySQL Shell 使用 InnoDB Cluster 隐藏了单独使用组件的许多细节(和繁琐)。例如,我们不再必须为复制编写专门的配置文件。

让我们看一个概念性的配置,以了解组件是如何交互的。在此使用案例中,一个由三台服务器组成的集群设置了一个主服务器,它是所有写入(更新)的目标。多个辅助服务器维护数据的副本,这些副本可以被读取,因此能够在不给主服务器增加负担的情况下读取数据,从而实现读取的可伸缩性(但是所有服务器都参与协商和协调)。

组复制的引入意味着集群是容错的,并且组成员是自动管理的。MySQL 路由器缓存 InnoDB 集群的元数据,并执行到 MySQL 服务器实例的高可用性路由,从而更容易编写应用来与集群进行交互。图 10-1 显示了这些组件是如何在概念上排列以形成 InnoDB 集群的。

img/478423_1_En_10_Fig1_HTML.jpg

图 10-1

InnoDB 集群的典型配置(由 Oracle 提供)

您可能想知道这与使用标准复制的读出可伸缩性设置有何不同。从高层次来看,这些解决方案似乎正在解决同一个用例。但是,使用 InnoDB Cluster,您可以从 MySQL Shell 创建、部署和配置集群中的服务器。也就是说,您可以通过 shell 使用 X AdminAPI(也称为 AdminAPI)使用 JavaScript 或 Python 以编程方式创建和管理 InnoDB 集群。

此外,您可以通过 MySQL Shell 在沙盒中部署 InnoDB 集群。更具体地说,您可以部署一个在本地计算机上运行的测试集群,并在将其部署到生产环境之前进行试验。幸运的是,所有的步骤都是相同的,只是在一些函数名中添加了关键字sandbox和一些用于创建本地 MySQL 实例的额外函数。我们将在第十一章中看到在沙箱中运行 InnoDB 集群的深入报道。

什么是沙盒?

沙箱是一个用来描述数据和元数据(配置文件等)的情况的术语。)的组织方式使得数据和元数据不会影响(替换)产品的现有数据或安装。在 MySQL AdminAPI 的例子中,它实现的沙箱确保 InnoDB 集群中服务器的任何配置都不会影响机器上 MySQL 的任何现有安装。

在生产环境中部署 InnoDB 集群需要单独设置服务器,然后从 shell 连接到它们并准备它们。同样,这一步与沙盒部署中的步骤相同。唯一的区别是您使用的是现有的正在运行的 MySQL 服务器,而不是运行在本地机器上的另一个实例。

现在我们已经对 InnoDB 集群有了一个大致的了解,让我们从 InnoDB 存储引擎开始,看看 InnoDB 集群的核心组件。

什么是存储引擎?

存储引擎是一种以各种方式存储数据的机制。例如,有一种存储引擎允许您与逗号分隔值(文本)文件(CSV)进行交互,另一种为写日志文件(归档)进行了优化,一种只在内存中存储数据(内存),甚至还有一种根本不存储任何东西(黑洞)。除了 InnoDB,MySQL 服务器还附带了几个存储引擎。以下部分描述了一些更常用的替代存储引擎。请注意,随着 MySQL 的发展,一些存储引擎已经不再受支持,包括 Berkeley Database (BDB)存储引擎。

InnoDB 存储引擎

InnoDB 集群的核心组件是 InnoDB 存储引擎。自 MySQL 5.5 以来,InnoDB 一直是 MySQL 的旗舰存储引擎(也是默认引擎)。Oracle 已经慢慢脱离了多存储引擎模型,专注于现代数据库服务器应该做的事情—支持事务存储机制。InnoDB 是满足这一需求的答案。

InnoDB 是一个通用存储引擎,它平衡了高可靠性和高性能。使用 InnoDB 存储引擎的决定是在多次尝试为 MySQL 构建一个健壮、高性能的存储引擎之后做出的。考虑到 InnoDB 的成熟性和复杂性,使用已经存在的东西更有意义。另外,甲骨文同时拥有 MySQL 和 InnoDB。

当您需要使用事务时,可以使用 InnoDB 存储引擎。InnoDB 支持传统的 ACID 事务和外键约束。InnoDB 中的所有索引都是 B 树,索引记录存储在树的叶页面中。InnoDB 是高可靠性和事务处理环境的首选存储引擎。

酸代表原子性、一致性、隔离性和持久性。也许是数据库理论中最重要的概念之一,它定义了数据库系统必须表现出的行为,才能被认为是可靠的事务处理。下面简要描述每个方面。

  • 原子性意味着对于包含多个命令的事务,数据库必须允许在“全有或全无”的基础上修改数据。也就是说,每个事务都是原子的。如果命令失败,则整个事务失败,并且事务中到该点为止的所有更改都将被丢弃。这对于在高交易环境(如金融市场)中运行的系统尤其重要。

  • 一致性意味着只有有效的数据才会存储在数据库中。也就是说,如果事务中的命令违反了一致性规则之一,则整个事务将被丢弃,数据将返回到事务开始之前的状态。相反,如果事务成功完成,它将以遵守数据库一致性规则的方式更改数据。

  • 隔离意味着同时执行的多个事务不会互相干扰。这是并发性的真正挑战最明显的地方。数据库系统必须处理事务(变更、删除等)的情况。)不能违反正在另一个事务中使用的数据。有很多方法可以解决这个问题。大多数系统使用一种称为锁定的机制,在第一个事务完成之前,防止数据被另一个事务使用。尽管隔离属性没有规定先执行哪个事务,但它确实确保了它们不会相互干扰。

  • 持久性意味着任何交易都不会导致数据丢失,在交易过程中创建或更改的任何数据也不会丢失。耐用性通常由强大的备份和恢复维护功能提供。一些数据库系统使用日志记录来确保任何未提交的数据可以在重启时恢复。

也许 InnoDB 区别于 MySQL 早期存储引擎的最重要的特性是它的可配置性。虽然一些早期的存储引擎是可配置的,但没有一个能够达到配置 InnoDB 的规模。您可以使用几十个参数来调整 InnoDB,以满足您独特的存储需求。

警告

修改 InnoDB 参数时要小心。有可能会使您的系统降级到损害性能的程度。像任何调优练习一样,总是先查阅文档(和专家),然后计划针对特定的参数。确保一次调整一个参数,并在继续之前进行测试、确认或恢复。

虽然 InnoDB 使用精心选择的缺省值,开箱即可工作,而且对大多数人来说可能不需要太多的调优,但那些需要调优 MySQL 的人会发现他们需要的一切,甚至更多,以使他们的数据库系统以最高效率运行。

参见 https://dev.mysql.com/doc/refman/8.0/en/innodb-introduction.html 了解有关 InnoDB 存储引擎的更多信息,包括众多配置和调优选项。

小费

关于配置 MySQL 和 InnoDB 的技巧和建议的另一个很好的来源是特卡琴科扎依采夫 Schwartz 的书高性能 MySQL:优化、备份、复制(O ' Reilly,2012)。

为了更好地理解我们是如何实现 InnoDB 集群的,让我们简短地浏览一下 MySQL 8.0 和早期版本中的其他存储引擎。

小费

如果您想查看 MySQL 服务器上有哪些可用的存储引擎,可以使用SHOW ENGINES命令。参见 https://dev.mysql.com/doc/refman/8.0/en/create-table.html 了解更多如何使用CREATE TABLE命令指定存储引擎。

档案馆

归档存储引擎旨在以压缩格式存储大量数据。存档存储机制最适合用于存储和检索大量很少访问的存档或历史数据。这种数据包括安全访问数据日志。虽然这不是您想要搜索甚至日常使用的东西,但是如果发生安全事故,关心安全的数据库专业人员会希望拥有它。没有为归档存储机制提供索引,唯一的访问方法是通过表扫描。因此,归档存储引擎不应用于正常的数据库存储和检索。

黑洞

黑洞存储引擎是一个有趣的功能,具有惊人的效用,它被设计成允许系统写入数据,但数据永远不会被保存。但是,如果启用了二进制日志记录,SQL 语句将被写入日志。这允许数据库专业人员通过切换表类型来临时禁用数据库中的数据接收。在您希望测试应用以确保它正在写入您不想存储的数据的情况下,这可能很方便,例如在创建用于过滤复制的中继从属服务器时。

战斗支援车

CSV 存储引擎旨在以表格形式创建、读取和写入逗号分隔值(CSV)文件。虽然 CSV 存储引擎不会将数据复制为另一种格式,但图纸布局或元数据会与服务器上指定的文件名一起存储在数据库文件夹中。这允许数据库专业人员快速导出存储在电子表格中的结构化业务数据。CSV 存储引擎不提供任何索引机制,因此对于大量数据来说不切实际。它旨在用作存储数据和在电子表格应用中可视化数据之间的链接。

联邦的

联邦存储引擎旨在从多个数据库系统创建单个表引用。因此,联邦存储引擎的工作方式类似于合并存储引擎,但是它允许您跨数据库服务器将数据(表)链接在一起。这种机制在目的上类似于其他数据库系统中可用的链接数据表。联邦存储机制最适合在分布式或数据集市环境中使用。

联邦存储引擎最有趣的方面是它不移动数据,也不要求远程表是同一个存储引擎。这说明了可插拔存储引擎层的真正威力。数据在存储和检索过程中被转换。

记忆

内存存储引擎(有时称为堆表)是一个内存中的表,它使用哈希机制来快速检索经常使用的数据。因此,这些表比那些从磁盘存储和引用的表要快得多。它们的访问方式与其他存储引擎相同,但是数据存储在内存中,并且仅在 MySQL 会话期间有效。数据在关机(或崩溃)时被刷新和删除。

内存存储引擎通常用于静态数据被频繁访问且很少被更改的情况。这种情况的例子包括邮政编码、州、县、类别和其他查找表。堆表也可以用于利用快照技术进行分布式或历史数据访问的数据库中。

我的天

MyISAM 存储引擎最初是 MySQL 中的默认引擎,被大多数 LAMP 堆栈、数据仓库、电子商务和企业应用使用。MyISAM 文件是 ISAM 的扩展,通过额外的优化构建,如高级缓存和索引机制。这些表是使用压缩特性和索引优化来提高速度的。

此外,MyISAM 存储引擎通过提供表级锁定来支持并发操作。MyISAM 存储机制为各种应用提供可靠的存储,同时提供快速的数据检索。当考虑读取性能时,MyISAM 是首选的存储引擎。

合并(MyISAM)

合并存储引擎(有时称为 MRG _ 我的表)是使用一组具有相同结构(元组布局或模式)的我的表构建的,这些表可以作为单个表引用。因此,表是根据各个表的位置进行分区的,但是没有使用额外的分区机制。所有表必须驻留在同一台机器上(由同一台服务器访问)。使用单个操作或语句来访问数据,例如SELECTUPDATEINSERTDELETE。幸运的是,当在合并表上发出DROP时,只有合并规范被删除。原始表格没有改变。

这种表类型最大的好处就是速度。可以将一个大表分割成不同磁盘上的几个小表,使用合并表规范将它们组合起来,并同时访问它们。搜索和排序将执行得更快,因为每个表中需要操作的数据更少。例如,如果按谓词划分数据,则可以只搜索包含要搜索的类别的特定部分。同样,对表的修复更有效,因为修复几个较小的单个文件比修复单个大表更快更容易。据推测,大多数错误将局限于一个或两个文件内的区域,因此不需要重建和修复所有数据。不幸的是,这种配置有几个缺点:

  • 您只能使用相同的 MyISAM 表或架构来形成一个合并表。这限制了合并存储引擎在 MyISAM 表中的应用。如果合并存储引擎接受任何存储引擎,合并存储引擎将更加通用。

  • 不允许替换操作。

  • 已经证明索引访问比单个表的效率低。

合并存储机制最适合用在超大型数据库(VLDB)应用中,例如数据驻留在一个或多个数据库的多个表中的数据仓库。

性能模式

性能模式存储引擎是一个特殊的报告引擎,用于在较低的级别监控 MySQL 服务器的执行。虽然它显示在可用存储引擎列表中,但它不是存储数据的可用选项。

现在,我们对什么是存储引擎,尤其是 InnoDB 存储引擎有了更多的了解,让我们看看 InnoDB 集群的其他组件。

组复制

如果您使用过 MySQL 复制,那么您无疑非常熟悉如何在构建高可用性解决方案时利用它。事实上,您很可能已经发现了许多使用 MySQL 复制来提高应用可用性的方法。而且,如果你一直在看这本书,你也已经了解了组复制。

组复制于 2016 年 12 月作为 GA 发布(从 5.7.17 版本开始),以插件的形式与 MySQL 服务器捆绑。因为组复制是作为服务器插件实现的,所以您可以安装插件并开始使用组复制,而不必重新安装服务器,这使得试验新功能变得容易。

回想一下,组复制也使同步复制(在属于同一个组的节点之间)成为现实,而现有的 MySQL 复制特性是异步的(或者至多是半同步的)。因此,在任何时候都提供了更强的数据一致性(数据在所有成员上都可用,没有延迟)。

我们了解到,组复制和标准复制之间的另一个重要区别是,组中的所有服务器都可以参与更新数据,并自动解决冲突。但是,您可以将组复制配置为只允许一台服务器(称为主服务器)进行更新,其他服务器充当辅助服务器或备份服务器(用于故障转移)。

因此,组复制是 InnoDB 集群的核心组件也就不足为奇了。我们获得了组复制的所有好处,而没有配置和维护组的复杂性。

MySQL Shell

回想一下,MySQL Shell 被设计为使用新的 X 协议通过 X 插件与服务器进行通信,这允许 Shell 与 MySQL 服务器及其组件进行通信,公开新的 API 来处理数据和管理。我们在第六章和第七章中学习了 X DevAPI。管理 API 被称为 X AdminAPI,它允许 shell 与 InnoDB Cluster 进行设置和管理通信。

X DevAPI

回想一下,X DevAPI 是一个类库和方法库,为 MySQL 实现了一个新的 NoSQL 接口。具体来说,X DevAPI 旨在允许与 JSON 文档和关系数据轻松交互。X DevAPI 有专门支持这两个概念的类,允许开发人员在他们的应用中使用其中一个(或两个)。X DevAPI 允许我们使用 JavaScript 或 Python 处理 MySQL,并与 AdminAPI 结合使用,为管理 InnoDB 集群提供了一个强大的机制。

小费

参见 https://dev.mysql.com/doc/x-devapi-userguide/en/ 了解更多关于 X DevAPI 的信息。

AdminAPI

管理应用编程接口,即 AdminAPI,是一个类库和方法库,为 InnoDB 集群实现了一个新的管理接口。具体来说,AdminAPI 旨在允许使用 MySQL Shell 中的脚本语言与 InnoDB Cluster 轻松交互。因此,MySQL Shell 包含 AdminAPI,它使您能够部署、配置和管理 InnoDB 集群。AdminAPI 包含两个用于访问 InnoDB 集群功能的类,如下所示。

  • dba:使您能够使用 AdminAPI 管理 InnoDB 集群。dba 类使您能够管理集群,例如创建新的集群、使用沙盒配置(一种在同一台机器上使用几个 MySQL 实例来试验 InnoDB 集群、检查实例和集群的状态的方法)。

  • cluster:InnoDB 集群的管理句柄。cluster 类使您能够使用集群来添加实例、删除实例、获取集群的状态(健康状况)等。

随着我们探索如何设置和管理 InnoDB 集群,我们将在第十一章中看到更多的 AdminAPI。

小费

参见 https://dev.mysql.com/doc/dev/mysqlsh-api-python/8.0/group___admin_a_p_i.html 了解更多关于 AdminAPI 的信息。

MySQL 路由器

MySQL 路由器是 MySQL 中相对较新的组件。它最初是为现在已经过时的 MySQL Fabric 产品而构建的,经过了显著的改进和修改,可以与 InnoDB Cluster 一起使用。事实上,它是 InnoDB 集群的重要组成部分。

MySQL 路由器是一个轻量级的中间件组件,在应用和 MySQL 服务器之间提供透明的路由。虽然它可以用于各种各样的用例,但它的主要目的是通过有效地将数据库流量路由到适当的 MySQL 服务器来提高高可用性和可伸缩性。

传统上,对于处理故障转移的客户端应用,它们需要了解 InnoDB 集群拓扑,并知道哪个 MySQL 实例是主(写)服务器。虽然应用可以实现这种逻辑,但是 MySQL 路由器可以为您提供和处理这种功能。

此外,当与 InnoDB Cluster 一起使用时,MySQL Router 充当代理来隐藏网络上的多个 MySQL 实例,并将数据请求映射到集群中的一个实例。如果有足够多的在线副本,并且组件之间的通信完好无损,应用将能够(重新)连接到其中一个副本。MySQL 路由器通过简单地重新打印应用来连接到路由器而不是直接连接到 MySQL,也使这种情况成为可能。

小费

参见 https://dev.mysql.com/doc/mysql-router/8.0/en/ 了解更多关于 MySQL 路由器的信息。

Innodb 集群和 NDB 集群的区别是什么?

如果您仔细阅读 MySQL 网站,您会发现另一个名称中带有“cluster”的产品。它被迷人地命名为 NDB 星团。NDB 集群是一个独立于 MySQL 服务器的产品,它采用了一种技术,能够在无共享系统中实现内存数据库的集群。无共享架构使系统能够与廉价的硬件一起工作,并且对硬件或软件的特定要求最小。

NDB 集群的设计不存在任何单点故障。在无共享系统中,每个组件都有自己的内存和磁盘,不建议或不支持使用共享存储机制,如网络共享、网络文件系统和 San。See https://dev.mysql.com/doc/refman/8.0/en/mysql-cluster-compared.html 了解更多关于 NDB 集群及其与 InnoDB 的关系。

要了解更多关于 NDB 集群的信息,请参阅 Krogh 和 Okuno 的优秀书籍《Pro MySQL NDB 集群》。这本书涵盖了 NDB 集群的各个方面,是任何对部署和管理 NDB 集群感兴趣的人的必读之作。

在应用中使用 InnoDB

您可能想知道 InnoDB 集群如何在您的环境中发挥作用(甚至想知道如何使用它)。也就是说,我们知道 InnoDB 集群的好处是更好的高可用性形式,但是 MySQL 路由器和我们的应用如何适应这种情况呢?

当与 MySQL InnoDB Cluster(通过底层组复制)一起使用以跨多个服务器复制数据库,同时在服务器出现故障时执行自动故障转移时,路由器充当代理来隐藏网络上的多个 MySQL 实例,并将数据请求映射到其中一个集群实例。

如果有足够的在线副本并且可靠的网络通信是可能的,那么使用该路由器的应用将能够联系剩余的服务器之一。路由器通过让应用连接到 MySQL 路由器而不是直接连接到特定的 MySQL 服务器来实现这一点。图 10-2 显示了路由器相对于您的应用和 InnoDB 集群所处位置的逻辑视图。

img/478423_1_En_10_Fig2_HTML.jpg

图 10-2

使用 MySQL 路由器的应用架构(由 Oracle 公司提供)

注意,我们描述了两个应用,每个都连接到 MySQL 路由器的一个实例。路由器的工作原理是位于应用和 MySQL 服务器之间。当一个应用连接到路由器时,路由器从候选服务器池中选择一个合适的 MySQL 服务器,并将所有网络流量从应用转发到该服务器(以及从服务器返回的响应到应用)。

在后台,路由器存储来自 InnoDB 集群的服务器列表及其状态。虽然服务器列表(或缓存)最初是从配置文件中读取的,但路由器和集群之间的后续通信确保了它在拓扑结构发生变化时得到更新。这意味着当服务器丢失时,它们会被路由器标记为离线,路由器会跳过它们。同样,如果集群中添加了新的服务器,路由器的缓存也会更新以包含这些服务器。

因此,为了保持高速缓存更新,路由器保持与集群中的一个服务器的开放连接,从性能模式数据库查询集群元数据。每当检测到集群状态发生变化时,组复制就会实时更新这些表(或视图)。例如,如果一个 MySQL 服务器意外关闭。

最后,该路由器使开发人员能够使用插件为定制用例扩展 MySQL 路由器。这意味着,如果您的应用需要不同的路由策略,或者您想在解决方案中构建数据包检测,您可以使用自定义插件来扩展路由器。虽然为路由器构建定制插件超出了本章的范围,也没有例子可以学习,但是一定要查看 MySQL 开发者网站( dev。mysql。com )和 MySQL 工程博客( https://mysqlserverteam.com/ )获取最新信息和示例。

现在我们已经了解了构成 InnoDB 集群的组件,让我们简单讨论一下设置和配置 InnoDB 集群需要做些什么。我们将在下一章演示 InnoDB 集群。

设置和配置

除了我们在第二章中所做的,InnoDB 集群的设置和配置不需要任何新的复杂步骤。如果你还记得在第二章的 MySQL 安装过程中,我们没有为 MySQL 服务器和 InnoDB 集群选择沙盒选项。我们也没有选择自动配置 MySQL 路由器。如果您选择了这些选项,您就可以让 InnoDB Cluster 运行在一个沙箱中,并准备好使用 MySQL 路由器。

虽然您可以通过重新运行安装来完成(例如,在 Windows 上,运行 MySQL Installer for Windows),但我们将使用更传统的手动设置,以便您可以了解如何设置 InnoDB Cluster 和 MySQL Router,这将为您在环境中部署 InnoDB Cluster 和 MySQL Router 提供更好的基础。

准备您的系统运行 InnoDB Cluster 作为测试的过程并不困难,其中几个 MySQL 实例在本地运行,并且配置路由器也运行。我们将在本章中看到该过程的概述,并确保第十一章的演示。但首先,让我们更深入地讨论一个在规划新的企业级功能(如 InnoDB Cluster)时经常出现的话题——升级现有服务器。

升级检查器

使用 InnoDB 集群有几个先决条件。首先也是最重要的是,你必须使用一个支持包括组复制在内的所有组件的 MySQL 版本。幸运的是,还有一个更重要的步骤可以确保 MySQL 服务器为 InnoDB 集群做好准备。您可以使用 MySQL Shell 升级检查工具来检查每台服务器。

升级检查器是 MySQL Shell 中的一项功能,它允许您验证服务器是否已针对升级进行了正确配置。如果您正在使用较旧版本的 MySQL 或一组具有不同 MySQL 版本的服务器,升级检查器实用程序可以在部署组复制或 InnoDB 集群等功能时为您省去许多麻烦。

升级检查器适用于较新版本的 MySQL,但也可以用于 MySQL 5.7 服务器,检查它们的兼容性和安装中的错误。从 8.0.16 版开始,升级检查器还可以检查配置文件中具有非默认值的系统变量。

要使用升级检查器,只需运行 shell 并连接到您想要检查的 MySQL 服务器。最好总是在同一台服务器上运行 shell,但是除了检查配置文件之外,这不是必需的。

升级检查器函数包含在util库中,命名为util.check_for_server_upgrade()。升级检查器可以接受两个可选参数:1)到服务器的连接信息(如果还没有建立的话),以及 2)选项字典。有许多选项,包括以下选项。

  • targetVersion:您计划升级到的 MySQL 服务器版本

  • configPath:MySQL 服务器实例的配置文件路径

  • outputFormat:显示结果的格式——TEXTJSON

为了更好地使用升级检查器,下面演示了在较旧的 MySQL 5.7.22 服务器上运行该实用程序。我们将检查它是否可以升级到 8.0.16,我们将请求文本输出(默认)。正如您将看到的,该实用程序检查了相当多的东西。清单 10-1 显示了运行实用程序的完整输出。

> util.check_for_server_upgrade('root@localhost:13000', {'targetVersion':'8.0.16', 'outputFormat':'TEXT'})
The MySQL server at localhost:13000, version 5.7.22-log - MySQL Community
Server (GPL), will now be checked for compatibility issues for upgrade to MySQL
8.0.16...

1) Usage of old temporal type
  No issues found

2) Usage of db objects with names conflicting with reserved keywords in 8.0
  No issues found

3) Usage of utf8mb3 charset
  No issues found

4) Table names in the mysql schema conflicting with new tables in 8.0
  No issues found

5) Foreign key constraint names longer than 64 characters
  No issues found

6) Usage of obsolete MAXDB sql_mode flag
  No issues found

7) Usage of obsolete sql_mode flags
  No issues found

8) ENUM/SET column definitions containing elements longer than 255 characters
  No issues found

9) Usage of partitioned tables in shared tablespaces
  No issues found

10) Usage of removed functions
  No issues found

11) Usage of removed GROUP BY ASC/DESC syntax

  No issues found

12) Removed system variables for error logging to the system log configuration
  To run this check requires full path to MySQL server configuration file to be specified at 'configPath' key of options dictionary
  More information:
    https://dev.mysql.com/doc/relnotes/mysql/8.0/en/news-8-0-13.html#mysqld-8-0-13-logging

13) Removed system variables
  To run this check requires full path to MySQL server configuration file to be specified at 'configPath' key of options dictionary
  More information:
    https://dev.mysql.com/doc/refman/8.0/en/added-deprecated-removed.html#optvars-removed

14) System variables with new default values
  To run this check requires full path to MySQL server configuration file to be specified at 'configPath' key of options dictionary
  More information:
    https://mysqlserverteam.com/new-defaults-in-mysql-8-0/

15) Schema inconsistencies resulting from file removal or corruption
  No issues found

16) Issues reported by 'check table x for upgrade' command
  No issues found

17) New default authentication plugin considerations
  Warning: The new default authentication plugin 'caching_sha2_password' offers
    more secure password hashing than previously used 'mysql_native_password'
    (and consequent improved client connection authentication). However, it also
    has compatibility implications that may affect existing MySQL installations.
    If your MySQL installation must serve pre-8.0 clients and you encounter
    compatibility issues after upgrading, the simplest way to address those
    issues are to reconfigure the server to revert to the previous default
    authentication plugin (mysql_native_password). For example, use these lines
    in the server option file:

    [mysqld]
    default_authentication_plugin=mysql_native_password

    However, the setting should be viewed as temporary, not as a long term or
    permanent solution, because it causes new accounts created with the setting
    in effect to forego the improved authentication security.
    If you are using replication please take time to understand how the
    authentication plugin changes may impact you.
  More information:
    https://dev.mysql.com/doc/refman/8.0/en/upgrading-from-previous-series.html#upgrade-caching-sha2-password-compatibility-issues
    https://dev.mysql.com/doc/refman/8.0/en/upgrading-from-previous-series.html#upgrade-caching-sha2-password-replication

Errors:   0

Warnings: 1

Notices:  0

No fatal errors were found that would prevent an upgrade, but some potential issues were detected. Please ensure that the reported issues are not significant before upgrading.

Listing 10-1Demonstration of the Upgrade Checker Utility

请注意,该实用程序发现了一个警告。在这种情况下,它是身份验证插件的使用。测试的服务器没有使用更新、更安全的插件。幸运的是,该实用程序为我们如何克服这个问题提供了一些提示。

还要注意,我们没有提供配置文件的路径,因此跳过了这一步。它没有显示为警告,因为这是一个可选参数(check ),可以在选项字典中指定。

您可以使用其他几种配置来检查服务器的兼容性。有关升级检查器实用程序的更多信息,请参见位于 https://dev.mysql.com/doc/mysql-shell/8.0/en/mysql-shell-utilities-upgrade.html 的实用程序的在线参考手册。

安装 InnoDB 集群概述

安装 InnoDB Cluster 的过程非常简单,只要你安装几次,你就会发现这比设置 MySQL 复制或 MySQL 组复制要简单得多。唯一可能需要多做一点工作的步骤是配置路由器,但幸运的是这不是一个常规操作(设置一次就让它运行)。所需步骤的概要如下所示。我们将在第十一章中看到这些步骤中的每一步。

  • 选择 MySQL 实例的数量(为了容错)

  • 为 MySQL 实例和路由器选择端口号

  • 创建并配置 MySQL 实例

  • 创建集群

  • 向集群添加 MySQL 实例

  • 检查集群的状态

  • 配置(引导)路由器

虽然路由器的配置和在您的应用中使用它与 MySQL Shell 无关,但演示没有它的 InnoDB Cluster 将是一个错误,因为路由器提供应用级故障转移,这对于一些高可用性要求高可靠性的用例来说可能是至关重要的。

摘要

我们现在有了一个完整的高可用性故事。虽然 MySQL InnoDB Cluster 为我们的数据提供了高可用性,但它并不能(直接)帮助我们在应用级别实现高可用性。是的,您可以编写您的应用来查询集群,并获取信息,以便在服务器离线时帮助您的应用“恢复”,但是实践表明这是一个非常脆弱的解决方案,它过于依赖于已知的参数。如果集群配置发生任何变化,应用可能会失败,或者需要重新启动。

这对大多数组织来说并不理想。我们需要的是能够快速轻松地使我们的应用适应集群中的变化。更具体地说,如果集群中的服务器脱机或被脱机或其角色改变,应用不应该停止。这就是 MySQL 路由器大放异彩的地方。

MySQL Router 将连接路由的负担从应用中分离出来,将其放在自己的轻量级、易于配置的实例中。现在,应用可以依赖路由器进行所有连接路由,包括故障转移事件或正常的高可用性恢复事件。

在下一章中,我们将在一组机器上部署 MySQL InnoDB 集群,并使用一个简单的应用配置路由器,以展示如何为自己的应用实现高可用性目标。我们将展示如何将 MySQL Shell 作为管理控制台与 InnoDB Cluster 配合使用,并快速了解 AdminAPI。

十一、示例:InnoDB 集群设置和管理

现在,我们已经了解了更多关于什么是 InnoDB 集群以及构成该特性的组件的信息,我们几乎已经掌握了足够的信息,可以开始使用一个小型的实验性 InnoDB 集群了。我们还需要学习一些东西,包括如何在 shell 中使用 AdminAPI,以及熟悉设置 InnoDB 集群和 MySQL 路由器所需的步骤。

在本章中,我们将看到 InnoDB 集群部署的更简单形式的演示——通过 MySQL Shell 和 AdminAPI 在沙盒部署方法中运行它。我们将创建一个 InnoDB 集群,在本地机器上运行四个实例。我们不仅将看到如何设置集群以供使用,还将看到集群如何处理故障转移,最后将看到如何设置 MySQL 路由器并演示它如何与应用一起工作。

但是首先,让我们从 AdminAPI 的概述开始。

入门指南

允许我们设置实验性 InnoDB 集群的关键组件称为沙箱。AdminAPI 有几种方法可以在本地机器上的沙箱中使用 MySQL 服务器。然而,AdminAPI 也有一些类,它们的方法用于在远程机器上使用 MySQL 服务器的 InnoDB 集群。在本章中,我们将看到 AdminAPI 中可用的类和方法的概述。在下一节的 InnoDB 集群演示中,我们将使用本节中讨论的一些方法。

AdminAPI 中有两个主要的类:dbacluster。让我们来看看每个类的细节。

注意

以下是在线可用文档的压缩版本,旨在提供概述,而不是具体的使用示例。

工商管理学博士

dba 类使您能够使用 AdminAPI 管理 InnoDB 集群。dba 类使您能够管理集群,例如创建新的集群、使用沙盒配置(一种在同一台机器上使用几个 MySQL 实例来试验 InnoDB 集群、检查实例和集群的状态的方法)。

因为这个类是 API 的设置和配置部分,它有使用沙箱的方法和使用远程服务器的方法。表 11-1 显示了可用于处理沙箱中实例(名称中带有沙箱的实例)的方法 1

表 11-1

沙盒方法(dba 类)

|

返回

|

功能

|

描述

|
| --- | --- | --- |
| 没有人 | delete_sandbox_instance(int port, dict options) | 删除本地主机上现有的 MySQL 服务器实例 |
| 情况 | deploy_sandbox_instance(int port, dict options) | 在本地主机上创建新的 MySQL 服务器实例 |
| 没有人 | kill_sandbox_instance(int port, dict options) | 终止本地主机上正在运行的 MySQL 服务器实例 |
| 没有人 | start_sandbox_instance(int port, dict options) | 在本地主机上启动现有的 MySQL 服务器实例 |
| 没有人 | stop_sandbox_instance(int port, dict options) | 停止本地主机上正在运行的 MySQL 服务器实例 |

请注意,有一些方法可以在沙箱中部署实例,以及删除或终止实例(delete 删除实例,kill 停止实例,但保留数据和元数据),以及启动和停止实例(kill 发出不受控制的关闭)。在后面的小节中,我们将在沙箱中演示 InnoDB 集群时使用这些方法中的大部分。

还要注意,这些方法需要一个端口号和一个选项字典。AdminAPI 中可以用于这些方法和其他方法的选项取决于方法本身,因为每个方法都允许一个或多个选项。表 11-2 显示了表 11-1 中方法可用的选项。

表 11-2

沙盒方法的选项(dba 类)

|

功能

|

[计]选项

|

描述

|
| --- | --- | --- |
| delete_sandbox_instance | sandboxDir | 将被删除的新实例位置的路径 |
| deploy_sandbox_instance | portx | 新实例将侦听 X 协议连接的端口 |
|   | sandboxDir | 将部署新实例的路径 |
|   | password | 新实例上 MySQL root 用户的密码 |
|   | allowRootFrom | 创建远程根帐户,仅限于给定的地址模式(%) |
|   | ignoreSslError | 默认情况下,为新实例添加 SSL 支持时忽略错误:true |
| kill_sandbox_instance | sandboxDir | 将部署新实例所在的路径 |
| start_sandbox_instance | sandboxDir | 将启动新实例的路径 |
| stop_sandbox_instance | password | 已部署实例上 MySQL root 用户的密码 |
|   | sandboxDir | 新实例将停止的路径 |

这些选项以简单 JSON 文档的形式在字典中指定。例如,如果您想要停止端口 13004 上的实例,并指定沙箱目录和密码,您可以按如下方式调用方法。

> stop_sandbox_instance(13004, {'sandboxDir':'/home/cbell/data1', 'password':'secret'})

警告

如果您使用sandboxDir选项来改变沙箱的位置,您必须确保为每个允许它的方法使用它;否则,您的一些沙盒实例可能会被放置在默认位置。

表 11-3 显示了该类中用于设置和配置 MySQL 实例和集群的其余方法。

表 11-4

dba 类方法的选项

|

面积

|

[计]选项

|

描述

|
| --- | --- | --- |
| 一般 | 大多数方法的通用选项 |
|   | verifyMyCnf | 实例的 MySQL 配置文件的可选路径。如果给定了这个选项,那么除了全局 MySQL 系统变量之外,还将验证配置文件的预期选项值。 |
|   | outputMycnfPath | 写入实例的 MySQL 配置文件的备选输出路径 |
|   | password | 要在连接上使用的密码 |
|   | clusterAdmin | 要创建的 InnoDB 集群管理员用户的名称。支持的格式是标准的 MySQL 帐户名格式。 |
|   | clusterAdminPassword | InnoDB 集群管理员帐户的密码 |
|   | clearReadOnly | 用于确认必须禁用 super_read_only 的布尔值 |
|   | interactive | 用于在命令执行中禁用向导的布尔值,即不向用户提供提示,也不显示确认提示 |
| URI 还是字典 | 安全连接的选项 |
|   | ssl-mode | 要在连接中使用的 SSL 模式 |
|   | ssl-ca | PEM 格式的 X509 证书颁发机构的路径 |
|   | ssl-capath | 包含 PEM 格式的 X509 证书颁发机构的目录路径 |
|   | ssl-cert | PEM 格式的 X509 证书的路径 |
|   | ssl-key | PEM 格式的 X509 密钥的路径 |
|   | ssl-crl | 包含证书吊销列表的文件的路径 |
|   | ssl-crlpath | 包含证书吊销列表文件的目录路径 |
|   | ssl-cipher | 要使用的 SSL 密码 |
|   | tls-version | 安全连接允许的协议列表 |
|   | auth-method | 认证方法 |
|   | get-server-public-key | 向服务器请求基于 RSA 密钥对的密码交换所需的公钥。在禁用 SSL 模式的情况下,使用经典 MySQL 会话连接 MySQL 8.0 服务器时使用。 |
|   | server-public-key-path | 文件的路径名,该文件包含服务器进行基于 RSA 密钥对的密码交换所需的公钥的客户端副本 |
| 连接词典 | 连接参数 |
|   | scheme | 要在连接上使用的协议 |
|   | user | 要在连接上使用的 MySQL 用户名 |
|   | dbUser | 用户的别名 |
|   | password | 要在连接上使用的密码 |
|   | dbPassword | 与密码相同 |
|   | host | TCP 连接上使用的主机名或 IP 地址 |
|   | port | TCP 连接中使用的端口 |
|   | socket | 要在通过 unix 套接字的连接上使用的套接字文件名 |
|   | schema | 连接完成后要选择的模式。 |

表 11-3

实例和集群方法(dba 类)

|

返回

|

功能

|

描述

|
| --- | --- | --- |
| 数据 | check_instance_configuration(InstanceDef instance, dict options) | 验证 MySQL InnoDB 集群使用情况的实例 |
| 没有人 | configure_local_instance(InstanceDef instance, dict options) | 验证和配置 MySQL InnoDB 集群使用的本地实例 |
| 没有人 | configure_instance(InstanceDef instance, dict options) | 验证和配置 MySQL InnoDB 集群使用的实例 |
| 串 | create_cluster(str name, dict options) | 创建一个 MySQL InnoDB 集群 |
| 没有人 | drop_metadata_schema(dict options) | 删除元数据架构 |
| 串 | get_cluster(str name, dict options) | 从元数据存储中检索到一个群集 |
| 没有人 | reboot_cluster_from_complete_outage(str clusterName, dict options) | 当所有成员脱机时,使群集重新联机 |

这些方法的选择更多。事实上,有些方法允许一长串选项。下面的列表没有列出每种方法的每个选项,而是将选项总结为三个类别。在演示过程中,我们将看到其中一些技术的实际应用。某些方法需要更具体的选项。

cluster 类是 InnoDB 集群的句柄(想想对象实例)。cluster 类使您能够使用集群来添加实例、删除实例、获取集群的状态(健康状况)等。

由于该类用于直接处理实例和集群,因此大多数方法都设计为处理通过 dba 类检索的特定集群实例。表 11-5 列出了集群类中的方法。

表 11-5

Cluster 类的方法

|

返回

|

功能

|

描述

|
| --- | --- | --- |
| 没有人 | add_instance(InstanceDef instance, dict options) | 向群集添加实例 |
| 词典 | check_instance_state (InstanceDef instance, str password) | 验证与集群相关的实例 GTID 状态 |
| 潜艇用热中子反应堆(submarine thermal reactor 的缩写) | describe() | 描述集群的结构 |
| 没有人 | disconnect() | 断开群集对象使用的所有内部会话 |
| 没有人 | dissolve(Dictionary options) | 溶解群集 |
| 没有人 | force_quorum_using_partition_of(InstanceDef instance, str password) | 从仲裁丢失中恢复群集 |
| 潜艇用热中子反应堆(submarine thermal reactor 的缩写) | get_name() | 检索群集的名称 |
| 没有人 | rejoin_instance(InstanceDef instance, dict options) | 将实例重新加入集群 |
| 没有人 | remove_instance(InstanceDef instance, dict options) | 从集群中删除实例 |
| 没有人 | rescan() | 重新扫描集群 |
| 潜艇用热中子反应堆(submarine thermal reactor 的缩写) | status() | 描述集群的状态 |
| 没有人 | switch_to_single_primary_mode(InstanceDef instance) | 将群集切换到单主模式 |
| 没有人 | switch_to_multi_primary_mode() | 将群集切换到多主模式... |
| 没有人 | set_primary_instance(InstanceDef instance) | 选举一个特定的集群成员作为新的主集群成员 |
| 潜艇用热中子反应堆(submarine thermal reactor 的缩写) | options(dict options) | 列出了群集配置选项 |
| 没有人 | set_option(str option, str value) | 更多地更改整个集群的配置选项值... |
| 没有人 | set_instance_option(InstanceDef instance, str option, str value) | 更改集群成员中配置选项的值... |
| 空的 | invalidate () | 将群集标记为无效(例如,已解散) |

请注意,我们有添加、移除和重新加入实例的方法。在管理集群中的实例时,我们会经常用到它们。还有几种方法可以获得信息、状态,并强制更新元数据,如get_name()status()rescan()

我们还有切换实例模式的方法,如果我们想要手动将主要角色更改为特定实例,这将非常方便。我们还有显示和设置集群常规选项的方法,这同样有助于维护。

注意,也像dba类一样,一些方法接受选项字典。这些选项也是该方法所特有的,但是通常使用上一节中描述的相同选项来连接到实例。如前所述,有些允许特定于方法的选项。

该类有一个属性:集群的名称。该属性简单地命名为 name,可以通过编程方式设置,但通常是在使用dba类创建集群时设置的。

小费

参见 https://dev.mysql.com/doc/dev/mysqlsh-api-python/8.0/group___admin_a_p_i.html 了解更多关于 AdminAPI 的信息。

现在,我们已经对 AdminAPI 中的类和方法有了一个简要的概述,让我们通过在沙箱中设置 InnoDB 集群来看看它的运行情况。

设置和配置

要准备使用沙箱,您只需要决定一些参数,并在系统上准备一个区域来处理集群的数据。有一个参数是必需的。我们必须决定实验集群要使用的端口号。在这种情况下,我们将使用端口 3311-3314 作为服务器监听端口。

我们还可以指定一个目录来包含沙盒数据。虽然这不是必需的,但如果您希望以后重新初始化群集,建议这样做。不需要另外指定目录,因为 AdminAPI 使用沙箱的预定路径。例如,在 Windows 上,它位于名为MySQL\mysql-sandboxes的用户目录中。该文件夹形成了存储沙箱的所有数据和元数据的根。例如,当您使用端口 3312 将一个实例部署到沙箱时,您将看到一个具有该名称的文件夹,如下所示。

C:\Users\cbell\MySQL\mysql-sandboxes\3312

如果您计划重用或重启集群,您可能希望使用sandboxDir选项指定一个特定的文件夹。例如,您可以在 AdminAPI 中将字典指定为{'sandboxDir':'c://idc_sandbox'}。然而,这个文件夹必须存在,否则当你调用deploy_sandbox_instance()方法时会得到一个错误。清单 11-1 显示了 Windows 上的一个定制沙箱目录,在端口 3311 上部署了一个实例。

C:\idc_sandbox>dir
 Volume in drive C is Local Disk
 Volume Serial Number is AAFC-6767

 Directory of C:\idc_sandbox

05/09/2019  07:18 PM    <DIR>          .
05/09/2019  07:18 PM    <DIR>          ..
05/09/2019  07:18 PM    <DIR>          3311               0 File(s)              0 bytes
               3 Dir(s)  172,731,768,832 bytes free

C:\idc_sandbox>dir 3311
 Volume in drive C is Local Disk
 Volume Serial Number is AAFC-6767

 Directory of C:\idc_sandbox\3311

05/09/2019  07:19 PM    <DIR>          .
05/09/2019  07:19 PM    <DIR>          ..
05/09/2019  07:19 PM                 6 3311.pid
05/09/2019  07:18 PM               726 my.cnf
05/09/2019  07:18 PM    <DIR>          mysql-files
05/09/2019  07:18 PM    <DIR>          sandboxdata
05/09/2019  07:18 PM               147 start.bat
05/09/2019  07:18 PM               207 stop.bat
               4 File(s)          1,086 bytes
               4 Dir(s)  172,557,893,632 bytes free

Listing 11-1Creating a Directory for the Sandbox

注意

要重用实例数据,必须启动实例。尝试使用相同的端口重新部署它将会产生错误,因为目录不为空。

现在,让我们来看一个在沙箱中设置集群的演示。创建 InnoDB 集群的沙盒部署有几个步骤。它们如下。

  • 在沙箱中创建和部署实例:设置和配置我们的 MySQL 服务器。

  • 创建集群:创建集群类的对象实例。

  • 向集群添加实例:向集群添加沙盒实例。

  • 检查集群状态:检查集群健康状况。

我们还将通过终止其中一个实例来演示故障转移在集群中是如何工作的。我们开始吧!

在沙箱中创建和部署实例

让我们从启动 shell 并使用 AdminAPI 部署四台服务器开始。在这种情况下,我们将使用端口 3311-3314 和 dba 对象中的deploy_sandbox_instance()方法为每个服务器创建新的实例。所有这些都将在我们的本地主机上运行。

注意

没有必要导入dba类。MySQL Shell 使它在您切换到 Python 模式时可用。

沙箱是在第一次调用 deploy 方法时创建的。让我们现在部署四台服务器。deploy 方法将要求您为 root 用户提供密码。建议您对所有四台服务器使用相同的密码。

注意

您必须创建为沙箱指定的文件夹。

清单 11-2 展示了如何部署四台服务器。使用的命令以粗体突出显示,有助于从消息中识别命令。请注意,我以 Python 模式启动了 shell。为了便于参考,这四个命令以粗体显示。

$ mysqlsh --py

MySQL Shell 8.0.16
...
Type '\help' or '\?' for help; '\quit' to exit.

> sandbox_options = {'sandboxDir':'/home/cbell/idc_sandbox'}

> dba.deploy_sandbox_instance(3311, sandbox_options)

A new MySQL sandbox instance will be created on this host in
/home/cbell/idc_cluster/3311
Warning: Sandbox instances are only suitable for deploying and
running on your local machine for testing purposes and are not
accessible from external networks.
Please enter a MySQL root password for the new instance: ∗∗∗∗
Deploying new MySQL instance...
Instance localhost:3311 successfully deployed and started.
Use shell.connect('root@localhost:3311'); to connect to the instance.

> dba.deploy_sandbox_instance(3312, sandbox_options)

A new MySQL sandbox instance will be created on this host in
/home/cbell/idc_cluster/3312
Warning: Sandbox instances are only suitable for deploying and
running on your local machine for testing purposes and are not
accessible from external networks.
Please enter a MySQL root password for the new instance: ∗∗∗∗
Deploying new MySQL instance...
Instance localhost:3312 successfully deployed and started.
Use shell.connect('root@localhost:3312'); to connect to the instance.

> dba.deploy_sandbox_instance(3313, sandbox_options)

A new MySQL sandbox instance will be created on this host in
/home/cbell/idc_cluster/3313
Warning: Sandbox instances are only suitable for deploying and
running on your local machine for testing purposes and are not
accessible from external networks.
Please enter a MySQL root password for the new instance: ∗∗∗∗
Deploying new MySQL instance...
Instance localhost:3313 successfully deployed and started.
Use shell.connect('root@localhost:3313'); to connect to the instance.

> dba.deploy_sandbox_instance(3314, sandbox_options)

A new MySQL sandbox instance will be created on this host in
/home/cbell/idc_cluster/3314
Warning: Sandbox instances are only suitable for deploying and
running on your local machine for testing purposes and are not
accessible from external networks.
Please enter a MySQL root password for the new instance: ∗∗∗∗
Deploying new MySQL instance...
Instance localhost:3314 successfully deployed and started.
Use shell.connect('root@localhost:3314'); to connect to the instance.

Listing 11-2Creating Local Server Instances

注意,deploy_sandbox_instance()方法显示沙箱数据和元数据的位置(例如,c:\\idc_sandbox\\3314),并提示我们输入实例的密码。如果您打算重新启动或重新使用群集,请确保使用一个容易记住的密码。可以对所有实例使用相同的密码。一旦您运行了所有的命令,您将有四个实例在本地机器上运行。

dba 类中有一个经常被忽略的特性。check_instance_configuration()方法允许您检查一个实例是否被正确配置用于 InnoDB 集群。您可以在沙盒实例上运行它(但是它们总是兼容的),或者更好的是,您可以在远程实例上运行它,以便在将它们添加到集群之前检查它们。清单 11-3 演示了运行检查。这是构建集群之前的推荐步骤。请注意,您必须首先连接到实例。

> \connect root@localhost:3311

Creating a session to 'root@localhost:3311'
Please provide the password for 'root@localhost:3311': ∗∗∗∗
Save password for 'root@localhost:3311'? [Y]es/[N]o/Ne[v]er (default No): Y
Fetching schema names for autocompletion... Press ^C to stop.
Your MySQL connection id is 12
Server version: 8.0.16 MySQL Community Server - GPL
No default schema selected; type \use <schema> to set one.

> dba.check_instance_configuration()

Validating local MySQL instance listening at port 3311 for use in an InnoDB cluster...
Instance detected as a sandbox.
Please note that sandbox instances are only suitable for deploying test clusters for use within the same host.

This instance reports its own address as OPTIPLEX-7010
Clients and other cluster members will communicate with it through this address by default. If this is not correct, the report_host MySQL system variable should be changed.

Checking whether existing tables comply with Group Replication requirements...
No incompatible tables detected

Checking instance configuration...
Instance configuration is compatible with InnoDB cluster

The instance 'localhost:3311' is valid for InnoDB cluster usage.

{
    "status": "ok"
}

Listing 11-3Checking an Instance for Configuration Compatibility

还有一种自动配置本地实例的方法。使用configure_local_instance()方法进行任何必要的更改,以便为 InnoDB 集群正确配置本地实例。如果您正在使用已配置 MySQL 复制的现有 MySQL 服务器,这将非常方便。

创建集群

我们需要做的下一件事是建立一个新的集群。我们使用 dba 对象中的create_cluster()方法来实现这一点,该方法为集群类创建一个对象实例。但是首先,我们必须连接到我们希望作为主服务器的服务器。请注意,这是我们的 shell 会话的延续,演示了如何创建新的集群。注意清单 11-4 中是如何做到的。

\connect root@localhost:3311

Creating a session to 'root@localhost:3311'
Fetching schema names for autocompletion... Press ^C to stop.
Your MySQL connection id is 9
Server version: 8.0.16 MySQL Community Server - GPL
No default schema selected; type \use <schema> to set one.

> my_cluster = dba.create_cluster('MyClusterSB')

A new InnoDB cluster will be created on instance 'root@localhost:3311'.

Validating instance at localhost:3311...
Instance detected as a sandbox.
Please note that sandbox instances are only suitable for deploying test clusters for use within the same host.

This instance reports its own address as localhost

Instance configuration is suitable.
Creating InnoDB cluster 'MyClusterSB' on 'root@localhost:3311'...

Adding Seed Instance...
Cluster successfully created. Use Cluster.add_instance() to add MySQL instances.
At least 3 instances are needed for the cluster to be able to withstand up to
one server failure.

Listing 11-4Creating a Cluster in InnoDB Cluster MySQL Py >

注意,我们将集群命名为MyClusterSB,并使用一个名为my_cluster的变量来存储从create_cluster()方法返回的对象。还要注意,我们首先连接的服务器已经成为主服务器,AdminAPI 检测到我们正在沙箱中运行。

无法创建集群

如果您的本地机器运行的 MySQL 实例不是复制拓扑的一部分,您可能会得到一个错误,抱怨主机不可用于组复制或解析为127.0.0.1,您必须停止您的每个沙盒实例,编辑my.cnf,将report_host选项设置为localhost,将report_port选项设置为实例的端口。然后启动实例,如下文所示。对每个部署的实例重复上述步骤。还要注意,在每个实例文件夹中都有一个特殊的启动和停止脚本,您可以使用它来快速启动和停止实例。酷吧。

$ cd ~/idc_sandbox
$ ./3311/stop.sh
Stopping MySQL sandbox using mysqladmin shutdown... Root password is required.
Enter password:
$ nano ./3311/my.cnf
...
report_host='localhost'
report_port=3311
$ ./3311/start.sh
Starting MySQL sandbox

注意,如果您在 shell 中使用dba.stop_sandbox_instance(),这将从沙箱中完全删除实例。最好使用助手脚本(stop.sh 或 stop.bat)来停止实例。但是,您可以通过使用传入实例端口的start_sandbox_instance()方法从 shell 内部启动实例。

> dba.start_sandbox_instance(3314, sandbox_options)
The MySQL sandbox instance on this host in
/home/cbell/idc_sandbox/3314 will be started
Starting MySQL instance...
Instance localhost:3314 successfully started.

小费

如果退出 shell,可以用get_cluster()方法检索正在运行的集群。例如,您可以重启 shell,然后发出命令my_cluster = dba.get_cluster('MyClusterSB')

将实例添加到集群

接下来,我们添加另外两个服务器实例来完成集群。我们现在使用保存在变量my_cluster中的集群类实例,并使用add_instance()。我们将剩余的三个实例添加到集群中。这些服务器自动成为组中的辅助服务器。清单 11-5 展示了如何将实例添加到集群中。

> my_cluster.add_instance('root@localhost:3312')

A new instance will be added to the InnoDB cluster. Depending on the amount of data on the cluster this might take from a few seconds to several hours.

Adding instance to the cluster ...

Please provide the password for 'root@localhost:3312': ∗∗∗∗
Save password for 'root@localhost:3312'? [Y]es/[N]o/Ne[v]er (default No): Y
Validating instance at localhost:3312...
Instance detected as a sandbox.
Please note that sandbox instances are only suitable for deploying test clusters for use within the same host.

This instance reports its own address as localhost

Instance configuration is suitable.
The instance 'root@localhost:3312' was successfully added to the cluster.

> my_cluster.add_instance('root@localhost:3313')

A new instance will be added to the InnoDB cluster. Depending on the amount of data on the cluster this might take from a few seconds to several hours.

Adding instance to the cluster ...

Please provide the password for 'root@localhost:3313': ∗∗∗∗
Save password for 'root@localhost:3313'? [Y]es/[N]o/Ne[v]er (default No): Y
Validating instance at localhost:3313...
Instance detected as a sandbox.
Please note that sandbox instances are only suitable for deploying test clusters for use within the same host.

This instance reports its own address as localhost

Instance configuration is suitable.
The instance 'root@localhost:3313' was successfully added to the cluster.

> my_cluster.add_instance('root@localhost:3314')

A new instance will be added to the InnoDB cluster. Depending on the amount of data on the cluster this might take from a few seconds to several hours.

Adding instance to the cluster ...

Please provide the password for 'root@localhost:3314': ∗∗∗∗
Save password for 'root@localhost:3314'? [Y]es/[N]o/Ne[v]er (default No): Y
Validating instance at localhost:3314...
Instance detected as a sandbox.
Please note that sandbox instances are only suitable for deploying test clusters for use within the same host.

This instance reports its own address as localhost

Instance configuration is suitable.
The instance 'root@localhost:3314' was successfully added to the cluster.

Listing 11-5Adding Instances to the Cluster

注意,add_instance()方法接受一个带有 URI 连接信息的字符串。在这种情况下,它只是用户名、at 符号(@)、主机名和端口,格式为<user>@<host>:<port>。另请注意,该方法提示输入实例的密码。

至此,我们已经了解了 InnoDB Cluster 如何设置服务器并将它们添加到组中。花点时间回想一下组复制教程。您在幕后看不到的是所有的组复制机制—您可以免费获得它们!这有多酷?

显然,使用 shell 来设置和管理集群比设置和管理标准的组复制设置要容易得多。具体来说,您不必手动配置复制!更好的是,如果服务器出现故障,您不必担心重新配置您的应用或拓扑来确保解决方案保持可行——InnoDB Cluster 会自动为您完成这一任务。

检查集群的状态

一旦创建了集群并添加了实例,我们就可以使用我们的my_cluster对象的status()方法来获取集群的状态,如清单 11-6 所示。在本例中,我们还将看到如何通过连接\connect 命令并使用 dba 类中的get_cluster()方法,从一个正在运行的服务器实例中检索集群。您还可以使用命令行(mysqlsh --uri root@localhost:3313)连接到服务器实例。请注意,您不必连接到第一个(或主)服务器实例来检索集群。您可以连接到任何服务器来检索集群。

> \connect root@localhost:3313

Creating a session to 'root@localhost:3313'
Fetching schema names for autocompletion... Press ^C to stop.
Your MySQL connection id is 30
Server version: 8.0.16 MySQL Community Server - GPL
No default schema selected; type \use <schema> to set one.

> my_cluster = dba.get_cluster('MyClusterSB')

> my_cluster.status()

{
    "clusterName": "MyClusterSB",
    "defaultReplicaSet": {
        "name": "default",
        "primary": "localhost:3311",
        "ssl": "REQUIRED",
        "status": "OK",
        "statusText": "Cluster is ONLINE and can tolerate up to ONE failure.",
        "topology": {
            "localhost:3311": {
                "address": "localhost:3311",
                "mode": "R/W",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE",
                "version": "8.0.16"
            },
            "localhost:3312": {
                "address": "localhost:3312",
                "mode": "R/O",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE",
                "version": "8.0.16"
            },
            "localhost:3313": {
                "address": "localhost:3313",
                "mode": "R/O",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE",
                "version": "8.0.16"
            },
            "localhost:3314": {
                "address": "localhost:3314",
                "mode": "R/O",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE",
                "version": "8.0.16"
            }
        },
        "topologyMode": "Single-Primary"
    },
    "groupInformationSourceMember": "localhost:3311"
}

Listing 11-6Getting the status of the cluster

注意,输出是 JSON 文档的形式,包含关于集群的元数据,包括所有实例、它们的角色和状态。您希望确保所有实例都在线。

现在,让我们来看看群集如何自动处理故障转移的演示。

故障转移演示

现在,让我们来模拟一个故障场景。在这种情况下,我们将有目的地删除其中一个实例。让我们杀了在 3311 端口上运行的那个。我们可以通过多种方式实现这一点,例如使用操作系统终止 mysqld 进程,从 shell 或 MySQL 客户端或 dba 类使用 shutdown SQL 命令。清单 11-7 显示了如何终止实例以及实例停止后的状态结果。

> sandbox_options = {'sandboxDir':'/home/cbell/idc_sandbox'}

 MySQL  Py > \connect root@localhost:3311
Creating a session to 'root@localhost:3311'
Fetching schema names for autocompletion... Press ^C to stop.
Your MySQL connection id is 55
Server version: 8.0.16 MySQL Community Server - GPL
No default schema selected; type \use <schema> to set one.

> dba.kill_sandbox_instance(3311, sandbox_options)

The MySQL sandbox instance on this host in
/home/cbell/idc_sandbox/3311 will be killed

Killing MySQL instance...

Instance localhost:3311 successfully killed.

> \connect root@localhost:3312

Creating a session to 'root@localhost:3312'
Fetching schema names for autocompletion... Press ^C to stop.
Your MySQL connection id is 38
Server version: 8.0.16 MySQL Community Server - GPL
No default schema selected; type \use <schema> to set one.

> my_cluster = dba.get_cluster('MyClusterSB')

> my_cluster.status()

{
    "clusterName": "MyClusterSB",
    "defaultReplicaSet": {
        "name": "default",
        "primary": "localhost:3313",
        "ssl": "REQUIRED",
        "status": "OK_PARTIAL",
        "statusText": "Cluster is ONLINE and can tolerate up to ONE failure. 1 member is not active",
        "topology": {
            "localhost:3311": {
                "address": "localhost:3311",
                "mode": "n/a",
                "readReplicas": {},
                "role": "HA",
                "status": "(MISSING)"
            },
            "localhost:3312": {
                "address": "localhost:3312",
                "mode": "R/O",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE",
                "version": "8.0.16"
            },
            "localhost:3313": {
                "address": "localhost:3313",
                "mode": "R/W",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE",
                "version": "8.0.16"
            },
            "localhost:3314": {
                "address": "localhost:3314",
                "mode": "R/O",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE",
                "version": "8.0.16"
            }
        },
        "topologyMode": "Single-Primary"
    },
    "groupInformationSourceMember": "localhost:3313"
}

Listing 11-7
Failover Demonstration

请注意,我们终止了运行在端口 3311 上的服务器。然而,当我们去获取集群时,我们得到了一个错误。这是因为我们已经连接到我们杀死的服务器(3311)。因此,我们需要连接到另一个服务器并再次检索集群来刷新数据。然后,我们可以获得状态,当我们这样做时,我们会看到端口 3311 上的服务器实例被列为缺失,端口 3312 上的服务器已经接管了读写功能。

此时,我们可以尝试恢复端口 3311 上的服务器实例,或者将其从集群中删除。清单 11-8 演示了如何从集群中删除它。请注意,我们使用 recover 方法删除实例,因为我们无法连接到它(它已关闭)。

> my_cluster.rescan()

Rescanning the cluster...

Result of the rescanning operation for the 'default' ReplicaSet:

{

    "name": "default",
    "newTopologyMode": null,
    "newlyDiscoveredInstances": [],
    "unavailableInstances": [
        {
            "host": "localhost:3311",
            "label": "localhost:3311",
            "member_id": "27e8019b-8315-11e9-9f3e-5882a8945ac2"
        }
    ]

}

The instance 'localhost:3311' is no longer part of the ReplicaSet.

The instance is either offline or left the HA group. You can try to add it to the cluster again with the cluster.rejoinInstance('localhost:3311') command or you can remove it from the cluster configuration.

Would you like to remove it from the cluster metadata? [Y/n]: y

Removing instance from the cluster metadata...

The instance 'localhost:3311' was successfully

removed from the cluster.

> my_cluster.status()

{
    "clusterName": "MyClusterSB",
    "defaultReplicaSet": {
        "name": "default",
        "primary": "localhost:3313",
        "ssl": "REQUIRED",
        "status": "OK",
        "statusText": "Cluster is ONLINE and can tolerate up to ONE failure.",
        "topology": {
            "localhost:3312": {
                "address": "localhost:3312",
                "mode": "R/O",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE",
                "version": "8.0.16"
            },
            "localhost:3313": {
                "address": "localhost:3313",
                "mode": "R/W",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE",
                "version": "8.0.16"
            },
            "localhost:3314": {
                "address": "localhost:3314",
                "mode": "R/O",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE",
                "version": "8.0.16"
            }
        },
        "topologyMode": "Single-Primary"
    },
    "groupInformationSourceMember": "localhost:3313"
}

Listing 11-8Removing Downed Instance from Cluster

要添加丢失的实例,只需通过运行位于沙盒数据目录中的 start 命令(例如,idc_sandbox\3311\start.bat)重新启动它,并使用如下的rescan()方法将其添加回集群。使用此方法添加以前属于群集的实例。如果要用一个新的实例来替换整个实例,可以使用add_instance()方法。

> my_cluster.rescan()

Rescanning the cluster...

Result of the rescanning operation for the 'default' ReplicaSet:
{
    "name": "default",
    "newTopologyMode": null,
    "newlyDiscoveredInstances": [
        {
            "host": "localhost:3311",
            "member_id": "1e8f53e2-7335-11e9-9bb1-4ccc6ae8a7a4",
            "name": null,
            "version": "8.0.16"
        }
    ],
    "unavailableInstances": []
}
A new instance 'localhost:3311' was discovered in the ReplicaSet.
Would you like to add it to the cluster metadata? [Y/n]: y
Adding instance to the cluster metadata...
The instance 'localhost:3311' was successfully added to the cluster metadata.

既然我们已经了解了如何创建 InnoDB 集群,那么让我们简单地讨论一下如何设置 MySQL Router 以用于集群和一个测试应用。

使用 MySQL 路由器

如前所述,如果不简要了解一下 MySQL 路由器,对 InnoDB 集群的任何介绍都是不完整的。在本节中,我们将看到一个设置路由器并将其用于应用的简短教程。我们将使用一个简单的 Python 脚本来演示应用级故障转移。

引导路由器

回想一下,配置路由器是创建 InnoDB 集群的步骤,这可能需要一些额外的设置。虽然这是事实,但在大多数情况下,路由器可以自行配置。只有当您的群集变得很大,或者您已经为性能(读取扩展)定制了群集,或者您想要路由到多个应用或多个群集时,才需要这样做。

小费

如果您还没有安装 MySQL 路由器,现在必须安装。有些平台,如 Linux,可能需要单独下载软件包。

幸运的是,我们可以使用特殊的引导选项连接到您的集群,读取元数据,然后自动更新配置文件。如果您使用沙盒安装,这是设置路由器的最快和最可靠的方法。即使您没有使用沙盒安装,也可以使用这种方法快速设置基本配置,以后可以根据需要进行更改。

让我们看看如何使用引导选项。要使用该选项,我们需要一些其他参数。我们需要以下内容。简而言之,必须提供连接信息和用户来保护配置文件。我们还添加了一个可选参数来为配置提供一个名称,如果您正在使用不同的集群或配置,这可能会很有帮助。

  • --bootstrap <server_url>:引导并配置路由器,以便与 MySQL InnoDB 集群一起运行。也可以使用快捷键-B

  • --name:(可选)给路由器实例一个符号名。

  • --user <username>:以指定的用户名运行路由器(在 Windows 上不可用)。也可以使用快捷键-u

在本例中,我们以 URI(如<username>:<password>@<hostname>:<port>)的形式提供带有引导选项的连接信息。我们还将使用本地用户,以便更容易地使用沙盒安装来运行路由器,沙盒安装也是在当前用户下运行的。因此,我们将使用以下命令。我们使用提升的权限,因为路由器文件的默认位置是受保护的。

$ sudo mysqlrouter --bootstrap root:secret@localhost:3313 \
  --name sandbox --user cbell

当我们运行这个命令时,路由器将联系我们指定的服务器,并检索集群的所有元数据,为我们自动创建路由。清单 11-9 显示了该命令的输出示例。

$ sudo mysqlrouter --bootstrap root:secret@localhost:3313 --name sandbox --user cbell
# Bootstrapping system MySQL Router instance...
Fetching Group Replication Members
disconnecting from mysql-server
trying to connecting to mysql-server at localhost:3313
- Checking for old Router accounts
  - No prior Router accounts found
- Creating mysql account mysql_router1_9twobjgwueud@'%' for cluster management
- Storing account in keyring
- Adjusting permissions of generated files
- Creating configuration /etc/mysqlrouter/mysqlrouter.conf

# MySQL Router 'sandbox' configured for the InnoDB cluster 'MyClusterSB'

After this MySQL Router has been started with the generated configuration

    $ /etc/init.d/mysqlrouter restart
or
    $ systemctl start mysqlrouter
or
    $ mysqlrouter -c /etc/mysqlrouter/mysqlrouter.conf

the cluster 'MyClusterSB' can be reached by connecting to:

## MySQL Classic protocol

- Read/Write Connections: localhost:6446
- Read/Only Connections:  localhost:6447

## MySQL X protocol

- Read/Write Connections: localhost:64460
- Read/Only Connections:  localhost:64470

Existing configuration backed up to '/etc/mysqlrouter/mysqlrouter.conf.bak'

Listing 11-9Configuration with the Bootstrap Option

请注意,我们看到路由器已经分别使用默认端口 6446 和 6447 为我们标识了读写(RW)和只读(RO)连接。我们还看到引导步骤分别在端口 64460 和 64470 上创建使用 X 协议的路由。在我们测试路由器之前,让我们了解一下自举方法为我们做了什么。具体来说,我们将查看修改后的配置文件。

现在,我们可以启动路由器了。

启动路由器

我们可以使用以下命令启动路由器。这将启动路由器,它将读取配置文件。请注意,我们没有使用提升的特权。这是因为我们在引导步骤中提供了一个用户选项,允许用户读取文件。这对于保护您的安装非常重要,我们将在后面的章节中探讨这一点。

$ mysqlrouter &
Loading all plugins.
  plugin 'logger:' loading
  plugin 'metadata_cache:MyClusterSB' loading
  plugin 'routing:MyClusterSB_default_ro' loading
  plugin 'routing:MyClusterSB_default_rw' loading
  plugin 'routing:MyClusterSB_default_x_ro' loading
  plugin 'routing:MyClusterSB_default_x_rw' loading
Initializing all plugins.
  plugin 'logger' initializing
logging facility initialized, switching logging to loggers specified in configuration

现在我们已经配置了路由器,让我们用示例 Python 连接脚本来测试它。

示例应用

现在我们已经安装了路由器,为 InnoDB 集群进行了配置,并且正在运行,让我们看看如何使用一个简单的 Python 脚本来测试路由器。

清单 11-10 展示了一个通过路由器连接到 InnoDB 集群的简单 Python 脚本。回想一下,我们在我们的机器上安装了路由器,因此这个脚本(为了一致性,如果不是练习的话)应该在同一台机器上执行。花点时间检查一下代码。如果您正在跟进,您可以在您的机器上将文件保存为router_connection_test.py

#
# Introducing MySQL Shell
#
# This example shows how to use the MySQL Router to connect to
# the cluster. Notice how connecting via the router port 6446
# results in a seamless transport to one of the cluster servers,
# in this case, the server with the primary role.
#
# Dr. Charles Bell, 2019
#
import mysql.connector

# Simple function to display results from a cursor
def show_results(cur_obj):
  for row in cur:
    print(row)

my_cfg = {
  'user':'root',
  'passwd':'secret',
  'host':'127.0.0.1',
  'port':6446   # <<<< Router port (R/W)
}

# Connecting to the server
conn = mysql.connector.connect(∗∗my_cfg)

print("Listing the databases on the server.")
query = "SHOW DATABASES"
cur = conn.cursor()
cur.execute(query)
show_results(cur)

print("\Retrieve the port for the server to which we’re connecting.")
query = "SELECT @@port"
cur = conn.cursor()
cur.execute(query)
show_results(cur)

# Close the cursor and connection
cur.close()
conn.close()

Listing 11-10Router Connection Test

代码的第一部分简单地导入连接器并定义一个连接术语字典。在这种情况下,是路由器的用户、口令、主机和端口。我们使用的端口号是 6446,如路由器配置过程中所示。

接下来,代码打开一个连接,然后运行两个查询:一个获取数据库列表并显示它们(使用定义为show_results()的函数),另一个选择服务器的当前端口。我们将看到,第二个查询结果可能会让您感到惊讶。

要执行代码,请保存名为 router_connect_test.py 的文件(扩展名将其标识为 Python 脚本)。然后,使用以下命令运行代码。

$ python ./router_connection_test.py
Listing the databases on the server.
(u'information_schema',)
(u'mysql',)
(u'mysql_innodb_cluster_metadata',)
(u'performance_schema',)
(u'sys',)
Retrieve the port for the server to which we’re connecting.
(3313,)

等等!为什么输出显示端口 3313?不应该显示端口 6446 吗?毕竟,这是我们在代码中使用的端口。回想一下,路由器只是将通信路由到适当的服务器,它本身不是服务器连接。因此,路由器成功地将我们的连接路由到端口 331 上的机器。回想一下,这台机器是主机器(在集群中列为读写机器)。

那么我们如何连接到集群中的只读服务器呢?我们现在需要做的就是修改程序来连接只读服务器(在端口 6447 上)。当我们重新运行脚本时,我们将看到以下输出。

$ python ./router_connection_test.py
Listing the databases on the server.
(u'information_schema',)
(u'mysql',)
(u'mysql_innodb_cluster_metadata',)
(u'performance_schema',)
(u'sys',)
Retrieve the port for the server to which we're connecting.
(3312,)

现在我们看到我们连接到的不是端口 3311 上的服务器。回想一下沙箱设置,端口 3312、3313 和 3314 上的机器都是只读的。

虽然这个例子很简单,但它确实说明了路由器如何将连接重定向到其他 MySQL 服务器。它还有助于强化这样一个概念,即我们必须将我们的应用连接到路由器本身,而不是集群中的机器,并允许路由器为我们完成所有繁重的连接路由。正如您所看到的,它非常复杂,并根据其初始(和后来缓存的)配置知道基于路由器监听的端口请求哪些服务器。在这种情况下,我们使用 6446 进行读写连接,使用 6447 进行只读连接。是的,就是这么简单。不再有复杂的硬编码端口!

应用故障转移演示

现在,让我们只在应用层尝试故障转移。更具体地说,我们将终止集群中的读写(或主)服务器,然后再次启动脚本。回想一下,我们想要连接到路由器正在使用的读写端口,在本例中是端口 6446。在运行这个测试之前,一定要检查脚本中的my_cfg字典。

花点时间验证集群中的哪个实例具有读写角色(主实例)。我们可以如下使用 shell 来实现这一点。这里我们看到端口 3313 上的服务器具有读写模式。

$ mysqlsh --uri root@localhost:3312 --py
> dba.get_cluster('MyClusterSB').status()
...
            "localhost:3313": {
                "address": "localhost:3313",
                "mode": "R/W",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE",
                "version": "8.0.16"
            },

让我们首先在不终止任何实例的情况下连接到集群,并查看为读写连接返回了什么端口。

$ python ./router_connection_test.py
Listing the databases on the server.
(u'information_schema',)
(u'mysql',)
(u'mysql_innodb_cluster_metadata',)
(u'performance_schema',)
(u'sys',)

Retrieve the port for the server to which we're connecting.
(3313,)

好的,它映射到端口 3313。很好。现在,让我们毫不客气地杀死那个服务器实例。

more ~/idc_sandbox/3313/3313.pid
10264
$ sudo kill -9 10264

然后,再次运行该脚本,并查看为读写服务器显示了哪个端口。

$ python ./router_connection_test.py
Listing the databases on the server.
(u'information_schema',)
(u'mysql',)
(u'mysql_innodb_cluster_metadata',)
(u'performance_schema',)
(u'sys',)

Retrieve the port for the server to which we're connecting.
(3311,)

在这里,我们看到我们确实通过路由器连接到了 InnoDB 集群,我们看到报告的端口是 3311,这是新的主(读写)服务器的端口。酷!

该演示展示了即使集群中存在故障转移,我们也可以继续运行我们的应用。在这种情况下,我们只是简单地重新运行应用(脚本),但是在生产(或开发)中,您只需构建您的应用来重试读写端口或只读端口的连接。这样,您的应用甚至不需要重启,您只需重新连接并继续运行。这有多酷?

小费

参见 https://dev.mysql.com/doc/mysql-router/8.0/en/mysql-router-configuration.html 了解更多关于为您的应用和环境配置路由器的信息。

在我们结束关于 InnoDB 集群的讨论之前,让我们简要地讨论一下您可能想要执行的一些管理任务。

管理

正如您已经发现的,设置 InnoDB Cluster 并不困难,除了学习如何使用 MySQL Shell 和 AdminAPI 中的一些类和方法之外,配置 InnoDB Cluster 的步骤也同样简单。然而,我们从经验中知道,设置和后续管理在复杂性上并不总是一致的。

在这一节中,我们将从较高的层次来看一下您可能需要在 InnoDB 集群上执行的管理任务。我们还将看到一些您可能希望在沙箱中运行的 InnoDB 集群上执行的特定任务,这可能有助于您从测试环境过渡到开发和后续生产。

常见任务

使用 InnoDB Cluster 的首选工具是 MySQL Shell,这是本书的重点。特别有用的是,您可以用 Java 或 Python 编写自己的特殊脚本来处理集群。下面列出了使用 InnoDB 集群的常见管理任务。还有其他几种,但这些是最常见的。

  • 获取集群:当使用 MySQL Shell 管理 InnoDB 集群时,我们必须首先请求集群类的一个实例。回想一下,我们在本书中已经多次看到这种情况。为了检索集群实例,我们使用了dba.get_cluster()方法。

  • 检查集群状态:和上一个任务一样,我们已经看到了如何检索集群状态报告。回想一下,我们使用的是cluster.status()方法。我们必须连接到集群中的一个服务器,检索它,然后使用 status 方法。

  • 描述集群:您还可以获得关于集群的信息,比如集群中机器的主机名以及每个 MySQL 实例使用的端口。我们使用的命令是cluster.describe()方法。

  • 检查实例是否适合与 InnoDB 集群一起使用:我们已经看到了两种方法,您可以使用这两种方法来准备实例以与 InnoDB 集群一起使用。第一个是dba.configure_local_instance(),用于准备在集群中使用的本地机器。第二,dba.check_instance_configuration()可以用来测试服务器的设置是否正确。与第一种方法不同,检查实例配置方法可以远程运行。

  • 检查和实例化集群状态:您还可以使用dba.check_instance_state()方法检查和实例化集群的当前或最后已知状态。此方法将服务器连接信息作为参数,并返回其状态。

  • 将实例加入集群:在本书中,我们已经多次看到如何将实例加入集群。回想一下,我们使用传入连接信息的cluster.add_instance()方法连接到一个实例,以加入当前集群。

  • 从集群中删除一个实例:如果你需要对一台物理机器或者运行在服务器上的 MySQL 实例进行维护,你应该首先从集群中删除它。我们可以用cluster.remove_instance()方法做到这一点。

现在,让我们来看几个任务,它们对于探索 InnoDB 集群和使用沙盒部署可能很方便。

示例任务

以下是您可能希望在 InnoDB 集群上执行的一些特定任务,例如关闭并重新启动集群或重新启动集群。

通常,InnoDB 集群(或任何高可用性系统)永远不会完全关闭。事实上,保持系统始终运行是我们的目标。然而,对于我们运行沙箱的开发集群,我们可能不希望实例长时间运行。我们不仅希望关闭集群,还可能希望稍后重新启动它。本节介绍一种安全关闭并重新启动集群的方法。

正在关闭集群

简而言之,InnoDB 集群不是为随意打开和关闭而设计的。相反,关闭所有服务器将导致集群完全失去集群连续性。虽然这对于生产系统、对于我们的开发集群(或者任何类似的集群)来说是非常糟糕的,但是这并不是一个严重的问题。如果您发现自己想要保留集群,因为其中有您不想丢失的数据或依赖于它的应用,那么您已经超出了使用沙箱或类似的小型实验性安装的范围。

那你是做什么的?AdminAPI 在dba模块中包含从完全丢失中恢复集群的方法。但是,只有在对集群中的服务器执行受控关闭时,这种方法才有效。下面概述了可以用来关闭集群电源的过程。

  • 获取集群状态并记录读写服务器

  • 连接到每个只读服务器并关闭它们

  • 关闭读写服务器

回想一下,我们可以连接到集群中的任何机器,获取集群,并使用status()方法找到读写服务器。应该通过 MySQL Shell 或 MySQL 客户端发出如下 shutdown SQL 命令来连接到只读服务器并关闭它们。

$ mysqlsh --uri root@localhost:3311 --sql -e "SHUTDOWN"

对其他只读服务器重复此命令,然后对读写服务器重复此命令。记下哪个服务器是读写服务器。

重新启动集群

虽然您可能希望通过重启集群中的所有服务器来重新建立集群,但事实并非如此。当从头开始重新启动集群时,我们必须在 AdminAPI 中使用一种特殊的方法。这种方法适用于没有出现任何错误的集群,即那些已经成功关闭的集群。这就是所谓的从完全中断中恢复集群。然而,这只有在所有服务器都已重启,MySQL 已在所有服务器上启动,并且它们可以访问网络(以及相互访问)的情况下才有效。

下面演示了如何从完全中断中恢复集群。具体来说,服务器已经全部重启(mysqld 重启),您需要从最后一个已知良好的位置重启集群。我们将使用dba.reboot_cluster_from_complete_outage()方法重启集群。首先,当您关闭服务器并运行清单 11-11 中所示的命令时,登录到读写服务器。

$ ./3311/start.sh

Starting MySQL sandbox

$ ./3312/start.sh

Starting MySQL sandbox

$ ./3313/start.sh

Starting MySQL sandbox

$ ./3314/start.sh

Starting MySQL sandbox

$ mysqlsh --py --uri root@localhost:3313

MySQL Shell 8.0.16

Copyright (c) 2016, 2019, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its affiliates.
Other names may be trademarks of their respective owners.

Type '\help' or '\?' for help; '\quit' to exit.
Creating a session to 'root@localhost:3313'
Fetching schema names for autocompletion... Press ^C to stop.
Your MySQL connection id is 57
Server version: 8.0.16 MySQL Community Server - GPL
No default schema selected; type \use <schema> to set one.

> my_cluster = dba.reboot_cluster_from_complete_outage('MyClusterSB')

Reconfiguring the cluster 'MyClusterSB' from complete outage...

The instance 'localhost:3312' was part of the cluster configuration.
Would you like to rejoin it to the cluster? [y/N]: y

The instance 'localhost:3314' was part of the cluster configuration.
Would you like to rejoin it to the cluster? [y/N]: y

The instance 'localhost:3311' was part of the cluster configuration.
Would you like to rejoin it to the cluster? [y/N]: y

The cluster was successfully rebooted.

Listing 11-11Restarting a Cluster from Complete Outage

请注意,该命令读取集群元数据,并尝试重新连接(重新加入)所有服务器。如果成功,您将会看到指示集群已重新启动的消息。如果遇到错误,请确保所有服务器都在运行并且可以访问网络,更正任何问题,然后重试该命令。

小费

有关 InnoDB Cluster 的更多信息以及如何使用 InnoDB Cluster 的更深入报道,包括如何调整您的应用,请参见我的书介绍 InnoDB Cluster ,Bell(2018 年 4 月)。

摘要

MySQL Shell 改变了 MySQL 的游戏规则,这听起来像是炒作,但现在应该开始看起来更像事实了。我们探讨了如何在构建应用时使用 shell,或者通过它的 SQL 接口,或者通过 NoSQL 接口。在这样做的过程中,我们通过使用一个示例应用探索了 X DevAPI。在本章中,我们研究了在沙盒中创建 InnoDB 集群时使用的 AdminAPI。

既然您已经看到了如何设置 MySQL 复制以及后来的组复制,那么您现在应该对 MySQL Shell 和 AdminAPI 在用户友好性方面的巨大进步有所了解了。简而言之,InnoDB Cluster 使得使用组复制成为学习一些 API 类和方法的简单事情。最棒的是,它为 InnoDB 集群的开发操作和自动化打开了大门——迄今为止,这需要昂贵的定制工具。是的,使用 MySQL Shell,InnoDB Cluster 更易于管理和自动化。

我们的 MySQL Shell 之旅到此结束。到目前为止,您应该渴望有机会开始使用它来满足您所有的 MySQL 需求,从简单的 SQL 命令到 InnoDB 集群的管理。Shell 完成了所有的工作。

十二、附录

本书中的示例应用是使用 Python 和 Flask 框架编写的 web 应用。如果您想要实现示例应用,您需要在您的计算机上安装一些东西。

本附录将帮助您为计算机准备所需的工具;您需要安装什么以及如何配置您的环境。我们还将看到关于 Flask 框架的简短介绍,以及运行示例应用的演练。让我们先看看如何设置我们的计算机来运行示例应用。

设置您的环境

对你的环境的改变并不困难,也不漫长。我们将安装 Flask 和一些扩展,这是应用用户界面所需要的。这些 web 库使得用 Python 开发 web 应用比使用原始的 HTML 代码并为请求编写自己的处理程序和代码要容易得多。另外,Flask 并不难学。我们需要安装的库如表 A-1 所示。该表列出了库/扩展的名称、简短描述以及产品文档的 URL。

餐桌 A-1

所需库的列表

|

图书馆

|

描述

|

文件

|
| --- | --- | --- |
| 瓶 | Python Web API | http://flask.pocoo.org/docs/0.12/installation/ |
| 烧瓶脚本 | Flask 的脚本支持 | https://flask-script.readthedocs.io/en/latest/ |
| 烧瓶自举 | 用户界面的改进和增强 | https://pythonhosted.org/Flask-Bootstrap/ |
| 烧瓶-WTF | WTForms 集成 | https://flask-wtf.readthedocs.io/en/latest/ |
| WTForms | 表单验证和呈现 | https://wtforms.readthedocs.io/en/latest/ |

注意

根据您的系统配置,您可能会看到为本节安装的组件安装了更多或更少的组件。

当然,您应该已经在系统上安装了 Python。如果没有,请确保下载并安装最新版本的 2。x 或 3。x 版。本章中的示例代码是用 Python 2.7.10 和 Python 3.6.0 测试的。

要安装这些库,我们可以使用 Python 包管理器 pip 从命令行安装这些库。pip 工具包含在大多数 Python 发行版中,但是如果您需要安装它,您可以在 https://pip.pypa.io/en/latest/installing/ 查看安装文档。

如果需要在 Windows 上安装 pip,需要下载一个安装程序,get-pip.py ( https://pip.pypa.io/en/stable/installing/#installing-with-get-pip-py ,然后将安装目录的路径添加到 PATH 环境变量中。有几篇文章更详细地记录了这个过程。你可以谷歌“在 Windows 10 上安装 pip”,找到包括 https://matthewhorne.me/how-to-install-python-and-pip-on-windows-10/ 在内的几个,这是最准确的。

注意

如果您的系统上安装了多个 Python 版本,pip 命令将安装到默认的 Python 版本环境中。要使用 pip 安装到特定版本,请使用pipN,其中 N 是版本。例如,pip3在 Python 3 环境中安装包。

pip 命令非常方便,因为它使得安装注册的 Python 包(那些在 Python 包索引中注册的包,缩写为 PyPI ( https://pypi.python.org/pypi ))非常容易。pip 命令将使用一个命令下载、解压缩和安装。让我们来看看如何安装我们需要的每个包。

小费

有些系统可能需要以提升的权限运行 pip,例如 sudo (Linux、macOS),或者在命令窗口中以管理员用户身份运行(Windows 10)。如果安装由于权限问题而无法复制文件,您将知道是否需要提升权限。

安装烧瓶

清单 A-1 演示了如何使用命令pip install flask安装 Flask。请注意,该命令下载必要的组件,提取它们,然后运行每个组件的安装程序。在这种情况下,我们看到 Flask 由几个组件组成,包括 Werkzeug、MarkupSafe 和 Jinja2。我们将在烧瓶初级读本部分了解更多。

$ pip3 install flask
Collecting flask
  Using cached Flask-0.12.2-py2.py3-none-any.whl
Collecting Werkzeug>=0.7 (from flask)
  Downloading Werkzeug-0.14.1-py2.py3-none-any.whl (322kB)
    100% |█████████████████████| 327kB 442kB/s
Collecting Jinja2>=2.4 (from flask)
  Using cached Jinja2-2.10-py2.py3-none-any.whl
Collecting itsdangerous>=0.21 (from flask)
  Using cached itsdangerous-0.24.tar.gz
Collecting click>=2.0 (from flask)
  Downloading click-6.7-py2.py3-none-any.whl (71kB)
    100% |█████████████████████| 71kB 9.4MB/s
Collecting MarkupSafe>=0.23 (from Jinja2>=2.4->flask)
  Using cached MarkupSafe-1.0.tar.gz
Installing collected packages: Werkzeug, MarkupSafe, Jinja2, itsdangerous, click, flask
  Running setup.py install for MarkupSafe ... done
  Running setup.py install for itsdangerous ... done
Successfully installed Jinja2-2.10 MarkupSafe-1.0 Werkzeug-0.14.1 click-6.7 flask-0.12.2 itsdangerous-0.24

Listing A-1Installing Flask

安装烧瓶-脚本

清单 A-2 展示了如何使用命令pip install flask-script安装 Flask-Script。注意,在这种情况下,我们看到安装检查先决条件及其版本。

$ pip3 install flask-script
Collecting flask-script
  Using cached Flask-Script-2.0.6.tar.gz
Requirement already satisfied: Flask in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from flask-script)
Requirement already satisfied: click>=2.0 in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from Flask->flask-script)
Requirement already satisfied: Jinja2>=2.4 in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from Flask->flask-script)
Requirement already satisfied: Werkzeug>=0.7 in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from Flask->flask-script)
Requirement already satisfied: itsdangerous>=0.21 in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from Flask->flask-script)
Requirement already satisfied: MarkupSafe>=0.23 in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from Jinja2>=2.4->Flask->flask-script)
Installing collected packages: flask-script
  Running setup.py install for flask-script ... done
Successfully installed flask-script-2.0.6

Listing A-2Installing Flask-Script

安装烧瓶-引导程序

清单 A-3 演示了如何使用命令pip install flask-bootstrap .安装 Flask-Bootstrap,我们再次看到安装检查先决条件及其版本,以及安装相关组件。

$ pip3 install flask-bootstrap
Collecting flask-bootstrap
  Downloading Flask-Bootstrap-3.3.7.1.tar.gz (456kB)
    100% |█████████████████████| 460kB 267kB/s
Requirement already satisfied: Flask>=0.8 in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from flask-bootstrap)
Collecting dominate (from flask-bootstrap)
  Downloading dominate-2.3.1.tar.gz
Collecting visitor (from flask-bootstrap)
  Downloading visitor-0.1.3.tar.gz
Requirement already satisfied: click>=2.0 in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from Flask>=0.8->flask-bootstrap)
Requirement already satisfied: Jinja2>=2.4 in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from Flask>=0.8->flask-bootstrap)
Requirement already satisfied: Werkzeug>=0.7 in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from Flask>=0.8->flask-bootstrap)
Requirement already satisfied: itsdangerous>=0.21 in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from Flask>=0.8->flask-bootstrap)
Requirement already satisfied: MarkupSafe>=0.23 in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from Jinja2>=2.4->Flask>=0.8->flask-bootstrap)
Installing collected packages: dominate, visitor, flask-bootstrap
  Running setup.py install for dominate ... done
  Running setup.py install for visitor ... done
  Running setup.py install for flask-bootstrap ... done
Successfully installed dominate-2.3.1 flask-bootstrap-3.3.7.1 visitor-0.1.3

Listing A-3Installing Flask-Bootstrap

安装烧瓶-WTF

清单 A-4 演示了如何使用命令pip install flask-wtf安装 Flask-WTF。

$ pip3 install flask-wtf
Collecting flask-wtf
  Downloading Flask_WTF-0.14.2-py2.py3-none-any.whl
Requirement already satisfied: WTForms in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from flask-wtf)
Requirement already satisfied: Flask in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from flask-wtf)
Requirement already satisfied: Jinja2>=2.4 in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from Flask->flask-wtf)
Requirement already satisfied: click>=2.0 in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from Flask->flask-wtf)
Requirement already satisfied: Werkzeug>=0.7 in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from Flask->flask-wtf)
Requirement already satisfied: itsdangerous>=0.21 in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from Flask->flask-wtf)
Requirement already satisfied: MarkupSafe>=0.23 in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from Jinja2>=2.4->Flask->flask-wtf)
Installing collected packages: flask-wtf
Successfully installed flask-wtf-0.14.2

Listing A-4Installing Flask-WTF

安装 WTForms

下面演示了如何使用命令pip install wtforms安装 WTForms。在这种情况下,安装很简单,因为我们只需要一个包。

$ pip3 install wtforms
Collecting wtforms
  Using cached WTForms-2.1.zip
Installing collected packages: wtforms
  Running setup.py install for wtforms ... done
Successfully installed wtforms-2.1

安装连接器/Python

您还应该安装 MySQL 连接器/Python 8.0.16 或更高版本的数据库连接器。如果没有,从 https://dev.mysql.com/downloads/connector/python/ 下载并安装。如果您安装了多个版本的 Python,请确保将其安装在您想要使用的所有 Python 环境中。否则,在启动代码时,您可能会看到如下错误。

$ python3 ./mygarage_v1.py runserver -p 5001
Traceback (most recent call last):
  File "./mygarage_v1.py", line 18, in <module>
    from database.mygarage import Databases
  File ".../Ch06/database/mygarage.py", line 15, in <module>
    import mysql.connector
ModuleNotFoundError: No module named 'mysql'

Pip 也可以用来安装 MySQL 连接器/Python。下面显示了如何使用 PIP 安装连接器。

$ pip3 install mysql-connector-python
Collecting mysql-connector-python
  Downloading mysql_connector_python-8.0.16-cp36-cp36m-macosx_10_12_x86_64.whl (3.2MB)
    100% |█████████████████████| 3.2MB 16.9MB/s
Installing collected packages: mysql-connector-python
Successfully installed mysql-connector-python-8.0.16

现在我们的计算机已经安装好了,让我们上一堂关于 Flask 及其相关扩展的速成课。以下不会教你 Flask 的每一个细微之处;相反,我们的目标是让您熟悉应用的布局以及各个部分是如何组合在一起的。

弗拉斯克第一

Flask 是与 Python 一起使用的几个 web 应用库(有时称为框架或应用编程接口)之一。Flask 在众多选择中是独一无二的,因为它很小,一旦你熟悉了它的工作原理,就很容易使用。也就是说,一旦编写了初始化代码,使用 Flask 的大部分工作将局限于创建网页、重定向响应和编写功能代码。

Flask 被认为是一个微框架,因为它体积小、重量轻,而且它不会强迫你进入一个专门编写代码来与框架交互的盒子。它提供了您需要的一切,没有什么是您不需要的,让您自己选择在代码中使用什么。

Flask 由提供基本功能的两个主要组件组成:处理所有工作托管网页的 Web 服务器网关接口(WSGI)和用于简化网页开发的模板库,该模板库减少了学习 HTML 的需要,消除了重复的构造,并为 HTML 代码提供了脚本功能。WSGI 组件被命名为 Werkzeug,它从德语中大致翻译过来的意思是“工作”“东西”( htt p s://palletsprojects.com/p/werkzeug// )。模板组件被命名为 Jinja2,并模仿 Django ( http://jinja.pocoo.org/docs/2.10/ )。两者都是由 Flask 的创始人开发和维护的。

Flask 也是一个可扩展的库,允许其他开发人员创建基本库的附件(扩展)来添加功能。在上一节中,我们看到了如何安装 Flask 可用的一些扩展。我们将在示例应用中使用脚本、引导和 WTForms 扩展。

您可能认为 Flask“缺少”的组件之一是与其他服务(如数据库系统)交互的能力。这是一个有目的的设计,像这样的功能可以通过扩展来实现。我们已经安装了我们需要的扩展以及 Connector/Python,我们需要它来与 MySQL 交互。

Flask 以及前面描述的扩展提供了用 Python 制作 web 应用所需的所有连接和管道。它消除了编写 web 应用所需的几乎所有负担,例如解释客户机响应包、路由、HTML 表单处理等等。如果您曾经用 Python 编写过 web 应用,您将会体会到创建健壮的 web 页面的能力,而无需编写 HTML 和样式表的复杂性。

现在,让我们开始学习 Flask!如果您不着急,尝试一下示例应用,您的第一个 Flask 应用将在第一次尝试时就能工作。学习 Flask 最难的部分已经过去了——安装 Flask 及其扩展。剩下的就是学习在 Flask 中编写应用的概念。在此之前,让我们了解一下 Flask 中的术语,以及如何设置我们将用来初始化本章中使用的应用实例的基本代码。

术语

Flask 旨在简化编写 web 应用的繁琐过程。按照 Flask 的说法,使用代码的两个部分来呈现一个 web 页面:一个视图,在 HTML 文件中定义;一个路由,处理来自客户端的请求。回想一下,我们可以看到两个请求中的一个:一个 GET 请求请求加载一个 web 页面(从客户端的角度读取),一个 POST 请求通过 web 页面将数据从客户端发送到服务器(从客户端的角度写入)。这两个请求都在 Flask 中使用您定义的函数进行处理。

然后,这些函数呈现网页,并将其发送回客户端以满足请求。Flask 调用函数视图函数(或简称视图)。Flask 知道调用哪个方法的方式是使用识别 URL 路径(在 Flask 中称为路由)的装饰器。你可以用一条或多条路线来装饰一个功能,这样就可以提供多种到达视图的方式。用的装饰师是@app.route(<path>)。以下显示了查看功能的多条路线的示例。为了简洁起见,这里显示了该函数的一小段摘录。

@app.route('/handtool', methods=['GET', 'POST'])
@app.route('/handtool/<int:handtool_id>', methods=['GET', 'POST'])
def handtool(handtool_id=None):
    """Manage handtool CRUD operations."""
    handtool_table = Handtool(mygarage)
    form = HandtoolForm()
    # Get data from the form if present
    form_handtoolid = form.handtoolid.data
    # Handtool type choices
    form.handtooltype.choices = HANDTOOL_TYPES
    vendor_list = Vendor(mygarage)
    vendors = vendor_list.read()
    vendor_list = []
...
    return render_template("handtool.html", form=form)

注意这里有多个装饰者。第一个是handtool,它允许我们使用类似于localhost:5000/handtool的 URL,这使得 Flask 将执行路由到handtool()函数。第二个是handtool/<handtool_id>,演示了如何使用变量向视图传递信息。在这种情况下,如果用户(应用)使用 URL localhost:5000/handtool/4842,Flask 将值4842放入handtool_id变量中。这样,我们可以动态地将信息传递给我们的视图。

在函数结束时,我们通过调用render_template()函数(从 flask 模块导入)返回,该函数告诉 flask 返回(刷新)带有我们获取或分配的数据的网页。网页,handtool.html,而视图的一部分在 Flask 中被称为表单。我们将使用这个概念从数据库中检索信息并将其发送给用户。我们可以返回一个简单的 HTML 字符串(或整个文件)或所谓的表单。因为我们使用 Flask-WTF 和 WTForms 扩展,所以我们可以返回一个呈现为 form 类的模板。我们将在后面的章节中讨论表单、表单类以及其他路径和视图。正如您将看到的,模板是另一个强大的功能,它使创建网页变得很容易。

Flask 构建了一个应用中所有路径的列表,使得应用在被请求时可以很容易地将执行路由到正确的函数。但是,如果请求了一条路线,但该路线在应用中不存在,会发生什么情况呢?默认情况下,您会收到一条类似“未找到”的一般性错误消息。在服务器上找不到请求的 URL。我们将在后面的小节中看到如何添加我们自己的自定义错误处理路由。

现在我们已经了解了 Flask 中使用的术语以及它是如何与网页一起工作的,让我们来看看一个典型的 Flask 应用是如何构造的,它带有我们需要的扩展。

初始化和应用实例

Flask 及其扩展为您的 web 应用提供了入口点。Flask 会为您完成这些工作,而不是自己编写所有繁重的代码!我们将在本章使用的 Flask 扩展包括 Flask-Script、Flask-Bootstrap、Flask-WTF 和 WTForms。以下各节简要介绍了每一种方法。

烧瓶脚本

Flask-Script 通过添加一个命令行解析器(表现为 manager)来启用 Flask 应用中的脚本,您可以使用该解析器链接到您编写的函数。这可以通过用@manager.command修饰函数来实现。理解这为我们做了什么的最好方法是通过一个例子。

下面是一个基本的原始 Flask 应用,它什么也不做。它甚至不是一个“hello,world”示例,因为没有显示任何内容,也没有托管任何网页——它只是一个原始的 Flask 应用。

from flask import Flask      # import the Flask framework
app = Flask(__name__)        # initialize the application
if __name__ == "__main__":   # guard for running the code
    app.run()                # launch the application

注意这个app.run()调用。这称为服务器启动,在我们使用 Python 解释器加载脚本时执行。当我们运行这段代码时,我们看到的只是来自 Flask 的默认消息,如下所示。请注意,我们无法查看帮助,因为没有这样的选项。我们还看到代码使用 web 服务器的缺省值启动(如果需要,我们可以在代码中更改)。例如,我们可以改变服务器监听的端口。

$ python ./sample-code.py --help
 ∗ Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

使用 Flask-Script,我们不仅添加了帮助选项,还添加了控制服务器的选项。下面的代码显示了添加语句来启用 Flask-Script 是多么容易。新语句以粗体突出显示。

from flask import Flask          # import the Flask framework
from flask_script import Manager # import the flask script manager class

app = Flask(__name__)            # initialize the application
manager = Manager(app)           # initialize the script manager class

# Sample method linked as a command-line option
@manager.command
def hello_world():
    """Print 'Hello, world!'"""
    print("Hello, world!")

if __name__ == "__main__":       # guard for running the code
    manager.run()                # launch the application via manager class

当运行这段代码时,我们可以看到还有其他选项可用。请注意,文档字符串(紧跟在方法定义之后)显示为所添加命令的帮助文本。

$ python ./flask-script-ex.py --help
usage: flask-script-ex.py [-?] {hello_world,shell,runserver} ...

positional arguments:
  {hello_world,shell,runserver}
    hello_world         Print 'Hello, world!'
    shell               Runs a Python shell inside Flask application context.
    runserver           Runs the Flask development server i.e. app.run()

optional arguments:
  -?, --help            show this help message and exit

注意我们看到了我们添加的命令行参数(命令),hello_world,但是我们也看到了 Flask-Script 提供的两个新参数:shellrunserver。启动服务器时,必须选择其中一个命令。shell 命令允许您在 Python 解释器或类似工具中使用代码,而runserver执行启动 web 服务器的代码。

我们不仅可以获得关于命令和选项的帮助,Flask-Script 还提供了从命令行对服务器的更多控制。事实上,通过附加- help 选项,我们可以看到每个命令的所有选项,如下所示。

$ python ./flask-script-ex.py runserver --help
usage: flask-script-ex.py runserver [-?] [-h HOST] [-p PORT] [--threaded]
                                    [--processes PROCESSES]
                                    [--passthrough-errors] [-d] [-D] [-r] [-R]
                                    [--ssl-crt SSL_CRT] [--ssl-key SSL_KEY]

Runs the Flask development server i.e. app.run()

optional arguments:
  -?, --help            show this help message and exit
  -h HOST, --host HOST
  -p PORT, --port PORT
  --threaded
  --processes PROCESSES
  --passthrough-errors
  -d, --debug           enable the Werkzeug debugger (DO NOT use in production
                        code)
  -D, --no-debug        disable the Werkzeug debugger
  -r, --reload          monitor Python files for changes (not 100% safe for
                        production use)
  -R, --no-reload       do not monitor Python files for changes
  --ssl-crt SSL_CRT     Path to ssl certificate
  --ssl-key SSL_KEY     Path to ssl key

请注意,在这里我们可以看到,我们可以控制服务器的所有方面,包括端口、主机,甚至它是如何执行的。

最后,我们可以执行我们修饰为命令行选项的方法,如下所示。

$ python ./flask-script-ex.py hello_world
Hello, world!

因此,Flask-Script 仅用几行代码就提供了一些非常强大的功能。你一定会喜欢的!

烧瓶自举

Flask-Bootstrap 最初由 Twitter 开发,用于制作统一、美观的 web 客户端。幸运的是,他们把它变成了一个 Flask 扩展,这样每个人都可以利用它的特性。Flask-Bootstrap 是一个独立的框架,它提供了更多的命令行控制和用户界面组件,以获得干净、漂亮的网页。它也兼容最新的网络浏览器。

该框架在幕后发挥了神奇的作用,它是一个级联样式表(CSS)和脚本的客户端库,这些样式表和脚本是从 Flask 中的 HTML 模板(通常称为 HTML 文件或模板文件)调用的。我们将在后面的章节中学习更多关于模板的知识。由于是客户端,在主应用中初始化就不会看到太多。不管怎样,下面显示了如何将 Flask-bootstrap 添加到我们的应用代码中。这里,我们看到我们有一个框架,其中初始化并配置了 Flask-Script 和 Flask-Bootstrap。

from flask import Flask          # import the Flask framework
from flask_script import Manager # import the flask script manager class
from flask_bootstrap import Bootstrap  # import the flask bootstrap extension

app = Flask(__name__)            # initialize the application
manager = Manager(app)           # initialize the script manager class
bootstrap = Bootstrap(app)       # initialize the bootstrap extension

if __name__ == "__main__":       # guard for running the code
    manager.run()                # launch the application via manager class

WTForms

WTForms 是我们需要用来支持 Flask-WTF 扩展的一个组件。它提供了 Flask-WTF 组件所提供的大部分功能(因为 Flask-WTF 组件是 WTForms 的 Flask 特定包装器)。因此,我们只需要安装它作为 Flask-WTF 的先决条件,我们将在 Flask-WTF 的上下文中讨论它。

烧瓶-WTF

Flask-WTF 扩展是一个有趣的组件,提供了几个非常有用的附加功能,最值得一提的是供我们使用,与 WTForms(一个框架不可知组件)的集成,允许创建表单类,以及以跨站点请求伪造(CSRF)保护的形式提供额外的 web 安全性。这两个特性允许您将 web 应用提升到更高的复杂程度。

表单类

表单类提供了一个类的层次结构,使得定义网页更加符合逻辑。使用 Flask-WTF,您可以使用两段代码定义表单;一个从FormForm类(从 Flask 框架导入)派生的特殊类,您可以使用一个或多个提供数据编程访问的附加类来定义字段,以及一个用于呈现网页的 HTML 文件(或模板)。这样,我们在 HTML 文件上看到了一个抽象层(表单类)。我们将在下一节看到更多关于 HTML 文件的内容。

使用表单类,您可以定义一个或多个字段,比如文本的TextField,字符串的StringField,等等。更好的是,您可以定义允许您以编程方式描述数据的验证器。例如,您可以为文本字段定义最小和最大字符数。如果提交的字符数超出范围,将生成一条错误消息。是的,您可以定义错误消息!下面列出了一些可用的验证器。查看 http://wtforms.readthedocs.io/en/latest/validators.html 获得验证器的完整列表。

为了形成类,我们必须导入类和任何我们想在应用的序言中使用的字段类。下面显示了一个导入表单类和表单域类的示例。在这个例子中,我们还导入了一些验证器,用于自动验证数据。

from flask_wtf import FlaskForm
from wtforms import (HiddenField, TextField, TextAreaField, SelectField,
                     SelectMultipleField, IntegerField, SubmitField)
from wtforms.validators import Required, Length

要定义一个表单类,我们必须从FlaskForm派生一个新类。从那里,我们可以构造我们想要的类,但是它允许您定义字段。FlaskForm父类包括 Flask 需要实例化和使用 form 类的所有必要代码。

让我们看一个简单的例子。下面显示了 handtool 网页的 form 类。我们将通过 view 函数链接到这个代码的 handtool 表包含几个字段。我们使用一个可用的字段类为我们想要放在页面上的每个字段添加一个类。因为 Id 字段不是用户需要看到的,所以我们将该字段设为隐藏字段,其他字段为TextField()类的衍生物。注意这些是如何在清单中用名称(标签)作为第一个参数来定义的。

class HandtoolForm(FlaskForm):
    handtoolid = HiddenField('Id')
    vendor = NewSelectField(
        'Vendor', validators=[Required(message=REQUIRED.format("Vendor"))]
    )
    description = TextField(
        'Description',
        validators=[Required(message=REQUIRED.format("Description")),
                    Length(min=1, max=125,
                    message=RANGE.format("Description", 1, 125))]
    )
    handtooltype = NewSelectField(
        'Handtool Type',
        validators=[Required(message=REQUIRED.format("Handtool Type"))]
    )
    toolsize = TextField('ToolSize')
    place = NewSelectField(
        'Location',
        validators=[Required(message=REQUIRED.format("Location"))]
    )
    create_button = SubmitField('Add')
    del_button = SubmitField('Delete')
    close_button = SubmitField('Close')

还要注意,我们已经为字段定义了一组验证器,其形式为从 WTForms 组件导入的函数调用。在每一种情况下,我们都为消息使用字符串,以使代码更容易阅读,更统一。

我们使用Required()验证器来指示字段必须有一个值。我们用字段的名称增加了默认的错误消息,使用户更容易理解。我们还使用了一个Length()验证函数,它定义了字段数据的最小和最大长度。我们再次增加了默认的错误消息。验证器仅适用于 POST 操作(当提交事件发生时)。

接下来,我们看到有三个SubmitField()实例:一个用于创建(添加)按钮,另一个用于删除按钮和关闭按钮。正如您可能猜到的,按照 HTML 的说法,这些字段被呈现为类型为“submit”的<input>字段。

有几个字段类可供使用。表 A-2 显示了最常用的字段类(也称为 HTML 字段)的示例。您还可以从这些字段派生来创建自定义字段类,并为可以显示在字段旁边的标签提供文本(例如,作为按钮文本)。我们将在后面的章节中看到一个这样的例子。

表 A-2

WTForms 字段类

|

字段类

|

描述

|
| --- | --- |
| BooleanField | 具有真值和假值的复选框 |
| DateField | 接受日期值 |
| DateTimeField | 接受日期时间值 |
| DecimalField | 接受十进制值 |
| FileField | 文件上传字段 |
| FloatField | 接受浮点值 |
| HiddenField | 隐藏文本字段 |
| IntegerField | 接受整数值 |
| MultipleFileField | 允许选择多个文件 |
| PasswordField | 密码(屏蔽)文本字段 |
| RadioField | 单选按钮列表 |
| SelectField | 下拉列表(选择一个) |
| SelectMultipleField | 下拉选项列表(选择一项或多项) |
| StringField | 接受简单文本 |
| SubmitField | 表单提交按钮 |
| TextAreaField | 多行文本字段 |

跨站点请求伪造(CSRF)保护

跨站点请求伪造(CSRF)保护是一种允许开发人员用加密密钥对网页进行签名的技术,这使得黑客欺骗 GET 或 POST 请求变得更加困难。这是通过首先在应用代码中放置一个特殊的键,然后在每个 HTML 文件中引用这个键来实现的。下面显示了一个应用序言的示例。请注意,我们需要做的就是用一个短语给app.config数组的SECRET_KEY索引赋值。这应该是一个不容易猜到的短语。

from flask import Flask          # import the Flask framework
from flask_script import Manager # import the flask script manager class
from flask_bootstrap import Bootstrap  # import the flask bootstrap extension

app = Flask(__name__)            # initialize the application
app.config['SECRET_KEY'] = "He says, he's already got one!"
manager = Manager(app)           # initialize the script manager class
bootstrap = Bootstrap(app)       # initialize the bootstrap extension

if __name__ == "__main__":       # guard for running the code
    manager.run()                # launch the application via manager class

要激活网页中的 CSRF,我们只需将form.csrf_token添加到 HTML 文件中。这是一个特殊的隐藏字段,Flask 使用它来验证请求。我们将在后面的部分中看到更多关于在哪里放置它的信息。但首先,我们来看看 Flask 的一个很酷的功能,叫做 flash。

信息闪烁

Flask 有很多很酷的功能。Flask 扩展的创建者似乎已经考虑到了一切——甚至是错误消息。考虑一个典型的 web 应用。你如何向用户传达错误?您是否重定向到一个新页面,发出一个弹出窗口,或者可能在页面上显示错误?Flask 有一个解决方案,叫做消息闪烁。

消息闪烁是使用 Flask 框架中的 flash()方法完成的。我们只需在代码的序言中导入它;然后当我们想要显示消息时,我们调用 flash()函数,传入我们想要看到的错误消息。Flask 将在表单顶部的一个格式良好的框中显示错误。它没有取代表单,也不是弹出窗口,但是它允许用户关闭消息。您可以使用 flash messaging 向用户传达错误、警告甚至状态更改。图 A-1 显示了一个快速消息的例子。在本例中,我们看到两条 flash 消息,表明您可以同时显示多条消息。请注意用于消除图像的消息右侧的小 X。

img/478423_1_En_12_Fig1_HTML.jpg

图 A-1

示例简讯

HTML 文件和模板

让我们回顾一下到目前为止的旅程。我们发现了如何用各种组件初始化应用,并了解了 Flask 如何通过 decorators 使用路由来为应用创建一组 URLs 这些路由指向一个视图函数,该函数实例化了 form 类。下一个难题是如何将 HTML 网页链接到 form 类。

回想一下,这是通过render_template()函数完成的,在这里我们传入一个 HTML 文件的名称进行处理。template 出现在名称中的原因是因为我们可以使用 Jinja2 模板组件来简化 web 页面的编写。更具体地说,HTML 文件包含 HTML 标记和 Jinja2 模板构造。

注意

所有 HTML 文件(模板)必须存储在 templates 文件夹中,与主应用代码位于同一位置。如果你把它们放在其他地方,Flask 将找不到 HTML 文件。

模板和表单类是设计用户界面的地方。简而言之,模板用于包含表示逻辑,HTML 文件用于包含表示数据。这些主题可能是一些人需要花一些时间来尝试如何使用它们的领域。下面几节将通过工作示例的演示,向您简要介绍 Jinja2 模板以及如何在我们的 HTML 文件中使用它们。有关更多详细信息,请参见在线文档。

Jinja2 模板概述

Jinja2 模板,也就是模板,用于包含任何表示逻辑,比如遍历数据数组,决定显示什么,甚至格式化和表示设置。如果您熟悉其他 web 开发环境,您可能已经看到过这种封装在脚本中或通过嵌入式脚本(如 JavaScript)实现的功能。

回想一下,我们在主代码中呈现了网页。这个函数告诉 Flask 读取指定的文件,并将模板结构转换(渲染)成 HTML。也就是说,Flask 会将模板结构扩展并编译成 web 服务器可以呈现给客户机的 HTML。

有几种模板结构可以用来控制执行流、循环甚至注释。每当你想使用一个模板结构(想想脚本语言),你用前缀和后缀{% %}把它括起来。这是为了让 Flask 框架将该构造识别为模板操作,而不是 HTML。

然而,看到模板结构与 HTML 标记混杂在一起并不罕见,也很正常。事实上,这正是你应该做的。毕竟,您将创建的文件被命名为. html。它们只是碰巧包含模板构造。这是否意味着在使用 Flask 时只能使用模板?不,当然不是。如果你愿意,你可以渲染一个纯 HTML 文件!

起初,查看模板可能会令人望而生畏。但也没那么难。只需查看所有将{%%}作为“代码”部分的行。你也可以看到以{# #}前缀和后缀形式的评论。

如果你看看模板,你会看到构造和标签,并使用两个空格缩进格式化。在标签和构造之外,缩进和空白通常无关紧要。然而,大多数开发人员会使用某种形式的缩进来使文件更容易阅读。事实上,大多数编码指南都要求缩进。

模板除了构造(想想代码)之外的一个很酷的特性是创建模板层次结构的能力。这允许您创建一个其他模板可以使用的“基础”模板。例如,您可以创建一个模板构造和 HTML 标记的样板文件,这样您的所有网页看起来都一样。

回想一下 Flask-Bootstrap,Bootstrap 提供了几个很好的格式化特性。其中一个功能是创建一个外观漂亮的导航栏。很自然,我们希望它出现在我们所有的网页上。我们可以通过在基本模板中定义它并在我们的其他模板(HTML)文件中扩展它来做到这一点。让我们看一下示例应用的基本模板。清单 A-5 显示了库应用的基本模板。

{% extends "bootstrap/base.html" %}
{% block title %}MyGarage{% endblock %}
{% block navbar %}
<div class="navbar navbar-inverse" role="navigation">
    <div class="container">
        <div class="navbar-header">
            <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand" href="/">MyGarage v1</a>
        </div>
        <div class="navbar-collapse collapse">
            <ul class="nav navbar-nav">
                <li><a href="/list/handtool">Handtools</a></li>
            </ul>
            <ul class="nav navbar-nav">
                <li><a href="/list/powertool">Powertools</a></li>
            </ul>
            <ul class="nav navbar-nav">
                <li><a href="/list/organizer">Organizers</a></li>
            </ul>
            <ul class="nav navbar-nav">
                <li><a href="/list/storage">Storage Equipment</a></li>
            </ul>
            <ul class="nav navbar-nav">
                <li><a href="/list/place">Storage Places</a></li>
            </ul>
            <ul class="nav navbar-nav">
                <li><a href="/list/vendor">Vendors</a></li>
            </ul>
        </div>
    </div>
</div>
{% endblock %}

{% block content %}
<div class="container">
    {% for message in get_flashed_messages() %}
    <div class="alert alert-warning">
        <button type="button" class="close" data-dismiss="alert">&times;</button>
        {{ message }}
    </div>
    {% endfor %}

    {% block page_content %}{% endblock %}
</div>
{% endblock %}

Listing A-5Sample Base Template

哇,这里发生了很多事情!花些时间通读一下。虽然这看起来像是发生在罗斯威尔的飞船事故,但理解起来并不困难。你可以在 http://jinja.pocoo.org/docs/2.10/ 找到关于模板和 Jinja2 的完整解释。

使用模板的 HTML 文件

现在我们知道模板文件为页面生成 HTML,我们准备看看如何显示我们在表单类中定义的字段类。让我们从如何在示例应用中显示供应商数据的演练开始讨论。我们从定义给视图函数的表单类和字段类开始,视图函数呈现模板,最后是模板本身。

回想一下,表单类是我们定义一个或多个表单字段的地方。我们将使用这些字段类实例来访问视图函数和模板中的数据。清单 A-6 显示了表单类。

class VendorForm(FlaskForm):
    """Vendor form class"""
    vendorid = HiddenField('VendorId')
    name = TextField(
        'Name',
        validators=[
            Required(message=REQUIRED.format("Name")),
            Length(min=1, max=50, message=RANGE.format("Name", 1, 50))]
        )
    url = TextField(
        'URL', validators=[
            Required(message=REQUIRED.format("URL")),
            Length(min=0, max=125, message=RANGE.format("URL", 0, 125))]
    )
    sources = TextField(
        'Sources',
        validators=[
            Required(message=REQUIRED.format("Sources")),
            Length(min=0, max=40, message=RANGE.format("Sources", 0, 40))]
    )
    create_button = SubmitField('Add')
    del_button = SubmitField('Delete')
    close_button = SubmitField('Close')

Listing A-6
Vendor Form Class

注意,form 类创建了四个字段;一个用于供应商 Id,这是一个隐藏字段,数据库表中的名称、URL 和源列各有一个。我们还看到三个提交字段(按钮);一个用于创建新数据(create_button),一个用于删除供应商数据(del_button),另一个用于关闭表单(close_button)。

在视图函数中实例化表单数据之后,当呈现表单数据时,我们将表单数据传递给模板。清单 A-7 显示了供应商数据的视图功能,为清晰起见,删除了数据库代码,占位符以粗体显示。这里,我们首先实例化供应商表单类,然后将其传递给模板。

@app.route('/vendor', methods=['GET', 'POST'])
@app.route('/vendor/<int:vendor_id>', methods=['GET', 'POST'])
def vendor(vendor_id=None):
    """Manage vendor CRUD operations."""
    form = VendorForm()
    if vendor_id:
        # Read operation goes here
        form.create_button.label.text = "Update"
    else:
        del form.del_button
    if request.method == 'POST':
        operation = "Create"
        if form.close_button.data:
            operation = "Close"
        if form.create_button.data:
            if form.create_button.label.text == "Update":
                operation = "Update"
        if form.del_button and form.del_button.data:
            operation = "Delete"
            # Delete operation goes here
        if form.validate_on_submit():
            # Get the data from the form here
            if operation == "Close":
                return redirect('/list/vendor')
            elif operation == "Create":
                # Create operation goes here
            elif operation == "Update":
                # Delete operation goes here
        else:
            flash_errors(form)
    return render_template("vendor.html", form=form)

Listing A-7Vendor View Function (no database access)

请注意,这里我们看到了为视图定义的路线。还要注意,我们已经为请求设置了包括 GET 和 POST 的方法。注意,我们可以检查请求是否是 POST(提交数据)。在这种情况下,我们可以从 form 类实例中检索数据,并将其保存到数据库中。

最后,请注意,我们实例化了供应商表单类(form)的一个实例,然后将其作为参数传递给render_template("vendor.html", form=form)调用。在这种情况下,我们现在呈现存储在 templates 文件夹中的vendor.html模板。

好了,现在我们有了表单类和视图函数。现在的焦点是当我们呈现 HTML 模板文件时会发生什么。清单 A-8 显示了供应商数据的 HTML 文件(模板)。

{% extends "base.html" %}
{% block title %}MyGarage Search{% endblock %}
{% block page_content %}
  <form method=post> {{ form.csrf_token }}
    <fieldset>
      <legend>Vendor - Detail</legend>
      {{ form.hidden_tag() }}
      <div style=font-size:20pz; font-weight:bold; margin-left:150px;s>
        {{ form.name.label }} <br>
        {{ form.name(size=50) }} <br>
        {{ form.url.label }} <br>
        {{ form.url(size=100) }} <br>
        {{ form.sources.label }} <br>
        {{ form.sources(size=40) }} <br>
        <br>
        {{ form.create_button }}
        {% if form.del_button %}
          {{ form.del_button }}
        {% endif %}
        {{ form.close_button }}
      </div>
    </fieldset>
  </form>
{% endblock %}

Listing A-8
Vendor HTML File

注意,模板从扩展(继承)我们之前讨论过的base.html模板文件开始。我们看到一个块定义了标题,另一个块定义了页面内容。在该块中,我们看到了如何在页面上定义字段,从表单类实例(form)引用字段类实例。实际上,注意我们引用了字段的标签和数据。标签是在声明字段类时定义的,数据是存储值的地方。当我们想要填充表单(GET)时,我们将数据元素设置为值,当我们想要读取数据(POST)时,我们引用数据元素。

还要注意,为了安全起见,我们添加了 CSRF 令牌,用form.hidden_tag()函数呈现隐藏字段,并有条件地包含提交字段,包括删除提交字段(del_button)。

咻!这就是 Flask 呈现网页的方式。一旦您习惯了,这是一种很好的方式来分离几层功能,并使从用户那里获取数据或呈现给用户变得容易。

现在,让我们看看如何在我们的应用中构建定制的错误处理程序,以及稍后如何将应用中的控制重定向到正确的视图函数。

错误处理程序

回想一下,我们提到过可以为应用中的错误创建自己的错误处理机制。有两种这样的错误机制你应该考虑;一个用于 404(未找到)错误,另一个用于 500(应用错误)。为了定义每一个,我们首先创建一个用@app.errorhandler(num)修饰的视图函数、一个视图函数和一个 HTML 文件。让我们看看每个例子。

未找到(404)错误

为了处理 404(未找到)错误,我们创建了一个带有特殊错误处理程序路由函数的视图函数,该函数呈现 HTML 文件。Flask 会自动将所有未找到的错误条件定向到此视图。下面显示了 404 未找到错误处理程序的视图函数。如你所见,这很简单。

@app.errorhandler(404)
def page_not_found(e):
    return render_template('404.html'), 404

相关的错误处理程序 HTML 代码位于名为 404.html 的文件中,如下所示。请注意,我们从 base.html 文件继承而来,因此生成的网页看起来与应用中的任何其他网页一样,都包含来自引导组件的菜单。注意,我们还可以定义错误消息的文本和标题。随意修饰你自己的错误处理程序,让你的用户更感兴趣。

{% extends "base.html" %}
{% block title %}MyGarage ERROR: Page Not Found{% endblock %}
{% block page_content %}
<div class="page-header">
    <h1>Page not found.</h1>
</div>
{% endblock %}

应用(500)错误

为了处理 500 个(应用)错误,我们遵循与前面相同的模式。下面是应用错误的错误处理程序。

@app.errorhandler(500)
def internal_server_error(e):
    return render_template('500.html'), 500

相关的错误处理程序 HTML 代码位于名为 500.html 的文件中,如下所示。请注意,我们从 base.html 文件继承而来,因此生成的网页看起来与应用中的任何其他网页一样,都带有来自 bootstrap 组件的菜单。

{% extends "base.html" %}
{% block title %}MyGarage ERROR{% endblock %}
{% block page_content %}
<div class="page-header">
    <h1>OOPS! Application error.</h1>
</div>
{% endblock %}

强烈建议所有 Flask 应用创建这些基本的错误处理程序。在开发应用时,您可能会发现应用错误处理程序非常有用。您甚至可以扩充代码,以提供要在网页中显示的调试信息。

重新寄送

此时,您可能想知道 Flask 应用如何以编程方式将执行从一个视图定向到另一个视图。答案是 Flask 中的另一个简单构造:重定向。我们使用带有 URL 的redirect()函数(从 flask 模块导入)将控制重定向到另一个视图。例如,假设您有一个列表表单,根据用户单击的按钮(通过 POST 提交表单),您希望显示不同的 web 页面。下面演示了如何使用redirect()函数来实现这一点。

if kind == 'handtool':
    form.form_name.label = 'Handtools'
    if request.method == 'POST':
        return redirect('handtool')
...
elif kind == 'organizer':
    form.form_name.label = 'Organizers'
    if request.method == 'POST':
        return redirect('organizer')
...
elif kind == 'powertool':
    form.form_name.label = 'Powertools'
    if request.method == 'POST':
        return redirect('powertool')
...

这里,我们看到 POST 请求后有三个重定向。在每种情况下,我们都使用应用中定义的一个路由来告诉 Flask 调用相关的视图函数。这样,我们可以创建一个菜单或一系列提交字段,允许用户从一个页面移动到另一个页面。

redirect()函数需要一个有效的路径,在大多数情况下,它只是您在装饰器中提供的文本。但是,如果您需要形成一个复杂的 URL 路径,您可以在重定向之前使用url_for()函数来验证路由。如果您重组或更改路线,该功能还有助于避免断开链接。例如,您可以使用redirect(url_for('vendor'))来验证路线并为其形成一个 URL。

附加功能

除了我们在这个速成班中所看到的,Flask 还有更多的内容。您可能有兴趣进一步了解一些未讨论的内容,包括以下内容(这只是其中的一部分)。如果您对这些感兴趣,可以考虑在在线文档中查找它们。

小费

有关 Flask 以及如何使用它及其相关包的更多信息,以下书籍是该主题的绝佳参考: Flask Web Development:用 Python 开发 Web 应用 (O'Reilly Media 2014),Miguel Grinberg。

烧瓶审查:样品应用

现在我们已经对 Flask 有了一个简单的了解,让我们看看如何在我们的一个示例应用中实现这一切。在本节中,我们将回顾如何设置示例应用以及如何启动它们。在这一节中,我们将看到来自第五章的示例应用。后面章节中的示例应用以非常相似的方式工作。一旦您熟悉了如何启动这里的示例并与之交互,您就应该能够运行其他示例应用了。我们从如何下载和复制文件开始。

准备您的电脑

你要做的第一件事就是从图书网站 https://www.apress.com/us/book/97814NNNNNNNN 下载这本书的源代码。您应该会看到代表每章源代码的文件夹。只需下载与您想要的章节相匹配的文件夹。下载源代码并解压后,找到要使用的示例应用的文件夹。例如,第五章中的示例应用位于 https://github.com/apress/introducing-mysql-8-shell 的一个同名文件夹中。

此时,您必须选择运行应用的位置。如果您想从下载和解压缩文件的位置运行它,您可以。但是,最好将代码移动到另一个位置。

例如,在 Linux 或 macOS 上,您可以将它放在您的home文件夹中名为 source 的文件夹中。或者,在 Windows 10 上,你可以把它放在你的Documents文件夹里。一旦决定了位置,就可以创建一个文件夹来包含示例应用(mygarage_v1)。接下来,将提取章节代码时创建的文件复制到mygarage_v1文件夹中,包括子目录。如果您随后显示该文件夹中的文件列表,您应该有主可执行文件和两个子文件夹,如下所示。

C:\Users\cbell\Documents\mygarage_v1>dir
 Volume in drive C is Local Disk
 Volume Serial Number is AAFC-6767

 Directory of C:\Users\cbell\Documents\mygarage_v1

03/14/2019  02:20 PM    <DIR>          .
03/14/2019  02:20 PM    <DIR>          ..
03/14/2019  02:20 PM    <DIR>          database
03/08/2019  11:02 PM            38,045 mygarage_v1.py
03/14/2019  02:20 PM    <DIR>          templates
               1 File(s)         38,045 bytes
               4 Dir(s)  124,419,055,616 bytes free

两个子文件夹databasetemplates用于存储我们需要的代码模块和分类文件。database文件夹是我们放置数据库代码模块的地方,templates文件夹是我们放置.html文件的地方。如果你愿意,你可以浏览这些文件夹的内容。

现在,您应该已经在您的计算机上安装并运行了 MySQL。您将需要您想要用来连接到 MySQL 的用户帐户和密码,以便输入命令来启动应用。在此之前,让我们确保已经创建并填充了数据库。

回想一下第五章的讨论,示例源代码包含一个名为database/garage_v1.sql的文件,该文件包含用于创建示例数据库并使用示例数据填充该数据库的 SQL 语句。如果您还没有这样做,让我们现在就开始吧。

转到数据库文件夹,发出以下命令,告诉 shell 打开文件并执行语句。运行它不需要一分钟,因为我们是在批处理模式下运行的,完成后将退出 shell。清单 A-9 显示了运行这些命令的结果。

C:\Users\cbell\Documents\mygarage_v1>cd database
C:\Users\cbell\Documents\mygarage_v1\database>mysqlsh --uri root@localhost:3306 --sql -f garage_v1.sql
Records: 31  Duplicates: 0  Warnings: 0
Records: 6  Duplicates: 0  Warnings: 0
Records: 250  Duplicates: 0  Warnings: 0
Records: 3  Duplicates: 0  Warnings: 0
Records: 0  Duplicates: 0  Warnings: 0
Records: 0  Duplicates: 0  Warnings: 0
Records: 0  Duplicates: 0  Warnings: 0
Records: 0  Duplicates: 0  Warnings: 0
Records: 22  Duplicates: 0  Warnings: 0
Records: 0  Duplicates: 0  Warnings: 0
Records: 0  Duplicates: 0  Warnings: 0
Records: 0  Duplicates: 0  Warnings: 0
Records: 0  Duplicates: 0  Warnings: 0
Records: 22  Duplicates: 0  Warnings: 0
Records: 0  Duplicates: 0  Warnings: 0
Records: 0  Duplicates: 0  Warnings: 0
Records: 2  Duplicates: 0  Warnings: 0
Records: 3  Duplicates: 0  Warnings: 0

Listing A-9Populating the Example Database (Windows 10)

既然我们已经创建并填充了数据库,我们就可以第一次启动示例应用了。

运行示例应用

回到mygarage_v1文件夹,用下面粗体显示的命令运行应用。注意,系统会提示您输入 MySQL 用户 Id(用户帐户)和密码。

C:\Users\cbell\Documents\mygarage_v1>python mygarage_v1.py runserver
User Id: root
Password:
 ∗ Serving Flask app "mygarage_v1" (lazy loading)
 ∗ Environment: production
   WARNING: Do not use the development server in a production environment.
   Use a production WSGI server instead.
 ∗ Debug mode: off
 ∗ Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

如果您运行的是 Flask 和 Python 的新版本,您可能会看到一个或多个反对警告,如“DeprecationWarning: Required is going away in WTForms 3.0, use DataRequired”。如果发生这种情况,您可以使用选项-W ignore::DeprecationWarning抑制警告,如下所示。

C:\Users\cbell\Documents\mygarage_v1>python -W ignore::DeprecationWarning mygarage_v1.py runserver

请注意输出末尾的那一行。这将向您显示在浏览器中用于连接和使用应用的 URL。它正在运行,我们只需要连接到它。请将该 URL 复制到您的浏览器中。当您按下时,输入(或点击 Go 等。),您将看到如图 A-2 所示的应用登录页面。

img/478423_1_En_12_Fig2_HTML.jpg

图 A-2

登录页面(my garage _ v1)-存储设备列表视图

在这里,我们看到的登陆或默认页面是存储设备列表。这是默认设置,因为这是车库存储解决方案的最高视图级别,即车库中的所有容器。

请注意,在应用的顶部,我们看到了 Flask 横幅,其中包含应用中每个视图的链接(按钮),包括手动工具、电动工具、组织器、存储设备、存储位置和供应商。每个链接都显示视图中的项目列表。因此,我们称之为列表视图。

请注意,视图中的每一行都有一个修改/查看链接,我们可以使用它来查看某个项目的更多详细信息,或者我们可以编辑(或删除)该项目。顶部的 New 按钮允许我们为活动列表视图创建一个新项目。

正如我们在第五章中了解到的,其中的每一个都代表数据库中的一个表。手工工具、电动工具和供应商代表了车库中的工具及其制造商。其他视图是我们在车库中用来组织工具的容器。

正如你在阅读第五章时所猜测的,每个视图都是以列表的形式呈现的。因此,我们将在每个视图(表)中看到所有项目(记录)的列表。让我们按照横幅上显示的顺序简要看一下其中的每一项。图 A-3 为手工具视图,图 A-4 为手工具详图。

img/478423_1_En_12_Fig3_HTML.jpg

图 A-3

手工工具列表视图

在此视图中,我们可以看到工具类型、描述、尺寸、存放在哪个存储设备中、位置类型和位置。因此,我们一眼就能看到我们的 8" C 夹钳存放在滚动工具柜的底部抽屉里。还要注意,我们有每个项目的修改/查看链接,这允许我们编辑项目(行)或查看关于手持工具的更多细节。

img/478423_1_En_12_Fig4_HTML.jpg

图 A-4

手提包视图

在这里,我们看到底部有更新、删除或关闭操作的按钮。如果我们想进行更改,我们使用更新按钮。如果我们想删除项目(记录/行),我们使用删除按钮。关闭按钮只是关闭表单并返回列表视图。所有详细视图表单上都有这些按钮。图 A-5 显示了电动工具列表视图,其布局与手动工具列表视图相似。

img/478423_1_En_12_Fig5_HTML.jpg

图 A-5

电动工具列表视图

图 A-6 显示了组织者列表视图。

img/478423_1_En_12_Fig6_HTML.jpg

图 A-6

组织者列表视图

图 A-7 显示了存储设备列表视图,也是开始或着陆视图。

img/478423_1_En_12_Fig7_HTML.jpg

图 A-7

存储设备列表视图

现在,您开始意识到所有的列表视图都有相似的布局。不同之处在于列,因为一些列表视图使用不同的列来显示项目。让我们完成对列表视图的调查。

图 A-8 显示了存储位置列表视图。在这里,我们看到与表中存储的数据相匹配的略有不同的列。

img/478423_1_En_12_Fig8_HTML.jpg

图 A-8

存储位置列表视图

最后,图 A-9 显示了供应商列表视图。

img/478423_1_En_12_Fig9_HTML.jpg

图 A-9

供应商列表视图

此时,如果您已经启动了应用,您应该会在终端(命令窗口)中看到几条消息。每一个都告诉我们当你点击视图时应用的反应。以下是摘录。这是完全正常的,如果出现问题,可以帮助您诊断任何问题。

127.0.0.1 - - [17/Mar/2019 17:06:48] "GET /list/handtool HTTP/1.1" 200 -
127.0.0.1 - - [17/Mar/2019 17:07:22] "GET /list/powertool HTTP/1.1" 200 -
127.0.0.1 - - [17/Mar/2019 17:07:42] "GET /list/organizer HTTP/1.1" 200 -
127.0.0.1 - - [17/Mar/2019 17:08:03] "GET /list/storage HTTP/1.1" 200 -
127.0.0.1 - - [17/Mar/2019 17:08:26] "GET /list/place HTTP/1.1" 200 -
127.0.0.1 - - [17/Mar/2019 17:08:55] "GET /list/vendor HTTP/1.1" 200 -
127.0.0.1 - - [17/Mar/2019 17:09:17] "GET /list/place HTTP/1.1" 200 -
127.0.0.1 - - [17/Mar/2019 17:09:18] "GET /list/storage HTTP/1.1" 200 -
127.0.0.1 - - [17/Mar/2019 17:09:20] "GET /list/organizer HTTP/1.1" 200 -
127.0.0.1 - - [17/Mar/2019 17:09:22] "GET /list/powertool HTTP/1.1" 200 -
127.0.0.1 - - [17/Mar/2019 17:09:23] "GET /list/handtool HTTP/1.1" 200 –

现在,让我们回顾一下如何使用应用来存储和定位车库中的工具,包括如何表示创建、读取、更新和删除(CRUD)操作。

如何使用该应用

让我们从解释如何使用示例应用开始。虽然你在书中找到的大多数示例应用只是为了简单地演示一个概念,但是很少有一个完整的、可用的应用。第 5 和第六章中的应用旨在让任何人都可以使用,不仅是为了演示概念,也是为了使用!

我们已经看到了如何获取表(数据库)中各种项目的列表,并且我们已经了解到我们可以编辑我们想要进行更改的任何项目,但是当您想要添加一个新项目时,您该怎么做呢?

例如,当您获得一个新工具,或者可能丢失或赠送了一个工具时,您可以通过添加一个新工具来标识其存储位置,从而编辑数据。也就是说,您确定组织者、存储位置和存储设备。例如,如果您获得了一个新的锤子,您可以在 handtools 列表视图中点击 New 按钮,填写数据,并使用下拉列表选择它的存放位置。注意,当你打开一个新的详细视图时,现在显示删除按钮(与创建操作无关)。图 A-10 展示了一个例子。

img/478423_1_En_12_Fig10_HTML.jpg

图 A-10

添加新锤子

请注意,这里有供应商、工具类型和位置的下拉列表。这些都在数据库中有所表示。回想一下,工具类型是 handtools 表中的一个枚举。供应商只是在 vendors 表中查找供应商名称。位置下拉列表的填充更加复杂,因为它是存储设备和存储位置表的组合(连接),因此您可以选择单个条目,而不是从几个下拉列表中进行选择。

那么,这对你组织事情有什么帮助呢?回想一下,我们还可以添加新的组织者、存储位置和存储设备。例如,如果您想要添加一个存储箱来存储物品,您可以在“组织者”视图中为它创建一个新记录,然后确定它在哪里——也许您可以将它放在搁架单元的架子上。通过这种方式,你可以增加车库或工作室的储物空间,而不会忘记东西放在哪里。当然,这是你遵守的储藏室原则:事物按照告诉你事物走向的事物走向,而不是按照你认为它们应该走向的地方。 2

应用中的 CRUD 操作

为了完成对示例应用的浏览,现在让我们看看应用是如何实现 CRUD 操作的。下面简要说明每个 CRUD 操作是如何表示的。

  • Create :每个列表视图上的 New 按钮允许您在表格中创建新的记录(行)。

  • 读作:这有两种表现方式;您可以使用列表视图查看每个表的所有行,并且您可以通过单击列表视图中行上的修改/查看链接来查看记录的完整详细信息。

  • 更新:每个条目的详细视图上的更新按钮允许您更新表格中的记录(行)。

  • 删除:每个条目的详细视图上的删除按钮允许您删除表格中的记录(行)。

既然我们已经看到了应用中所有可用的视图(除了每个细节视图),我们现在就有了安装和使用示例应用所需的知识。同样,本书中的其他示例应用将以类似的方式工作,尽管有些视图可能有所不同。

还有一件事要学:如何关掉它。

关闭示例应用

要关闭应用,只需返回到启动应用的终端窗口,按下 CTRL-C 。您可以随时关闭浏览器,但如果您在停止后尝试使用该应用,您可能会看到如图 A-11 和 A-12 所示的can’t reach this pageunable to connect等错误。

img/478423_1_En_12_Fig12_HTML.jpg

图 A-12

找不到错误(Windows 10–Firefox)

img/478423_1_En_12_Fig11_HTML.jpg

图 A-11

找不到错误(Windows 10–Edge)

posted @ 2024-08-09 17:40  绝不原创的飞龙  阅读(1)  评论(0编辑  收藏  举报