MySQL8-文档存储入门指南-三-

88 阅读1小时+

MySQL8 文档存储入门指南(三)

原文:Introducing the MySQL 8 Document Store

协议:CC BY-NC-SA 4.0

六、X 插件

X Dev API 是一种与 MySQL 交互的全新方式。正如我们所了解的,新的 NoSQL 机制建立在 X DevAPI、X 插件和 X 协议之上。您可能会有这样的印象,这些技术就在那里,一旦启用,就不再需要什么了。这在很大程度上是正确的,但是和所有好的特性一样,这个故事不仅仅是启用这个特性。

在这一章中,我们仔细看看 X 插件。正如您将看到的,它不仅仅是简单地打开它。事实上,它只是默认工作意味着它非常稳定,并适用于大多数情况。但是,您可以用几种方式来配置它,包括一个非常有趣的保护连接的选项。然而,在接下来的章节中会有更多关于这个甚至如何监控 X 插件的内容。

Note

我在这一章中使用术语“插件”来指代一般的插件,而“X 插件”来指代 X 插件的特定特性。

概观

回想一下第 2 章中的内容,X 插件是 MySQL 的一个单独编译的组件,可以在运行时加载和卸载。Oracle 将 X 插件命名为mysqlx,并在服务器中以该名称列出。一旦加载(安装),插件将在每次服务器重启时自动启动。此外,回想一下 MySQL 中的插件特性是 Oracle 用来扩展服务器功能的主要机制,无需从头开始重新构建代码。尽管插件技术在 MySQL 中已经存在了一段时间,并且最初用于存储引擎,但它已经成为 Oracle 用来扩展和添加新功能到服务器的默认机制。

在这方面,X 插件是一个很好的例子,它展示了插件可以给服务器带来的强大力量。例如,默认情况下,服务器使用固定的协议与客户端通信,该协议通常称为 MySQL 客户端/服务器协议,简称为 MySQL 协议或旧协议。这个协议被内置到服务器中,除了在 MySQL 的生命周期中有一些小的变化;从 MySQL 4 开始就没有太大变化。x 代码库。在 X 插件出现之前,这是客户端与服务器通信的唯一方式。 1 现在,一旦你加载了 X 插件,它就为使用 X 协议的客户端和服务器启用了一个新的通信协议。

How Do MySQL Plugins Work?

在最一般的意义上,当插件被安装或在启动时启动时,服务器和插件使用特殊的插件 API 进行通信,该 API 允许插件将自己注册为服务器的一部分。例如,插件提供了处理状态变量的回调方法以及启用其功能的方法。这个协商过程就是插件如何扩展服务器的功能,而不必强制服务器重启,也不需要重新编译服务器。

也就是说,需要注意的是,插件是针对公共服务器库编译的,因此必须与特定版本和平台的服务器相匹配(例如,您不能使用针对 Windows 上的 Linux 编译的插件)。使用在插件启动期间检查的特殊版本控制机制来提供兼容性检测。大多数插件都清楚地列出了支持的服务器版本。当你决定使用一个新的插件时,一定要检查它是否与你的服务器版本兼容。有关插件的更多信息,请参见在线 MySQL 参考手册中的“MySQL 插件 API”一节。

特征

同样,X 插件的主要目的是支持与服务器通信的 X 协议,以启用 X DevAPI (NoSQL)接口。虽然这是它的主要关注点,但是有一些有趣的特性可以帮助您获得更好的体验。这些包括配置插件使用不同于服务器的安全套接字层(SSL)设置,以及使用系统变量更改插件的行为。我们将在下面几节中看到如何更改 SSL 设置以及如何更改默认端口。我们将在后面的章节中看到更多关于其他系统变量的内容。

Note

尽管文档和其他文本以大写字母显示了 X 插件的变量,但是变量在 SQL 结果中以小写字母显示。例如,您可能会看到前缀Mysqlx_,但是服务器的输出显示为mysqlx_。幸运的是,大多数平台上的大多数 SQL 命令都可以接受这两种版本。

安全套接字层(SSL)连接

如果您在 MySQL 服务器上使用 SSL 连接,并希望对 X 插件(和您的 NoSQL 应用)使用安全连接,您可以设置 X 插件使用不同于服务器的 SSL 选项值。这意味着您可以设置 X 插件使用一个 SSL 证书,而服务器使用另一个证书。这非常有助于确保 NoSQL 应用的安全,而无需在客户机/服务器和 X 协议之间共享 SSL 数据。

您可以将系统变量及其值放在my.cnf文件中,或者通过服务器启动命令(命令行)传递系统变量。以这种方式使用时,系统变量通常被称为启动选项。使用以下命令可以列出系统变量及其当前值。注意,我使用 MySQL Shell 通过批处理模式获取信息。

$ mysqlsh -uroot -hlocalhost --sql -e "SHOW VARIABLES LIKE 'mysqlx_ssl%'"
Enter password:
+--------------------+-------+
| Variable_name      | Value |
+--------------------+-------+
| mysqlx_ssl_ca      |       |
| mysqlx_ssl_capath  |       |
| mysqlx_ssl_cert    |       |
| mysqlx_ssl_cipher  |       |
| mysqlx_ssl_crl     |       |
| mysqlx_ssl_crlpath |       |
| mysqlx_ssl_key     |       |
+--------------------+-------+

您可以在您的配置文件(my.cnf)中设置这些变量,方法是将它们放在名为[msyqld]的服务器部分,但是您应该省略破折号。下面的摘录展示了如何为服务器和 X 插件使用不同的 SSL 配置。

[mysqld]
...
ssl-ca=/my_ssl/certs/ca_server.pem
ssl-cert=/my_ssl/certs/server-cert.pem
ssl-key=/my_ssl/certs/server-key.pem
...
mysqlx-ssl-ca=/my_ssl/certs/ca_xplugin.pem
mysqlx-ssl-cert=/my_ssl/certs/xplugin-cert.pem
mysqlx-ssl-key=/my_ssl/certs/xplugin-key.pem
...

注意,我已经包含了两组 SSL 选项,只有 X 插件选项以前缀mysqlx_命名。

Note

一般来说,大多数系统变量都有相应的启动选项,并且在配置文件中以相同的名称使用,只是下划线改为了破折号。例如,mysqlx_ssl_ca 系统变量的启动选项是- mysqlx-ssl-ca。然而,--mysqlx_ssl_ca版本也适用于那些健忘的人。

要临时或作为 shell 或批处理文件的一部分更改这些值,可以在命令行上将系统变量指定为选项,如下所示。请注意,我们使用了与前面所示相同的值。

$ mysqld  ... --mysqlx-ssl-ca=/my_ssl/certs/ca_xplugin.pem --mysqlx-ssl-cert=/my_ssl/certs/xplugin-cert.pem \
                     --mysqlx-ssl-key=/my_ssl/certs/xplugin-key.pem

虽然您可以像这样使用命令行上的选项,但这不是最好的方法。这是因为,除非您在某个地方记录新的命令行,或者在 shell 或批处理命令中使用它(即使这样),否则很容易忘记您使用了什么值,甚至是使用了哪些系统变量。因此,最好的方法是,始终将自定义系统变量更改放在 MySQL 配置文件中。

更改默认端口

回想一下,X 插件使用与服务器不同的端口。默认端口是 33060。如果您想更改默认端口,可以使用mysqlx_port系统变量。与 SSL 选项一样,您可以将它放在my.cnf文件中,或者在服务器启动命令(命令行)上将它作为启动选项传递。您也可以使用以下命令检查默认端口。有效值范围是 1-65535。例如,您可以设置 X 插件使用端口 3307。

$ mysqlsh -uroot -hlocalhost --sql -e "SHOW VARIABLES LIKE 'mysqlx_port'"
Enter password:
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| mysqlx_port   |  3307 |
+---------------+-------+

因为mysqlx_port系统变量只在启动时读取(原因很明显),所以更改该值需要重新启动以使用不同的端口。

与 SSL 选项一样,您可以在命令行上设置端口,如下所示。在这种情况下,我们在端口 3307 上启动服务器,X 插件在端口 3308 上监听。

$ mysqld  --port=3307 --datadir... --socket=...mysql.sock --mysqlx-port=3308 --mysqlx-socket=...mysqlx.sock

同样,这也不是推荐的方法,因为命令行选项如果不放在 shell 或批处理文件中,很容易被遗忘。

更深入——探索源代码

如果你想通过检查源代码来了解 X 插件是如何工作的,你可以从 http://dev.mysql.com/downloads/mysql/ 下载源代码。要下载 MySQL 8 源代码,请从平台下拉框中选择源代码,并下载与您的平台匹配的文件。如果您没有看到与您的平台相匹配的版本,并且您只想研究源代码,那么请选择通用 Linux 选项。图 6-1 显示了网站的摘录,突出显示了选项卡和下拉框。

A432285_1_En_6_Fig1_HTML.jpg

图 6-1

Downloading the MySQL 8 source code

下载完成后,你可以在rapid/plugin/x文件夹中找到 X 插件的源代码。您可以浏览源代码,看看它是如何工作的,甚至它是如何在启动时与服务器进行协商的。例如,要查看系统变量,打开rapid/plugin/x/src文件夹中的xpl_plugin.cc文件,向下滚动到大约第 240 行。你会发现一个类似清单 6-1 中的例子的结构,它列出了插件支持的变量。

...
static struct st_mysql_sys_var* xpl_plugin_system_variables[]= {
  MYSQL_SYSVAR(port),
  MYSQL_SYSVAR(max_connections),
  MYSQL_SYSVAR(min_worker_threads),
  MYSQL_SYSVAR(idle_worker_thread_timeout),
  MYSQL_SYSVAR(max_allowed_packet),
  MYSQL_SYSVAR(connect_timeout),
  MYSQL_SYSVAR(ssl_key),
  MYSQL_SYSVAR(ssl_ca),
  MYSQL_SYSVAR(ssl_capath),
  MYSQL_SYSVAR(ssl_cert),
  MYSQL_SYSVAR(ssl_cipher),
  MYSQL_SYSVAR(ssl_crl),
  MYSQL_SYSVAR(ssl_crlpath),
  MYSQL_SYSVAR(socket),
  MYSQL_SYSVAR(bind_address),
  MYSQL_SYSVAR(port_open_timeout),
  MYSQL_SYSVAR(wait_timeout),
  MYSQL_SYSVAR(interactive_timeout),
  MYSQL_SYSVAR(read_timeout),
  MYSQL_SYSVAR(write_timeout),
  NULL
};
...
Listing 6-1System Variable Definition (X Plugin)

注意,有一个宏定义MYSQL_SYSVAR,用于定义系统变量。还有按名称列出的系统变量。一旦插件启动,您可以使用清单 6-2 中的命令看到系统变量。请注意,这些变量以前缀mysqlx_命名,所有 14 个变量都存在(主机系统运行的是 MAC OS——您的结果可能会有所不同)。

MySQL  localhost:33060+ ssl  SQL > SHOW VARIABLES LIKE 'mysqlx_%';
+-----------------------------------+------------------+
| Variable_name                     | Value            |
+-----------------------------------+------------------+
| mysqlx_bind_address               | *                |
| mysqlx_connect_timeout            | 30               |
| mysqlx_idle_worker_thread_timeout | 60               |
| mysqlx_max_allowed_packet         | 1048576          |
| mysqlx_max_connections            | 100              |
| mysqlx_min_worker_threads         | 2                |
| mysqlx_port                       | 33060            |
| mysqlx_port_open_timeout          | 0                |
| mysqlx_socket                     | /tmp/mysqlx.sock |
| mysqlx_ssl_ca                     |                  |
| mysqlx_ssl_capath                 |                  |
| mysqlx_ssl_cert                   |                  |
| mysqlx_ssl_cipher                 |                  |
| mysqlx_ssl_crl                    |                  |
| mysqlx_ssl_crlpath                |                  |
| mysqlx_ssl_key                    |                  |
+-----------------------------------+------------------+
16 rows in set (0.00 sec)
Listing 6-2Listing the System Variables for the X Plugin

我们将在下一节中发现更多关于系统变量的内容。如果您喜欢冒险,请继续阅读该文件中的代码,以获得更多关于状态变量的线索。提示:看看名为xpl_global_status_variables.h的文件。

选项和变量

正如我们在上一节中看到的,X 插件有几个系统变量,可以在启动时在配置文件或服务器命令行中设置。可以控制的配置项目包括默认端口、配置连接参数和建立超时限制等项目。您还可以看到 X 插件报告的关于性能、统计数据等的几个状态变量。这些状态变量可以用来监控 X 插件,以帮助您调整它的选项来匹配您的环境。我将在下面几节中探讨常用的启动选项、系统变量和状态变量。

Note

我使用变量这个术语来描述启动选项、系统变量和状态变量共有的性质和特性。

变量可以有两个范围级别:适用于所有连接的全局和仅适用于当前连接(会话)的会话,即您当前正在使用的连接。没有从您当前未使用的其他会话中捕获数据的规定。

变量还可以支持可以在运行时设置的动态值和只能在启动时设置的值。尽管您可以查看任何变量的值,而不考虑范围,但是您只能在运行时为动态变量设置值。设置全局变量时必须小心,以免对其他连接产生负面影响。

如何查看变量的值

有几种方法可以查看变量的值。我们在上一节中看到,您可以使用 SQL 命令SHOW VARIABLES查看系统变量,使用SHOW STATUS命令查看状态变量的值。记住,启动选项与一个系统变量相关联,所以使用SHOW VARIABLES命令就可以看到这些选项。

您还可以通过使用特殊形式的SELECT命令来查看系统变量的值,使用特殊的符号或快捷方式,例如在全局范围内使用@@GLOBAL表示值,在会话范围内使用@@SESSION表示值。虽然 X 插件目前没有会话级系统变量,但下面显示了全局系统变量mysqlx_connect_timeout

MySQL  localhost:33060+ ssl  SQL > SELECT @@GLOBAL.mysqlx_connect_timeout;
+---------------------------------+
| @@GLOBAL.mysqlx_connect_timeout |
+---------------------------------+
|                              30 |
+---------------------------------+
1 row in set (0.00 sec)

您还可以使用 PERFORMANCE_SCHEMA 表(视图)查看变量的值。在这种情况下,您可以通过会话或全局范围查看状态变量。或者您可以编写一个 SQL 查询来将数据与范围结合起来,如清单 6-3 所示(您的结果可能会有所不同)。我格式化了下面的 SQL 语句,以便于阅读。

SELECT *, 'SESSION' as SCOPE FROM PERFORMANCE_SCHEMA.session_status
WHERE variable_name LIKE 'mysqlx_%'
UNION SELECT *, 'GLOBAL' as SCOPE FROM PERFORMANCE_SCHEMA.global_status
WHERE variable_name LIKE 'mysqlx_%'

MySQL  localhost:33060+ ssl  SQL > SELECT *, 'SESSION' as SCOPE FROM PERFORMANCE_SCHEMA.session_status WHERE variable_name LIKE 'mysqlx_%' UNION SELECT *, 'GLOBAL' as SCOPE FROM PERFORMANCE_SCHEMA.global_status WHERE variable_name LIKE 'mysqlx_%' \G
*************************** 1\. row ***************************
 VARIABLE_NAME: Mysqlx_address
VARIABLE_VALUE: ::
         SCOPE: SESSION
*************************** 2\. row ***************************
 VARIABLE_NAME: Mysqlx_bytes_received
VARIABLE_VALUE: 1002
         SCOPE: SESSION
*************************** 3\. row ***************************
 VARIABLE_NAME: Mysqlx_bytes_sent
VARIABLE_VALUE: 8851
         SCOPE: SESSION
*************************** 4\. row ***************************
 VARIABLE_NAME: Mysqlx_connection_accept_errors
VARIABLE_VALUE: 0
         SCOPE: SESSION
*************************** 5\. row ***************************
 VARIABLE_NAME: Mysqlx_connection_errors
VARIABLE_VALUE: 0
         SCOPE: SESSION
...
*************************** 119\. row ***************************
 VARIABLE_NAME: Mysqlx_worker_threads
VARIABLE_VALUE: 2
         SCOPE: GLOBAL
*************************** 120\. row ***************************
 VARIABLE_NAME: Mysqlx_worker_threads_active
VARIABLE_VALUE: 1
         SCOPE: GLOBAL
120 rows in set (0.00 sec)
Listing 6-3X Plugin Status Variables with Scope

请注意,我们看到了相同的变量及其范围。

Note

使用性能模式的完整描述和教程超出了本书的范围。有关性能模式的更多信息,请参见在线 MySQL 参考手册中的“MySQL 性能模式”一节。

您可能已经注意到,在前面的例子中,我使用了SHOW SQL 命令来查看变量的值。有两个SHOW命令:一个用于系统变量(SHOW VARIABLES,另一个用于状态变量(SHOW STATUS)。您可以使用 LIKE 子句来查找所有的 X 插件变量。LIKE子句允许您指定名称的一部分并使用通配符。例如,您可以使用以下两个命令找到 X 插件的所有系统和状态变量。

SHOW VARIABLES LIKE 'mysqlx_%';
SHOW STATUS LIKE 'mysqlx_%';

注意,我使用了使用mysqlx_%LIKE子句。这将显示所有以mysqlx_开头的变量。因为所有的 X 插件变量都有这个前缀,所以我们看到了 X 插件的所有变量。

Tip

LIKE 子句在另一方面也非常方便。您可以使用它来搜索一个变量,您可能只是通过使用一个关键字就忘记了它的名称。例如,如果您想查看名称中包含dir的所有变量,请使用LIKE '%dir%'

到目前为止,您可能认为我们正在使用大量的 SQL 命令。您可能想知道是否有办法使用 NoSQL 接口查看变量值。在撰写本文时,X DevAPI 或 MySQL Shell 的一部分中还没有可以用来获取变量及其值的信息的对象。 2 这就是我之前在书中提到的一些日常维护任务仍然需要 SQL 接口的原因。检查和设置变量是需要使用 SQL 命令的维护和配置任务之一。

What About Information_Schema?

如果您熟悉特殊的INFORMATION_SCHEMA数据库,您可能想知道使用 session_和 global_表(视图)来显示变量值发生了什么。从服务器版本 5.7.6 开始,这些表(视图)已被弃用。这是因为在PERFORMANCE_SCHEMA中它们被表格(视图)取代了。有关变更和迁移到PERFORMANCE_SCHEMA的更多信息,请参见在线 MySQL 参考手册中的“迁移到性能模式系统和状态变量表”一节。

如何设置变量的值

我们已经发现可以在配置文件中设置系统变量,并且可以使用启动选项来设置系统变量。这些方法用于只能在启动时设置的变量。但是,对于那些可以动态设置的变量,您可以使用 set 命令和前面显示的@@SESSION 和@@GLOBAL 符号来更改它们在会话或全局范围内的值。然而,因为目前没有会话变量,我们只能为全局变量设置值,如清单 6-4 所示。

$ mysqlsh -uroot -hlocalhost --sql --json=pretty -e "SELECT @@GLOBAL.mysqlx_connect_timeout"
{
    "password": "Enter password: "
}

{
    "executionTime": "0.00 sec",
    "warningCount": 0,
    "warnings": [],
    "rows": [
        {
            "@@GLOBAL.mysqlx_connect_timeout": 30
        }
    ],
    "hasData": true,
    "affectedRowCount": 0,
    "autoIncrementValue": 0
}
$ mysqlsh -uroot -hlocalhost --sql --json=pretty -e "SET @@GLOBAL.mysqlx_connect_timeout = 90"
{
    "password": "Enter password: "
}

{
    "executionTime": "0.00 sec",
    "warningCount": 0,
    "warnings": [],
    "rows": [],
    "hasData": false,
    "affectedRowCount": 0,
    "autoIncrementValue": 0
}
$ mysqlsh -uroot -hlocalhost --sql --json=pretty -e "SELECT @@GLOBAL.mysqlx_connect_timeout"
{
    "password": "Enter password: "
}

{
    "executionTime": "0.00 sec",
    "warningCount": 0,
    "warnings": [],
    "rows": [
        {
            "@@GLOBAL.mysqlx_connect_timeout": 90
        }
    ],
    "hasData": true,
    "affectedRowCount": 0,
    "autoIncrementValue": 0
}

Listing 6-4Setting Global System Variables

如果引入了会话动态系统变量,可以用SET @@SESSION.<variable_name>命令设置它们的值。

Tip

可以在运行时更改的系统变量称为动态变量。这只适用于那些在 X 插件运行时可以改变的系统变量。

现在我们知道了更多关于变量以及如何查看和设置值的知识,让我们看看 X 插件的具体变量。让我们从那些可以放在配置文件中的系统变量开始。

系统变量和启动选项

回想一下,大多数系统变量都有一个相应的选项,可以用来在启动时配置系统。也就是说,我们称可以用这种方式设置的系统变量为启动选项。其他系统变量可以在运行时更改,通常称为动态系统变量。但是,有些变量只能在配置文件或命令行中使用。正如您所猜测的,一些变量可以用作启动选项。表 6-1 列出了那些可以用作 X 插件启动选项的系统变量(也是系统变量)。我还包括哪些变量可以动态设置,以及对每个变量的简短描述。

表 6-1

System Variables and Startup Options (X Plugin)

| 名字 | 默认 | 系统瓦尔 | 动态的 | 描述 | | :-- | :-- | :-- | :-- | :-- | | `mysqlx_bind_address` | * | 是 | 不 | X 插件用于连接的网络地址。 | | `mysqlx_connect_timeout` | Thirty | 是 | 是 | 等待从新连接的客户端接收第一个数据包的秒数 | | `mysqlx_idle_worker_thread_timeout` | Sixty | 不 | 不 | 空闲工作线程终止之前的时间(秒) | | `mysqlx_max_allowed_packet` | One million forty-eight thousand five hundred and seventy-six | 不 | 是 | X 插件可以处理的网络数据包的最大大小。 | | `mysqlx_max_connections` | One hundred | 是 | 是 | X 插件可以接受的最大并发客户端连接数。 | | `mysqlx_min_worker_threads` | Two | 不 | 是 | X 插件用于处理客户端请求的最小工作线程数。 | | `mysqlx_` `port` | Thirty-three thousand and sixty | 是 | 不 | 指定 x 插件监听连接的端口 | | `mysqlx_port_open_timeout` | Zero | 是 | 不 | X 插件等待 TCP/IP 端口空闲的时间(秒)。 | | `mysqlx_socket` | 依赖于平台 | 是 | 不 | X 插件监听连接的套接字。 | | `mysqlx_ssl_ca` |   | 是 | 不 | 包含可信 SSL CAs 列表的文件的路径。 | | `mysqlx_ssl_capath` |   | 是 | 不 | 包含 PEM 格式的可信 SSL CA 证书的目录的路径。 | | `mysqlx_ssl_cert` |   | 是 | 不 | 用于建立安全连接的 SSL 证书文件的名称。 | | `mysqlx_ssl_cipher` |   | 不 | 不 | 允许用于 SSL 加密的密码列表。 | | `mysqlx_ssl_crl` |   | 是 | 不 | 包含 PEM 格式的证书吊销列表的文件路径。 | | `mysqlx_ssl_crl_path` |   | 是 | 不 | 包含文件的目录路径,这些文件包含 PEM 格式的证书吊销列表。 | | `mysqlx_ssl_key` |   | 是 | 不 | 用于建立安全连接的 SSL 密钥文件的名称。 |

正如你所看到的,我们可以为 X 插件设置很多东西,包括设置 SSL 连接,调整 X 插件的最大连接数限制,最小工作线程数,甚至设置数据包的大小(一个数据包中可以通过网络发送多少数据)。当然,我们也可以改变 X 插件使用的端口。

状态变量

召回系统变量是那些只报告插件的统计数据和其他数据的变量。状态变量不能在运行时设置。但是,每当服务器重新启动时,大多数都会被重置。也就是说,计数器会在重新启动时重置。

X 插件有相当多的状态变量来报告 X 插件中的几个区域。我们不是单独查看状态变量(如果算上会话和全局范围,有 120 多个),而是查看状态变量报告的组或区域。我们将在下一节看到更多关于特定状态变量的内容,在下一节我们将看到如何监控 X 插件。

下面列出了一些更常见的状态变量,并简要说明了为什么要检查这些值。符号mysqlx_*表示包含多个变量的区域的状态变量。比如mysqlx_bytes_*包括mysqlx_bytes_sentmysqlx_bytes_received

  • mysqlx_connections_*:接受、拒绝和关闭的连接数。
  • mysqlx_sessions_*:统计已接受、已关闭、已终止、已拒绝等会话。
  • mysqlx_stmt_*:集合的执行、删除、列表、创建统计。

您可能想要检查一些其他的离散状态变量,包括启动时的错误(mysqlx_init_error)和发送到客户端的行数(mysqlx_rows_sent)。关于 X 插件可用状态变量的完整列表,请参见在线 MySQL 参考手册中的“X 插件的状态变量”一节。

现在让我们简单地看看你可以监控 X 插件的一些方法,以及你为什么要这么做。

监控 X 插件

如果您想监控 X 插件以确保一切正常工作、诊断问题、验证配置或调整性能,您可以使用 X 插件的系统变量来监控 X 插件。这需要在特定时间或事件发生时读取值。回想一下,有些状态变量同时具有会话和全局作用域。因此,您可能希望使用前面讨论过的@@符号来查询会话或全局范围值。

您可以通过几种方式查看状态变量的值,包括使用SHOW STATUS命令以及从PERFORMANCE_SCHEMA数据库中读取表格(视图)。清单 6-5 显示了可用于读取状态变量值的表格(视图)。

$ mysqlsh -uroot -hlocalhost --sql -e "SHOW TABLES FROM PERFORMANCE_SCHEMA LIKE '%status%'"
Enter password:
+-------------------------------------------+
| Tables_in_performance_schema (%status%)   |
+-------------------------------------------+
| global_status                             |
| replication_applier_status                |
| replication_applier_status_by_coordinator |
| replication_applier_status_by_worker      |
| replication_connection_status             |
| session_status                            |
| status_by_account                         |
| status_by_host                            |
| status_by_thread                          |
| status_by_user                            |
+-------------------------------------------+
Listing 6-5Performance Schema Views for Status Variables

请注意,有一些状态变量的表(视图),包括复制和按范围的表。只要记住在查询 X 插件的状态变量时使用LIKE子句。然而,正如我前面提到的,使用性能模式的完整教程超出了本书的范围。幸运的是,带有@@符号的SHOW STATUSSELECTSQL 命令对于大多数应用来说足够好了。 3

尽管 X 插件有很多状态变量,但是状态变量可以组织在几个区域中。下面的列表总结了我定义的类别。

  • 通信:关于发送和接收的消息和数据的信息。
  • 连接:关于连接的信息,包括接受、拒绝和删除。
  • CRUD 操作:创建、读取、更新和删除操作的统计数据。
  • 错误和警告:关于启动时或发送到客户端的错误或警告的信息。
  • 会话:关于会话的信息,包括接受、拒绝和删除。
  • SSL:关于安全连接的信息。
  • 语句:关于文档存储的执行、创建等的统计信息。
  • Worker threads:关于 X 插件中工作线程的信息。

以下部分更详细地描述了这八个方面,包括您可能希望使用变量执行的任务的建议。每个部分还包括相关状态变量的完整列表、它们的范围和简短描述。您可以在诊断过程中使用这些章节作为探索 X 插件的指南,或者只是出于好奇。

沟通

通信类别包括状态变量,这些变量报告发送到客户端或从客户端接收的信息。您可以观察网络上一个会话或全局的通信量,查看发送到客户端的会话和全局的行数,并检查 X 协议的期望块。

当管道中有可能失败的消息时,X 协议使用期望块机制来管理情况。即在块结束之前执行的其他相关任务。期望块是确保整个块安全、可靠地失败的一种方式(想想事务)。预期障碍有几个方面,不太可能要求你去监控它们。如果您想了解更多关于期望块的信息,请参见 https://dev.mysql.com/doc/internals/en/x-protocol-expect-expectations.html

6-2 列出了通信类别的所有状态变量。

表 6-2

Communication Status Variables (X Plugin)

| 变量 | 范围 | 描述 | | :-- | :-- | :-- | | `mysqlx_bytes_received` | 两者 | 通过网络接收的字节数。 | | `mysqlx_bytes_sent` | 两者 | 通过网络发送的字节数。 | | `mysqlx_expect_close` | 两者 | 关闭的期望块数。 | | `mysqlx_expect_open` | 两者 | 打开的期望块数。 | | `mysqlx_rows_sent` | 两者 | 发送回客户端的行数。 |

您可能希望使用这些状态变量的任务类型包括观察发送和接收了多少数据,以及向客户端发送了多少行(在结果集中)。你也可以看到期望块数据,但是这可能比大多数监控 X 插件时需要的更高级。

连接

连接类别包括用于检查连接状态的状态变量。您可以使用连接错误变量来查看有多少连接出现了错误。这些变量同时具有会话和全局作用域,这使得它们对于诊断单个连接问题很有意义。您还可以看到接受(打开)、关闭和拒绝(由于登录失败、权限不足、密码错误等)的连接数的统计数据。).这些状态变量只有全局范围,因此它们只显示所有连接的聚合。表 6-3 列出了连接类别的所有状态变量。

表 6-3

Connection Status Variables (X Plugin)

| 变量 | 范围 | 描述 | | :-- | :-- | :-- | | `mysqlx_connection_accept_errors` | 两者 | 导致接受错误的连接数。 | | `mysqlx_connection_errors` | 两者 | 导致错误的连接数。 | | `mysqlx_connections_accepted` | 全球的 | 已被接受的连接数。 | | `mysqlx_connections_closed` | 全球的 | 已关闭的连接数。 | | `mysqlx_connections_rejected` | 全球的 | 被拒绝的连接数。 |

您可能希望使用这些状态变量的任务类型包括监控连接错误状态变量,以防出现大量失败(错误)。这可能是简单的应用使用了错误的凭据,也可能是恶意的尝试发现登录帐户和密码。

还可以使用 accepted、closed 和 rejected 系统变量来监控使用的连接数。也就是说,如果使用您的应用的用户少于 10 个,您将会看到这些状态变量的值相当低。高数值可能表示应用连接和断开太频繁(不总是一件坏事),或者应用的实例比您想象的要多。

CRUD 操作

CRUD 操作类别提供了文档存储上的创建、读取(查找)、更新和删除操作的统计信息。注意,这些是用于 X DevAPI 的计数器,而不是专门用于 SQL 语句执行的。您可以在会话或全局范围内看到每个 CRUD 操作的值。表 6-4 列出了 CRUD 操作类别的所有状态变量。

表 6-4

CRUD Status Variables (X Plugin)

| 变量 | 范围 | 描述 | | :-- | :-- | :-- | | `mysqlx_crud_create_view` | 两者 | 收到的 create view 请求数。 | | `mysqlx_crud_delete` | 两者 | 收到的删除请求数。 | | `mysqlx_crud_drop_view` | 两者 | 收到的删除视图请求数。 | | `mysqlx_crud_find` | 两者 | 收到的查找请求数。 | | `mysqlx_crud_insert` | 两者 | 收到的插入请求数。 | | `mysqlx_crud_modify_view` | 两者 | 收到的修改视图请求数。 | | `mysqlx_crud_update` | 两者 | 收到的更新请求数。 |

您可能希望使用这些状态变量的任务类型包括监控文档存储应用的活动,例如发出了多少个删除请求、添加(插入)了多少个新数据项等等。因为状态变量具有会话和全局作用域,所以您可以看到特定会话的活动,并将其与全局作用域的值进行比较(总体统计)。

错误和警告

“错误和警告”类别提供了一种查看启动时发生的错误数量以及发送给客户端的通知或错误的方法。此类别中的所有状态变量都具有会话和全局范围,因此可用于检查单个连接(会话)的统计数据或所有会话的聚合值。

通知是 X 协议在会话或全局范围向客户机发送附加信息的一种方式。当在会话级别(在内部手册中称为本地)发送时,它们可以包括提交的事务标识符、事务状态更改、SQL 警告和变量更改的列表。在全局级别发送时,可能包括服务器关闭、组复制中的连接断开、表删除等。请记住,状态变量只是计数器,因此尽管您看不到消息(通知)本身,但您可以看到发送了多少消息,以及它们是信息性的(警告)还是对错误或其他严重事件的响应。有关 X 协议中通知的更多信息,请参见 http://dev.mysql.com/doc/internals/en/x-protocol-notices-notices.html

6-5 列出了错误和警告类别的所有状态变量。

表 6-5

Errors and Warnings Status Variables (X Plugin)

| 变量 | 范围 | 描述 | | :-- | :-- | :-- | | `mysqlx_errors_sent` | 两者 | 发送到客户端的错误数。 | | `mysqlx_init_error` | 两者 | 初始化过程中的错误数。 | | `mysqlx_notice_other_sent` | 两者 | 发送回客户端的其他类型通知的数量。 | | `mysqlx_notice_warning_sent` | 两者 | 发送回客户端的警告通知的数量。 |

您可能希望使用这些状态变量的任务类型包括检查会话是否有过多的错误,这可能表明应用(或用户的使用)有问题。通知状态变量可能有助于收集数据,用于诊断发送给客户端的错误和警告。也就是说,它可能表示您可能希望在日志中查找其他数据。例如,这些变量在会话级别的高计数可能表明应用正在尝试做一些它不应该做的事情,或者执行操作过于频繁。

然而,当开始使用 X 插件或改变其配置时,这一类别中最重要的状态变量是mysqlx_init_error状态变量。检查这个变量以确保在启动(初始化)时没有错误,如果有问题,跟踪它们以确保所有的配置都是正确的。虽然有时一个错误可能是好的,但一般来说,您不应该看到任何为初始化而注册的错误。

会议

会话类别提供了一种方法来跟踪有多少会话已被创建(接受)、关闭、由于错误导致关闭、被随意终止或由于登录或建立会话时的其他错误而被拒绝。所有可用的状态变量都只有全局范围。表 6-6 列出了会话类别的所有状态变量。

表 6-6

Session Status Variables (X Plugin)

| 变量 | 范围 | 描述 | | :-- | :-- | :-- | | `mysqlx_sessions` | 全球的 | 已打开的会话数。 | | `mysqlx_sessions_accepted` | 全球的 | 已被接受的会话尝试次数。 | | `mysqlx_sessions_closed` | 全球的 | 已关闭的会话数。 | | `mysqlx_sessions_fatal_error` | 全球的 | 因致命错误而关闭的会话数。 | | `mysqlx_sessions_killed` | 全球的 | 已被终止的会话数。 | | `mysqlx_sessions_rejected` | 全球的 | 被拒绝的会话尝试次数。 |

您可能希望使用这些状态变量的任务类型包括检查有多少会话失败(msyqlx_sessions_fatal_error)、被管理员之类的人终止(mysqlx_sessions_killed),以及有多少会话成功打开或关闭。与连接尝试一样,您可以使用该类别中的状态变量来监控创建和使用会话的频率和数量。太多可能意味着会话比您最初计划的要多,广泛使用增加了,等等。每当您发现或认为创建会话可能有问题时,或者当会话开始频繁失败时,请检查这些状态变量。

加密套接字协议层

SSL 类别是最大的类别之一,包括许多用于监控安全连接的状态变量。这一点非常重要,因为信息技术专家必须保持持续的警惕,以保护系统和数据不被意外使用、误用或利用。如果您决定使用 SSL 连接,您将需要检查这些状态变量,以确保您的 SSL 连接设置正常工作。您可以检查证书状态的有效性,查看密码列表、使用的 SSL 版本等等。表 6-7 列出了 SSL 类别的所有状态变量。

表 6-7

SSL Status Variables (X Plugin)

| 变量 | 范围 | 描述 | | :-- | :-- | :-- | | `mysqlx_ssl_accepts` | 全球的 | 接受的 SSL 连接数 | | `mysqlx_ssl_active` | 两者 | 如果 SSL 处于活动状态 | | `mysqlx_ssl_cipher` | 两者 | 当前的 SSL 密码(对于非 SSL 连接为空) | | `mysqlx_ssl_cipher_list` | 两者 | 可能的 SSL 密码列表(非 SSL 连接为空) | | `mysqlx_ssl_ctx_verify_depth` | 两者 | ctx 中当前设置的证书验证深度限制 | | `mysqlx_ssl_ctx_verify_mode` | 两者 | ctx 中当前设置的证书验证模式 | | `mysqlx_ssl_finished_accepts` | 全球的 | 与服务器的成功 SSL 连接数 | | `mysqlx_ssl_server_not_after` | 全球的 | SSL 证书有效的最后日期 | | `mysqlx_ssl_server_not_before` | 全球的 | SSL 证书有效的第一个日期 | | `mysqlx_ssl_verify_depth` | 全球的 | SSL 连接的证书验证深度 | | `mysqlx_ssl_verify_mode` | 全球的 | SSL 连接的证书验证模式 | | `mysqlx_ssl_version` | 两者 | 用于连接 ssl 的协议的名称 |

您可能希望使用这些状态变量的任务类型包括检查以确保为一个会话或所有会话打开 SSL(mysqlx_ssl_active)、查看接受的 SSL 连接数(mysqlx_ssl_finished_accepts)以及有效 SSL 证书的日期。这最后一个操作可以把你从一大堆兔子洞诊断 4 追着奇怪的错误信息中解救出来。

请注意,有些变量同时具有会话和全局作用域,因此您可以使用这些变量来帮助在会话级别诊断 SSL 连接问题。例如,如果客户端无法使用 SSL 正确连接到 X 插件,需要很长时间才能连接,或者在连接过程中出现错误。

有关这些状态变量的更多信息,可以参阅在线 MySQL 参考手册中的“使用安全连接”一节。因为这些状态变量中的大多数与服务器使用的相同,所以应用了相同的技术和描述。

声明

语句类别是一个非常有趣的类别,在诊断或观察与 X DevAPI 相关的操作时非常方便。特别是,有一些状态变量可以计算集合创建和删除的数量、集合索引、执行事件的数量、列出客户端的数量等等。

回想一下,在 X DevAPI 的说法中,语句是一个执行一个或多个 CRUD 操作的动作。尽管 CRUD 操作是这类状态变量的主要焦点,但我们也将该术语用于 SQL 命令,SQL 语句也有状态变量。可用的状态变量具有会话和全局作用域,因此它们可用于监控会话活动或聚合详细信息。表 6-8 列出了声明类别的所有状态变量。

表 6-8

Statement Status Variables (X Plugin)

| 变量 | 范围 | 描述 | | :-- | :-- | :-- | | `mysqlx_stmt_create_collection` | 两者 | 收到的 create collection 语句数。 | | `mysqlx_stmt_create_collection_index` | 两者 | 收到的 create collection index 语句的数目。 | | `mysqlx_stmt_disable_notices` | 两者 | 收到的禁用通知语句的数量。 | | `mysqlx_stmt_drop_collection` | 两者 | 收到的 drop collection 语句数。 | | `mysqlx_stmt_drop_collection_index` | 两者 | 收到的删除集合索引语句的数目。 | | `mysqlx_stmt_enable_notices` | 两者 | 收到的启用通知语句数。 | | `mysqlx_stmt_ensure_collection` | 两者 | 收到的确保集合语句的数量。 | | `mysqlx_stmt_execute_mysqlx` | 两者 | 命名空间设置为 mysqlx 时接收的 StmtExecute 消息数。 | | `mysqlx_stmt_execute_sql` | 两者 | 为 SQL 命名空间接收的 StmtExecute 请求数。 | | `mysqlx_stmt_execute_xplugin` | 两者 | 为 X 插件命名空间接收的 StmtExecute 请求数。 | | `mysqlx_stmt_kill_client` | 两者 | 收到的 kill client 语句数。 | | `mysqlx_stmt_list_clients` | 两者 | 收到的 list client 语句数。 | | `mysqlx_stmt_list_notices` | 两者 | 收到的列表通知语句的数量。 | | `mysqlx_stmt_list_objects` | 两者 | 收到的 list object 语句数。 | | `mysqlx_stmt_ping` | 两者 | 收到的 ping 语句数。 |

您可能希望使用这些状态变量的任务类型包括监控文档存储以创建和删除集合及相关索引。如果您正在监控文档存储应用如何使用集合,这可能会有所帮助。也就是说,频繁的集合创建可能表明数据没有被经常保存或者是动态生成的。这可能会让您发现改进应用使用数据方式的方法。

其他任务包括监控通知(消息)、发送客户机终止请求的次数(不一定成功执行),以及列出通知、客户机和对象。这些状态变量中的大多数都超出了正常监控的范围。事实上,这些状态变量中的一些只是在文档中被简单地引用,除了源代码本身,很少在其他地方被引用。

最后一个可能有用的状态变量是mysqlx_stmt_ping状态变量,用于查看客户机检查服务器的次数,以确定它是否处于活动状态。此处的高值可能表示潜在的网络连接问题。

工作线程

工作线程是 X 插件用来执行任务的线程。该类别中只有两个状态变量,允许您查看可用工作线程的总数(仅限全局)和当前活动线程的数量(也仅限全局)。您可以使用系统变量mysqlx_min_worker_threads增加工作线程的最小数量。表 6-9 列出了线程类别的所有状态变量。

表 6-9

Worker Threads Status Variables (X Plugin)

| 变量 | 范围 | 描述 | | :-- | :-- | :-- | | `mysqlx_worker_threads` | 全球的 | 可用的工作线程数 | | `mysqlx_worker_threads_active` | 全球的 | 当前使用的工作线程数。 |

您可能希望使用这些状态变量的任务类型包括当存在与较慢的执行有关的性能问题时。如果活动的工作线程数量超过了系统可以处理的数量,或者没有足够的工作线程用于所有连接和任务执行请求,就会发生这种情况。

随着 X 插件的成熟,可能会有更多的任务需要您去执行,比如诊断问题、调整性能,或者简单地配置插件。如果您对监控 X 插件感兴趣,请务必查看在线 MySQL 参考手册,因为 MySQL 8 的每个新版本都会发布状态变量的更新以及监控 X 插件的任务。

摘要

X 插件是 MySQL 服务器的扩展,可以动态加载。这非常重要,因为 X 插件启用了文档存储特性,允许存储和检索 JSON 文档。具体来说,X 插件允许服务器和客户机之间使用 X 协议进行通信,并与 X DevAPI 进行交互,以允许符合 ACID 的存储。此外,使用 X DevAPI,您可以使用类似 NoSQL 的语法对文档存储执行 CRUD 操作。正是 X 插件将所有的功能联系在一起,将 MySQL 服务器变成了一个文档库。

在这一章中,我们学习了更多关于 X 插件及其工作原理。特别是,我们看到了如何配置 X 插件,比如改变端口和通过 SSL 启用独立于服务器的安全连接。我们还发现了其他系统变量以及一长串状态变量,您可以用它们来监控 X 插件。最后,我们发现了一些关于 X 插件的有趣的内部事实,比如它是如何注册系统变量的。

如果您仍然对 X 插件及其内部工作方式感到好奇,那么没有比源代码本身更好的文档了。虽然对门外汉来说可能不太容易,但研究源代码就像阅读希腊原著一样。

在下一章中,我将仔细研究新的 X 协议是如何工作的,包括服务器如何与客户机交换数据包。正如您将看到的,它与旧协议有很大不同。这主要是由于用于设计和实现新协议的构件。

Footnotes 1

应该注意的是,MySQL 复制使用了内置于原始协议中的扩展。

  2

如果我们有这样的对象,它将使与服务器的交互更加容易。

  3

有些人可能会说 SQL 命令更容易使用。

  4

我称之为兔子洞诊断,因为它经常令人沮丧,很少导致正确的诊断。SSL 证书过期就是其中一个原因。

  5

源代码是用 C++写的,而且是真正的 C++形式(可悲的是)代码几乎没有内联文档。

七、X 协议

X 协议代表了 MySQL 中第一个与现有的客户机/服务器协议的重大偏离。X 协议被设计成可扩展的,最大化安全性,并确保良好的性能。当 X 协议被设计时,这三个类别都是必须具备的特性和需求的首要条件。

尽管 X 协议主要被包装(实现它)的客户端(如 X 插件和数据库连接器)隐藏在一个抽象层之后,但如果您计划使用 X 协议实现自己的应用,了解它的工作方式是很重要的。我们将在第 8 章和第 9 章中介绍。即使您从未打算开发 MySQL 客户端,仔细研究 X 协议也会发现并进一步强调 MySQL 8 中技术飞跃的一个例子。

在这一章中,我们将探索 X 协议并发现它是如何工作的。我们还将了解如何通过数据库连接器开始使用 X 协议。我们看到了一些通过连接器/Python 库用 Python 编写小脚本与 X 协议交互的例子。让我们从 X 协议及其起源的详细概述开始。

Note

我提出了许多我们在前面章节中发现的概念,为了简洁起见,我只在需要清楚的地方重复了一些信息。

概观

如果您曾经编写过从头开始设计的通信协议,或者如果您不得不编写代码来实现通信协议,那么您会意识到以毫不动摇的精度处理数据交换的复杂性和严格要求。当从一个系统到另一个系统交换消息时,根本没有“足够好”的质量。发送到另一个系统或从另一个系统接收的数据必须按照约定的格式进行安排,包括数据对齐(先进行什么)和数据表示方式(编码)。如果做得不好,可能会导致灾难。

较老的客户机/服务器 MySQL 协议是从头开始设计的通信协议的一个很好的例子。虽然它已经使用了几十年,只有相对较小的变化,但有一段时间它限制了 MySQL 工程师。由于旧的客户机/服务器协议不可扩展,他们在尝试实现新功能时反复挣扎。

然而,在协议的发展过程中,添加新功能并不是唯一需要处理的问题。对于 MySQL 中的客户机/服务器协议,安全性是一个主要问题。尽管 SSL 扩展被添加到协议中,但默认情况下并不强制实施安全性。也就是说,除了登录密码的交换之外,客户端/服务器消息不需要加密。因此,如果没有启用 SSL 或其他形式的加密,有人就有可能发现发送到服务器或从服务器接收的数据。

性能是为特定的、有限的命令和消息集设计的现有协议可能受到影响的另一个方面。也就是说,更新的技术已经表明,如果使用类似流水线的技术来设计协议交换,则有可能实现更好的性能。

将这些特性添加到现有的客户机/服务器协议中是不可行的。更具体地说,工程师们知道,要扩展客户机/服务器协议,每个系统(客户机、应用、服务器等)。)必须被更新或修改以与新的扩展一起工作。这很严重,因为你不能指望 MySQL 的每个用户都突然更新他们的 MySQL 工具、定制应用、脚本等的每个版本,以符合协议的新扩展。由于这个原因和许多类似的原因,在过去,改变客户机/服务器协议是被禁止的,并且仅限于那些确保现有客户机尽管改变也能继续工作的改变。

尽管有这样的要求,在客户机/服务器协议的发展过程中还是有一些小的变化。最近一次发生在 5.7 版本开发发行期间,涉及 Ok 消息的返回。但即使是这种微小的变化也是为了确保向后兼容性。迄今为止,客户机/服务器协议继续支持 Ok 前和 Ok 后的消息协议更改。这是长期通信协议的祸根:总是不得不以牺牲进步为代价来保持某种程度的向后兼容性。

当工程师们开始设计 MySQL 中现在的文档存储库,包括新的 MySQL Shell、X Plugin 和 X DevAPI 时,很明显是时候实现一个可以增强新特性的新协议了。更具体地说,很明显,现有的客户机/服务器协议不足以满足 MySQL 8 特性和产品的所有目标。因此,我们需要一个新的协议,它被称为 X 协议,以遵循新的命名约定。 1

X 协议已经集成到大多数 MySQL 产品套件中,包括以下产品。我提供了一个链接,可以下载列出的每一个产品。注意,这里包含了几个数据库连接器(使用客户机/服务器协议或 X 协议与 MySQL 服务器交互的特定语言的库)。未来寻找更多实现 X 协议的产品。

Note

连接器产品通常缩写为 C/J、C/Net、C/Node.js 和 C/Py。

在后面的章节中,我们将会看到一个连接器/Python 连接器如何实现和公开 X 协议的例子。现在让我们看看开发和实现 X 协议的目标和动机。

X 协议的目标

如上所述,X 协议旨在解决的三个主要领域(称为设计约束或简称目标)包括可扩展性、安全性和性能。接下来的几个部分展示了 X 协议的三个主要设计约束的一些驱动力。

Tip

如果你想看一些用于设计 X 协议的实际工程文档,请看 http://dev.mysql.com/worklog/task/?id=8639 项目的工作日志 2

What About the Client/Server Protocol?

你可能想知道 X 协议是否只适用于所有的 X。也就是说,它不适用于旧的协议。答案是 X 协议也支持客户机/服务器协议。这就是 MySQL Shell 不需要使用中间库就可以连接到旧服务器的方式。更具体地说,X 协议包括一个使用旧的客户机/服务器协议进行通信的选项。

展开性

当软件被称为具有可扩展性的目标时,它意味着软件必须能够被修改以添加新的特性,而不需要大的返工或重组。尽管组织可能对返工的含义有稍微不同的定义或示例,但在客户端/服务器协议的情况下,它是不可扩展的,因为在不对代码进行重大更改的情况下,扩展协议以包括新消息、命令和数据的空间非常小,并且可能与旧产品不兼容。

工程师们希望确保新协议从一开始就考虑到可扩展性。在这种情况下,可扩展性包括添加功能和特性的能力,而不会导致现有产品失败或返工以适应变化。

X 协议需要可扩展性的一些领域包括能够添加新的消息、添加新的特性(例如,确保协议支持诸如流水线之类的东西以减少往返行程)、允许添加新的认证机制、改变或添加新的加密和压缩设施等等。

安全性

在这个物联网的现代世界中,随着现代文明人口的快速增长,系统越来越安全变得越来越重要。也就是说,提供最佳选项来保护数据和用户免受意外或故意的利用。

Tip

有关物联网和 MySQL 的更多信息,请参见我的书《物联网的 MySQL》,查尔斯·贝尔(Apress 2016) https://www.apress.com/us/book/9781484212943

甲骨文的工程师非常重视安全性。事实上,它是几乎所有设计、评审和质量控制机制的关键方面。在甲骨文公司,安全性至关重要。因此,当开发新协议时,安全机制比客户机/服务器协议有了很大的改进。特别地,X 协议中的安全缺省仅使用可信的、经过验证的标准,例如传输层安全(TLS) 3 和简单认证和安全层(SASL)。 4

表演

与安全性一样,性能是 Oracle 用来评估产品质量的另一个关键领域。在这种情况下,性能必须使系统能够适当地执行其任务,而没有不必要的等待时间、延迟或长时间运行的任务。与安全性不同,性能通常是以主观和轶事的方式进行评估的。也就是说,新版本的运行速度不能慢于以前的版本。

在 X 协议的情况下,通过使用可靠的基础技术和利用诸如流水线之类的特性来确保性能目标,流水线允许一次传递多个消息,减少往返次数(往返于服务器和客户端),并且在发送多个命令时不等待来自服务器的响应,从而不占用客户端来等待响应。

在下一节中,我们将通过研究设计的基础来了解 X 协议的基础。

x 协议和协议缓冲区

MySQL 工程师想要克服的最大问题之一是从头开始开发协议机制的各个方面需要很长的时间。特别是,工程师们希望利用成熟的、记录良好的、卓越的技术。毕竟,创建一个可扩展的、安全的、高性能的通信协议的问题已经被许多人解决了,并取得了不同程度的成功。

虽然对几种选择进行了评估和讨论,但重要的是该技术必须是成熟的和开源的。此外,该技术必须支持快速实施,很少或没有第三方依赖性,独立于语言和平台,并且不需要重新装备开发工具和流程来使用它。

被选中的技术叫做谷歌的协议缓冲区( https://developers.google.com/protocol-buffers/ )。Google Protocol Buffers 被亲切地命名为 protobuf,它是一个可扩展的、独立于语言和平台的机制,用于序列化结构化数据。它是为速度、紧凑和简单而设计的。Protobuf 允许您快速轻松地定义消息交换协议。在这方面,protobuf 与 XML 和其他变体有点类似。Protobuf 可用于多种语言,包括 C++、C#、Go、Java 和 Python。最新版本的 protobuf(版本 3)支持其他语言,比如 Ruby。

然而,这种意义上的语言支持意味着有一个编译器选项可以将 protobuf 定义文件翻译成该语言可以使用的特定于语言的代码。例如,要在 C++中使用 protobuf,您必须将 protobuf 定义文件从它们的本机 protobuf 定义编译成可由 C++编译器读取和编译的文件。

Protobuf 本质上是一种组织数据的方法,这样就可以用结构化的方式定义数据(称为消息)。也就是说,我们可以定义如何表示数据的精确集合。这允许你以约定的结构发送和接收数据。这听起来可能没什么大不了的,除非考虑到可扩展性方面,即使有消息的新版本,旧消息仍然有效。大多数语言都支持结构化数据机制,但具有不同程度的类型严格性。然而,这些很少是可扩展的,对结构的任何改变都会导致格式不兼容(大多数情况下)。Protobuf 旨在允许您扩展数据组织,而无需重新构建。

为了理解 protobuf 的强大,我们来看一个简短的例子。在这种情况下,我们将使用前面章节中联系人的 rolodex 示例的变体。我们需要两条消息(数据结构);存储联系人姓名和电话号码的方法(每个联系人可能不止一个),以及存储所有联系人的消息。正如您将看到的,这允许我们编写一些非常简单的代码来读写数据。

Note

虽然完整的 protobuf 教程已经超出了本书的范围,但是下面还是会给你一个 protobuf 的鸟瞰图。然而,如果您需要了解更多关于 protobuf 的信息,Google 已经提供了大量的文档。

安装 Protobuf 编译器

我们需要安装两件东西。我们必须安装 protobuf 编译器和 protobuf 库。

可以从 https://github.com/google/protobuf/releases/tag/v3.0.0 下载 protobuf 编译器。向下滚动到页面底部,下载与您的平台匹配的文件。大多数都是压缩文件的形式,你可以下载和解压缩。对于大多数平台,不需要安装。您可以从下载的 bin 文件夹中运行 protobuf 编译器(名为protoc)。例如,我为 macOS 下载了名为protoc-3.0.2-osx-x86_64.zip的文件,因此可以作为./protoc-3.0.2-osx-x86_64/bin/protoc运行 protobuf 编译器。或者,你可以在你的路径中放置protoc的位置。

有几种方法可以安装 protobuf 库。有关如何安装 protobuf 的说明,请参见 https://github.com/google/protobuf/#protobuf-runtime-installation 中针对您的语言的运行时安装说明。对于 Linux 和 macOS 平台,可以使用 PyPi (pip)安装 protobuf 库,如下所示。请注意,如果您使用提升的权限(例如 sudo)安装 pip,您可能需要指定 sudo 来安装 protobuf。

$ pip install protobuf
Collecting protobuf
  Downloading protobuf-3.5.1-py2.py3-none-any.whl (388kB)
    100% |█████████████████████| 389kB 1.0MB/s
Requirement already satisfied: setuptools in /System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python (from protobuf)
Requirement already satisfied: six>=1.9 in /Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/six-1.10.0-py2.7.egg (from protobuf)
Installing collected packages: protobuf
Successfully installed protobuf-3.5.1

Note

您还必须在系统上安装 Python。请参见 https://www.python.org/ 在您的系统上下载并安装 Python。本章中的示例脚本是为 Python 版编写的,并可在 Python 版中正确执行。如果您使用的是 Python 3.0 或更高版本,您可能需要对代码进行微小的更改。

Protobuf 示例

让我们先来看看 protobuf 定义文件。Protobuf 文件以扩展名.proto命名。我们将把我们的 protobuf 定义文件命名为contacts.proto。清单 7-1 显示了 protobuf 文件contacts.proto的内容。将这个文件放在一个文件夹中,因为我们将添加额外的文件来编译和测试 protobuf 定义。这是您将在其他文档中看到的标准示例模式——一个数据项定义后跟一个包含数据项的数组(或列表)。

syntax = "proto2";

message Contact {
  required string first = 1;
  required int32 id = 2;
  optional string last = 3;

  message PhoneNumber {
    required string number = 1;
  }

  repeated PhoneNumber phones = 5;
}

message Contacts {
  repeated Contact list = 1;
}

Listing 7-1Contacts Protobuf Definition

这里我们看到的代码看起来很像 C++。这不是偶然的,之所以选择它是因为几种语言使用相似的语法,这让大多数开发者都很熟悉。我们看到的第一行是 protobuf 编译器使用该语言版本 2(版本 3 是当前版本)的指令。MySQL 也使用版本 2。

在名为Contact的第一条消息中,我们定义了两个必填字段,一个 id 和一个名字。Id是一个整数,first名是一个字符串。我们还可以为last名称定义一个可选字段。在该消息中还有另一条名为PhoneNumber的消息,它存储了电话number的必填字段。但是,因为这是一条消息,所以我们添加了另一个名为phones的字段来存储 0 个或多个电话号码。也就是说,重复声明表明它可以包含 0 个或多个消息。注意每个数据项的= N。这是一个必需的标签,必须是唯一的。大多数人只是用一个从 1 开始的数字。最后,我们看到一个名为Contacts的消息,我们存储了 0 个或多个名为list的联系人。

要使用新的 protobuf 定义,我们必须编译它。对于这个例子,我将编译它以用于 Python。要使用的命令如下。这会生成一个名为contacts_pb2.py的文件,我们可以在 Python 脚本中导入它。我们用选项--python_out告诉编译器两件事:1)我们要为 Python 编译;以及 2)我们希望编译器的输出出现在当前文件夹(。).您将看不到该命令的任何附加输出—它都被写入文件。确保您的路径上有协议可执行文件的位置,或者使用如下所示的位置(路径)直接调用它。

$ protoc-3.0.2-osx-x86_64/bin/protoc --python_out=. contacts.proto

回想一下,protobuf 支持几种语言。下面列出了编译时支持的语言和使用的正确选项(<out dir>是结果源文件的输出目录)。如您所见,有几个选项涵盖了当今使用的大多数编程语言。如果要用另一种编程语言实现此示例,请使用下面显示的适用于您的编程语言的选项。

  • C++: --cpp_out=<out_dir>
  • C# --csharp_out=<out_dir>
  • Java: --java_out=<out_dir>
  • Java Nano --javanano_out=<out_dir>
  • JavaScript: --js_out=<out_dir>
  • 目标 C: --objc_out=<out_dir>
  • Python: --python_out=<out_dir>
  • 露比:--ruby_out=<out_dir>

contacts_pb2.py 文件的内容不是很有趣。其实挺复杂的。更有趣的是我们如何使用新协议。因为这是用于存储联系人的数据结构,所以让我们编写一个脚本,使用新消息将几个联系人写入一个文件。清单 7-2 显示了一个简单的 Python 脚本,用于将两个联系人写入一个二进制文件。为什么是二进制?因为 protobuf 的设计允许我们在保留类型化(二进制)数据的同时快速轻松地序列化数据。和书中前面的例子一样,如果你不了解 Python,也不用太担心。这是一种非常简单的脚本语言(详见本章后面的边栏)。

import contacts_pb2

# Open the file
f = open("my_contacts", "wb")

# Create a contacts class instance

contacts = contacts_pb2.Contacts()

# Create a new contact message

new_contact = contacts.list.add()

new_contact.id = 90125
new_contact.first = "Andrew"

# Add phone numbers

phone_number = new_contact.phones.add()

phone_number.number = '212-555-1212'

phone_number = new_contact.phones.add()

phone_number.number = '212-555-1213'

# Create a new contact message

new_contact = contacts.list.add()

new_contact.id = 90126
new_contact.first = "William"
new_contact.last = "Edwards"

# Add phone numbers

phone_number = new_contact.phones.add()

phone_number.number = '301-555-1111'

phone_number = new_contact.phones.add()

phone_number.number = '301-555-3333'

# Write the data

f.write(contacts.SerializeToString())

# Close the file
f.close()

Listing 7-2Writing Contacts to a File (Protobuf Example)

我在这里使用了内联编码风格,而不是循环,向您展示了如何使用 protobuf 中的add()方法添加新消息。但是,首先要注意,我们必须导入用 protobuf 编译器(contacts_pbs2)创建的文件。然后我们为 protobuf 编译器生成的Contacts类创建一个实例。回想一下,这是一个类型为Contact的数组(列表)。当调用add()方法时,我们得到一个Contact结构的实例,我们可以使用字段名给它赋值。因此,我设置了 id、名字,然后通过引用名为phones的嵌套消息创建一个新的电话号码结构并填充它来添加电话号码。请注意,每次想要添加新消息时,都必须调用add()。最后,我使用SerializeToString()方法序列化我在内存中构建的所有消息,并将其写入名为my_contacts的文件。花一些时间通读代码,直到你理解它是如何工作的。

Tip

不要太担心次要的细节或改进代码的方法。我包含了示例代码来演示 protobuf,而不是使用 Python 来演示。我们将在后面的章节中看到更多关于 Python 的内容。

如果您想运行代码,创建一个名为write_contacts.py的文件,输入代码,保存它,然后用下面的命令执行它。这里您也看不到任何输出,因为它创建了文件my_contacts

$ python ./write_contacts.py

如果您想知道这些数据在文件中是什么样子,下面显示了文件 my_contacts 的十六进制转储。注意,它确实是一个二进制文件。

$ hexdump -C my_contacts
00000000  0a 2c 0a 06 41 6e 64 72 65 77 10 8d c0 05 2a 0e |.,..Andrew....*.|
00000010  0a 0c 32 31 32 2d 35 35 35 2d 31 32 31 32 2a 0e |..212-555-1212*.|
00000020  0a 0c 32 31 32 2d 35 35 35 2d 31 32 31 33 0a 36 |..212-555-1213.6|
00000030  0a 07 57 69 6c 6c 69 61 6d 10 8e c0 05 1a 07 45 |..William......E|
00000040  64 77 61 72 64 73 2a 0e 0a 0c 33 30 31 2d 35 35 |dwards*...301-55|
00000050  35 2d 31 31 31 31 2a 0e 0a 0c 33 30 31 2d 35 35 |5-1111*...301-55|
00000060  35 2d 33 33 33 33                               |5-3333|

00000066

现在,让我们看看如何从文件中读取联系人。这段代码要短得多,也更容易阅读。我们再次导入contacts_pb2文件,然后打开该文件进行读取。然而,在本例中,我们创建了 Contacts 类的一个新实例,然后使用ParseFromString()方法从文件中读取。这将在内存中创建联系人列表,然后我们可以遍历并打印数据。下面显示了读取联系人列表的完整代码。

import contacts_pb2

contacts = contacts_pb2.Contacts()

# Read the existing contacts.
with open("my_contacts", "rb") as f:
    contacts.ParseFromString(f.read())

# Print out the contacts
for contact in contacts.list:
    print contact
f.close()

与 write 示例中一样,我们可以执行这段代码,但在本例中,我们将看到联系人列表被打印出来。清单 7-3 显示了输出。请注意,我们看到一个类似 C++(和 JSON 有点像)的格式良好的输出。

$ python ./read_contacts.py
first: "Andrew"
id: 90125
phones {
  number: "212-555-1212"
}
phones {
  number: "212-555-1213"
}

first: "William"
id: 90126
last: "Edwards"
phones {
  number: "301-555-1111"
}
phones {
  number: "301-555-3333"
}

Listing 7-3Reading the Contact List (protobuf example)

当然,您可以编写代码来使用点语法访问单个字段。例如,您可以用下面的示例代码只打印出名字和姓氏。

# Print out the contacts
for contact in contacts.list:
    print contact.first, contact.last,
    for phone in contact.phones:
        print phone.number,
    print

当您执行这个文件时,您会看到如下所示的输出。

$ python ./read_contacts.py
Andrew  212-555-1212 212-555-1213
William Edwards 301-555-1111 301-555-3333

正如您所看到的,使用 protobuf 使读写结构化数据变得更加容易,并且比我们自己编写结构要简单得多。如果这个例子很有意思,我鼓励你去尝试一下,并随心所欲地修饰它。如果您想了解更多关于 protobuf 的信息,包括如何开始构建您自己的消息和协议,请参阅位于 https://developers.google.com/protocol-buffers/docs/overview 的在线文档。

那么,MySQL protobuf 称为 X 协议是什么呢?不应该被命名为“MySQL 协议缓冲区”吗?Recall protobuf 是一种可以用来设计协议的技术。因此,X 协议是使用 protobuf 定义组成新协议的消息、命令等的产物。因此,X 协议是使用 protobuf 语言的通信协议的定义。酷吧。

既然我们对 X 协议有了更多的了解,知道了它是如何(以及为什么)被设计的,那么让我们更仔细地看看它在代码和 protobuf 级别是如何工作的。

x 协议:引擎盖下

虽然开发者不太可能需要编写直接与 X 协议接口的低级代码,但是浏览一下 X 协议是如何实现的还是很有帮助的。为了简洁起见,在开始详细研究一个数据库连接器如何实现 X 协议之前,我们将只看 X 协议的一小部分。如果你是一个代码狂热者,你现在可以采取你最好的编码姿势。 6

让我们先来看看定义 MySQL X 协议的 protobuf 定义文件。

Protobuf 实现

MySQL protobuf 定义文件可以在任何实现 X 协议的产品的源代码下载中找到。例如,您可以在 MySQL 服务器的源代码中的rapid/plugin/x/protocol文件夹中找到它们,该文件夹以前缀mysqlx和文件扩展名.proto命名。也可以在 https://github.com/mysql/mysql-server/blob/5.7/rapid/plugin/x/protocol 从 GitHub 看到并下载 X 协议 protobuf 定义文件。

我展示 Github 存储库,而不是让您下载服务器代码,因为您可以使用 Github 存储库深入查看文件,而不必下载任何东西。只需使用之前的 URL 并点击 mysqlx.proto 文件链接。图 7-1 显示了在 Github 中查看文件的示例。

A432285_1_En_7_Fig1_HTML.jpg

图 7-1

The mysqlx.proto file (Github)

但是,如果您喜欢下载服务器代码,您可以。只需访问 https://dev.mysql.com/downloads/mysql/ ,在选择操作系统下拉列表中选择源代码条目,为您的平台选择一个文件,并下载它。一旦您解压缩了文件,您就可以在您自己的 PC 上探索服务器源代码。

这些是未编译的原始 protobuf 定义文件。表 7-1 列出了组成 X 协议的 protobuf 定义文件,包括文件名和简短描述。注意,文件名与 X DevAPI 中的主要概念相关联,显示了 protobuf 到 X 协议的清晰映射。

表 7-1

Protobuf Definition Files (X Protocol)

| 文件 | 描述 | | :-- | :-- | | `mysqlx.proto` | 定义客户端、服务器以及常规 Ok 和错误消息。这是导入所有其他文件的主文件。 | | `mysqlx_connection.proto` | 定义用于在连接协商过程中确定服务器功能的消息(见下文) | | `mysqlx_crud.proto` | 定义用于处理 CRUD 操作的消息 | | `mysqlx_datatypes.proto` | 定义使用标量数据类型的消息 | | `mysqlx_expect.proto` | 定义用于处理管道消息的消息 | | `mysqlx_expr.proto` | 定义使用表达式的消息 | | `mysqlx_notice.proto` | 定义用于发布通知(如会话和变量状态更改)的消息 | | `mysqlx_resultset.proto` | 为包括行和列的结果集定义消息;这个文件是 X 协议的关键组件,展示了 protobuf 的强大功能。 | | `mysqlx_sql.proto` | 定义用于执行语句的消息 | | `mysqlx_session.proto` | 定义管理会话的消息 |

为了让您对文件包含的内容有所了解,清单 7-4 显示了来自mysqlx.proto文件的错误消息。

...
// generic Error message
//
// A ``severity`` of ``ERROR`` indicates the current message sequence is
// aborted for the given error and the session is ready for more.
//
// In case of a ``FATAL`` error message the client should not expect
// the server to continue handling any further messages and should
// close the connection.
//
// :param severity: severity of the error message
// :param code: error-code
// :param sql_state: SQL state
// :param msg: human readable error message
message Error {
  optional Severity severity = 1 [ default = ERROR ];
  required uint32 code = 2;
  required string sql_state = 4;
  required string msg = 3;

  enum Severity {
    ERROR = 0;
    FATAL = 1;
  };
}
...

Listing 7-4Generic Error Message (mysqlx.proto)

请注意,该消息定义得非常好,包含了您在查看客户机/服务器协议时所期望看到的内容。特别是,我们看到一个可选的严重性设置、错误代码、SQL 状态代码(字符串)和一条错误消息(字符串)。严重性是一个枚举值,当前可以设置为错误(0)或失败(1)。酷吧。

您可能想知道 protobuf 编译器在编译时对这段代码做了什么。让我们来看看由此产生的 Python 代码。清单 7-5 显示了通用错误消息的编译代码。为了简洁起见,我省略了一些代码。

...
_ERROR = _descriptor.Descriptor(
  name='Error',
  full_name='Mysqlx.Error',
  filename=None,
  file=DESCRIPTOR,
  containing_type=None,
  fields=[
    _descriptor.FieldDescriptor(
      name='severity', full_name='Mysqlx.Error.severity', index=0,
      number=1, type=14, cpp_type=8, label=1,
      has_default_value=False, default_value=0,
      message_type=None, enum_type=None, containing_type=None,
      is_extension=False, extension_scope=None,
      options=None),
    _descriptor.FieldDescriptor(
      name='code', full_name='Mysqlx.Error.code', index=1,
      number=2, type=13, cpp_type=3, label=1,
      has_default_value=False, default_value=0,
      message_type=None, enum_type=None, containing_type=None,
      is_extension=False, extension_scope=None,
      options=None),
...
  ],
  extensions=[
  ],
  nested_types=[],
  enum_types=[
    _ERROR_SEVERITY,
  ],
  options=None,
  is_extendable=False,
  syntax='proto3',
  extension_ranges=[],
  oneofs=[
  ],
  serialized_start=872,
  serialized_end=1001,
)
...
Listing 7-5Python Generic Error Message (mysqlx_pb2.proto)

啊!这一点也不简单,也不容易读懂。这是一个很好的例子,展示了 protobuf 可以为我们做多少事情。显然,用 protobuf 定义消息比用 Python 定义消息要重要得多(相对而言)。如果您感到好奇,用其他语言编译 protobuf 定义文件同样会产生复杂且看似难以理解的代码。但是不用担心;我们不需要直接读取编译后的文件!这很好,不是吗?

为了了解 X 协议的复杂性(和完整性),让我们看看 Connector/Python 是如何实现 X 协议的。在下一节中,我们将通过几个简单的例子,包括连接过程,来看看 X 协议是如何工作的。

Tip

我鼓励您探索其他* .proto文件,看看它们定义的消息。

x 协议示例

我们探索 X 协议的两个实例:1)概述如何从协商、认证开始建立连接,然后是命令;以及 2)如何处理 SQL 插入。这些例子很容易理解,如果你好奇的话,可以很容易地在 protobuf 定义文件中找到。

示例 1:身份验证

为简单起见,让我们假设我们想要使用旧的身份验证连接到服务器。这将让您很好地了解通信协议是如何工作的,而不像我们在新机制中看到的那样费力。目标是通过举例来理解典型的通信协议是如何工作的。毕竟,您不太可能构建自己的身份验证协议(但您可以通过构建自己的身份验证插件)。

该过程的生命周期从协商阶段开始,在这个阶段,客户机使用CapabilitiesGet()方法向服务器请求认证(和其他)能力。服务器用CapabilitiesGet消息响应(在mysqlx_connection.proto文件中定义)。客户端然后设置功能(比如设置 TLS 之类的认证扩展),通过CapabilitiesSet()方法发送完整的消息。假设数据是正确的,服务器回复Ok消息。

然后,客户端使用AuthenticateStart()方法启动身份验证。然后,服务器可以发出一个AuthenticateContinue()方法调用,向客户机请求更多数据。然后,客户端可以用相同的AuthenticateContinue()方法调用进行响应,一旦认证完成,服务器就用AuthenticateOk()方法调用进行响应。从那里,客户端可以启动命令。图 7-2 显示了消息传输方向的生命周期示例(执行相关方法的结果)。

A432285_1_En_7_Fig2_HTML.jpg

图 7-2

X Protocol connection procedure (Courtesy of Oracle)

我们来看一下CapabilitiesSet的消息。清单 7-6 显示了来自mysqlx_connection.proto文件的摘录。

...
// a Capability
//
// a tuple of a ``name`` and a :protobuf:msg:`Mysqlx.Datatypes::Any`
message Capability {
  required string name = 1;
  required Mysqlx.Datatypes.Any value = 2;
}

// Capabilities
message Capabilities {
  repeated Capability capabilities = 1;
}
...
// :precond: active sessions == 0
// :returns: :protobuf:msg:`Mysqlx::Ok` or :protobuf:msg:`Mysqlx::Error`
message CapabilitiesSet {
  required Capabilities capabilities = 1;
};
...

Listing 7-6CapabilitiesSet Message (mysqlx_connection.proto)

注意,我们看到CapabilitiesSet消息有一个名为Capabilities类型消息的能力的字段。这是一个占位符,供客户端用数据完成消息并将其发送回服务器。其他值包括SCALAR (1)、OBJECT (2)或ARRAY (3),可以在mysqlx_datatypes.proto文件中找到。

示例 2:简单插入

在本例中,我们将检查发出 SQL 语句时会发生什么。特别是,对一个简单的表执行两个INSERT语句。此时,我们正在处理一个 SQL 对象和位于名称奇怪的mysqlx_sql.proto文件中的StmtExecute消息。

该过程从客户端使用Sql.StmtExecute()方法向服务器发送语句开始。然后,服务器可以用Sql.StmtExecuteOk()方法进行响应。如图 7-3 所示,对下一条 INSERT 语句重复该过程。

A432285_1_En_7_Fig3_HTML.jpg

图 7-3

X Protocol simple inserts (Courtesy of Oracle)

我们来看一下Sql.StmtExecute的消息。清单 7-7 显示了来自mysqlx_sql.proto文件的摘录。

...
// execute a statement in the given namespace
//
// .. uml::
//
//   client -> server: StmtExecute
//   ... zero or more Resultsets ...
//   server --> client: StmtExecuteOk
//
// Notices:
//   This message may generate a notice containing WARNINGs generated by its execution.
//   This message may generate a notice containing INFO messages generated by its execution.
//
// :param namespace: namespace of the statement to be executed
// :param stmt: statement that shall be executed.
// :param args: values for wildcard replacements
// :param compact_metadata: send only type information for :protobuf:msg:`Mysqlx.Resultset::ColumnMetadata`, skipping names and others
// :returns:
//    * zero or one :protobuf:msg:`Mysqlx.Resultset::` followed by :protobuf:msg:`Mysqlx.Sql::StmtExecuteOk`
message StmtExecute {
  optional string namespace = 3 [ default = "sql" ];
  required bytes stmt = 1;
  repeated Mysqlx.Datatypes.Any args = 2;
  optional bool compact_metadata = 4 [ default = false ];
}
...
Listing 7-7Sql.StmtExecute Message (mysqlx_sql.proto)

注意,我们有用于namespace(默认设置为 SQL)的字段,SQL 语句存储在stmt中。注意,它的类型是 byte,所以我们可以处理任何字符集,包括二进制数据。然后,我们可以有零个或多个参数(args),以允许参数化查询。最后,我们可以有一个可选的compact_metadata设置,允许服务器只将类型信息发送回客户端。

正如你所看到的,X 协议有很多内幕。然而,我们不需要了解 X 协议的所有知识就可以使用它。事实上,使用 X 协议的最佳方式是通过 MySQL Shell,这一点我们在第 4 章中有详细介绍,或者通过支持 X 协议的数据库连接器。让我们看看一个数据库连接器是如何实现 X 协议的。

Wait! Where’s the Rest of the Code?

如果您花时间检查 protobuf 定义文件,您可能会注意到缺少了两个主要的东西。Protobuf 是一种协议定义语言(API ),但它不支持通过网络直接传输消息,也不支持加密、压缩和其他数据传输技术。

因此,X 协议是所有这些代码存在的地方。现在你可以明白为什么 X 协议不仅仅是一个 protobuf 实现了。X 协议还实现了其他一些不属于 protobuf 消息定义的功能。这些包括与服务器握手、错误消息定义等等。

x 协议演练

为了更好地理解 X 协议的强大和优雅,我们将研究一个数据库连接器是如何实现 X 协议的。这在 protobuf 定义文件上提供了一个抽象层,这让我们了解了 protobuf,这是一件非常好的事情。正如您将看到的,连接器使得使用 X 协议非常容易,从而延续了 protobuf 的目标,使通信协议易于创建和使用。

我们在本节和下一节中使用的数据库连接器是 Connector/Python,C/Py。我再次选择 C/Py 是因为它的简单性和可读性。如果您想继续学习并查看上下文中的代码,可以在 http://dev.mysql.com/downloads/connector/python/ 下载 Connector/Python 8 . 0 . 5 版或更高版本的源代码。请注意,您可能需要单击开发版本选项卡,然后从下拉列表中选择独立于平台的条目。

我们看一下上一节中每个例子的 C/Py 代码。因此,我们将看到连接到服务器并执行 SQL INSERT语句的代码。

示例 1:身份验证

我们在位于/lib/mysqlx文件夹中的名为connection.py的 C/Py 源代码文件中找到了认证过程的代码。清单 7-8 显示了实现该过程的源代码(方法)的摘录。为了简洁起见,我省略了收集和传递连接信息的细节。重点关注的起点是Connection类中的connect()方法。

...

def connect(self):
    # Loop and check
    error = None
    while self._can_failover:
        try:
            self.stream.connect(self._connection_params())
            self.reader_writer = MessageReaderWriter(self.stream)
            self.protocol = Protocol(self.reader_writer)
            self._handle_capabilities()
            self._authenticate()
            return
        except socket.error as err:
            error = err

    if len(self._routers) <= 1:
        raise InterfaceError("Cannot connect to host: {0}".format(error))
    raise InterfaceError("Failed to connect to any of the routers.", 4001)

def _handle_capabilities(self):
    if self.settings.get("ssl-mode") == SSLMode.DISABLED:
        return
    if self.stream.is_socket:
        if self.settings.get("ssl-mode"):
            _LOGGER.warning("SSL not required when using Unix socket.")
        return

    data = self.protocol.get_capabilites().capabilities
    if not (get_item_or_attr(data[0], "name").lower() == "tls"
            if data else False):
        self.close_connection()
        raise OperationalError("SSL not enabled at server.")

    is_ol7 = False
    if platform.system() == "Linux":
        # pylint: disable=W1505
        distname, version, _ = platform.linux_distribution()
        try:
            is_ol7 = "Oracle Linux" in distname and version.split(".")[0] == "7"
        except IndexError:
            is_ol7 = False
    if sys.version_info < (2, 7, 9) and not is_ol7:
        self.close_connection()
        raise RuntimeError("The support for SSL is not available for "
            "this Python version.")

    self.protocol.set_capabilities(tls=True)
    self.stream.set_ssl(self.settings.get("ssl-mode", SSLMode.REQUIRED),
                        self.settings.get("ssl-ca"),
                        self.settings.get("ssl-crl"),
                        self.settings.get("ssl-cert"),
                        self.settings.get("ssl-key"))

def _authenticate(self):
    auth = self.settings.get("auth")
    if (not auth and self.stream.is_secure()) or auth == Auth.PLAIN:
        self._authenticate_plain()
    elif auth == Auth.EXTERNAL:
        self._authenticate_external()
    else:
        self._authenticate_mysql41()
...

Listing 7-8Connection Methods for Authenticate Procedure (C/Py)

注意,在 connect()方法中,我们看到发生了几件事情。首先,我们看到 C/Py 打开了一个到服务器的流连接(通过 _connection_params()方法,该方法先前返回了数据集),然后代码创建了一个到读取器/写入器的实例。这就是连接器向/从服务器传输邮件的方式。

接下来,代码实例化协议类的一个实例,这是 X 协议的抽象。稍后我们将看到该代码的更多细节。

现在,关注 connect()方法中的最后两条语句。这里我们看到 _handle_capabilities()中对 CapabilitiesGet/Set 方法的方法调用和 _authenticate()中的身份验证阶段。花一些时间通读代码,这样您就可以看到图 7-1 中的所有步骤都显示出来了。

Protocol类的 CapabilitiesGet/Set 方法可以在 C/Py 源代码的/lib/mysqlx 文件夹中的 protocol.py 文件中找到,如清单 7-9 所示。

...
def get_capabilites(self):
    msg = Message("Mysqlx.Connection.CapabilitiesGet")
    self._writer.write_message(
        mysqlxpb_enum("Mysqlx.ClientMessages.Type.CON_CAPABILITIES_GET"),
        msg)
    return self._reader.read_message()

def set_capabilities(self, **kwargs):
    capabilities = Message("Mysqlx.Connection.Capabilities")
    for key, value in kwargs.items():
        capability = Message("Mysqlx.Connection.Capability")
        capability["name"] = key
        capability["value"] = self._create_any(value)
        capabilities["capabilities"].extend([capability.get_message()])
    msg = Message("Mysqlx.Connection.CapabilitiesSet")
    msg["capabilities"] = capabilities
    self._writer.write_message(
        mysqlxpb_enum("Mysqlx.ClientMessages.Type.CON_CAPABILITIES_SET"),
        msg)
    return self.read_ok()
)
...

Listing 7-9CapabilitiesGet/Set Methods for Authenticate Procedure (C/Py)

在这一点上,我们可以看到通过由 protobuf 编译器生成的MySQLx*类对 protobuf 代码的调用。

示例 2:简单插入

这个例子更容易理解,所以我们将比上一个例子更深入一些。我们在 C/Py 源代码的/lib/mysqlx文件夹中名为statement.py的 C/Py 源代码文件中找到了认证过程的代码。清单 7-10 显示了源代码的摘录,它实现了一个用于执行INSERT SQL 语句的类。

...
class InsertStatement(WriteStatement):
    """A statement for insert operations on Table.

    Args:
        table (mysqlx.Table): The Table object.
        *fields: The fields to be inserted.
    """
    def __init__(self, table, *fields):
        super(InsertStatement, self).__init__(table, False)
        self._fields = flexible_params(*fields)

    def values(self, *values):
        """Set the values to be inserted.

        Args:
            *values: The values of the columns to be inserted.

        Returns:
            mysqlx.InsertStatement: InsertStatement object.
        """
        self._values.append(list(flexible_params(*values)))
        return self

    def execute(self):
        """Execute the statement.

        Returns:
            mysqlx.Result: Result object.
        """
        return self._connection.send_insert(self)
...

Listing 7-10SQL INSERT Class (C/Py)

如您所见,代码很容易阅读。首先要注意的是该类是从一个名为 WriteStatement 的基类派生而来的(也在statement.py中)。这个基类有一个名为execute()的抽象(虚拟)方法,这个派生类实现了这个方法。然而,在这种情况下,它从连接类(在connection.py中)调用send_insert()方法。下面展示了send_insert()的方法。

@catch_network_exception
def send_insert(self, statement):
    self.protocol.send_insert(statement)
    ids = None
    if isinstance(statement, AddStatement):
        ids = statement._ids
    return Result(self, ids)

如您所见,这用清单 7-11 所示的语句调用了protocol.py文件中的协议类方法send_insert()

...
def send_insert(self, stmt):
    data_model = mysqlxpb_enum("Mysqlx.Crud.DataModel.DOCUMENT"
                               if stmt._doc_based else
                               "Mysqlx.Crud.DataModel.TABLE")
    collection = Message("Mysqlx.Crud.Collection",
                         name=stmt.target.name,
                         schema=stmt.schema.name)
    msg = Message("Mysqlx.Crud.Insert", data_model=data_model,
                  collection=collection)

    if hasattr(stmt, "_fields"):
        for field in stmt._fields:
            expr = ExprParser(field, not stmt._doc_based) \
                .parse_table_insert_field()
            msg["projection"].extend([expr.get_message()])

    for value in stmt._values:
        row = Message("Mysqlx.Crud.Insert.TypedRow")
        if isinstance(value, list):
            for val in value:
                row["field"].extend([build_expr(val).get_message()])
        else:
            row["field"].extend([build_expr(value).get_message()])
        msg["row"].extend([row.get_message()])

    msg["upsert"] = stmt._upsert
    self._writer.write_message(
        mysqlxpb_enum("Mysqlx.ClientMessages.Type.CRUD_INSERT"), msg)
...

Listing 7-11The send_insert() Method in the Protocol Class (C/Py)

和前面的例子一样,我们现在可以看到 protobuf 接口,并按照代码来查看图 7-2 中概述的步骤。

Tip

如果你想了解更多关于 X 协议是如何工作的,请看位于 https://dev.mysql.com/doc/internals/en/x-protocol.html 的 MySQL 内部文档。

既然我们对 X 协议有了更多的了解,并且能够理解 X 插件和 Shell 以及数据库连接器提供的抽象,那么让我们看看如何编写利用 MySQL 连接器提供的 X 协议的客户端应用。在这种情况下,我们将继续通过使用 Connector/Python 来掌握 X 协议。

创建 X 客户端

创建使用 X 协议的 MySQL 客户端应用最好是使用 MySQL Shell 或者最终使用一个数据库连接器,并在服务器上安装 X 插件。在本节中,我们将看到两个独立客户端的示例。一个是使用 MySQL 作为文档存储编写的,另一个只使用关系数据模型。

我们将使用的编程语言是一种非常简单的脚本语言,叫做 Python。正如您将看到的,这些命令非常直观,非常有表现力。出于本演示的目的,您不需要成为这种语言的专家。我会提供所有的代码和命令,你需要我们一起去。

Python? Isn’t That a Snake?

Python 编程语言是一种高级语言,旨在尽可能接近阅读英语,同时简单、易学且非常强大。皮托尼斯塔斯会告诉你设计师们确实达到了这些目标。

Python 在使用前不需要编译步骤。相反,Python 应用(其文件名以.py结尾)是动态解释的。这是非常强大的;但是除非使用 Python 开发环境,否则一些语法错误(比如不正确的缩进)直到应用执行后才会被发现。幸运的是,Python 提供了一个健壮的异常处理机制。

如果您从未使用过 Python,或者您想了解更多,下面是几本介绍这种语言的好书。互联网上也有很多资源,包括位于 http://www.python.org/doc/ 的 Python 文档页面:

  • 《树莓派编程》,西蒙·蒙克著(麦格劳-希尔出版社,2013 年)。
  • Python 入门,从新手到专业人士,第 2 版。马格努斯·李·赫特兰德著。
  • 大卫·比兹利和布莱恩·k·琼斯的《Python 食谱》(O'Reilly Media,2013 年)。

有趣的是,Python 是以英国喜剧团 Monty Python 而不是爬行动物命名的。当你学习 Python 的时候,你可能会遇到对 Monty Python 剧集的无聊引用。我对巨蟒小组情有独钟,觉得这些参考资料很有趣。当然,您的里程可能会有所不同。

首先,您可以输入示例中所示的代码,或者从本书的网站下载源代码。在编写 Python 脚本时,您可以使用任何想要的代码编辑器。我们从如何设置环境来运行示例的简短描述开始。

Tip

有很多可用的,包括 JetBrains 的一个非常强大的 IDE,名为 PyCharm ( http://www.jetbrains.com/pycharm/ )。如果您想要一个优秀的 Python 开源软件,请查看 PyCharm 社区版。

示例的设置

要使用本节中的示例,您需要安装一些东西。您必须下载 Google Protocol Buffers Python 库并安装编程语言运行时。您还必须下载 C/Py 的源代码。

回想一下,我们需要安装 protobuf 编译器和 protobuf 库。如果您还没有这样做,请参考上一节“安装 Protobuf 编译器”

特定语言的运行时库可以从 https://github.com/google/protobuf 下载。您应该通过单击克隆或下载按钮来下载整个包。下载完成后,您会看到一个名为protobuf-master.zip的文件,您可以解压缩它。要安装所选语言的库,请导航到以该语言命名的文件夹,并阅读 README.md 文件以获取特定的安装说明。例如,我们将在本章中使用 Python。这个文件夹被命名为/protobuf-master/python。要在 macOS 上安装 Python,可以运行以下命令。

$ python ./setup.py build
$ sudo python ./setup.py install

在其他系统上安装 Python 库是类似的。在 Windows 上安装它的唯一区别是你不需要使用 sudo(超级用户)。然而,在我的系统上,定位 protobuf 编译器有一个问题。我收到了类似如下的错误。

protoc is not installed nor found in ../src.  Please compile it or install the binary package.

一旦我将 protobuf 编译器可执行文件(protoc)放在指定的目录(../src)中,我就可以用前面的命令安装 Python protobuf 库了。您可能会在其他平台上遇到类似的问题。

Tip

向下滚动到页面底部的 https://github.com/google/protobuf ,点击表格中的链接,查看安装其他语言的 protobuf 库的说明。

如果您还没有,您必须从 http://dev.mysql.com/downloads/connector/python/ 下载 C/Py 8 . 0 . 5 版或更高版本的源代码。确保从下拉列表中下载独立于平台的选项。在我们的例子中,我们将使用 C/Py 源文件树中的一些源文件。

我选择这样做是为了帮助您了解 protobuf 如何与 Python 一起工作以及 C/Py 如何实现 X 协议的细节。尽管这些示例将展示 C/Py 中的 X 协议抽象层,但是您可以使用您最喜欢的调试器或 Python IDE 来深入研究代码,看看事情是如何工作的。因此,我为我们当中的好奇者树立了这个榜样。然而,如果你不想走那么远,你可以不走那么远。相反,您可以专注于示例是如何工作的,以便更好地理解如何通过数据库连接器使用新的 X 协议。

也许更重要的是,因为我们使用的 C/Py 示例是一个开发里程碑版本(考虑 beta 版),复制源代码不会影响系统上 C/Py 的任何其他安装,从而允许您运行这些示例,而不必安装连接器的开发里程碑版本。

我们需要的文件在/lib/mysqlx文件夹里。但是首先,在你的系统上创建一个新的文件夹。随便你怎么命名比如xclient。接下来,将 C/Py 档案中的mysqlx文件夹复制到xclient文件夹中。当你为下面的例子创建文件时,把它们保存在xclient文件夹中。例如,我将文档存储示例命名为xclient_json.p y,将关系数据示例命名为xclient_sql.py

Tip

如果出现找不到一个或多个库的错误,请确保将 mysqlx 文件夹复制到与 xclient_json.py 和 xclient_sql.py 文件相同的文件夹中。

文档存储示例

这个例子创建了一个简单的客户机来演示如何使用 C/Py 中的 X 协议抽象。这个例子使用了我们在第 1 章中遇到的联系人的概念。在这种情况下,代码将连接到服务器,在模式中创建一个模式和集合,并用文档填充集合。然后,代码将检索所有文档并打印出来。但是我们不仅仅打印原始文档。该代码演示了如何在集合上执行查找操作,并遍历文档,为找到的每个联系人文档打印电话号码。

下面简要描述了代码部分。我强调了相关的代码语句,以引起您对 X 协议抽象方法的注意。大多数调用对你来说都很熟悉,因为我们在第 5 章和本书的其他地方遇到过它们。因此,我保持解释简短。如果你需要更多关于例子中使用的类和方法的信息,请参考第 5 章。

我们需要做的第一件事是导入mysqlx库。回想一下,这是从 C/Py 下载的一组文件。它包含了我们前面看到的 X 协议文件的 C/Py 抽象。如果您检查该文件夹,您会注意到.proto文件丢失了。这是因为我们只需要运行 protobuf 编译器时生成的.py文件。幸运的是,所有这些文件都存在于mysqlx文件夹中。

接下来,我们要求用户提供登录凭证(用户 id、密码、主机和端口)。我们使用这些信息来打开一个到服务器的会话(连接)。为此,我们使用get_session()方法,并将会话对象的结果实例赋给变量mysqlx_session。如果发生了我们无法连接的情况,我们会检查会话的状态,如果会话没有打开,就退出。注意,我们在这个例子中使用 X 会话,因为我们只执行 CRUD 操作,不需要任何 SQL 支持。

接下来,我们使用mysqlx_session对象实例,并尝试用get_schema()方法获取模式。 8 这将设置默认模式,以便当我们创建集合(或者其他对象)时;它们将包含在模式中。我使用一个常量来存储模式名和集合名。如果模式不在服务器上,我们用create_schema()方法创建它。无论哪种方式,我们都会得到一个 schema 对象实例,我们可以用这个实例用create_collection()方法创建集合,这个方法为我们提供了一个集合的对象实例。注意,我使用了remove()方法来清空集合。这允许我们在不复制数据的情况下重新运行代码(我没有检查文档 id)。

在继续之前,让我们看一下代码。清单 7-12 显示了完整的代码。花一些时间通读代码,以便您可以看到到目前为止描述的所有方法和操作。您应该对 contacts.remove()调用之前的所有代码都很熟悉。如果您想执行这段代码来看看它做了什么,您可以将这段代码放在一个名为 xclient_json.py 的文件中。

#
# Introducing the MySQL 8 Document Store - xclient_json
#
# This file contains and example of how to read a collection from a MySQL
# server using the X Protocol via a Session object
#
# Dr. Charles Bell, 2018
#
import getpass
import mysqlx

# Declarations
TEST_SCHEMA = "rolodex"
TEST_COL = "contacts"

# Get user information
print("Please enter the connection information.")
user = raw_input("Username: ")
passwd = getpass.getpass("Password: ")
host = raw_input("Hostname [localhost]: ") or 'localhost'
port = raw_input("Port [33060]: ") or '33060'

# Get a session object using a dictionary of terms
mysqlx_session = mysqlx.get_session({'host': host, 'port': port, 'user': user, 'password': passwd})

# Check to see that the session is open. If not, quit.
if not mysqlx_session.is_open():
    exit(1)

# Get the schema and create it if it doesn't exist
schema = mysqlx_session.get_schema(TEST_SCHEMA)
if not schema.exists_in_database():
    schema = mysqlx_session.create_schema(TEST_SCHEMA)

# Create a collection or use it if it already exists
contacts = schema.create_collection(TEST_COL)

# Empty the collection
contacts.remove()

# Insert data with inline JSON
contacts.add({"name": {"first": "Allen"},
              "phones": [{"work": "212-555-1212"}]}).execute()
contacts.add({"name": {"first": "Joe", "last": "Wheelerton"},
              "phones": [{"work": "212-555-1213"}, {"home": "212-555-1253"}],
              "address": {"street": "123 main", "city": "oxnard", "state": "ca", "zip": "90125"},
              "notes": "Excellent car detailer. Referrals get $20 off next detail!"}).execute()

# Get all of the data
doc_results = contacts.find().execute()

# Show the results
print("\nList of Phone Numbers")
document = doc_results.fetch_one()
while document:
    print("{0}:\t".format(document.name['first'])),
    for phone in document.phones:
        for key, value in phone.iteritems():
            print("({0}) {1}".format(key, value)),
    print("")
    document = doc_results.fetch_one()

# Drop the collection
schema.drop_collection(TEST_COL)

# Drop the schema
mysqlx_session.drop_schema(TEST_SCHEMA)

# Close the session
mysqlx_session.close()

Listing 7-12
X Client Source Code (JSON)

Tip

如果您使用的是 Python 3.0 或更高版本,您需要将raw_input()调用改为input(),将iteritems()改为items()。这是因为在 Python 的后续版本中不再支持raw_input()iteritems()

接下来,我们可以添加一些联系人。我们使用集合对象实例的add()方法来实现这一点。在本例中,我们添加了几个文档;一个是我们只知道他们的名字和电话号码的人,另一个是我们知道他们的全名、几个电话号码和一些我们做的笔记的人。这说明了使用文档存储的强大之处:存储您需要的东西,不要强迫数据遵守严格的结构或存储机制!

一旦添加了文档,我们就对集合使用find()方法,而不使用任何表达式。我们用execute()方法链接查找操作。这只是以文档结果对象实例的形式返回集合中的所有文档。然后我们可以用这个对象通过fetch_one()方法迭代文档。请注意,这将返回一个文档对象实例,我们可以使用该实例通过命名属性(一个强大的表达式)直接获取数据元素。花点时间通读一下获取文档的代码。注意,当收集结束时,fetch_one()返回None,while 循环终止。

最后,我们用drop_collection()方法删除集合,用drop_schema()方法删除模式,这样我们就可以重新运行代码并避免重复。但是,您可能会注意到,我添加了代码来防止意外执行。例如,如果您使用调试器并在结束前终止代码,代码顶部的语句将使用该架构(如果它已经存在)并清空集合。

现在让我们看看脚本的运行情况。在这种情况下,我们希望只看到 rolodex 中的人的名字和电话号码列表(在这种情况下只有两个条目)。

$ python ./xclient_json.py
Please enter the connection information.
Username: root
Password:
Hostname [localhost]:
Port [33060]:

List of Phone Numbers
Joe:     (work) 212-555-1213 (home) 212-555-1253
Allen:   (work) 212-555-1212

如果您想知道这是否是一个精心策划的诡计,我们创建的集合和文档不知何故存储在 MySQL 的其他地方,如果您禁用了drop_*()调用并再次运行该程序,您可以登录到服务器并查看底层表的构造,如清单 7-13 所示。

$ mysqlsh root@localhost:33060 --sql --json=pretty --schema=rolodex -e "EXPLAIN contacts"
{
    "password": "Enter password: "
}

{
    "executionTime": "0.00 sec",
    "warningCount": 0,
    "warnings": [],
    "rows": [
        {
            "Field": "doc",
            "Type": "json",
            "Null": "YES",
            "Key": "",
            "Default": null,
            "Extra": ""
        },
        {
            "Field": "_id",
            "Type": "varchar(32)",
            "Null": "NO",
            "Key": "PRI",
            "Default": null,
            "Extra": "STORED GENERATED"
        }
    ],
    "hasData": true,
    "affectedRowCount": 0,
    "autoIncrementValue": 0
}

Listing 7-13Definition of the Contacts Collection

如果您运行一个SELECT语句从该表中获取所有数据,您将看到类似于清单 7-14 中所示的结果。结果的顺序可能有所不同,但您应该在结果中看到相同的数据。注意,文档 id 被添加到每个 JSON 文档中。

$ mysqlsh root@localhost:33060 --sql --json=pretty --schema=rolodex -e "SELECT * FROM contacts"
{
    "password": "Enter password: "
}

{
    "executionTime": "0.00 sec",
    "warningCount": 0,
    "warnings": [],
    "rows": [
        {
            "doc": "{\"_id\": \"9801A79DE09382A811E806BFAD2FA2CF\", \"name\": {\"first\": \"Allen\"}, \"phones\": [{\"work\": \"212-555-1212\"}]}",
            "_id": "9801A79DE09382A811E806BFAD2FA2CF"
        },
        {
            "doc": "{\"_id\": \"9801A79DE0938DFD11E806BFAD314DE1\", \"name\": {\"last\": \"Wheelerton\", \"first\": \"Joe\"}, \"notes\": \"Excellent car detailer. Referrals get $20 off next detail!\", \"phones\": [{\"work\": \"212-555-1213\"}, {\"home\": \"212-555-1253\"}], \"address\": {\"zip\": \"90125\", \"city\": \"oxnard\", \"state\": \"ca\", \"street\": \"123 main\"}}",
            "_id": "9801A79DE0938DFD11E806BFAD314DE1"
        }
    ],
    "hasData": true,
    "affectedRowCount": 0,
    "autoIncrementValue": 0
}

Listing 7-14Results of SELECT Statement for Contacts Collection

那很酷,不是吗?当我们探索一个完整的文档存储应用示例时,我们将在第 8 章中看到更多这样的代码。但是首先,让我们看一个使用 X 协议执行 SQL 命令的 Connector/Python 示例。

关系数据示例

现在让我们看一个使用 X 协议的关系数据例子。我们将使用与上一个示例相同的 C/Py 代码,只是这次我们将执行 SQL 语句,而不是处理数据。我选择这个简单的例子是因为,如果不是一开始,最终您的 MySQL 文档存储应用将使用越来越少的 SQL 操作。即便如此,如果您想检查变量、状态或类似的服务器操作,您可能需要不时地执行一条 SQL 语句。

这个示例使用一个会话连接到服务器,并执行 SQL 语句SHOW VARIABLES LIKE,来检索 X 插件的所有系统变量。这与我们在第 6 章中看到的 SQL 语句相同。尽管我们没有访问任何数据,但是从SHOW VARIABLES语句返回的结果集与查询表时返回的结果集是相同的。因此,我们将看到如何处理来自 SQL 命令的结果集,而不需要创建任何示例数据。

和上一个例子一样,我们从导入mysqlx库开始,并提示用户输入登录凭证。请注意,我演示了如何为用户输入使用默认值。接下来,我们用get_session()方法得到一个会话。这将返回一个会话对象实例。然后,我们检查连接是否打开,如果不是(例如,连接失败),我们退出。清单 7-15 显示了这个例子的完整代码。花点时间通读一下,这样你就可以看到到目前为止讨论的所有概念。

#
# Introducing the MySQL 8 Document Store - xclient_sql
#
# This file contains an example of how to read a database (SQL) from a MySQL
# server using the X Protocol via a Session object
#
# Dr. Charles Bell, 2018
#
import getpass
import mysqlx

# Get user information
print("Please enter the connection information.")
user = raw_input("Username: ")
passwd = getpass.getpass("Password: ")
host =  raw_input("Hostname [localhost]: ") or 'localhost'
port = raw_input("Port [33060]: ") or '33060'

# Get a session object since we want to execute SQL statements
mysqlx_session = mysqlx.get_session({'host': host, 'port': port, 'user': user, 'password': passwd})

# Check to see that the session is open. If not, quit.
if not mysqlx_session.is_open():
    exit(1)

# Get an SqlStatements object
sql_stmt = mysqlx_session.sql("SHOW VARIABLES LIKE 'mysqlx_%'")

# Execute and get a SqlResult object
sql_result = sql_stmt.execute()

print("\nVariables for the X Plugin:")
# Print the column labels (names)
for col in sql_result.columns:
    print("{0}\t".format(col.get_column_name())),
print("\n-------------------------------------------")

# Print the rows
for row in sql_result.fetch_all():
    for col in row:
        print("{0}\t".format(col)),
    print("")

# Close the session
mysqlx_session.close()

Listing 7-15
X Client Source Code (SQL)

Tip

如果您使用的是 Python 3.0 或更高版本,您可能需要将raw_input()调用改为input()。这是因为在 Python 的后续版本中不再支持raw_input()

为了执行 SQL 语句,我们需要通过传入我们想要执行的 SQL 语句,向会话请求 SQL statement 对象实例。我们通过调用会话对象实例的 sql()方法来实现这一点。我们可以使用该对象来执行语句,并获得一个结果对象实例作为回报。

接下来,我们可以迭代结果集中的列,打印它们的名称。这说明了如何捕获结果集中的列名。

接下来,我们使用fetch_all()方法获取列表中的所有行,在 for 循环中遍历它们,并打印找到的每一列的值。注意,我们在这里使用“行”和“列”,因为这不是一个被返回的文档——它是一个老式的 SQL 结果集(嗯,通过 X 协议)。最后,我们结束会议。清单 7-16 展示了脚本运行的一个例子。您应该能够将输出等同于源代码中的print()语句。请注意,MySQL 的更高版本可能会有额外的变量,一些默认值可能会有所不同。

$ python ./xclient_sql.py
Please enter the connection information.
Username: root
Password:
Hostname [localhost]:
Port [33060]:

Variables for the X Plugin:
Variable_name    Value
-------------------------------------------
mysqlx_bind_address     *
mysqlx_connect_timeout  30
mysqlx_idle_worker_thread_timeout        60
mysqlx_max_allowed_packet        1048576
mysqlx_max_connections  100
mysqlx_min_worker_threads        2
mysqlx_port      33060
mysqlx_port_open_timeout         0
mysqlx_socket   /tmp/mysqlx.sock
mysqlx_ssl_ca
mysqlx_ssl_capath
mysqlx_ssl_cert
mysqlx_ssl_cipher
mysqlx_ssl_crl
mysqlx_ssl_crlpath
mysqlx_ssl_key

Listing 7-16X Client Results (SQL)

注意这里我们看到了 X 插件的所有系统变量(那些以mysqlx_开头的变量)。我们还可以看到每个系统变量的值。SSL 条目没有任何值,因为示例中使用的连接没有通过安全连接进行连接。

如您所见,即使使用像 Python 这样的语言,也很容易编写利用 X 协议和 X DevAPI 的客户端。当然,这在 Connector/Python 中都是可能的,它实现了 X 协议。有关 X 协议的更多信息,请参见在线 MySQL 内部参考手册 https://dev.mysql.com/doc/internals/en/ 中的“X 协议”部分。有关使用连接器编写客户端的具体信息,请参见位于 https://dev.mysql.com/doc 的单个连接器在线文档。你可以在 https://dev.mysql.com/doc/dev/connector-python/ 找到关于使用 X DevAPI 和 Connector/Python 的信息。

摘要

X 协议是 MySQL 中一个革命性的新特性,它克服了旧的客户机/服务器协议的许多限制。X 协议是为可扩展性而设计的,因此它可以在不影响依赖它的客户端的情况下进行扩展。X 协议也被设计成具有更高的安全性和更好的性能。几十年来,MySQL 客户端第一次可以使用现代、可靠的技术与服务器连接和交互,并有望成为未来更多新功能的催化剂。

在这一章中,我们从创建 X 协议的动机、设计的主要原则或目标以及如何使用 protobuf 作为基础来实现 X 协议开始进行了研究。我们还看到了 X 协议的一些部分如何为简单用例工作的演练。然后我们看了如何在我们的应用中使用 protobuf 在代码中移动数据(消息)(在磁盘上,通过网络等)。),说明了 protobuf 的强大。

我们还通过检查实际 C/Py 源代码的一部分,简短地介绍了 C/Py 如何实现 X 协议。然后,我们在独立的 Python 脚本中使用 C/Py 中的 X 协议抽象层来演示 X 协议是如何工作的——它易于实现,也是本书中到目前为止介绍的技术的一个具体示例。

与 X 插件一样,我们也发现 X 协议不仅仅是一个特性,它是一个精心制作和良好抽象的机制,是 MySQL 未来的基础之一。即使我们知道在使用支持 X 协议的连接器时,我们使用的是 X 协议,X 协议也能正常工作,而且工作得非常好。

在第 8 章中,我提供了一个使用 X DevAPI 编写应用的教程,我们现在知道它是通过 X 插件和 X 协议实现的。该项目将使用 MySQL 文档存储来构建一个基于 Python web 的解决方案,用于存储有关书籍的信息。

Footnotes 1

那么,为什么是 MySQL 8 而不是 MySQL X 呢?

  2

工作日志是一个内部文档,用于捕获在 MySQL 中实现特性的设计和需求。

  3

SSL 的一种演变: https://en.wikipedia.org/wiki/Transport_Layer_Security .

  4

一个认证和数据安全的框架: https://en.wikipedia.org/wiki/Simple_Authentication_and_Security_Layer .

  5

充其量是渐进成功;总有美中不足的地方。

  6

换句话说,把你的椅子放在半倾斜的状态,放上你最喜欢的音乐,确保手边有足够的你最喜欢的饮料。

  7

或者像我有时被指责的那样“没有把事情做好”。有罪。我从小就在拆东西。有时我会把它们放回一起!

  8

在 SQL 术语中使用它。

八、库应用:用户界面

现在我们已经了解了什么是 MySQL 文档存储以及如何通过 MySQL Shell 使用它,我们可以探索一个更复杂的示例,演示所描述的三种形式的数据存储:一个纯关系数据库解决方案,一个混合解决方案,其中我们使用 X DevAPI 的 SQL 特性来使用一个或多个 JSON 字段,以及一个纯文档存储解决方案,它专门使用 X DevAPI(NoSQL 解决方案)。因此,我们将看到应用在三个独立的实现中实现。

但是,我们必须首先了解示例应用是如何设计的,以及它是如何工作的。毕竟,最好的例子应该是读者可以在自己的环境中使用的。因此,这个例子必须足够复杂和完整才有意义。

为了延续前面章节中代码的可理解性,我们将在应用中使用 Python,因为 Python 非常容易学习,代码阅读起来比其他语言更清晰。但是如果你喜欢另一种语言,也不用担心。您可以很容易地将本章中的代码改写成支持 X DevAPI 的连接器的任何语言。

另一方面,用户界面使事情变得有点复杂。我们可以通过使用熟悉的用户界面设计来缓解这一问题。为此,我们将使用一个 web 应用。不幸的是,用纯 Python 编写一个 web 应用是单调乏味的,并且需要更多关于 web 应用如何工作的知识,这超出了人们对这种规模的工作的期望。

为了克服这个挑战,我们将使用一个流行的 Python web 应用框架。在这种情况下,我们将使用 Flask,包括入门、教程和用户界面代码的演练。正如您将看到的,Flask 也很容易学习,只需要学习少量的细微差别和概念。Flask 最初是由阿明·罗纳彻开发的,已经被证明是 Python 最简单、最稳定的 web 平台之一。

在第 9 章中,我们将添加前面描述的数据库访问方法来完成应用。

入门指南

如果您想继续并实现示例项目,您需要在您的计算机上安装一些东西。本节将帮助您为计算机准备所需的工具:您需要安装什么以及如何配置您的环境。我们还将看到一个关于用户界面工具的简短介绍。让我们从更详细的应用描述开始。

库应用

本章中的示例应用是一个相当简单的应用,旨在演示概念。它是完整的,因为它支持对数据的创建、读取、更新和删除(CRUD)操作。错误处理和用户界面组件不太复杂,以便将重点放在与数据的交互上。也就是说,我们将看到如何使用 Flask 在 Python 中实现一个健壮且美观的 web 界面。

应用的数据是一个简单的图书数据库。我们将存储书籍的基本信息,如 ISBN、书名、出版商等等。我们还会有一个笔记区,这样我们可以在书上做笔记。我在我的许多研究论文甚至一些更高级的项目中使用了类似的东西。操作的概念是记录每本书的书目信息以及关于内容的注释,以便以后可以使用它来创建参考文献列表。例如,如果一本书包含了与论文中某个主题相关的信息,我会添加一个注释,指明主题,列出页码和其他重要信息。笔记中的信息因我记录的内容而异,所以只需要在一个简单的文本字段中进行搜索。

与我用于研究的允许存储书籍、杂志、文章、博客等信息的应用不同,这一章的应用被简化为只存储书籍。这使得项目足够小,可以在没有不必要的细节的情况下进行讨论。本章的重点是研究迁移到文档存储的好处,而不是如何最好地实现媒体参考应用。

因此,基本操作将是存储和检索关于书籍、作者和出版商的信息。用户界面被设计为呈现数据库中所有书籍的列表,并具有编辑列表中任何书籍的选项。默认视图是 books,但是该应用的第一个版本(1 和 2)将允许您查看作者和出版商的列表。用户还可以创建新书(作者和出版商),编辑和删除书籍。

当我们看到改变数据存储和检索方式对应用设计的影响时,应用的每个版本的行为都会稍有不同。每个项目的更详细的解释包含在后面讨论项目版本的章节中。

现在,让我们看看如何设置我们的计算机来运行示例应用项目。

设置您的环境

对你的环境的改变并不困难,也不漫长。我们将安装 Flask 和一些扩展,这是应用用户界面所需要的。Flask 是可以与 Python 一起使用的几个 web 库之一。这些 web 库使得用 Python 开发 web 应用比使用原始的 HTML 代码并为请求编写自己的处理程序和代码要容易得多。另外,Flask 并不难学。

我们需要安装的库如表 8-1 所示。该表列出了库/扩展的名称、简短描述以及产品文档的 URL。

表 8-1

List of Libraries Required

| 库 | 描述 | 文件 | | :-- | :-- | :-- | | 瓶 | Python Web API | [`http://flask.pocoo.org/docs/0.12/installation/`](http://flask.pocoo.org/docs/0.12/installation/) | | 烧瓶脚本 | Flask 的脚本支持 | [`https://flask-script.readthedocs.io/en/latest/`](https://flask-script.readthedocs.io/en/latest/) | | 烧瓶自举 | 用户界面的改进和增强 | [`https://pythonhosted.org/Flask-Bootstrap/`](https://pythonhosted.org/Flask-Bootstrap/) | | 烧瓶-WTF | WTForms 集成 | [`https://flask-wtf.readthedocs.io/en/latest/`](https://flask-wtf.readthedocs.io/en/latest/) | | WTForms | 表单验证和呈现 | [`https://wtforms.readthedocs.io/en/latest/`](https://wtforms.readthedocs.io/en/latest/) |

Note

根据您的系统配置,您可能会看到为本节安装的组件安装了更多或更少的组件。

当然,您应该已经在系统上安装了 Python。如果没有,请务必下载并安装最新版本的 2。x 或 3。x 版。本章中的示例代码是用 Python 2.7.10 和 Python 3.6.0 测试的。

要安装这些库,我们可以使用 Python 包管理器pip,从命令行安装这些库。大多数 Python 发行版中都包含了pip实用程序,但是如果您需要安装它,可以在 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/ 在内的几个,都是最准确的。

Note

如果您的系统上安装了多个版本的 Python,那么pip命令将安装到默认的 Python 版本环境中。要使用pip安装到特定版本,请使用pipN,其中N是版本。例如,pip3在 Python 3 环境中安装包。

pip命令非常方便,因为它使得安装注册的 Python 包——那些在 Python 包索引中注册的包,缩写为 PyPI1(https://pypi.python.org/pypi)——非常容易。pip命令将使用一个命令下载、解压和安装。让我们来看看如何安装我们需要的每个包。

Caution

一些系统可能需要使用提升的权限运行 pip,例如sudo (Linux、macOS),或者在命令窗口中以管理员用户身份运行(Windows 10)。如果安装由于权限问题而无法复制文件,您将知道是否需要提升权限。

安装烧瓶

清单 8-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 8-1Installing Flask

安装烧瓶-脚本

清单 8-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 8-2Installing Flask-Script

安装烧瓶-引导程序

清单 8-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 8-3Installing Flask-Bootstrap

安装烧瓶-WTF

清单 8-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 8-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

Using Python Virtual Environments

使用 Python 的一个好处是,你可以使用虚拟环境来尝试一些东西。虚拟环境是 Python 的本地(认为是私有的)安装,您可以安装软件包并对 Python 环境进行更改,而不会影响系统上的全局 Python 安装。因此,例如,如果您使用虚拟环境安装 Flask,它只对该虚拟环境可用,不会影响任何其他虚拟环境或全局 Python 安装。

要使用虚拟环境,您必须安装virtualenv应用。不是所有的系统都有这个功能,事实上也不是所有的平台都支持这个功能(但是很多平台都支持)。要在 Linux 上安装虚拟环境,可以使用命令sudo apt-get install python-virtualenv。要在 macOS 上安装虚拟环境,请使用命令sudo easy_install virtualenv。要在 Windows 10 上安装虚拟环境,必须从 https://github.com/pypa/setuptools 下载ez_setup.py(setuptools的一部分)。下载完成后,使用管理权限打开命令窗口,然后输入命令python ez_setup.py安装easy_install,然后输入命令easy_install virtualenv安装虚拟环境。

要创建和使用虚拟环境,发出命令virtualenv project1。这会创建一个名为project1的文件夹,其中包含虚拟环境文件,这些文件会跟踪在该环境中所做的所有更改。要激活环境,使用source命令。请注意,我们正在新的虚拟环境文件夹中调用一个脚本。这将改变您的提示,以表明您正在使用虚拟环境。要停用环境,在虚拟环境激活时使用deactivate命令。这将使您的 Python 环境返回到全局默认值。下面演示了 macOS 上的这些命令。

$ mkdir virtual_environments
$ cd virtual_environments
$ virtualenv project1
New python executable in /virtual_environments/project1/bin/python
Installing setuptools, pip, wheel...done.
$ source ./project1/bin/activate
[Do something Python related here. Changes apply only to the active virtual environment.]
(project1) $ deactivate

删除虚拟环境只需删除环境文件夹(在停用它之后):

$ deactivate
$ rm -r /virtual_environments/project1

有些人建议在尝试 Python 中的新事物时总是使用虚拟环境,对于一些东西,比如不受信任或未经尝试的库或者与现有安装的库冲突的库,这是一个好的实践。然而,对于主流项目,如 Flask 及其支持库,并不需要它。如果你想在进行的项目中使用虚拟环境,请随意。只需记住在发出任何 Python 命令之前激活它,并在完成后停用它。

要了解更多关于虚拟环境的信息,请参见 https://virtualenv.pypa.io/en/stable/

您还应该安装 MySQL 连接器/Python 8.0.5 或更高版本的数据库连接器。如果没有,从 https://dev.mysql.com/downloads/connector/python/ 下载并安装。如果您安装了多个版本的 Python,请确保将其安装在您想要使用的所有 Python 环境中。否则,在启动代码时,您可能会看到如下错误。

$ python3 ./mylibrary_v1.py runserver -p 5001
Traceback (most recent call last):
  File "./mylibrary_v1.py", line 18, in <module>
    from database.library_v1 import Library, Author, Publisher, Book
  File ".../Ch08/version1/database/library_v1.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.6-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.6

如果您手动或从源代码安装了 MySQL Connector/Python,您可能还需要安装 Protobuf。你可以使用pip来安装它,如下图所示。

$ pip3 install protobuf
Collecting protobuf
  Downloading protobuf-3.5.1-py2.py3-none-any.whl (388kB)
    100% |████████████████████████████████| 389kB 414kB/s
Requirement already satisfied: setuptools in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from protobuf)
Requirement already satisfied: six>=1.9 in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/six-1.10.0-py3.6.egg (from protobuf)
Installing collected packages: protobuf
Successfully installed protobuf-3.5.1

现在我们的计算机已经安装好了,让我们上一堂关于 Flask 及其相关扩展的速成课。

弗拉斯克第一

Flask 是与 Python 一起使用的几个 web 应用库(有时称为框架或应用编程接口——API)之一。Flask 在众多选择中是独一无二的,因为它很小,一旦你熟悉了它的工作原理,就很容易使用。也就是说,一旦编写了初始化代码,使用 Flask 的大部分工作将局限于创建网页、重定向响应和编写功能代码。

Flask 被认为是一个微型框架,因为它体积小、重量轻,而且它不会强迫你进入一个专门编写代码来与框架交互的盒子中。它提供了您需要的一切,让您自己选择在代码中使用什么。

Flask 由提供基本功能的两个主要组件组成:一个 Web 服务器网关接口(WSGI ),处理托管网页的所有工作;以及一个模板库,用于简化 web 页面开发,减少了学习 HTML 的需要,删除了重复的结构,并为 HTML 代码提供了脚本功能。WSGI 组件被命名为 Werkzeug,它是从德语中大致翻译过来的意思,“工作的东西”( http://werkzeug.pocoo.org/ )。模板组件被命名为 Jinja2,并模仿 Django ( http://jinja.pocoo.org/docs/2.10/ )。两者都是由 Flask 的创始人开发和维护的。最后,当您安装 Flask 时,这两个组件都会被安装。

Flask 也是一个可扩展的库,允许其他开发者创建基本库的附件(扩展)来添加功能。在上一节中,我们看到了如何安装 Flask 可用的一些扩展。我们将在本章中使用脚本、引导和 WTForms 扩展。能够挑选您想要的扩展意味着您可以保持您的应用尽可能小,只添加您需要的。

您可能认为 flask“缺少”的组件之一是与其他服务(如数据库系统)交互的能力。这是一个有目的的设计,像这样的功能可以通过扩展来实现。事实上,Flask 有几个可用的数据库扩展,包括那些允许您使用 MySQL 的扩展。但是,因为我们想要使用 X DevAPI,所以我们必须使用 Oracle 提供的连接器 MySQL Connector/Python。这不仅是可能的,也说明了你在使用 Flask 时的自由度;我们不局限于数据库服务器访问这样的特定功能,我们可以使用我们想要或需要的任何其他 Python 库。 2

Tip

如果你对 MySQL 对 Flask 的支持很好奇,请看 http://flask-mysql.readthedocs.io/en/latest/

Flask 和前面描述的扩展一起,提供了用 Python 制作 Web 应用所需的所有连接和管道。它消除了编写 web 应用所需的几乎所有负担,例如解释客户机响应包、路由、HTML 表单处理等等。如果您曾经用 Python 编写过 web 应用,您将会体会到创建健壮的 web 页面的能力,而无需编写 HTML 和样式表的复杂性。一旦你熟悉如何使用 Flask,它将允许你专注于你的应用的代码,而不是花大量的时间编写用户界面。

现在,让我们开始学习 Flask!如果您不着急,尝试一下示例应用,您的第一个 Flask 应用将在第一次尝试时就能工作。学习 Flask 最难的部分已经过去了——安装 Flask 及其扩展。剩下的就是学习在 Flask 中编写应用的概念。在此之前,让我们了解一下 Flask 中的术语,以及如何设置我们将用来初始化本章中使用的应用实例的基本代码。

Tip

如果你想进一步了解 Flask,你应该考虑阅读在线文档、用户指南和 http://flask.pocoo.org/docs/0.12/ 的例子。

术语

Flask 旨在简化编写 web 应用的繁琐过程。按照 Flask 的说法,使用代码的两个部分来呈现一个 web 页面:一个视图,它是在 HTML 文件中定义的;一个路由,它处理来自客户端的请求。回想一下,我们可以看到两个请求中的一个:一个是请求加载网页的GET请求(从客户端的角度读取),另一个是从客户端通过网页向服务器发送数据的POST请求(从客户端的角度写入)。这两个请求都在 Flask 中使用您定义的函数进行处理。

然后,这些函数呈现网页,并将其发送回客户端以满足请求。Flask 调用函数视图函数(或简称视图)。Flask 知道调用哪个方法的方式是使用识别 URL 路径(在 Flask 中称为路由)的装饰器。你可以用一条或多条路线来装饰一个功能,这样就可以提供多种到达视图的方式。用的装饰师是@app.route(<path>)。以下显示了查看功能的多条路线的示例。

@app.route('/book', methods=['GET', 'POST'])
@app.route('/book/<string:isbn_selected>', methods=['GET', 'POST'])
def book(isbn_selected=None):
    notes = None
    form = BookForm()
    form.publisher.choices = []
    form.authors.choices = []
    new_note = ""
    if request.method == 'POST':
        pass
    return render_template("book.html", form=form, notes=notes)

注意这里有多个装饰者。第一个是 book,它允许我们使用一个 URL,比如localhost:5000/book,这使得 Flask 将执行路由到book()函数。第二个是book/<isbn_selected>,演示了如何使用变量向视图传递信息。在这种情况下,如果用户(应用)使用 URL localhost:5000/book/978-1-4842-1294-3,Flask 将值978-1-4842-1294-3放在isbn_selected变量中。这样,我们可以动态地将信息传递给我们的视图。

还要注意,路由指定了每条路由允许的方法。在这个应用中,我们可以为任何一个路由设置一个GETPOST。如果你离开装饰器,默认是GET只让网页只读。

最后,请注意,在函数的末尾,我们返回了对render_template()函数的调用(从 flask 模块导入),该函数告诉 flask 返回(刷新)带有我们获取或分配的数据的网页。网页,book.html,虽然视图的一部分在 Flask 中被称为表单。我们将使用这个概念从数据库中检索信息并将其发送给用户。我们可以返回一个简单的 HTML 字符串(或整个文件)或所谓的表单。因为我们使用 Flask-WTF 和 WTForms 扩展,所以我们可以返回一个呈现为表单类的模板。我们将在后面的章节中讨论表单、表单类以及章节项目的其他路径和视图。正如您将看到的,模板是另一个强大的功能,它使创建网页变得很容易。

What’s a Decorator?

在 Python 中,我们可以通过使用 decorators 来指定特殊的处理参数。装饰器只是改变函数行为的一种方式。例如,您可以使用 decorators 来添加更强的类型检查、定义宏以及在执行前后调用函数。Flask for routing 中的 decorator 是正确使用 decorator 的一些最好的例子。要了解更多关于装饰者的信息,请参见 https://www.python.org/dev/peps/pep-0318

Flask 构建了一个应用中所有路径的列表,使得应用在被请求时可以很容易地将执行路由到正确的函数。但是,如果请求了一条路由,但它不在应用中,会发生什么情况呢?默认情况下,您将得到一个类似于“Not Found. The requested URL was not found on the server.”的一般错误消息。我们将在后面的小节中看到如何添加我们自己的自定义错误处理路线。

现在我们已经了解了 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 ./flask-ex.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 提供的两个新参数;Shell 和runserver。启动服务器时,您必须选择其中一个命令。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 的上下文中讨论它。

Note

Flask-WTF 的一些包装装置可以包括 WTForms。

烧瓶-WTF

Flask-WTF 扩展是一个有趣的组件,提供了几个非常有用的附加功能:最值得注意的是与 WTForms(一个框架不可知组件)的集成,它允许创建表单类,并以跨站点请求伪造(CSRF)保护的形式提供了额外的 web 安全性。这两个特性允许您将 web 应用提升到更高的复杂程度。

表单类

表单类提供了一个类的层次结构,使得定义网页更加符合逻辑。使用 Flask-WTF,您可以使用两段代码定义表单;一个从 FormForm 类(从 Flask framework 导入)派生的特殊类,用于使用一个或多个提供对数据的编程访问的附加类以及一个用于呈现网页的 HTML 文件(或模板)来定义字段。这样,我们在 HTML 文件上看到了一个抽象层(表单类)。我们将在下一节看到更多关于 HTML 文件的内容。

使用 form 类,您可以定义一个或多个字段,例如 TextField 表示文本,StringField 表示字符串,等等。更好的是,您可以定义允许您以编程方式描述数据的验证器。例如,您可以为文本字段定义最小和最大字符数。如果提交的字符数超出范围,将生成一条错误消息。是的,你可以定义一个错误信息!下面列出了一些可用的验证器。查看 http://wtforms.readthedocs.io/en/latest/validators.html 查看验证器的完整列表。

  • DataRequired:确定输入栏是否为空
  • Email:确保该字段遵循电子邮件 ID 约定
  • IPAddress:验证 IP 地址
  • Length:确保文本长度在给定范围内
  • NumberRange:确保文本是数字,并且在给定的范围内
  • URL:验证 URL

为了形成类,我们必须导入类和任何我们想在应用的序言中使用的字段类。下面显示了一个导入表单类和表单域类的示例。在这个例子中,我们还导入了一些验证器,用于自动验证数据。

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 类的所有必要代码。

让我们看一个简单的例子。下面显示了作者网页的 form 类。作者表包含三个字段,我们将通过 view 函数将它链接到这段代码;自动递增字段(authorid)、作者的名(firstname)和作者的姓(lastname)。因为用户不需要看到 author id 字段,所以我们将该字段设置为隐藏字段,其他字段是TextField()类的派生字段。请注意这些是如何在清单中用名称(标签)作为第一个参数来定义的。

class AuthorForm(FlaskForm):
    authorid = HiddenField('AuthorId')
    firstname = TextField('First name', validators=[
            Required(message=REQUIRED.format("Firstname")),
            Length(min=1, max=64, message=RANGE.format("Firstname", 1, 64))
        ])
    lastname = TextField( 'Last name', validators=[
            Required(message=REQUIRED.format("Lastname")),
            Length(min=1, max=64, message=RANGE.format("Lastname", 1, 64))
        ])
    create_button = SubmitField('Add')
    del_button = SubmitField('Delete')

还要注意,我们以从 WTForms 组件为字段导入的函数调用的形式定义了一个验证器数组。在每种情况下,我们都为消息使用字符串,以使代码更容易阅读,更统一。这些字符串包括以下内容。

REQUIRED = "{0} field is required."
RANGE = "{0} range is {1} to {2} characters."

我们使用Required()验证器来指示字段必须有一个值。我们用字段的名称增加了默认的错误消息,使用户更容易理解。我们还使用了一个Length()验证函数,它定义了字段数据的最小和最大长度。我们再次增加了默认的错误消息。验证器只应用于POST操作(当提交事件发生时)。

接下来,我们看到有两个SubmitField()实例:一个用于创建(添加)按钮,另一个用于删除按钮。正如您可能猜测的那样,按照 HTML 的说法,这些字段被呈现为类型为“submit”的<input>字段。

最后,为了使用一个表单类,我们在一个视图函数中实例化这个类。下面显示了作者视图函数的存根。注意,我们实例化了名为AuthorForm()的表单类,并将其分配给名为 form 的变量,该变量被传递给render_template()函数。

@app.route('/author', methods=['GET', 'POST'])
@app.route('/author/<int:author_id>', methods=['GET', 'POST'])
def author(author_id=None):
    form = AuthorForm()
    if request.method == 'POST':
        pass
    return render_template("author.html", form=form)

有几个字段类可供使用。表 8-2 显示了最常用的字段类(也称为 HTML 字段)的示例。您还可以从这些字段派生来创建自定义字段类,并为可以显示在字段旁边的标签提供文本(例如,作为按钮文本)。我们将在后面的章节中看到一个这样的例子。

表 8-2

WTForms Field Classes

| 字段类 | 描述 | | :-- | :-- | | `BooleanField` | 具有真值和假值的复选框 | | `DateField` | 接受日期值 | | `DateTimeField` | 接受日期时间值 | | `DecimalField` | 接受十进制值 | | `FileField` | 文件上传字段 | | `FloatField` | 接受浮点值 | | `HiddenField` | 隐藏文本字段 | | `IntegerField` | 接受整数值 | | `PasswordField` | 密码(屏蔽)文本字段 | | `RadioField` | 单选按钮列表 | | `SelectField` | 下拉列表(选择一个) | | `SelectMultipleField` | 选项下拉列表(选择一项或多项) | | `StringField` | 接受简单文本 | | `SubmitField` | 表单提交按钮 | | `TextAreaField` | 多行文本字段 |
CSRF 保护

CSRF 保护是一种允许开发者用加密密钥签署网页的技术,这使得黑客更难欺骗GETPOST请求。这是通过首先在应用代码中放置一个特殊的键,然后在每个 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 应用。你如何向用户传达错误?你是否重定向到一个新页面,【3】弹出, 4 或者在页面上显示错误?Flask 有一个解决方案,叫做消息闪烁。

消息闪烁是使用 Flask 框架中的 flash()方法完成的。我们只需将它导入到代码的序言中,然后当我们想要显示一条消息时,我们调用 flash()函数传入我们想要看到的错误消息。Flask 将在表单顶部的一个格式良好的框中显示错误。它没有取代表单,也不是弹出窗口,但是它允许用户关闭消息。您可以使用 flash messaging 向用户传达错误、警告甚至状态更改。图 8-1 显示了一个闪光信息的例子。在本例中,我们看到两条 flash 消息,表明您可以同时显示多条消息。请注意用于消除图像的消息右侧的小 X。

A432285_1_En_8_Fig1_HTML.jpg

图 8-1

Example flash message

在下一节中,我们将看到一种将 flash 消息传递构建到所有网页中的机制。

HTML 文件和模板

让我们回顾一下到目前为止的旅程。我们发现了如何用各种组件初始化应用,了解了 Flask 如何通过 decorators 使用路由来为应用创建一组 URL,这些路由被定向到一个视图函数,该函数实例化了 form 类。下一个难题是如何将 HTML 网页链接到 form 类。

回想一下,这是通过render_template()函数完成的,在这里我们传入一个 HTML 文件的名称进行处理。template 出现在名称中的原因是因为我们可以使用 Jinja2 模板组件来简化 web 页面的编写。更具体地说,HTML 文件包含 HTML 标记和 Jinja2 模板构造。

Note

所有 HTML 文件(模板)必须存储在与主应用代码相同的位置的templates文件夹中。例如,如果你的代码在一个名为my-flask-app.py的文件中,那么在与my-flask-app.py相同的文件夹中应该有一个templates文件夹。如果你把它们放在其他地方,Flask 将找不到 HTML 文件。

模板和表单类是设计用户界面的地方。简而言之,模板用于包含表示逻辑,HTML 文件用于包含表示数据。这些主题可能是一些人需要花一些时间来尝试如何使用它们的领域。下面几节将通过工作示例的演示,向您简要介绍 Jinja2 模板以及如何在我们的 HTML 文件中使用它们。有关更多详细信息,请参见在线烧瓶文档。

Jinja2 模板概述

Jinja2 模板(或称模板)用于包含任何表示逻辑,如遍历数据数组、决定显示什么,甚至格式化和表示设置。如果您熟悉其他 web 开发环境,您可能已经看到过这种封装在脚本中或通过嵌入式脚本(如 JavaScript)实现的功能。

回想一下,我们在主代码中呈现了网页。这个函数告诉 Flask 读取指定的文件,并将模板结构转换(渲染)成 HTML。也就是说,Flask 会将模板结构扩展并编译成 web 服务器可以呈现给客户机的 HTML。

有几种模板结构可以用来控制执行流、循环甚至注释。每当你想使用一个模板结构(想想脚本语言),你用前缀和后缀{% %}把它括起来。这使得 Flask 框架能够将该构造识别为模板操作,而不是 HTML。

然而,看到模板结构与 HTML 标记混杂在一起并不罕见,也很正常。事实上,这正是你应该做的。毕竟,您将创建的文件被命名为. html。它们只是碰巧包含模板构造。这是否意味着在使用 Flask 时只能使用模板?不,当然不是。如果你愿意,你可以渲染一个纯 HTML 文件!

起初,查看模板可能会令人望而生畏。但也没那么难。只需查看所有将{%%}作为“代码”部分的行。 5 你也可以看到以{# #}前缀和后缀形式出现的评论。

Caution

所有模板构造都要求在{%之后和%}之前有一个空格。

如果你看看模板,你会看到构造和标签,并使用两个空格缩进格式化。在标签和构造之外,缩进和空白通常无关紧要。然而,大多数开发者会使用某种形式的缩进来使文件更容易阅读。事实上,大多数编码指南都要求缩进。

模板除了构造(想想代码)之外的一个很酷的特性是创建模板层次结构的能力。这允许您创建一个其他模板可以使用的“基础”模板。例如,您可以创建一个模板构造和 HTML 标记的样板文件,这样您的所有网页看起来都一样。

回想一下 Flask-Bootstrap,Bootstrap 提供了几个很好的格式化特性。其中一个功能是创建一个外观漂亮的导航栏。很自然,我们希望它出现在我们所有的网页上。我们可以通过在基本模板中定义它并在我们的其他模板(HTML)文件中扩展它来做到这一点。让我们看一下库应用的基本模板。清单 8-5 显示了库应用的基本模板。为了便于讨论,添加了行号。

01 {% extends "bootstrap/base.html" %}
02 {% block title %}MyLibrary{% endblock %}
03 {% block navbar %}
04 <div class="navbar navbar-inverse" role="navigation">
05     <div class="container">
06         <div class="navbar-header">
07             <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
08                 <span class="sr-only">Toggle navigation</span>
09                 <span class="icon-bar"></span>
10                 <span class="icon-bar"></span>
11                 <span class="icon-bar"></span>
12             </button>
13             <a class="navbar-brand" href="/">MyLibrary Base</a>
14         </div>
15         <div class="navbar-collapse collapse">
16             <ul class="nav navbar-nav">
17                 <li><a href="/list/book">Books</a></li>
18             </ul>
19             <ul class="nav navbar-nav">
20                 <li><a href="/list/author">Authors</a></li>
21             </ul>
22             <ul class="nav navbar-nav">
23                 <li><a href="/list/publisher">Publishers</a></li>
24             </ul>
25         </div>
26     </div>
27 </div>
28 {% endblock %}
29
30 {% block content %}
31 <div class="container">
32     {% for message in get_flashed_messages() %}
33     <div class="alert alert-warning">
34         <button type="button" class="close" data-dismiss="alert">&times;</button>
35         {{ message }}
36     </div>
37     {% endfor %}
38     {% block page_content %}{% endblock %}
39 </div>
40 {% endblock %}
Listing 8-5Sample Base Template

哇,这里发生了很多事情!注意第一行。这告诉我们,我们正在继承(扩展)另一个名为bootstrap/base.html template 的模板。这是在你安装 Flask-Bootstrap 时免费提供给你的,正是这个模板包含了对 Bootstrap 导航栏特性的支持。这是为 Flask 应用构建一组 HTML 文件的一种非常常见的方法,我们将在本节的后面看到。

让我们从鸟瞰图开始我们的旅行。请注意,有两个“块”用{ % block <> %}{% endblock %}表示(第 2、3、28、30、38 和 40 行)。这些是逻辑部分,我们可以在其中对块内的标记和构造应用格式。用编码术语来说,这就像一个代码块。第一个块定义页面的标题。在本例中是 MyLibrary,这是库应用的可执行名称。

第二块定义了应用的导航栏(思考菜单)。请注意,第 5–27 行定义了简单的 HTML <div>标签,这些标签构成了导航栏上的项目。值得注意的是第 13 行,它指定了用作应用名称的文本,该文本出现在导航栏的左侧,类似于“home”链接。第 15–24 行定义了三个网页(表单)的导航栏项目(提交按钮)。另请注意 collapse 关键字。这表明可以折叠导航栏。那么,导航栏是什么样子的呢?图 8-2 显示了正常、折叠和展开模式下的库应用的导航栏。当无法显示导航项目标签时,正常和折叠模式基于折叠的浏览器窗口的大小进行操作。当用户在折叠模式下点击右边的按钮时,展开模式开始工作。酷吧。

A432285_1_En_8_Fig2_HTML.jpg

图 8-2

Bootstrap navigation bar demonstration

第 30–39 行的最后一个块定义了模板结构和 flash 消息的 HTML 标签。让我们更深入地看看这段代码(为了方便起见,这里重复了一遍)。

30 {% block content %}
31 <div class="container">
32     {% for message in get_flashed_messages() %}
33     <div class="alert alert-warning">
34         <button type="button" class="close" data-dismiss="alert">&times;</button>
35         {{ message }}
36     </div>
37     {% endfor %}
38     {% block page_content %}{% endblock %}
39 </div>
40 {% endblock %}

这里,我们看到另一个包含按钮的标签。这是我们用来关闭简讯的按钮。注意,这个标签被放在一个用{% for ... %}指定的 for 循环中,并以{% endfor %}结束。在本例中,我们循环从get_flashed_messages()函数返回的消息,这些消息是由应用代码中的flash()函数收集的。这告诉我们几件事:我们可以在我们的模板中使用循环,模板允许显示多个图像(我们前面看到过),模板可以调用函数!这是模板威力的一个例子。

Note

模板不需要以任何方式格式化。也就是说,空白在 HTML 标记或模板构造之外不做任何事情。

最后,注意我们在第 32 行的 for 循环中定义的变量。这个变量 message 被定义在它所在的块的本地(在本例中是 for 循环),并且可以在任何时候通过将它包含在{{ }}中来引用。例如,我们在第 35 行看到,我们在<div>标签中使用了{{ message }},这意味着这个文本将出现在客户机上,由 Flask 就地呈现。当我们讨论如何用模板构建用户界面时,变量的使用将变得更加重要。

模板语言结构

Jinja2 模板有很多特性,对所有特性的完整讨论超出了本书的范围。然而,快速参考 Jinja2 的主要结构是很方便的。下面给出了一些常用的构造,包括我们在上一节中发现的一些(为了完整性)。每一个都有一个简短的例子,说明这个构造在模板中是如何出现的。在本章后面探索库应用或编写自己的 Flask 应用时,请随意参考本节。

评论

您可以在模板中嵌入自己的注释。您可能希望这样做,以确保您充分解释您正在做什么,并作为一个提醒,以防您以后重用代码。 6 下面是一个在模板中使用注释的例子。回想一下,注释以{#开始,以#}结束,可以跨多行。

{# This is a line comment written by Dr. Charles Bell on Dec. 12, 2017\. #}

{#
  Introducing the MySQL 8 Document Store

  This template defines the base template used for all of the HTML forms and
  responses in the MyLibrary application. It also defines the menu for the
  basic operations.

  Dr. Charles Bell, 2017
#}

包括

如果您的模板文件增长了,并且您发现有些部分是可重用的,比如一个<div>标签,您可以将标签和模板构造保存在一个单独的文件中,并使用{% include %}构造将它包含在其他模板中。{% include %}构造将您想要包含的文件的名称作为参数。与模板一样,它们必须位于 templates 文件夹中。这样,我们避免了重复和维护重复代码的麻烦和容易出错的任务。

{# Include the utilities common tags for a list. #}
{% include 'utilities.html' %}

宏指令

减少重复代码的另一种形式是创建一个在模板中使用的宏(想想函数)。在这种情况下,我们使用{% macro … %}{% endmacro %}构造来定义一个宏,稍后我们可以在代码中调用(使用)它。下面显示了一个定义简单宏并在循环中使用它的示例。请注意我们是如何将变量传递给宏来操作数据的。

{# Macro definition #}
{% macro bold_me(data) %}
    <b>{{ data }}</b>
{% endmacro %}

{# Invoke the macro #}
{% for value in data %}
    {{ bold_me(value) }}
{% endfor %}

导入

使用宏的最好方法之一是将它们放在一个单独的代码文件中,从而进一步增强可重用性。为了使用单独文件中的宏,我们使用{% import … %}构造来提供要导入的文件的名称。下面显示了一个在单独的文件中导入先前定义的宏的示例。与 include 一样,该文件必须位于 templates 文件夹中。注意,我们可以使用别名,并使用点符号来引用宏。

{% import 'utilities.html' as utils %}
...
{{ utils.bold_me(value) }}

扩展(继承)

我们可以通过继承(扩展)模板来使用模板的层次结构。我们在前面检查基本模板时看到了这一点。在这种情况下,我们使用{% extend … %}构造来提供我们想要扩展的模板的名称。下面显示了先前基本模板中的一个示例。

{% extends "base.html" %}

阻碍

块用于隔离执行和范围(对于变量)。每当我们想要隔离一组模板构造时,我们就使用块(想象一个代码块)。{% block … %}构造与{% endblock %}构造一起用于定义块。这些构件允许您命名块。下面显示了一个示例。

{% block if_true %}
...
{% endblock if_true %}

循环是多次执行同一个块的一种方式。我们用{% for <variable> in <data_array> %}构造来做这件事。在这种情况下,循环将迭代数组,用数组的每个索引中的值替换<variable>中的值。这种结构非常适合遍历数组来创建表格、显示数据列表以及类似的演示活动。下面显示了在构造表时使用的 for 循环。注意,我们使用了两个 for 循环:一个循环遍历名为 columns 的数组中的列,另一个循环遍历名为 rows 的数组中的行。

<table border="1" cellpadding="1" cellspacing="1">
  <tr>
    <td style="width:80px"><b>Action</b></td>
    {% for col in columns %}
      {{ col|safe }}
    {% endfor %}
  </tr>
  {% for row in rows %}
    <tr>
      <td><a href="{{ '/%s/%s'%(kind,row[0]) }}">Modify</a></td>
      {% for col in row[1:] %}
        <td> {{ col }} </td>
      {% endfor %}
    </tr>
  {% endfor %}
</table>

此时,您可能想知道列和行中的数据是如何到达模板的。调用render_template(功能。如果想要将数据传递给模板,只需在呈现模板时将数据列在参数中。在这种情况下,我们将按如下方式传递列和行。在这种情况下,row_datacol_data是在视图函数中定义的变量,并通过赋值传递给模板中的rowscolumns变量。酷吧。

render_template("list.html", form=form, rows=row_data, columns=col_data)

条件式

条件或“if”语句(在 Jinja2 文档中称为测试)允许您在模板中做出决定。我们使用{% if <condition> %}构造,它以{% endif %}构造结束。如果你想要一个“else ”,你可以使用{% else %}结构。更进一步,你可以用{% elif来连锁条件<condition> %}。您通常在条件中使用变量或表单元素,并且可以使用通用比较器(有关测试列表,请参见 http://jinja.pocoo.org/docs/2.10/templates/#builtin-tests )。

例如,您可能希望根据某个事件更改提交字段的标签。您可能希望定义一个提交按钮来添加或更新数据。也就是说,当网页用于添加新的数据项时,文本应该显示为“add ”,但是当您使用相同的网页更新数据时,我们希望文本显示为“update”。这是为GETPOST请求(读和写)重用模板的关键之一。下面显示了以这种方式使用的条件的一个示例。

{% if form.create_button.label.text == "Update" %}
  {{ form.new_note.label }}
  {{ form.new_note(rows='2',cols='100') }}
{% endif %}

{% if form.del_button %}
  {{ form.del_button }}
{% endif %}

在这个例子中有两个条件。第一个示例演示如何检查窗体上标签的文本。注意,这里我们用form.create_button引用表单上的元素,这是我们在表单类中定义的字段类的名称,它在呈现模板之前被实例化(我们将在后面的部分中看到如何做到这一点)。表单变量在render_template("book.html", form=form)调用中被传递给模板。在这种情况下,如果按钮文本被设置为“更新”,我们只显示new_note字段及其标签

第二个例子显示了一个简单的测试,如果表单上的delete_button是活动的(没有隐藏或删除),我们就显示它。这是一个如何显示可选提交字段的例子。

变量和变量过滤器

变量是保存数据值供以后处理的一种方式。变量最常见的用途是引用从视图函数传递到模板的数据(通过render_template()函数)。我们还可以在模板中使用变量来保存数据,比如计数器、循环数据值等等。回想一下,我们通过用花括号{{ variable }}将变量括起来来引用它,或者在 for 循环的情况下,在 for 循环结构中定义它。请注意,当在 HTML 标记中引用时,构造中的空格将被忽略。

您还可以在模板中使用过滤器来更改变量中的值。变量筛选器是一种以编程方式更改值的方法,以便在表示逻辑中使用。您可以更改大小写,删除空白,甚至去掉 HTML 标签或直接使用原始文本。在最后一种情况下,我们使用安全过滤器,它告诉模板使用文本,即使它有 HTML 标签。这有点棘手,因为它可能会为攻击打开方便之门,但是如果您使用 WTForms 的特殊安全特性(见下一节),通常可以这样做,但是要谨慎。表 8-3 显示了常用的可变滤波器。

表 8-3

Variable Filters

| 过滤器 | 描述 | | :-- | :-- | | `Capitalize` | 将文本的第一个字符转换为大写 | | `Lower` | 将文本转换为小写字符 | | `Safe` | 呈现文本,不转义特殊字符 | | `Striptags` | 从文本中删除 HTML 标签 | | `Title` | 将字符串中的每个单词大写 | | `Trim` | 删除前导和尾随空白 | | `Upper` | 将文本转换为大写 |

Tip

要更深入地了解 Jinja2 模板构造,请参见 http://jinja.pocoo.org/

现在我们已经对模板的工作原理有了一个大致的了解,并且已经为库应用定义了一个基本模板,让我们看看如何使用这个基本模板来为我们的 web 页面形成 HTML 文件。正如您将看到的,它涉及到我们一直在讨论的三个概念,并将把讨论引向 Flask 在构建网页并将它们发送给客户端时如何工作的结论。我们将在后面的小节中研究如何从客户端获取数据。

使用模板的 HTML 文件

现在,我们准备看看如何体现我们在表单类中定义的字段类。让我们从如何在库应用中显示发布者数据的演练开始讨论。我们从定义给视图函数的表单类和字段类开始,视图函数呈现模板,最后是模板本身。

回想一下,表单类是我们定义一个或多个表单字段的地方。我们将使用这些字段类实例来访问视图函数和模板中的数据。清单 8-6 显示了表单类(没有数据库访问)。

class PublisherForm(FlaskForm):
    publisherid = HiddenField('PublisherId')
    name = TextField('Name', validators=[
            Required(message=REQUIRED.format("Name")),
            Length(min=1, max=128, message=RANGE.format("Name", 1, 128))
        ])
    city = TextField('City', validators=[
            Required(message=REQUIRED.format("City")),
            Length(min=1, max=32, message=RANGE.format("City", 1, 32))
        ])
    url = TextField('URL/Website')
    create_button = SubmitField('Add')
    del_button = SubmitField('Delete')
Listing 8-6Publisher Form Class (No Database Code)

请注意,form 类创建了三个字段:一个用于发布者姓名(name),一个用于发布者所在的城市(city),另一个用于发布者 URL ( url)。我们还看到两个提交字段(按钮):一个用于创建新的发布者数据(create_button,另一个用于删除发布者数据(del_button)。我们还有一个隐藏的发布者 id 字段。

在视图函数中实例化表单数据之后,当呈现表单数据时,我们将表单数据传递给模板。清单 8-7 显示了发布者数据的查看功能。这里,我们首先实例化 publisher form 类,然后将其传递给模板。

#
# Publisher
#
# This page allows creating and editing publisher records.
#
@app.route('/publisher', methods=['GET', 'POST'])
@app.route('/publisher/<int:publisher_id>', methods=['GET', 'POST'])
def publisher(publisher_id=None):
    form = PublisherForm()
    if request.method == 'POST':
            pass
    return render_template("publisher.html", form=form)
Listing 8-7
Publisher View Function

请注意,这里我们看到了为视图定义的路线。还要注意,我们已经为请求设置了包含GETPOST的方法。我们可以检查这个请求是否是一个POST(提交数据)。在这种情况下,我们可以从 form 类实例中检索数据,并将其保存到数据库中。当我们添加数据库功能时,我们会更深入地了解这一点。

最后,请注意,我们实例化了 publisher form 类(form)的一个实例,然后将其作为参数传递给render_template("publisher.html", form=form)调用。在这种情况下,我们现在渲染存储在templates文件夹中的publisher.html模板。

好了,现在我们有了表单类和视图函数。现在的焦点是当我们呈现 HTML 模板文件时会发生什么。清单 8-8 显示了发布者数据的 HTML 文件(模板)。

{#
  Introducing the MySQL 8 Document Store

  This template defines the publisher template for use in the MyLibrary application
  using the base template.

  Dr. Charles Bell, 2017
#}
{% extends "base.html" %}
{% block title %}MyLibrary Search{% endblock %}
{% block page_content %}
  <form method=post> {{ form.csrf_token }}
    <fieldset>
      <legend>Publisher - Detail</legend>
      {{ form.hidden_tag() }}
      <div style=font-size:20pz; font-weight:bold; margin-left:150px;s>
        {{ form.name.label }} <br>
        {{ form.name(size=64) }} <br>
        {{ form.city.label }} <br>
        {{ form.city(size=48) }} <br>
        {{ form.url.label }} <br>
        {{ form.url(size=75) }} <br><br>
        {{ form.create_button }}
        {% if form.del_button %}
          {{ form.del_button }}
        {% endif %}
      </div>
    </fieldset>
  </form>
{% endblock %}  

Listing 8-8
Publisher 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 文件中继承了它,因此生成的 web 页面看起来与应用中的任何其他页面一样,都包含来自引导组件的菜单。注意,我们还可以定义错误消息的文本和标题。随意修饰你自己的错误处理程序,让你的用户更感兴趣。 7

{% extends "base.html" %}
{% block title %}MyLibrary 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 %}MyLibrary 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 == 'book' or not kind:
    if request.method == 'POST':
        return redirect('book')
    return render_template("list.html", form=form, rows=rows,
                           columns=columns, kind=kind)
elif kind == 'author':
    if request.method == 'POST':
        return redirect('author')
    return render_template("list.html", form=form, rows=rows,
                           columns=columns, kind=kind)
elif kind == 'publisher':
    if request.method == 'POST':
        return redirect('publisher')
    return render_template("list.html", form=form, rows=rows,
                           columns=columns, kind=kind)

这里,我们看到在一个POST请求之后有三个重定向。在每种情况下,我们都使用应用中定义的一个路由来告诉 Flask 调用相关的视图函数。这样,我们可以创建一个菜单或一系列提交字段,允许用户从一个页面移动到另一个页面。

redirect()函数需要一个有效的路径,在大多数情况下,它只是您在装饰器中提供的文本。但是,如果需要形成一个复杂的 URL 路径,可以在重定向之前使用url_for()函数来验证路由。如果您重组或更改路线,该功能还有助于避免断开链接。例如,您可以使用redirect(url_for(“author”))来验证路线并为其形成一个 URL。

附加功能

除了我们在这个速成班中所看到的,Flask 还有更多的内容。您可能有兴趣进一步了解一些未讨论的内容,包括以下内容(这只是其中的一部分)。如果您对这些感兴趣,可以考虑在在线文档中查找它们。

烧瓶审查:样品应用

现在我们已经对 Flask 有了一个简单的了解,让我们看看所有这些是如何工作的。在这一节中,我们将回顾我们所学的典型 Flask web 应用的基本布局。在本章的后面,我们将把它作为编写库应用的指南。不要太担心执行这段代码,因为它并没有做太多事情,只是作为章节项目的一个开始。然而,它确实演示了如何将我们所学的所有部分组合在一起,使 Flask web 应用在没有定义表单的情况下运行。

清单 8-9 显示了库应用的样例应用布局。花点时间通读一遍。您应该可以找到我们到目前为止讨论过的所有主题,包括字段类、表单类和视图函数的占位符。

#
# Introducing the MySQL 8 Document Store - Template
#
# This file contains a template for building Flask applications. No form
# classes, routes, or view functions are defined but placeholders for each
# are defined in the comments.
#
# Dr. Charles Bell, 2017
#
from flask import Flask, render_template, request, redirect, flash
from flask_script import Manager
from flask_bootstrap import Bootstrap
from flask_wtf import FlaskForm
from wtforms import (HiddenField, TextField, TextAreaField, SelectField,
                     SelectMultipleField, IntegerField, SubmitField)
from wtforms.validators import Required, Length

#
# Setup Flask, Bootstrap, and security.
#
app = Flask(__name__)
app.config['SECRET_KEY'] = "He says, he's already got one!"
manager = Manager(app)
bootstrap = Bootstrap(app)

#
# Utility functions
#
def flash_errors(form):
    for error in form.errors:
        flash("{0} : {1}".format(error, ",".join(form.errors[error])))

#
# Customized fields for skipping prevalidation
#
<custom field classes go here>

#
# Form classes - the forms for the application
#
<form classes go here>

#
# Routing functions - the following defines the routing functions for the
# menu including the index or "home", book, author, and publisher.
#
<routing functions (view functions) go here>

#
# Error handling routes
#
@app.errorhandler(404)
def page_not_found(e):
    return render_template('404.html'), 404

@app.errorhandler(500)
def internal_server_error(e):
    return render_template('500.html'), 500

#
# Main entry
#
if __name__ == '__main__':
    manager.run()

Listing 8-9Sample Flask Application Template

注意,在这个模板中有一件事我们还没有谈到——效用函数。这些是你自己的函数来支持你的应用。您可能想考虑在所有 Flask 应用中包含的一个函数是循环遍历表单上的错误并在 flash 消息中显示它们的函数。召回简讯在网页上显示为弹出框。为了清楚起见,下面给出了效用函数。注意,我们使用 for 循环来遍历显示每条消息的表单实例的 errors 数组。这允许您在网页上显示多条消息。

def flash_errors(form):
    for error in form.errors:
        flash("{0} : {1}".format(error, ",".join(form.errors[error])))

创建自己的 Flask 应用时,可以随意使用这个模板。我们还将在下一节中使用它来定义库应用的用户界面。

Tip

关于 Flask 以及如何使用它和它的相关包的更多信息,下面的书是关于这个主题的很好的参考:Flask Web Development:用 Python 开发 Web 应用第 2 版。,米格尔·格林伯格,(奥莱利传媒,2018)。

既然我们已经设置了 Flask 环境并发现了 Flask 及其扩展,那么让我们来看一下这个应用的三个版本共有的用户界面。

库应用用户界面设计

现在,我们对 Flask 以及如何构建 Flask 应用有了更多的了解,让我们看看库应用的用户界面。正如您可能猜测的那样,我们将数据库访问构建为一组独立的类,但是用户界面几乎完全可以在没有它的情况下构建。将用户界面与数据库访问机制分开研究,可以更容易地关注每个部分。我们将在下一节讨论数据库访问机制。

库应用的用户界面对于应用的所有三个版本都是相同的代码,只是对代码进行了一些修改以适应不同的数据库机制。特别是,我们在版本 1(关系数据库)中提供了完整的界面,在版本 2 中提供了简化的用户界面(混合:带有 JSON 的关系数据库),版本 3 将更加简洁(文档存储)。因此,我们将为用户界面中托管的所有网页编写表单类、视图函数和模板。

然而,在我们开始为应用编写表单类、视图函数和模板之前,我们需要创建几个目录。

准备目录结构

在我们开始实现库应用的三个版本之前,我们需要创建几个文件夹(目录)。回想一下 Flask Primer,我们需要文件夹来包含.html文件(表单模板)。我们还将与 MySQL 交互的代码放在一个名为 database 的文件夹中。最后,我们需要为应用的每个版本创建一个单独的文件夹。清单 8-10 显示了你需要的文件夹结构。您可以随意命名版本文件夹,但数据库和模板文件夹必须按如下所示命名。注意,我们还有一个名为“base”的文件夹,它包含基本的用户界面设计,但没有数据库文件夹,这将在下一节讨论。

root folder
  |
  +- base
  |        |
  |        +-- templates
  |
  +- version1
  |        |
  |        +-- database
  |        |
  |        +-- templates
  |
  +- version2
  |        |
  |        +-- database
  |        |
  |        +-- templates
  |
  +- version3
           |
           +-- database
           |
           +-- templates
Listing 8-10
Directory Structure

用户界面功能

library 应用将托管三种类型的数据:书籍、作者和出版商,将它们链接起来形成一个库。该应用的默认视图将是一个图书列表,它以简短书目的形式显示所有的图书。还会有所有作者和所有出版商的视图。用户还可以查看特定书籍、作者或出版商的数据,允许他们更新或删除数据项。因此,库应用演示了数据的基本创建、读取、更新和删除(CRUD)操作。

回想一下,我们将使用 bootstrap 导航栏,它为每个视图都提供了菜单项:图书、作者和出版商。让我们来看看默认视图——图书列表。图 8-3 显示了默认视图(无数据)。请注意,导航栏和每个视图的选项。还记得我们指定默认视图(通过单击 MyLibrary Base 到达)是书籍的相同视图。换句话说,它是典型的index.html或其他网络应用的大本营。

A432285_1_En_8_Fig3_HTML.jpg

图 8-3

MyLibrary application book list (default view)

虽然这个例子中没有数据,但是我们将编写代码为列表中的每个项目创建一个链接,用户可以单击该链接来编辑行中的数据。您将在后面的小节中看到这是如何实现的。还要注意New按钮。用户可以用它来创建一个如图 8-4 所示的新视图。这使用相同的表单类、视图函数和模板来查看和编辑数据。还记得我们将在视图上放置一个删除按钮,允许用户在编辑数据时选择删除数据。这个额外的步骤——首先编辑,然后删除——是避免“你确定吗?”大多数验证删除操作的应用的常见问题。这种方式允许用户在删除数据之前对其进行编辑和查看。你来判断它是否比“你确定吗?”提示。

A432285_1_En_8_Fig4_HTML.jpg

图 8-4

Book detail view

请注意,表单上有一个选择(下拉)字段。该字段由数据库中发布者的名称填充。同样,还有一个多选字段,允许用户在数据库中选择一个或多个作者。正如您将在我们讨论数据库设计时看到的,当使用关系数据时,这种布局在某种程度上是强加给我们的。我们将在视图函数中填充这些列表。请注意,我们还可以看到AddDelete提交字段(按钮)。回想一下,我们将禁用模板中的Delete按钮——在添加新数据项时,它通常不会被启用。

接下来是作者观点。与 books 视图一样,这里是数据库中的作者列表,包括用于编辑行的链接和用于创建新作者的新按钮。图 8-5 显示了作者视图。

A432285_1_En_8_Fig5_HTML.jpg

图 8-5

Author list view

当用户单击 New(或稍后,列表中的 edit 链接)时,将显示 author detail 视图。图 8-6 显示了作者详细视图。

A432285_1_En_8_Fig6_HTML.jpg

图 8-6

Author detail view

注意表格很短。它只显示了两个字段以及 Add 和Delete按钮,它们将在模板中被控制。

最后,我们有 publisher 视图,它显示了数据库中所有发布者的列表。图 8-7 显示了发布者视图。

A432285_1_En_8_Fig7_HTML.jpg

图 8-7

Publisher view

最后,当用户点击列表中的新建或编辑链接时,将显示发布者详细视图,如图 8-8 所示。注意,这里有三个发布者数据字段以及添加和删除按钮。

A432285_1_En_8_Fig8_HTML.jpg

图 8-8

Publisher detail view

现在我们已经了解了基本的用户界面,让我们看看如何为详细视图的三个表单类和一个用于显示列表的表单类构建表单类,它使用继承和一些模板构造在所有三个列表视图之间共享表单类和模板。酷吧。

表单类

library 应用的表单类需要三个表单类。作者、出版商和图书视图各有一个,可重用列表视图有一个表单类。让我们从最简单的表单类(author)开始,然后向更复杂的(book)前进。

作者表单类

author 表单非常简单,只需要三个字段:一个使用HiddenField字段类存储行的主键,一个存储名,一个存储姓。两个名称字段都使用一个TextField字段类。name 字段的验证设置为 required(提示:它们在数据库表中被定义为NOT NULL)以及最小和最大长度检查。我们还需要两个SubmitField字段类:一个用于Add,另一个用于Delete。回想一下,我们将以编程方式控制模板中的删除按钮。清单 8-11 显示了AuthorForm表单类。

class AuthorForm(FlaskForm):
    authorid = HiddenField('AuthorId')
    firstname = TextField('First name', validators=[
            Required(message=REQUIRED.format("Firstname")),
            Length(min=1, max=64, message=RANGE.format("Firstname", 1, 64))
        ])
    lastname = TextField( 'Last name', validators=[
            Required(message=REQUIRED.format("Lastname")),
            Length(min=1, max=64, message=RANGE.format("Lastname", 1, 64))
        ])
    create_button = SubmitField('Add')
    del_button = SubmitField('Delete')

Listing 8-11AuthorForm Class

Publisher 表单类

publisher 表单也非常简单,只需要四个字段:一个使用HiddenField字段类存储行的主键,一个存储出版商名称,一个存储出版商的原籍城市,另一个存储出版商的 URL。所有三个可见字段都使用一个TextField字段类。name 和 city 字段的验证都设置为 required(提示:它们在数据库表中被定义为NOT NULL)以及最小和最大长度检查。URL 字段没有验证器,因为它是数据的可选字段(它可以是数据库表中的NULL)。我们还看到了用于AddDelete按钮的两个SubmitFields()。清单 8-12 显示了PublisherForm表单类。

class PublisherForm(FlaskForm):
    publisherid = HiddenField('PublisherId')
    name = TextField('Name', validators=[
            Required(message=REQUIRED.format("Name")),
            Length(min=1, max=128, message=RANGE.format("Name", 1, 128))
        ])
    city = TextField('City', validators=[
            Required(message=REQUIRED.format("City")),
            Length(min=1, max=32, message=RANGE.format("City", 1, 32))
        ])
    url = TextField('URL/Website')
    create_button = SubmitField('Add')
    del_button = SubmitField('Delete')
Listing 8-12PublisherForm Class

图书表单类

book 表单稍微复杂一点,有许多数据字段。实际上有 10 个字段。表 8-4 列出了 book form 类所需的字段。包括字段名称、使用的字段类和有效性选项。

表 8-4

Field Classes for the Book Form Class

| 字段名 | 字段类 | 确认 | | :-- | :-- | :-- | | `ISBN` | `TextField()` | `Required()`,`Length()` | | `Title` | `TextField()` | `Required()` | | `Year` | `IntegerField()` | `Required()` | | `Edition` | `IntegerField()` | 没有人 | | `Language` | `TextField()` | `Required()`,`Length()` | | `Publisher` | `NewSelectField()` | `Required()` | | `Authors` | `NewSelectMultipleField()` | `Required()` | | `create_button` | `SubmitField()` | 不适用的 | | `del_button` | `SubmitField()` | 不适用的 | | `new_note` | `TextAreaField()` | 没有人 |

在我们讨论图书细节视图的表单类之前,有一个小问题需要解决。使用SelectField()SelectMultipleField()字段类时有一个众所周知的问题。如果没有选择默认值或者您以编程方式设置了默认值,预验证代码在验证时可能会出现一些不寻常的结果。为了克服这些限制,我们可以创建自己的派生字段类并覆盖预验证代码。清单 8-13 显示了用于创建这些字段类的定制版本以克服限制的代码。如果要使用这些字段类中的任何一个,就需要将它们放在代码中。

class NewSelectMultipleField(SelectMultipleField):
    def pre_validate(self, form):
        # Prevent "not a valid choice" error
        pass

    def process_formdata(self, valuelist):
        if valuelist:
            self.data = ",".join(valuelist)
        else:
            self.data = ""

class NewSelectField(SelectField):
    def pre_validate(self, form):
        # Prevent "not a valid choice" error
        pass

    def process_formdata(self, valuelist):
        if valuelist:
            self.data = ",".join(valuelist)
        else:
            self.data = ""

Listing 8-13Creating Custom Field Classes

请注意,在每种情况下,我们都覆盖了pre_validate()process_formdata()方法,从而允许我们忽略预先验证,并使更新值变得更容易。现在让我们看看 book form 类的代码。清单 8-14 显示了BookForm表单类的代码。注意,我们为 authors 和 publisher 字段使用了新的字段类。我们还看到两个SubmitFields()用于AddDelete按钮。

class BookForm(FlaskForm):
    isbn = TextField('ISBN ', validators=[
            Required(message=REQUIRED.format("ISBN")),
            Length(min=1, max=32, message=RANGE.format("ISBN", 1, 32))
        ])
    title = TextField('Title ',
                      validators=[Required(message=REQUIRED.format("Title"))])
    year = IntegerField('Year ',
                        validators=[Required(message=REQUIRED.format("Year"))])
    edition = IntegerField('Edition ')
    language = TextField('Language ', validators=[
            Required(message=REQUIRED.format("Language")),
            Length(min=1, max=24, message=RANGE.format("Language", 1, 24))
        ])
    publisher = NewSelectField('Publisher ',
                    validators=[Required(message=REQUIRED.format("Publisher"))])
    authors = NewSelectMultipleField('Authors ',
                    validators=[Required(message=REQUIRED.format("Author"))])
    create_button = SubmitField('Add')
    del_button = SubmitField('Delete')
    new_note = TextAreaField('Add Note')
Listing 8-14BookForm Class

列表表单类

为了节省重复的代码,我们将创建一个简单的 form 类,用于以 HTML 表格的形式创建一个简单的行列表。这样做的代码非常简单,因为所有的表示代码都在模板文件中。下面是ListForm表单类的代码。

class ListForm(FlaskForm):
    submit = SubmitField('New')

既然我们已经看到了库应用的所有表单类,现在我们来研究相关的视图函数。

查看功能

视图函数是 Flask 应用指导执行的地方和方式。加上我们定义的路由,我们可以构建没有循环或轮询的应用。让我们从最简单的视图函数开始。我们将看到定义了路由的视图函数(通过 decorators)。作者、出版商和图书视图功能的基本代码是相同的,不需要额外讨论。唯一的区别是图书视图功能中的路径和人口或选择和多选字段。每个函数分别显示在清单 8-15 (名为author)、清单 8-16 (名为publisher)和清单 8-17 (名为book)中。

@app.route('/author', methods=['GET', 'POST'])
@app.route('/author/<int:author_id>', methods=['GET', 'POST'])
def author(author_id=None):
    form = AuthorForm()
    if request.method == 'POST':
        pass
    return render_template("author.html", form=form)
Listing 8-15
Author View Function

@app.route('/publisher', methods=['GET', 'POST'])
@app.route('/publisher/<int:publisher_id>', methods=['GET', 'POST'])
def publisher(publisher_id=None):
    form = PublisherForm()
    if request.method == 'POST':
            pass
    return render_template("publisher.html", form=form)
Listing 8-16Publisher View Function

@app.route('/book', methods=['GET', 'POST'])
@app.route('/book/<string:isbn_selected>', methods=['GET', 'POST'])
def book(isbn_selected=None):
    notes = None
    form = BookForm()
    form.publisher.choices = []
    form.authors.choices = []
    new_note = ""
    if request.method == 'POST':
        pass
    return render_template("book.html", form=form, notes=notes)
Listing 8-17Book View Function

列表视图功能更复杂。回想一下,我们想要创建一个可以重用的列表。因此,我们需要能够创建一个 HTML 表,其中包含我们想要显示的列名和行。我们可以使用render_template()函数中的参数传递列和行。我们还想定义列的大小。我们可以通过向模板传递 HTML 代码来做到这一点。在这种情况下,我们将它们定义为列名的 HTML 标记,并在模板中使用安全过滤器显示它而不进行翻译。

我们还想为每一行创建一个包含该行主键的链接,我们将把它作为每一行的第一个数据项来传递。对于作者和出版商,它是自动增量主键。对于书籍,它是 ISBN。因此,ISBN 将在该行中列出两次。为了确定我们需要哪些数据,我们在 list route 中使用了一个变量。例如,如果我们想要书,我们的 URL 应该是 localhost:5000/list/book。酷。

最后,因为这个视图函数是默认视图,所以路径很简单:默认(索引)和列表。清单 8-18 显示了名为simple_list的列表视图函数的完整代码。花些时间通读一下,这样你就能理解代码了。

@app.route('/', methods=['GET', 'POST'])
@app.route('/list/<kind>', methods=['GET', 'POST'])
def simple_list(kind=None):
    rows = []
    columns = []
    form = ListForm()
    if kind == 'book' or not kind:
        if request.method == 'POST':
            return redirect('book')
        columns = (
            '<td style="width:200px">ISBN</td>',
            '<td style="width:400px">Title</td>',
            '<td style="width:200px">Publisher</td>',
            '<td style="width:80px">Year</td>',
            '<td style="width:300px">Authors</td>',
        )
        kind = 'book'
        # Here, we get all books in the database
        return render_template("list.html", form=form, rows=rows,
                               columns=columns, kind=kind)
    elif kind == 'author':
        if request.method == 'POST':
            return redirect('author')
        # Just list the authors
        columns = (
            '<td style="width:100px">Lastname</td>',
            '<td style="width:200px">Firstname</td>',
        )
        kind = 'author'
        # Here, we get all authors in the database
        return render_template("list.html", form=form, rows=rows,
                               columns=columns, kind=kind)
    elif kind == 'publisher':
        if request.method == 'POST':
            return redirect('publisher')
        columns = (
            '<td style="width:300px">Name</td>',
            '<td style="width:100px">City</td>',
            '<td style="width:300px">URL/Website</td>',
        )
        kind = 'publisher'
        # Here, we get all publishers in the database
        return render_template("list.html", form=form, rows=rows,
                               columns=columns, kind=kind)
    else:
        flash("Something is wrong!")

        return
Listing 8-18
List View Function

现在我们已经看到了表单类和视图函数的代码,让我们来看看这个难题的剩余部分:模板。

模板

模板是我们放置用于构建网页的所有 HTML 和模板构造的地方(在数据库应用、数据视图或简单的视图 8 )的上下文中。这些模板提供了简短的描述,供您参考,以便您可以一起查看所有部分。因为我们有四个视图函数,所以我们将创建四个模板文件,它们都将使用前面解释的基本模板。回想一下,基本模板定义了引导导航栏和 for 循环,用于显示 flash 消息的数组。

Note

记住,模板文件在templates文件夹中,按照惯例命名为XXX.html

作者模板

作者模板创建用于查看、编辑和创建作者数据的表单。因此,我们给页面一个图例,托管隐藏字段(用于自动增量主键),并将标签和表单字段放在每个字段的表单上。我们通过垂直列出字段来保持简单(但是您可以使用您想要的任何格式)。我们还使用表单字段默认函数来设置字段的大小。例如,要将名字字段的大小设置为 75 个字符,我们使用form.firstname(size=75)。最后,我们看到了打开 delete 按钮的逻辑(如果它被定义的话)(我们将在后面看到如何禁用它)。清单 8-19 显示了作者数据的完整模板(名为author.html)。

{% extends "base.html" %}
{% block title %}MyLibrary Search{% endblock %}
{% block page_content %}
  <form method=post> {{ form.csrf_token }}
    <fieldset>
      <legend>Author - Detail</legend>
      {{ form.hidden_tag() }}
      <div style=font-size:20pz; font-weight:bold; margin-left:150px;s>
        {{ form.firstname.label }} <br>
        {{ form.firstname(size=75) }} <br>
        {{ form.lastname.label }} <br>
        {{ form.lastname(size=75) }} <br><br>
        {{ form.create_button }}
        {% if form.del_button %}
          {{ form.del_button }}
        {% endif %}
      </div>
    </fieldset>
  </form>
{% endblock %} 

Listing 8-19Author Template (author.html)

发布者模板

publisher 模板创建用于查看、编辑和创建 publisher 数据的表单。因此,我们给页面一个图例,托管隐藏字段(用于自动增量主键),并将标签和表单字段放在每个字段的表单上。我们通过垂直列出字段来保持简单(但是您可以使用您想要的任何格式)。我们还设置了字段的大小。最后,我们看到了打开 delete 按钮的逻辑(如果它被定义的话)(我们将在后面看到如何禁用它)。清单 8-20 显示了发布者数据(名为publisher.html)的完整模板。

{% extends "base.html" %}
{% block title %}MyLibrary Search{% endblock %}
{% block page_content %}
  <form method=post> {{ form.csrf_token }}
    <fieldset>
      <legend>Publisher - Detail</legend>
      {{ form.hidden_tag() }}
      <div style=font-size:20pz; font-weight:bold; margin-left:150px;s>
        {{ form.name.label }} <br>
        {{ form.name(size=64) }} <br>
        {{ form.city.label }} <br>
        {{ form.city(size=48) }} <br>
        {{ form.url.label }} <br>
        {{ form.url(size=75) }} <br><br>
        {{ form.create_button }}
        {% if form.del_button %}
          {{ form.del_button }}
        {% endif %}
      </div>
    </fieldset>
  </form>
{% endblock %}
Listing 8-20Publisher Template (publisher.html)

书籍模板

图书模板稍微复杂一点。我们从图例和隐藏标签开始,它存储当前数据的 ISBN,然后构建列出标签和字段的表单,并在过程中垂直设置字段的大小。到目前为止,这与我们构建作者和发布者模板的方式类似。

当我们试图为选择字段设置字段大小时,事情变得更有趣了。在这种情况下,我们需要使用以像素为单位传入width参数的style参数。这是 Flask 模板为数不多的细微差别之一,可能有点棘手,因为size参数不适用于选择字段(但现在您知道如何绕过它)。与前面的模板一样,如果定义了 delete 按钮,我们可以看到打开它的逻辑(稍后我们将看到如何禁用它)。

之后,我们会看到一些处理笔记的附加逻辑。笔记功能允许用户在图书创建后添加笔记。因此,我们既需要显示任何现有的注释,也需要提供添加新注释的方法,但只有在页面用于更新操作时才需要。您可以在文件的底部看到这是如何完成的。

清单 8-21 显示了发布者数据(名为book.html)的完整模板。花一些时间通读文件,直到你确信你理解它是如何工作的。

{% extends "base.html" %}
{% block title %}MyLibrary Search{% endblock %}
{% block page_content %}
  <form method=post> {{ form.csrf_token }}
    <fieldset>
      <legend>Book - Detail</legend>
      {{ form.hidden_tag() }}
      <div style=font-size:20pz; font-weight:bold; margin-left:150px;>
        {{ form.isbn.label }} <br>
        {{ form.isbn(size=32) }} <br>
        {{ form.title.label }} <br>
        {{ form.title(size=100) }} <br>
        {{ form.year.label }} <br>
        {{ form.year(size=10) }} <br>
        {{ form.edition.label }} <br>
        {{ form.edition(size=10) }} <br>
        {{ form.language.label }} <br>
        {{ form.language(size=34) }} <br>
        {{ form.publisher.label }} <br>
        {{ form.publisher(style="width: 300px;") }} <br>
        {{ form.authors.label }} <br>
        {{ form.authors(style="width: 300px;") }}
        {# Show the new note text field if this is an update. #}
        {% if form.create_button.label.text == "Update" %}
          <br>{{ form.new_note.label }} <br>
          {{ form.new_note(rows='2',cols='100') }}
        {% endif %}
        <br><br>
        {{ form.create_button }}
        {% if form.del_button %}
          {{ form.del_button }}
        {% endif %}
        <br><br>
      </div>
      {# Show the list of existing notes if there is a list. #}
      {% if notes %}
        <div>
          <table border="1" cellpadding="1" cellspacing="1">
            <tr><td><b>Notes</b></td></tr>
            {% for note in notes %}
              <tr><td style="width:600px"> {{ note }} </td></tr>
            {% endfor %}
          </table>
          <br>
        </div>
      {% endif %}
    </fieldset>
  </form>
{% endblock %}  

Listing 8-21Book Template (book.html)

列表模板

尽管列表特性的视图功能相当复杂,但是列表视图的模板相当简单。我们只需在顶部添加新按钮(submit field ),提供一个图例,然后使用 view 函数中的 columns 数组格式化表格。然后,我们使用视图函数提供的行构建 HTML 表。列表 8-22 显示了列表数据的完整模板(名为list.html)。

{% extends "base.html" %}
{% block title %}MyLibrary Query Results{% endblock %}
{% block page_content %}
  <form method=post> {{ form.csrf_token }}
    <fieldset>
      {{ form.submit }} <br><br>
    </fieldset>
  </form>
  <legend>Query Results</legend>
  <table border="1" cellpadding="1" cellspacing="1">
    <tr>
      <td style="width:80px"><b>Action</b></td>
      {% for col in columns %}
        {{ col|safe }}
      {% endfor %}
    </tr>
    {% for row in rows %}
      <tr>
        <td><a href="{{ '/%s/%s'%(kind,row[0]) }}">Modify</a></td>
        {% for col in row[1:] %}
          <td> {{ col }} </td>
        {% endfor %}
      </tr>
    {% endfor %}
  </table>
{% endblock %}

Listing 8-22List Template (list.html)

其他模板

回想一下,还有三个我们之前见过的模板,我们将使用它们:404 和 500 错误处理程序(404.html500.html),如“错误处理程序”一节中所述,以及基本模板(base.html),如清单 8-5 所示。

应用代码

现在,让我们将这些概念放在应用代码中,完成前面介绍的基本布局。清单 8-23 显示了库应用的应用代码。因为这是库应用的基础版本,我们将该文件命名为mylibrary_base.py。我们可以将它作为三个版本的库应用(名为mylibrary_v1.pymylibrary_v2.pymylibrary_v3.py)的基础。

该列表是为了完整性,没有额外的讨论。前面讨论的代码部分用[...]占位符标记,以避免重复。更确切地说,该清单是为后面讨论这三个版本的部分提供的参考。请随意通读代码,以确保您理解代码的所有部分,并在阅读不同版本的部分时参考它。

#
# Introducing the MySQL 8 Document Store - Base
#
# This file contains the sample Python + Flask application for demonstrating
# how to build a simple relational database application. Thus, it relies on
# a database class that encapsulates the CRUD operations for a MySQL database
# of relational tables.
#
# Dr. Charles Bell, 2017
#
from flask import Flask, render_template, request, redirect, flash
from flask_script import Manager
from flask_bootstrap import Bootstrap
from flask_wtf import FlaskForm
from wtforms import (HiddenField, TextField, TextAreaField, SelectField,
                     SelectMultipleField, IntegerField, SubmitField)
from wtforms.validators import Required, Length

#
# Strings
#
REQUIRED = "{0} field is required."
RANGE = "{0} range is {1} to {2} characters."

#
# Setup Flask, Bootstrap, and security.
#
app = Flask(__name__)
app.config['SECRET_KEY'] = "He says, he's already got one!"
manager = Manager(app)
bootstrap = Bootstrap(app)

#
# Utility functions
#
def flash_errors(form):
[...]

#
# Customized fields for skipping prevalidation
#
class NewSelectMultipleField(SelectMultipleField):
[...]

class NewSelectField(SelectField):
[...]

#
# Form classes - the forms for the application
#
class ListForm(FlaskForm):
[...]

class PublisherForm(FlaskForm):
[...]

class AuthorForm(FlaskForm):
[...]

class BookForm(FlaskForm):
[...]

#
# Routing functions - the following defines the routing functions for the
# menu including the index or "home", book, author, and publisher.
#

#
# Simple List
#
# This is the default page for "home" and listing objects. It reuses a
# single template "list.html" to show a list of rows from the database.
# Built into each row is a special edit link for editing any of the rows,
# which redirects to the appropriate route (form).
#
@app.route('/', methods=['GET', 'POST'])
@app.route('/list/<kind>', methods=['GET', 'POST'])
def simple_list(kind=None):
[...]

#
# Author
#
# This page allows creating and editing author records.
#
@app.route('/author', methods=['GET', 'POST'])
@app.route('/author/<int:author_id>', methods=['GET', 'POST'])
def author(author_id=None):
[...]

#
# Publisher
#
# This page allows creating and editing publisher records.
#
@app.route('/publisher', methods=['GET', 'POST'])
@app.route('/publisher/<int:publisher_id>', methods=['GET', 'POST'])
def publisher(publisher_id=None):
[...]

#
# Book
#
# This page allows creating and editing book records.
#
@app.route('/book', methods=['GET', 'POST'])
@app.route('/book/<string:isbn_selected>', methods=['GET', 'POST'])
def book(isbn_selected=None):
[...]

#
# Error handling routes
#
@app.errorhandler(404)
def page_not_found(e):
[...]

@app.errorhandler(500)
def internal_server_error(e):
[...]

#
# Main entry
#
if __name__ == '__main__':
    manager.run()

Listing 8-23Base MyLibrary Application Code

既然我们已经有了 Flask 的坚实基础以及用户界面的设计方式,我们就可以开始为应用的每个版本编写代码了,从关系数据库版本开始。

摘要

使用一个好的框架来构建 MySQL 应用不仅是为了数据库访问,更重要的是为了用户界面。决定使用哪种语言和平台有时会变成一个科学项目,甚至是一次学术活动,或者是一项不可逾越的命令。用示例表示概念(如文档存储)可能会更加复杂,因为您必须选择一种易于使用和理解的语言和框架。也许更具挑战性的是,选择一个以有意义的方式阐释概念的应用。 9

在本书中,这些技术的选择是 Python、Flask 框架,当然还有 MySQL 连接器/Python 数据库连接器和 X DevAPI。Python 易于阅读,任何人——甚至那些没有写很多代码的人——都能理解它。另外,它是一种非常强大的语言。然而,Python 中的用户界面仅限于命令行(终端)输出,除非您使用用户界面框架。再次,选择一个可能是一个挑战。然而,web 应用的框架只是帮助构建一个看起来不错的示例的入场券,读者可以使用它作为自己的实验和应用的基础。

在这一章中,我们学习了一个名为 Flask 的新的 Python web 应用库。我们还看到了 Flask 是如何作为一个可扩展的框架构建的,该框架可以很容易地用组件进行扩充,从而使您的应用更加健壮。我们还介绍了基于我们对 Flask 的了解而构建的库应用的用户界面。

在下一章中,我将介绍应用的三个版本:使用旧协议的纯关系数据库解决方案、使用 X DevAPI 和 SQL 语句的 JSON(混合)关系数据库,以及纯文档存储版本。每个版本都提供了如何使用不同的数据库访问机制构建应用的基础。正如你将看到的,从旧的到新的有一个深刻的转变。

Footnotes 1

也叫奶酪店,参考了巨蟒剧团《飞行马戏团》( https://en.wikipedia.org/wiki/Cheese_Shop_sketch )中的奶酪店小品。

  2

如果您使用了足够多的框架,您最终会遇到那些不可扩展的框架,并迫使您使用它们的数据库功能,这些功能通常过于有限,可能无法满足您的需求。发现了一个新的框架,却发现无法访问数据,或者必须重构数据库才能在框架中使用数据,这是多么可悲啊。

  3

我发现这在输入数据时特别烦人,因为当你返回页面时数据经常会丢失。请不要用这种方法。

  4

如果你为了更好的安全锁定了你的浏览器,允许弹出窗口可能会有问题。

  5

很少有人会用这个词来描述模板构造,尽管不准确,但如果它有助于学习如何使用 Jinja2 模板,那么可以将其视为类似代码的组件。

  6

随着年龄的增长,你越来越经常地阅读代码并问:“这是谁写的?”可悲的是,往往是你自己的代码!这里或那里的一些评论将有助于记住你在做什么(以及为什么)。

  7

Github 上有一个很好的定制错误处理程序的例子。他们有一个自定义的背景和样式表,让其他网站无聊的 404 错误相形见绌。

  8

这很不幸,因为它很容易与 Flask view 函数混淆。

  9

遗憾的是,大多数文档丰富的教程很少有可以实际使用的例子。“你好,世界!”例子终究只能到此为止。