MySQL-Connection-Python-揭秘-全-

MySQL Connection/Python 揭秘(全)

原文:MySQL Connector/Python Revealed

协议:CC BY-NC-SA 4.0

一、简介和安装

您将踏上 MySQL Connector/Python 世界的旅程。欢迎登机!这是十步指南的第一章,将带您完成从安装到故障排除的所有内容。在这个过程中,您将熟悉连接器及其 API 的特性和工作方式。

本章将通过浏览版本、版本和 API 来介绍 MySQL 连接器/Python。本章的中间部分将讨论如何下载和安装连接器,最后一部分将讨论 MySQL 服务器,如何为本书中的示例设置服务器,并对示例本身说几句话。

介绍

MySQL Connector/Python 是 Python 程序和 MySQL 服务器数据库之间的粘合剂。它可用于使用数据定义语言(DDL)语句操作数据库对象,以及通过数据操作语言(DML)语句更改或查询数据。

也可以把 MySQL Connector/Python 称为数据库驱动。它是 Python 的官方 MySQL 连接器,由 Oracle 公司的 MySQL 开发团队开发和维护。它有效地支持三种不同的 API,尽管通常只直接使用两种。

本节介绍 MySQL 连接器/Python 版本、版本和三个 API。

版本

在 2012 年之前,没有 Oracle 维护的 Python 连接器。还有其他第三方连接器,比如 MySQL-python (MySQLdb)接口;然而,它越来越老,官方只支持 MySQL 5.5 和 Python 2.7。

MySQL 决定开发自己的连接器:MySQL Connector/Python。它被编写为与 MySQL-python 接口兼容,并与最新的 MySQL 服务器和 python 版本保持同步。最初的正式发布(GA)版本是 1.0.7,于 2012 年 9 月发布。版本 2.1 发生了重大更新;它引入了 C 扩展,允许更好的性能。截至 2018 年 4 月的最新 GA 版本是 8.0.11 版,其中额外引入了 X DevAPI。这是本书主要关注的版本。

注意

如果看一下 MySQL Connector/Python 的变化历史,可能会有点疑惑。8.0 之前的版本系列是 2.1,有几个 2.2 的 GA 前版本。8.0 版本的列表同样令人困惑:最新的 GA 前版本是 8.0.6,而第一个 GA 版本是 8.0.11。为什么跳跃?大多数 MySQL 产品的版本号是一致的,这要求发布号有些不规则,但现在这意味着 MySQL Server 8.0.11 和 MySQL Connector/Python 8.0.11 是一起发布的。

建议使用 GA quality 最新系列的最新补丁发布。只有最新的 GA 系列获得了所有的改进和错误修复。这意味着,在撰写本文时,建议使用最新的 MySQL Connector/Python 8.0 版本。虽然 MySQL Connector/Python 8.0 版本与 MySQL Server 和其他 MySQL 产品的版本结合在一起, 1 它们与较旧的 MySQL Server 版本向后兼容。所以,即使你还在用,比如 MySQL Server 5.7,你还是应该用 MySQL Connector/Python 8.0。

小费

使用 GA quality 最新版本系列的最新版本,确保您不仅可以获得所有最新功能,还可以获得最新的错误修复。最新的 MySQL 连接器/Python 版本可以与旧的 MySQL 服务器版本一起使用。另一方面,旧版本的 MySQL Connector/Python 可能与最新的 MySQL 服务器版本不兼容。例如,MySQL Server 8.0 默认使用caching_sha2_password身份验证插件,直到最近 MySQL Connector/Python 才支持该插件。

与任何正在开发的产品一样,新功能会定期添加,错误也会得到修复。您可以关注发行说明中的变化,这些信息可从 https://dev.mysql.com/doc/relnotes/connector-python/en/ .获得

除了各种版本的 MySQL Connector/Python 之外,还有两个不同的版本可供选择。让我们看看他们。

社区版和企业版

MySQL 产品有两个不同的版本:社区版和企业版。企业版是 Oracle 提供的商业产品。两个版本之间的差异因产品而异。例如,对于 MySQL Server,企业版有几个额外的插件。对于 MySQL Connector/Python,区别更微妙。

所有产品的一个共同区别是许可证。社区版是在 GNU 通用公共许可证 2.0 版下发布的,而企业版使用专有许可证。此外,企业版通过 MySQL 技术支持服务提供技术支持。对于 MySQL Connector/Python 本身来说,这是目前两个版本之间唯一的区别。

本书适用于两个版本中的任何一个,除了在本章后面简要讨论下载位置和安装方法时,不会提到版本。所有的例子都是用 Community Edition 编写和测试的。

相比之下,当涉及到 API 时,使用哪种 API 有很大的不同。

蜜蜂

MySQL Connector/Python 中实际上可以使用三种不同的 API。如何使用 API 是第 2 - 9 章的主要目的。在真正开始之前,有必要简要了解一下它们的区别。

表 1-1 显示了三个 API,它们在哪个 MySQL 连接器/Python 模块中可用,包括 API 支持的第一个 GA 版本,以及讨论它们的章节。

表 1-1

MySQL 连接器/Python API

|

应用接口

|

组件

|

第一版

|

|
| --- | --- | --- | --- |
| 连接器/Python API | mysql.connector | 1.0.7 | 2, 3, 4, 5, 9, 10 |
| c 扩展 API | _mysql_connector | 2.1.3 | four |
| X DevAPI | mysqlx | 8.0.11 | 6, 7, 8, 9, 10 |

此外,Connector/Python API 和 X DevAPI 既存在于纯 Python 实现中,也存在于使用 C 扩展的实现中。这两种实现是可以互换的。整本书都会提到这两种实现之间的一些差异。

正如您所看到的,主要的焦点是连接器/Python API 和 X DevAPI。连接器/Python API 和 C 扩展 API 专门使用 SQL 语句来执行查询。另一方面,X DevAPI 支持 NoSQL 方法来处理 JSON 文档和 SQL 表,并支持 SQL 语句。X DevAPI 也是其他编程语言的通用 API,包括 JavaScript (Node.js)、PHP、Java、DotNet 和 C++。

那么应该选择哪个 API 呢?从到目前为止的描述来看,选择 X DevAPI 听起来是显而易见的。然而,事情远不止如此。

如果专门使用 SQL 语句执行查询,C 扩展和 C 扩展 API 更成熟。例如,它们为参数绑定和预处理语句等特性提供了更好的支持。如果您需要连接池,它们也是可供选择的 API。如果您有现有的 Python 程序,它们也很可能使用连接器/Python API(启用或不启用 C 扩展实现)。

另一方面,X DevAPI 是一个新的 API,它是为适应现代需求而从头设计的。该 API 也适用于其他编程语言,当应用需要多种语言时,可以更容易地在语言之间进行切换。API 的 NoSQL 部分使得针对 SQL 表的简单查询和使用 JSON 文档变得更加简单。新的命令行客户端 MySQL Shell 也支持通过 Python 或 JavaScript 使用 X DevAPI。所以,X DevAPI 有很多新的项目。

由于 X DevAPI 本质上是 1.0 版本(MySQL 8.0 是 X DevAPI 的第一个 GA 版本),新特性更有可能在相对较短的时间内陆续推出。如果您缺少某个功能,请留意发行说明,看看该功能是否可用,或者在 https://bugs.mysql.com/ 注册您感兴趣的功能。

与“便利性”相比,是否使用 C 扩展在很大程度上是一个性能问题 C 扩展实现提供了更好的性能,尤其是在处理大型结果集和准备好的语句时。然而,纯 Python 实现可以在更多平台上使用,在自己构建 MySQL Connector/Python 时更容易使用,也更容易修改(顾名思义,纯 Python 实现完全是用 Python 编写的)。

MySQL 连接器/Python 的介绍到此结束。是时候开始安装过程了。第一步是下载 MySQL 连接器/Python。

下载

直接下载 MySQL 连接器/Python;但是,还是有几点考虑。这些注意事项和执行下载的步骤是本节的主题。

首先要问的是,您需要连接器的社区版还是企业版。这决定了下载和安装选项。社区版可以从几个地方获得,既有源代码形式,也有二进制发行版。企业版仅作为 Oracle 的二进制发行版提供。

小费

安装 MySQL Connector/Python 社区版的推荐方法是使用 Python 打包权威(PyPa)/Python 包索引(PyPi)中的包。这是使用pip工具完成的,不需要预先下载任何文件。使用 PyPi 的一个缺点是从发布到在 PyPi 中可用会有一个小的延迟。

表 1-2 概述了 MySQL Connector/Python 可用的交付方法,以及该方法是否可用于社区版和企业版。

表 1-2

MySQL 连接器/Python 下载选项

|

分配

|

社区版

|

企业版

|
| --- | --- | --- |
| Python 包(pip) | 可用;参见安装 |   |
| Windows 安装程序 | 有空的 | 有空的 |
| MSI 安装程序 | 有空的 | 有空的 |
| APT 知识库 | 有空的 |   |
| SUSE 知识库 | 有空的 |   |
| Yum 仓库 | 有空的 |   |
| RPM 下载 | 有空的 | 有空的 |
| DEB 包 | 有空的 | 有空的 |
| Solaris 软件包 | 有空的 | 有空的 |
| 苹果 | 有空的 | 有空的 |
| 独立于平台的 tar 或 zip 文件 | 有空的 | 有空的 |

如您所见,MySQL Connector/Python 可用于各种平台和不同的发行版。社区版可以直接使用pip命令行工具获得;为 Red Hat Enterprise Linux、Oracle Linux 和 Fedora Linux 使用 MySQL Yum 存储库;适用于 Debian 和 Ubuntu 的 MySQL APT 库;并为 SLES 使用 MySQL 数据库。pip和包存储库选项仅适用于社区版。

小费

MySQL 连接器/Python 的 MySQL 安装程序和 MSI 安装程序都可用于 Microsoft Windows。如果您想使用这些安装程序中的一个,建议使用 MySQL 安装程序,因为它也支持大多数其他 MySQL 产品。

表 1-3 显示了各种源代码和安装程序的下载位置的 URL。在这个上下文中,MySQL 存储库算作安装程序,即使它们更像是安装程序使用的定义文件。

表 1-3

下载资源

|

源/安装程序

|

统一资源定位器

|
| --- | --- |
| 社区:Microsoft Windows 的 MySQL 安装程序 | https://dev.mysql.com/downloads/installer/ |
| APT 知识库 | https://dev.mysql.com/downloads/repo/apt/ |
| SUSE 知识库 | https://dev.mysql.com/downloads/repo/suse/ |
| Yum 仓库 | https://dev.mysql.com/downloads/repo/yum/ |
| MySQL 下载 | https://dev.mysql.com/downloads/connector/python/ |
| 开源代码库 | https://github.com/mysql/mysql-connector-python |
| 企业:我的甲骨文支持 | https://support.oracle.com/ |
| 甲骨文软件交付云 | https://edelivery.oracle.com/ |

与社区版相关的下载可从 https://dev.mysql.com/downloads 下的页面获得。如果您需要源代码,可以从 MySQL 下载站点和 MySQL 的 GitHub 存储库中获得。 2

企业版可从 My Oracle Support (MOS)中的补丁&更新选项卡获得,也可从 Oracle 软件交付云获得(需要创建帐户并登录)。建议 MySQL 客户使用 My Oracle Support,因为它比 Oracle 软件交付云包含更多版本,更新更频繁。另一方面,Oracle 软件交付云提供了 MySQL 产品企业版的 30 天试用版。微软视窗的 MySQL 安装程序也有企业版;这可以从我的 Oracle 支持或 Oracle 软件交付云下载。

下载非常简单。图 1-1 显示了用于下载微软 Windows 的 MySQL 安装程序的下载屏幕。

img/463459_1_En_1_Fig1_HTML.jpg

图 1-1

下载 Microsoft Windows 的 MySQL 安装程序

点击下载后,如果您没有登录,将被带到图 1-2 中的页面。在这里,您可以选择登录一个现有的 Oracle Web 帐户,注册一个新的 Oracle Web 帐户,或者点击下载而不使用帐户。不,谢谢,开始下载吧。选择最适合自己的选项。Oracle Web 帐户还用于 My Oracle Support 和 Oracle Software Delivery Cloud,因此如果您是 Oracle 客户,您可以使用现有帐户。

img/463459_1_En_1_Fig2_HTML.jpg

图 1-2

准备下载

从社区下载页面下载其他 MySQL 产品,包括 MySQL Connector/Python,遵循相同的模式。主要区别在于,您需要选择操作系统,并且可以选择您正在使用的操作系统版本。选择的默认操作系统将是您正在浏览的操作系统。图 1-3 展示了下载 MySQL Connector/Python 时如何选择操作系统。

img/463459_1_En_1_Fig3_HTML.jpg

图 1-3

为 MySQL 连接器/Python 选择平台

一旦选择了平台,您就可以选择要下载的特定文件。区别可能在于使用哪个 Python 版本的 MySQL Connector/Python,以及它是纯 Python 还是 C 扩展实现。

一个关于 C 扩展和下载的词。根据平台的不同,C 扩展实现可能与下载的其余部分捆绑在一起并自动安装,或者可能有一个单独的文件要下载。在 Microsoft Windows 上,如果 C 扩展名适用于 Python 版本,则总是包括在内。一般来说,最新的几个受支持的 Python 版本将包含 C 扩展;对于较旧的 Python 版本,它不包括在内。对于 RPM 和 DEB 包,每个 MySQL Connector/Python 版本和支持的 Python 版本都有两个包:一个文件包含纯 Python 实现,另一个包含 C 扩展实现。

可以下载 MySQL Installer 和 MySQL Connector/Python 的企业版的网站设计不同,但思路是一样的。本书不会进一步讨论如何从 My Oracle Support 和 Oracle 软件交付云下载。相反,让我们看看安装过程本身。

装置

MySQL 连接器/Python 支持几种安装连接器的方法。可用的方法取决于操作系统。如果您习惯于安装软件,这些步骤应该不会让您感到惊讶。

如果您的安装方法包括是否安装 C 扩展的选项(例如,RPM 或 DEB 包),建议包括 C 扩展包。即使您不打算直接使用_mysql_connector模块,使用其他 API 的 C 扩展实现也可以提供更好的性能。

所需的安装类型与下载安装文件的方式密切相关。安装 MySQL Connector/Python 最独特的方式是使用 MySQL 安装程序。本节将使用pip命令、MySQL 安装程序和 MySQL Yum 存储库来完成安装。

pip–所有平台

如果使用 Community Edition,安装 MySQL Connector/Python 的推荐方法是使用pip命令从 Python Packaging Authority (PyPa)安装软件包。这确保了自动解决任何潜在的依赖性,并且相同的安装方法可以用于所有需要 MySQL Connector/Python 的平台。

如果您从 https://www.python.org/ 下载了 Python,那么pip命令是 Python 版本 2.7.9 和更高版本的正常 Python 安装的一部分。值得注意的例外是一些 Linux 发行版,如 RedHat Enterprise Linux、Oracle Linux 和 CentOS Linux。,它仍然使用相对较旧版本的 Python。一般安装说明见 https://pip.pypa.io/en/stable/installing/https://packaging.python.org/guides/installing-using-linux-tools/ 。侧栏“在 RedHat 系列 Linux 上安装 pip”包括一个如何在 RedHat Enterprise Linux、Oracle Linux 和 CentOS 上安装pip的示例。

pip可用时,使用install命令安装最新可用的 MySQL 连接器/Python 版本是很简单的。例如,安装的确切输出会有所不同,这取决于是否已经安装了 protobuf 之类的依赖项。输出示例如下

PS: Python> pip install mysql-connector-python
Collecting mysql-connector-python
  Downloading https://files.pythonhosted.org/.../mysql_connector_python-8.0.11-cp36-cp36m-win_amd64.whl
(3.0MB)
    100% |███████████████| 3.0MB 3.5MB/s
Collecting protobuf>=3.0.0 (from mysql-connector-python)
  Using cached https://files.pythonhosted.org/.../protobuf-3.5.2.post1-cp36-cp36m-win_amd64.whl
Requirement already satisfied: six>=1.9 in c:\users\jesper\appdata\local\programs\python\python36\lib\site-packages (from protobuf>=3.0.0->mysql-connector-python)
 (1.11.0)
Requirement already satisfied: setuptools in c:\users\jesper\appdata\local\programs\python\python36\lib\site-packages (from protobuf>=3.0.0->mysql-connector-python)
 (28.8.0)
Installing collected packages: protobuf, mysql-connector-python
Successfully installed mysql-connector-python-8.0.11 protobuf-3.5.2.post1

该示例来自在 PowerShell 中执行pip命令的 Microsoft Windows。该命令假设pip命令位于可执行文件的搜索路径中(这可以在 Windows 上安装 Python 时启用,在 Linux 上通常也是如此)。如果pip命令不在搜索路径中,你必须使用完整路径。当在其他平台上执行安装时,命令是相同的,输出也非常相似。

如果要卸载这个包,命令非常类似;只需使用uninstall命令即可。

因此

PS: Python> pip uninstall mysql-connector-python
Uninstalling mysql-connector-python-8.0.11:

  Would remove:
    c:\users\jesper\appdata\local\programs\python\python36\lib\site-packages\_mysql_connector.cp36-win_amd64.pyd
    c:\users\jesper\appdata\local\programs\python\python36\lib\site-packages\_mysqlxpb.cp36-win_amd64.pyd
    c:\users\jesper\appdata\local\programs\python\python36\lib\site-packages\libeay32.dll
    c:\users\jesper\appdata\local\programs\python\python36\lib\site-packages\libmysql.dll
    c:\users\jesper\appdata\local\programs\python\python36\lib\site-packages\mysql\*
    c:\users\jesper\appdata\local\programs\python\python36\lib\site-packages\mysql_connector_python-8.0.11.dist-info\*
    c:\users\jesper\appdata\local\programs\python\python36\lib\site-packages\mysqlx\*
    c:\users\jesper\appdata\local\programs\python\python36\lib\site-packages\ssleay32.dll
Proceed (y/n)? y
  Successfully uninstalled mysql-connector-python-8.0.11

在 Linux 的 Redhat 系列上安装 pip

在 Oracle Linux、RedHat Enterprise Linux 和 CentOS 上安装pip命令的最佳方式是使用 EPEL Yum 存储库。以下步骤假设您使用的是 Linux 发行版的版本 7。旧版本需要稍微不同的说明。步骤如下:

  1. https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm 下载 EPEL 存储库定义。

  2. 安装下载的 EPEL RPM。

  3. 安装 python-pip 和 python-wheel 包。

  4. 可选地,让pip使用pip install –upgrade pip命令升级自身。

python-wheel 包支持用于 python 包的 wheel 内置包格式。 https://pypi.org/project/wheel/亦见

在 Linux shell 中执行的组合步骤如下:

shell$ wget https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm

...

2018-03-10 20:26:28 (55.3 KB/s) - 'epel-release-latest-7.noarch.rpm' saved [15080/15080]

shell$ sudo yum localinstall epel-release-latest-7.noarch.rpm

...
Downloading packages:
Running transaction check
Running transaction test
Transaction test succeeded
Running transaction
  Installing : epel-release-7-11.noarch                      1/1
  Verifying  : epel-release-7-11.noarch                      1/1

Installed:
  epel-release.noarch 0:7-11

Complete!

shell$ sudo yum install python-pip python-wheel

...
Running transaction check
Running transaction test
Transaction test succeeded
Running transaction
  Installing : python-wheel-0.24.0-2.el7.noarch              1/2
  Installing : python2-pip-8.1.2-5.el7.noarch                2/2
  Verifying  : python2-pip-8.1.2-5.el7.noarch                1/2
  Verifying  : python-wheel-0.24.0-2.el7.noarch              2/2

Installed:
  python-wheel.noarch 0:0.24.0-2.el7
  python2-pip.noarch 0:8.1.2-5.el7

Complete!

shell$ sudo pip install --upgrade pip

Collecting pip
  Downloading pip-9.0.1-py2.py3-none-any.whl (1.3MB)
    100% |████████████████| 1.3MB 296kB/s
Installing collected packages: pip
  Found existing installation: pip 8.1.2
    Uninstalling pip-8.1.2:
      Successfully uninstalled pip-8.1.2
Successfully installed pip-9.0.1

此时,pip命令已经安装为/usr/bin/pip。在大多数情况下,/usr/bin中的命令可以在不指定完整路径的情况下执行。

Microsoft windows-MySQL 安装程序

对于在 Microsoft Windows 上的安装,由于某种原因您不希望使用pip命令,首选的安装方法是 MySQL Installer。一个优点是它可以用来安装 MySQL Connector/Python 的社区版和企业版。安装哪个版本取决于 MySQL 安装程序的版本。

以下说明假设您的计算机上已经安装了 MySQL 安装程序。如果不是这种情况,请参阅“微软视窗的 MySQL 安装程序”的说明。第一步是启动 MySQL 安装程序。首次使用安装程序时,会要求您接受许可条款。然后你会被带到一个屏幕,在这里你可以选择你想要安装的 MySQL 产品。在讨论完图 1-4 之后,我们将再次回到这一点。

如果你已经使用 MySQL Installer 安装产品,会出现图 1-4 的画面;这是已经安装的 MySQL 产品和可用操作的概述。

img/463459_1_En_1_Fig4_HTML.jpg

图 1-4

MySQL 安装程序屏幕显示已经安装的 MySQL 产品

如果您前段时间安装了 MySQL 安装程序,并且最近没有更新目录,建议首先单击右下角的目录操作,以确保您可以从所有最新版本中进行选择。这将把您带到一个屏幕,您可以在那里执行目录更新。该更新不会更改任何已安装的产品;它只更新 MySQL 安装程序用来通知升级的产品列表,您可以在安装新产品时从中选择。

目录更新后,您可以使用已安装产品列表右侧的 Add 操作添加新产品。这将把你带到图 1-5 所示的屏幕,这也是你第一次启动 MySQL 安装程序时被直接带到的屏幕。

img/463459_1_En_1_Fig5_HTML.jpg

图 1-5

选择要安装的内容

顶部的过滤器可用于缩小或扩大应包含的产品和版本。默认情况下,32 位和 64 位体系结构中的所有软件都包含最新的 GA 版本。如果您想要尝试一个开发里程碑版本或候选版本,您需要通过编辑过滤器来包含预发布版本。在图 1-6 中可以看到一个在 MySQL Connectors 类别下过滤搜索 Connector/Python GA 版本并要求其为 64 位的示例。

img/463459_1_En_1_Fig6_HTML.jpg

图 1-6

过滤产品列表

通过展开 MySQL 连接器组,可以在可用产品下找到 MySQL 连接器/Python。每个受支持的 Python 版本都有一个产品列表。MySQL 安装程序将检查是否安装了正确的 Python 版本。找到正确的版本后,点击指向右侧的箭头,然后点击下一步,将其添加到要安装的产品和功能列表中。

下一个屏幕显示了要安装的产品的概述。确认一切正确后,点击执行开始安装。执行可能需要一点时间,因为它包括下载连接器。一旦安装完成,点击下一步。这将允许您将日志复制到剪贴板并完成。

Microsoft Windows 的 MySQL 安装程序

用于 Microsoft Windows 的 MySQL 安装程序是管理各种 MySQL 产品的入口点(MySQL NDB 集群是一个例外)。它允许您从一个界面安装、升级和删除产品和功能。对配置 MySQL 服务器的支持也是有限的。

MySQL 安装程序有两种风格:一种包含 MySQL Server 版本,另一种不包含(“web”下载)。如果您知道您将安装 MySQL Server,那么使用捆绑了 MySQL Server 的下载会很方便,因为这样可以节省安装时间。无论选择哪种方式,如果您没有准备好本地安装文件,MySQL 安装程序都会下载该产品作为安装的一部分。

要安装 MySQL 安装程序,请按照以下步骤操作:

  1. 下载 MySQL 安装程序。如果您使用的是 MySQL 产品的社区版,请从 https://dev.mysql.com/downloads/installer/ 下载。如果使用企业版,从我的甲骨文支持( https://support.oracle.com/ )或甲骨文软件交付云( https://edelivery.oracle.com/ )下载。企业版的两个位置都需要使用现有的 Oracle 帐户或创建一个新帐户。如果您是现有客户,建议使用 My Oracle Support。

  2. 下载的文件是一个 MSI 安装程序,但对于企业版,它将包含在一个 zip 文件中,您可以解压缩该文件,然后执行 MSI 安装程序并按照说明进行操作。

  3. 如果您还没有下载最新版本的 MySQL 安装程序,您将有机会升级它。建议这样做。

  4. 安装完成后,MySQL 安装程序会自动启动。

安装程序也可以稍后启动,例如通过开始菜单。

Linux–MySQL Yum 存储库

在 Linux 发行版的 Community Edition 中安装 MySQL 产品的最简单方法是使用 MySQL 存储库。对于 RedHat Enterprise Linux、Oracle Linux、CentOS 和 Fedora,这意味着 MySQL Yum 存储库。这样,yum命令就可以找到包,Yum 将能够自动解析依赖关系。除了使用pip命令安装 MySQL Connector/Python 之外,如果您希望安装得到管理,这是安装 MySQL 软件的推荐方式。

管理安装意味着安装程序(pipyum)为您处理依赖关系,并且可以使用安装程序请求升级。对于安装和升级,软件都是自动从存储库中下载的。

MySQL Yum 库是使用 RPM 安装的,可以从 https://dev.mysql.com/downloads/repo/yum/ 下载。选择与您的 Linux 发行版相对应的 RPM。例如,可以使用yum localinstall命令安装 RPM:

shell$ sudo yum localinstall \
            mysql57-community-release-el7-11.noarch.rpm
...
Running transaction
  Installing : mysql57-community-release-el7-11.noarch      1/1
  Verifying  : mysql57-community-release-el7-11.noarch      1/1

Installed:
  mysql57-community-release.noarch 0:el7-11                     

Complete!

MySQL RPMs 用 GnuPG 签名。为了让rpm命令(由yum调用)检查签名并且不抱怨丢失密钥,您需要安装 MySQL 使用的公钥。有几种方法可以做到这一点,如 https://dev.mysql.com/doc/refman/en/checking-gpg-signature.html 中所述。一种选择是从该页面获取公钥,并将其保存在一个文件中。你需要从-----BEGIN PGP PUBLIC KEY BLOCK-----开始到-----END PGP PUBLIC KEY BLOCK-----结束的部分(包括一切)。将密钥保存到一个文件中,例如命名为mysql_pubkey.asc。然后,您将密钥导入 RPM 的密匙环:

shell$ sudo rpm --import mysql_pubkey.asc

一旦安装了存储库和公钥,就可以安装 MySQL Connector/Python,如清单 1-1 所示。

shell$ sudo yum install mysql-connector-python \
                        mysql-connector-python-cext
...
Downloading packages:
(1/2): mysql-connector-python-8.0.11-1.el7.x86_64.rpm | 418 kB 00:00
(2/2): mysql-connector-python-cext-8.0.11-1.el7.x86_6 | 4.8 MB 00:01
---------------------------------------------------------------
Total                                   3.3 MB/s | 5.2 MB 00:01
Running transaction check
Running transaction test
Transaction test succeeded
Running transaction
  Installing : mysql-connector-python-8.0.11-1.el7.x86_64   1/2
  Installing : mysql-connector-python-cext-8.0.11-1.el7.x86_64   2/2
  Verifying  : mysql-connector-python-8.0.11-1.el7.x86_64        1/2
  Verifying  : mysql-connector-python-cext-8.0.11-1.el7.x86_64   2/2

Installed:
  mysql-connector-python.x86_64 0:8.0.11-1.el7
  mysql-connector-python-cext.x86_64 0:8.0.11-1.el7

Complete!

Listing 1-1Installing MySQL Connector/Python Using Yum on Linux

这段代码安装了 MySQL Connector/Python 的纯 Python 和 C 扩展(名称中带有 cext )实现。在继续之前,让我们验证一下 MySQL 连接器/Python 的安装。

验证安装

验证 MySQL Connector/Python 安装工作的一个简单方法是创建一个小的测试程序来打印来自mysql.connector模块的一些属性。如果程序执行时没有错误,则安装成功。

清单 1-2 展示了一个检索 MySQL 连接器/Python 版本以及一些其他属性的例子。

import mysql.connector

print(
  "MySQL Connector/Python version: {0}"
  .format(mysql.connector.__version__)
)

print("Version as tuple:")
print(mysql.connector.__version_info__)

print("")
print("API level: {0}"
  .format(mysql.connector.apilevel))

print("Parameter style: {0}"
  .format(mysql.connector.paramstyle))

print("Thread safe: {0}"
  .format(mysql.connector.threadsafety))

Listing 1-2Verifying That the MySQL Connector/Python Installation Works

版本以两种不同的方式打印,一种是字符串,另一种是元组。如果您需要一个应用与 MySQL Connector/Python 的两个不同版本兼容,并且根据版本需要不同的代码路径,那么 tuple 会很有用。

API 级别、参数样式和线程安全属性通常不会改变。它们与mysql.connector模块实现的 Python 数据库 API 规范( https://www.python.org/dev/peps/pep-0249/ )有关。这三个属性是模块必需的全局属性。

使用 MySQL 连接器/Python 8.0.11 时的输出是

PS: Chapter 1> python listing_1_1.py
MySQL Connector/Python version: 8.0.11
Version as tuple:
(8, 0, 11, '', 1)

API Level: 2.0
Parameter style: pyformat
Thread safe: 1

MySQL 服务器

MySQL 连接器/Python 本身价值不高。除非您有一个 MySQL 服务器实例可以连接,否则您将被限制在检查版本之类的事情上,就像上一节中的例子一样。因此,如果您还没有安装 MySQL 服务器的权限,您也需要安装它。本节将简要概述 MySQL 服务器的安装和配置。

装置

如果您使用 MySQL 安装程序或 MySQL Yum 存储库,MySQL Server 的安装过程类似于针对 MySQL Connector/Python 描述的步骤。在这两种情况下,安装程序都会为您设置 MySQL。此外,还可以选择在 Microsoft Windows 上使用 zip 归档文件安装,或者在 Linux、macOS、Oracle Solaris 和 FreeBSD 上使用 tar 归档文件安装。

注意

此讨论假设一个新的安装。如果已经安装了 MySQL,也可以选择升级。但是,如果您当前的 MySQL 安装不是来自 MySQL 存储库,那么最好先删除现有的安装以避免冲突,然后进行全新安装。

由于在使用安装程序时,MySQL Server 的安装步骤与 MySQL Connector/Python 非常相似,所以本文将重点讨论使用 zip 或 tar 归档文件进行安装。在 Linux 上使用安装程序时,关于检索为管理员帐户(root@localhost)设置的密码的讨论也是相关的。由于 MySQL 安装程序是一个交互式安装程序,它会问你密码应该是什么,并为你设置。

小费

本书中关于 MySQL 服务器安装的讨论只涉及了一些基础知识。完整的安装说明见 https://dev.mysql.com/doc/refman/en/installing.htmlhttps://dev.mysql.com/doc/refman/en/data-directory-initialization-mysqld.html

如果您需要在同一台计算机上安装多个不同的版本,使用 zip 或 tar 归档文件会特别有用,因为它允许您找到您喜欢的安装位置。如果选择这种方法,您需要手动初始化数据目录。在以下示例中可以看到在 Microsoft Windows 上这样做的示例:

PS: Python> D:\MySQL\mysql-8.0.11-winx64\bin\mysqld
              --basedir=D:\MySQL\mysql-8.0.11-winx64
              --datadir=D:\MySQL\Data_8.0.11
              --log_error=D:\MySQL\Data_8.0.11\error.log
              --initialize

该命令被分成几行,以提高可读性。确保在执行时将所有部分组合成一行。该命令可能需要一点时间才能完成,尤其是在非基于内存(即旋转)的磁盘驱动器上安装时。

Linux 和其他类 Unix 系统中的命令非常相似,除了添加了-- user选项:

shell$ /MySQL/base/8.0.11/bin/mysqld \
          --basedir=/MySQL/base/8.0.11/ \
          --datadir=/MySQL/Data_8.0.11 \
          --log_error=/MySQL/Data_8.0.11/error.log \
          --user=mysql \
          --initialize

这两个命令使用几个参数。他们是

  • --basedir:这个选项告诉 MySQL 服务器二进制文件、库等在哪里。已安装。这个目录包括一个 bin、lib、share 和更多子目录,其中包含 MySQL 服务器所需的文件。

  • --datadir:该选项告知数据的存储位置。这是由命令初始化的目录。此目录必须不存在或为空。如果它不存在,--log_error选项不能指向数据目录中的文件。

  • --log_error:该选项告知日志信息的写入位置。

  • --user:在 Linux 和 Unix 上,这个选项用来告诉 MySQL 将以哪个用户的身份执行。只有当您以 root 用户身份初始化数据目录时,这才是必需的(但通常是允许的)。在这种情况下,MySQL 将确保新创建的文件归由--user参数指定的用户所有。传统上使用的用户是mysql用户,但是对于个人测试实例,您也可以使用您的普通登录用户。

  • --initialize:这个选项告诉 MySQL 初始化数据目录。

初始化包括设置root@localhost账户的密码。密码是随机的,可以在错误日志中找到;这也适用于使用 RPMs 等工具安装 MySQL 的情况。但是,MySQL 安装程序会在安装过程中要求输入密码并进行设置。如果您使用的是 macOS,密码也会显示在通知中。包含临时密码的错误日志示例如下

2018-03-11T05:01:08.871014Z 0 [System] [MY-010116] D:\MySQL\mysql-8.0.4-rc-winx64\bin\mysqld.exe (mysqld 8.0.4-rc) starting as process 3964 ...
2018-03-11T05:01:20.240818Z 0 [Warning] [MY-010068] CA certificate ca.pem is self signed.

2018-03-11T05:01:20.259178Z 5 [Note] [MY-010454] A temporary password is generated for root@localhost: fj3dJih6Ao*T

首次连接时,您需要此密码。连接后,您必须先更改密码,然后才能执行常规查询,因为安装过程中生成的随机密码会被标记为过期。您使用ALTER USER语句更改密码,如清单 1-3 所示。

PS: Python> D:\MySQL\mysql-8.0.11-winx64\bin\mysql --user=root --password
Enter password: ************
Welcome to the MySQL monitor.  Commands end with ; org.
Your MySQL connection id is 7
Server version: 8.0.11 MySQL Community Server - GPL

Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved. Oracle is a registered trademark ofOracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> ALTER USER 'root'@'localhost' IDENTIFIED BY '&lknjJ2lAc1)#';

Query OK, 0 rows affected (0.15 sec)

Listing 1-3Changing the Password of the root@localhost Account

请确保您选择了一个别人很难猜到的密码。您还可以使用该命令更改用户的其他设置,如 SSL 要求、身份验证插件等。

小费

MySQL 8.0 中默认的认证插件是caching_sha2_password插件。它提供了很好的安全性,因为它基于 sha256 加盐散列。同时,缓存使其性能良好。然而,由于它是一个新的认证插件,旧的连接器和客户端包括 MySQL Connector/Python 2.1.7 和更早的版本,MySQL Server 5.7 的 MySQL 命令行客户端,以及 PHP 和 Perl 等第三方连接器在编写时都不支持caching_sha2_password插件。如果您需要使用这些连接器或客户端中的一个来连接,您可以使用旧的mysql_native_password插件来代替(因为它是基于 sha1 的,没有 salted,所以不太安全)。有关CREATE USERALTER USER语句语法的更多信息,请参见 https://dev.mysql.com/doc/refman/en/create-user.htmlhttps://dev.mysql.com/doc/refman/en/alter-user.html

除非您使用 MySQL 安装程序,否则该实例将使用默认配置。最后要考虑的是如何改变配置。

配置

在某些情况下,有必要更改新安装的 MySQL 服务器实例的配置。通常,默认值是一个很好的起点。显然,对于生产服务器来说,有些更改是必需的,但通常情况下,少量更改比大量更改要好。对于本书中的示例来说,默认配置很好。

也就是说,在数据目录被手动初始化的例子中,您已经看到了一些非默认设置。此外,如果您正在开发将部署到生产环境的应用,建议使用尽可能接近生产环境的配置,以避免因差异而出现问题。当然,这并不意味着您正在开发的桌面应该能够为 InnoDB 缓冲池分配半 TB 的内存,因为生产服务器正在使用它,您可以使用一个类似的配置,但是要缩小规模。

小费

您可以在参考手册的 https://dev.mysql.com/doc/refman/en/server-system-variables.htmlhttps://dev.mysql.com/doc/refman/en/option-files.html 阅读更多关于配置 MySQL 的信息,包括选项的完整列表。

一般来说,最好使用 MySQL 配置文件来设置任何必需的选项。这样可以避免启动 MySQL 时遗漏一些选项。然后,您可以使用带有配置文件路径的--defaults-file选项启动 MySQL 守护进程(Linux/Unix 上的mysqld和 Microsoft Windows 上的mysqld.exe)。按照惯例,MySQL 配置文件在微软 Windows 上被命名为my.ini,在其他平台上被命名为my.cnf

如果您使用的是 Microsoft Windows 并选择安装 MySQL 作为服务,您将通过控制面板应用启动和停止 MySQL 服务(或让 Microsoft Windows 自动完成)。在这种情况下,配置文件甚至更有用,因为您可以指定在服务定义中使用它,这样可以避免在以后想要更改 MySQL 配置时修改服务。

配置文件遵循 INI 文件格式。下面是一个示例,其中包含本节前面 Microsoft Windows 上的初始化选项以及 TCP 端口号:

[mysqld]
basedir   = D:\MySQL\mysql-8.0.11-winx64
datadir   = D:\MySQL\Data_8.0.11
log_error = D:\MySQL\Data_8.0.11\error.log
port      = 3306

关于安装和配置 MySQL 服务器的讨论到此结束。一个相关的主题是如何创建应用用来连接 MySQL 的数据库用户。

创建应用用户

当应用连接到 MySQL 时,需要指定用于连接的用户名。此外,MySQL 考虑了连接来自的主机名,因此用户的帐户名形成为username@hostname。用户的权限决定了允许用户在数据库中做什么。

MySQL 服务器有一个可用于登录的标准用户,即root@localhost用户。该用户拥有所有权限;也就是说,它是一个管理员帐户。出于几个原因,应用不应该使用这个用户,这将在下面的讨论中解释。

一般来说,应用不应该拥有做任何事情的权限。例如,不应该允许应用访问它不需要的表,并且很少要求应用管理用户。此外,MySQL 对允许的并发连接数有限制(max_connections配置选项)。但是,为拥有CONNECTION_ADMIN ( SUPER)权限的用户保留了一个额外的连接。因此,如果应用用户拥有所有特权,它可以阻止数据库管理员调查为什么所有连接都在使用中。

深入 MySQL 特权系统的细节已经超出了本书的范围。主要的要点是,您应该为您的用户分配所需的最低权限,包括在开发阶段,因为当您准备好部署应用时,根据需要添加新权限比删除不必要的权限要容易得多。

小费

熟悉 MySQL 的安全特性是值得的,包括访问权限系统和用户帐户管理。MySQL 参考手册中的安全章节是一个极好的来源: https://dev.mysql.com/doc/refman/en/security.html

以下 SQL 语句可用于创建一个测试用户,该用户拥有本书中示例所需的权限:

mysql> CREATE USER 'pyuser'@'localhost'
              IDENTIFIED BY 'Py@pp4Demo';

mysql> GRANT ALL PRIVILEGES
             ON world.*
             TO 'pyuser'@'localhost';

mysql> GRANT ALL PRIVILEGES
             ON py_test_db.*
             TO 'pyuser'@'localhost';

假设测试程序将在安装 MySQL 的同一台主机上执行。如果不是这样,用执行测试程序的主机名替换localhostGRANT语句中的ALL PRIVILEGES给出了模式(数据库)级别上所有可用的特权,但不包括管理特权。这仍然超出了典型应用的需求,但是这里使用它是为了简单起见,并允许演示通常不会在应用中执行的查询。

密码被选择为Py@pp4Demo。这不是一个非常强的密码,强烈建议使用一个更难猜到的不同密码。

如果您想使用第七章中简要提到的world_x样本数据库,您还需要以下权限:

mysql> GRANT ALL PRIVILEGES
             ON world_x.*
             TO 'pyuser'@'localhost';

然而,本书中讨论的例子都没有使用world_x示例数据库。world_x示例数据库的安装说明与下一步非常相似,就是为第 3 、 4 和 5 章中的代码示例安装一些示例数据。

安装世界样本数据库

在整本书中,世界样本数据库被用于几个例子。示例数据库被视为“其他 MySQL 文档”的一部分,可以从 https://dev.mysql.com/doc/index-other.html 访问。世界数据库可以作为 gzip 文件或 zip 文件下载;无论哪种方式,解压后都是单个文件:world.sql

注意

world数据库和world_x数据库。第 3 、 4 和 5 章使用world数据库。world_x数据库不是必需的,但是如果您想让它用于您自己的测试,可以使用类似的步骤安装。

world.sql文件是独立的。如果存在的话,它将删除world模式,并用三个表重新创建它:countrycountrylanguagecity,包括一些示例数据。应用world.sql文件最简单的方法是使用与world.sql文件所在目录相同的mysql命令行客户端( https://dev.mysql.com/doc/refman/en/mysql.html ):

shell$ mysql --user=pyuser --password \
             --host=127.0.0.1 --port=3306 \
             --execute="SOURCE world.sql"
Enter password:

这假设mysql二进制文件在执行搜索路径中;否则,必须使用完整路径。在 Microsoft Windows 上,将整个命令放在同一行,并删除反斜杠。结果表格在清单 1-4 中列出。

mysql> SHOW TABLES FROM world;
+-----------------+
| Tables_in_world |
+-----------------+
| city            |
| country         |
| countrylanguage |
+-----------------+
3 rows in set (0.00 sec)

mysql> SHOW CREATE TABLE world.city\G
*************************** 1\. row ***************************
       Table: city
Create Table: CREATE TABLE `city` (
  `ID` int(11) NOT NULL AUTO_INCREMENT,
  `Name` char(35) NOT NULL DEFAULT '',
  `CountryCode` char(3) NOT NULL DEFAULT '',
  `District` char(20) NOT NULL DEFAULT '',
  `Population` int(11) NOT NULL DEFAULT '0',
  PRIMARY KEY (`ID`),
  KEY `CountryCode` (`CountryCode`),
  CONSTRAINT `city_ibfk_1` FOREIGN KEY (`CountryCode`) REFERENCES `country` (`code`)
) ENGINE=InnoDB AUTO_INCREMENT=4080 DEFAULT CHARSET=latin1
1 row in set (0.00 sec)

mysql> SELECT COUNT(*) FROM world.city;
+----------+
| COUNT(*) |
+----------+
|     4079 |
+----------+
1 row in set (0.00 sec)

mysql> SHOW CREATE TABLE world.country\G
*************************** 1\. row ***************************
       Table: country
Create Table: CREATE TABLE `country` (
  `Code` char(3) NOT NULL DEFAULT '',
  `Name` char(52) NOT NULL DEFAULT '',
  `Continent` enum('Asia','Europe','North America','Africa','Oceania','Antarctica','South America') NOT NULL DEFAULT 'Asia',
  `Region` char(26) NOT NULL DEFAULT '',
  `SurfaceArea` float(10,2) NOT NULL DEFAULT '0.00',
  `IndepYear` smallint(6) DEFAULT NULL,
  `Population` int(11) NOT NULL DEFAULT '0',
  `LifeExpectancy` float(3,1) DEFAULT NULL,
  `GNP` float(10,2) DEFAULT NULL,
  `GNPOld` float(10,2) DEFAULT NULL,
  `LocalName` char(45) NOT NULL DEFAULT '',
  `GovernmentForm` char(45) NOT NULL DEFAULT '',
  `HeadOfState` char(60) DEFAULT NULL,
  `Capital` int(11) DEFAULT NULL,
  `Code2` char(2) NOT NULL DEFAULT '',
  PRIMARY KEY (`Code`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
1 row in set (0.00 sec)

mysql> SELECT COUNT(*) FROM world.country;
+----------+
| COUNT(*) |
+----------+
|      239 |
+----------+
1 row in set (0.00 sec)

mysql> SHOW CREATE TABLE world.countrylanguage\G
*************************** 1\. row ***************************
       Table: countrylanguage
Create Table: CREATE TABLE `countrylanguage` (
  `CountryCode` char(3) NOT NULL DEFAULT '',
  `Language` char(30) NOT NULL DEFAULT '',
  `IsOfficial` enum('T','F') NOT NULL DEFAULT 'F',
  `Percentage` float(4,1) NOT NULL DEFAULT '0.0',
  PRIMARY KEY (`CountryCode`,`Language`),
  KEY `CountryCode` (`CountryCode`),
  CONSTRAINT `countryLanguage_ibfk_1` FOREIGN KEY (`CountryCode`) REFERENCES `country` (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
1 row in set (0.00 sec)

mysql> SELECT COUNT(*) FROM world.countrylanguage;
+----------+
| COUNT(*) |
+----------+
|      984 |
+----------+
1 row in set (0.00 sec)

Listing 1-4The Tables of the world Sample Database

小费

有关世界样本数据库的更多信息,包括安装说明,请参见 https://dev.mysql.com/doc/world-setup/en/world-setup-installation.html

在结束这一章之前,我们需要了解一下代码示例。

代码示例

这本书里有许多示例程序。这些程序已经用 Python 3.6 测试过了。对于其他 Python 版本,包括来自 Oracle Linux 7/Red Hat Enterprise Linux(RHEL)7/CentOS 7 的 Python 2.7,这些示例只需稍加修改即可使用。MySQL 连接器/Python 特定的部分不需要任何更改。

在 Python 2 中,建议加载打印函数from__future__:

from __future__ import print_function

此外,Python 2 中的 UTF-8 字符串处理是不同的,因此可能有必要使用encode()方法来打印字符串。例如:

print(
  "{0:15s}   {1:⁷s}   {2:4.1f}".format(
    city['Name'].encode('utf8'),
    city['CountryCode'].encode('utf8'),
    city['Population']/1000000
  )
)

使用mysql.connector模块的例子假设一个名为my.ini的文件存在于执行 Python 的目录中,该文件带有连接 MySQL 服务器所需的连接选项。一个示例配置文件是

[connector_python]
user     = pyuser
host     = 127.0.0.1
port     = 3306
password = Py@pp4Demo

使用mysqlx模块的示例将配置存储在名为config.py的文件中,该文件也位于执行 Python 的同一个目录中。示例配置如下

connect_args = {
  'host': '127.0.0.1',
  'port': 33060,
  'user': 'pyuser',
  'password': 'Py@pp4Demo',
};

示例中的编码风格是针对印刷品,尤其是 Kindle 等电子书阅读器而优化的。因为这留下了很少的空间来处理,所以这些行通常保持在 40 个字符以下,而“长”行最多 50 个字符,以最小化换行量。不利的一面是,这意味着不可能遵循一种标准的编码风格,例如 PEP 8 中指定的风格( https://www.python.org/dev/peps/pep-0008/ )。建议在自己的项目中遵循 PEP 8 或另一个成熟的编码标准。

列表中出现的所有示例程序都可以下载。文件名反映了清单编号;例如,清单 1-2 中的代码可以在文件listing_1_2.py中找到。有关如何下载源代码的说明,请参见该书的主页。

安装和准备工作到此结束。下一步是创建从 MySQL 连接器/Python 到 MySQL 服务器的连接,这是下一章的主题。

摘要

这一章帮助你开始并运行。首先介绍了 MySQL 连接器/Python。最新的 GA 发布系列是 8.0 版,与大多数其他 MySQL 产品一样。MySQL 产品有社区版和商业企业版。对于 MySQL Connector/Python,主要区别是许可和支持,两个版本都可以和本书一起使用。

MySQL Connector/Python 有三个 API:两个仅支持 SQL 语句的遗留 API 和一个支持 NoSQL 和 SQL 查询的名为 X DevAPI 的新 API。如何使用这三个 API 是本书其余部分的主题。

为了开始,您下载并安装了 MySQL Connector/Python 和 MySQL Server。有一个关于配置 MySQL 服务器的简短讨论,关于如何创建可用于本书的用户的说明,如何安装测试数据,以及关于代码示例的一句话。

你已经准备好使用 MySQL 连接器/Python 了。下一章将向您展示如何使用连接器/Python API 中的 API(mysql.connector模块)进行连接。

二、连接到 MySQL

在前一章中,您安装了 MySQL Connector/Python,并确保该模块工作正常。然而,打印连接器的版本字符串并不令人兴奋,所以本章将开始两个遗留 API 的特性之旅。

mysql.connector模块包括 Python 数据库 API 的实现,在 PEP249 ( https://www.python.org/dev/peps/pep-0249/ )中定义。这包括在使用相同 API 的同时使用 C 扩展的选项。这个 API 是第 2 - 5 章的主要焦点。此外,第四章简要讨论了实现 C 扩展 API 的_mysql_connector模块。

本章详细介绍了创建和配置 MySQL 连接的细节。建立联系很简单,也是你要学的第一件事。然而,这种联系不仅仅是创造出来的。本章的其余部分将讨论如何配置连接,包括避免将用户名和密码硬编码到应用中的技巧。本章最后讨论了其他与连接相关的选项,特别关注字符集。

从 Python 创建连接

到这一步需要做一些工作,但是现在您已经准备好第一次从 Python 连接到 MySQL 了。本节将介绍创建连接的语法、最常见的连接选项、创建连接、重新配置连接的示例以及一些连接的最佳实践。

句法

有几种方法可以创建连接。其中四个是

  • mysql.connector. connect()功能:这是最灵活的连接方式。它提供了一种使用 C 扩展创建连接或者启用连接池和故障转移相关选项的统一方式。该函数作为包装器工作,根据设置返回适当类的对象。

  • MySQLConnection()构造函数

  • MySQLConnection.connect()方法:它要求首先实例化不带参数的MySQLConnection类,然后创建连接。

  • 与之前使用的MySQLConnection.connect()方法相同,但不同之处在于MySQLConnection.config()方法被显式调用来配置连接。

MySQLConnection类是纯 Python 实现。或者,可以使用CMySQLConnection类,它为 Python 数据库 API 提供了 C 扩展后端的实现。

所有方法都以相同的连接对象结束,并且它们都将连接选项作为关键字参数。这意味着您可以选择任何方式来创建最适合程序的连接。然而,由于mysql.connector. connect()函数是最强大的,它是连接的首选方式,因为它使得在纯 Python 和 C 扩展实现之间切换或者启用连接池或故障转移变得更加容易。

小费

使用mysql.connector.connect()功能创建连接可访问所有与连接相关的功能。

图 2-1 显示了使用四种方式创建连接的基本流程。红色(深灰色)框直接从应用代码调用,黄色(浅灰色)框由最后一个间接调用的方法调用。该图使用了MySQLConnection类;然而,如果使用了CMySQLConnection类,同样适用。

img/463459_1_En_2_Fig1_HTML.jpg

图 2-1

创建连接的流程

最左边的路线是使用mysql.connector. connect()功能的路线。Python 程序调用带有连接参数的函数,然后该函数处理其余部分。该图假设创建了一个MySQLConnection连接(使用纯 Python 实现),但是如果使用 C 扩展,该函数也可以返回一个CMySQLConnection对象。mysql.connector.connect()函数的基本语法是

db = mysql.connector.connect(**kwargs)

左起第二条路径让 Python 程序在实例化MySQLConnection类时将连接参数发送给构造函数。这将触发构造函数调用connect()方法,后者又调用config()方法。使用MySQLConnection类时的语法是

db = mysql.connector.MySQLConnection(**kwargs)

在左起第三条路径中,首先实例化了MySQLConnection类,然后显式调用了connect()方法。代码语法变成了

db = mysql.connector.MySQLConnection()
db.connect(**kwargs)

最后,在最右边的路径中,所有步骤都是显式完成的。注意,与创建连接的其他三种方式相比,在这种情况下调用connect()config()方法的顺序颠倒了。语法是

db = mysql.connector.MySQLConnection()
db.config(**kwargs)
db.connect()

在创建一些真正的连接之前,有必要看一看创建连接时最常用的选项。

常见连接选项

表 2-1 总结了最常用的选项,用于指定如何连接到 MySQL、作为谁进行身份验证以及使用哪个密码。

表 2-1

常见的连接相关选项

|

争吵

|

缺省值

|

描述

|
| --- | --- | --- |
| host | 127.0.0.1 | 安装了您要连接的 MySQL 实例的主机的主机名。默认设置是连接到回环(即本地主机)。 |
| port | 3306 | MySQL 正在监听的端口。端口 3306 是标准的 MySQL 端口。 |
| unix_socket |   | 在 Linux 和 Unix 上,可以使用 Unix 套接字连接到本地主机上的 MySQL 实例。指定套接字文件的路径。 |
| user |   | 应用用户的用户名。不要包含@和以下主机名;这是为第一章中创建的测试用户准备的。只需指定pyuser。 |
| password |   | 用来进行身份验证的密码。对于测试用户,这将是Py@pp4Demo。 |
| ssl_ca |   | 包含 SSL 证书颁发机构(CA)的文件的路径。 |
| ssl_cert |   | 包含公共 SSL 证书的文件的路径。 |
| ssl_cipher |   | 用于连接的 SSL 密码。通过使用 SSL 连接 MySQL 并执行查询SHOW GLOBAL STATUS LIKE‘SSL _ cipher _ list’,可以获得有效密码的列表;当前使用的密码可以通过 Ssl_cipher 会话状态变量来确定。 |
| ssl_disabled |   | 强制使用非 SSL 连接。 |
| ssl_key |   | 包含私有 SSL 密钥的文件的路径。 |
| ssl_verify_cert | False | MySQL 连接器/Python 是否应该根据用ssl_ca选项指定的 CA 来验证 MySQL 服务器使用的证书。 |

例如,如果您使用过 MySQL 命令行客户端,那么这些选项名称可能看起来很熟悉。这不是巧合。使用这些选项,可以演示如何创建连接。

小费

本章中有许多示例程序。列表中出现的所有示例程序都可以下载。有关使用示例程序的更多信息,参见第一章中对示例程序的讨论。

连接示例

是时候将创建连接的四种方式以及最常见的连接选项结合起来,创建用于创建 MySQL Connector/Python 连接的源代码示例了。清单 2-1 展示了如何使用创建连接的四种方式进行连接。这些示例的顺序与本节前面讨论的顺序相同。

import mysql.connector

connect_args = {
  "host": "127.0.0.1",
  "port": 3306,
  "user": "pyuser",
  "password": "Py@pp4Demo",
};

# ---- connect() function ----

db1 = mysql.connector.connect(

  **connect_args

)

print(
  "MySQL connection ID for db1: {0}"
  .format(db1.connection_id)
)

db1.close()

# ---- Explicit MySQLConnection ----

db2 = mysql.connector.MySQLConnection(

  **connect_args

)

print(
  "MySQL connection ID for db2: {0}"
  .format(db2.connection_id)
)

db2.close()

# ---- Two steps manually ----

db3 = mysql.connector.MySQLConnection()

db3.connect(**connect_args)

print(
  "MySQL connection ID for db3: {0}"
  .format(db3.connection_id)
)

db3.close()

# ---- All three steps manually ----

db4 = mysql.connector.MySQLConnection()

db4.config(**connect_args)

db4.connect()

print(
  "MySQL connection ID for db4: {0}"
  .format(db4.connection_id)
)

db4.close()

Listing 2-1Examples of Connecting to MySQL

这四种连接使用相同的连接选项。一旦创建了连接,就使用连接的connection_id属性打印连接的连接 ID(来自 MySQL 服务器端)。最后,使用close()方法关闭连接。当应用完成连接时,最好总是显式关闭连接。

小费

当你完成时,总是关闭连接。关闭连接可以确保与 MySQL 服务器完全断开连接。在服务器终止连接之前,可能还需要一些时间;同时,它占用了一个可用的连接。

输出类似于以下示例,只是连接 id 不同:

MySQL connection ID for db1: 13
MySQL connection ID for db2: 14
MySQL connection ID for db3: 15
MySQL connection ID for db4: 16

还可以为现有的连接调用config()方法。接下来让我们讨论如何重新配置连接和重新连接。

重新配置和重新连接

通常不这样做,但是可以重新配置现有的连接并重新连接。在这种情况下,重新配置意味着可能要更改所有选项,包括应用所连接的 MySQL 服务器实例。当进行这样的更改时,有必要明确地告诉 MySQL Connector/Python 重新连接。

要重新配置一个连接,使用config()方法,方法与初始连接之前相同。一旦创建了所需的新配置,如果任何配置更改需要新的连接,就调用reconnect()方法。调用reconnect()关闭旧的连接并用新的配置创建一个新的连接。清单 2-2 展示了一个重新配置连接的例子。

import mysql.connector

initial_args = {
  "host": "127.0.0.1",
  "port": 3306,
  "user": "pyuser",
  "password": "Py@pp4Demo",
};

# Create initial connection
db = mysql.connector.connect(
  **initial_args
)

print(
  "Initial MySQL connection ID ...: {0}"
  .format(db.connection_id)
)

new_args = {

  "host": "<your_IP_goes_here_in_quotes>",

};

db.config(**new_args)

db.reconnect()

print(
  "New MySQL connection ID .......: {0}"
  .format(db.connection_id)
)

db.close()

Listing 2-2Reconfiguring a Connection

这个例子要求在同一个 MySQL 实例上有第二个用户帐户。用户与现有的pyuser@localhost连接相同,但被定义为从公共 IP 地址连接(替换为您计算机的 IP 地址,或者如果 IP 地址解析为主机名):

mysql> CREATE USER pyuser@'<your_IP_goes_here_in_quotes>'
                   IDENTIFIED BY 'Py@pp4Demo';
Query OK, 0 rows affected (0.84 sec)

任何防火墙允许连接也是必要的。

从示例中可以看出,没有必要更改所有的配置选项。那些没有在新选项集中显式设置的值将保持原来的值。程序的输出是(除了 IDs 之外)

Initial MySQL connection ID ...: 21
New MySQL connection ID .......: 22

本节的最后一个主题是一些关于连接的最佳实践。

连接最佳实践

当涉及到连接时,有一些很好的最佳实践可以遵循。最佳实践总是很困难,因为所有应用都有独特的需求。因此,建议将集中在 MySQL 连接器/Python 的技术方面。

主要的最佳实践是

  • 当你完成时,总是关闭连接。这个已经讨论过了。

  • 使用 SSL (TLS)加密连接。如果您要连接到远程主机,这一点尤其重要,如果连接是在不安全的网络上,这一点更为重要。一个例外是使用 Unix 套接字进行连接,因为即使不使用 SSL,这也总是被认为是安全的连接。

  • 不要将配置硬编码到源代码中。这尤其适用于密码。

注意

虽然 MySQL 使用ssl_作为与加密连接相关的选项的前缀,但实际上使用的是 TLS。

在 MySQL 8.0 和 MySQL 5.7 的某些版本中,默认情况下使用自签名证书启用 SSL,MySQL Connector/Python 默认情况下将使用加密连接。

到目前为止,这些例子有一个主要的缺陷:它们不仅硬编码了连接到哪里,还硬编码了用户名,尤其是密码。这使得代码更难维护,而且这也是一个安全问题,因为任何有权访问源代码的人都可以看到密码。硬编码连接选项还意味着开发和生产系统必须共享连接选项,或者部署过程需要更改源代码来更新配置参数。

警告

不要将密码存储在源代码中。

两者都不是好的选择,因此必须找到替代方案。下一节将讨论另一种选择:使用配置文件。

配置文件

在调用中直接指定连接选项以创建到 MySQL 的连接的方法对于快速测试非常有用,但是在实际应用中这样做既不实用也不安全(对于密码来说)。这一节将首先讨论一些替代方案,然后将详细讨论如何使用 MySQL 配置文件。

硬编码配置的替代方法

有几种方法可以避免将连接配置硬编码到源代码中。每种方法都有利弊,所以这不是一个一刀切的问题。将讨论四种方法:

  • 交互询问信息

  • 使用环境变量

  • 从应用自己的配置文件或作为命令行参数读取信息

  • 使用 MySQL 配置文件。

如果你正在编写一个可以被不同用户使用的程序,那么交互式方法是很棒的,所以不知道程序将以谁的身份连接。这也是将密码传递给程序的最安全的方式。然而,对于更多类似守护进程的进程来说,每次需要重新启动进程时都要求手动启动它并不方便。

环境变量可用于指定会话选项。子进程将继承父进程的环境,因此环境变量可用于将设置传递给子进程,例如从 shell 传递给应用。这是一种配置应用的好方法,不需要文件或解析命令行上的选项。例如,配置在 Docker 等容器中运行的应用是一种常见的方式。 1

使用环境变量有一些缺点。当自动启动进程时,有必要将环境变量存储在一个文件中,这意味着它最终会成为一个配置文件的替代格式。环境通常也是长寿的;例如,如果应用自己启动新进程,它将默认传递其环境,包括潜在的秘密信息,如密码。具有高权限的用户也可以读取环境变量。因此,在使用环境变量时应该小心。

使用应用自己的配置文件或提供选项作为命令行参数意味着所有的配置都在一个地方完成。在这种情况下,MySQL 选项的处理方式与其他选项相同,编写代码时只需将选项及其值传递给 MySQL 连接。

警告

使用密码作为命令行选项时要非常小心。主机上的其他用户可能会看到传递给程序的参数,比如在 Linux 上使用ps命令。因此,建议不要将密码指定为命令行参数。

然而,还有另一种方法。MySQL Connector/Python 拥有读取 MySQL 配置文件的原生支持。在应用自己的配置文件上使用这种方法的一些原因是,除了与 MySQL 相关的选项之外,应用可能不需要配置文件,或者应用配置和 MySQL 配置可能有不同的所有者。如果开发人员负责定义应用本身的行为,而数据库管理员负责 MySQL 特定的选项,则可能会发生后一种情况。

因为这本书是关于使用 MySQL 连接器/Python 特性的,而不是一般的 Python 编程,所以四个选项中唯一要详细讨论的是使用 MySQL 配置文件的选项。

使用 MySQL 配置文件

MySQL 的配置文件使用INI文件格式。以下是一个使用 MySQL Connector/Python 的简单示例,使用了与本章前面相同的配置:

[connector_python]
user     = pyuser
host     = 127.0.0.1
port     = 3306
password = Py@pp4Demo

有两个连接选项控制 MySQL 配置文件的使用:

  • option_files:该选项指定要读取的一个或多个配置文件的路径。该值可以是字符串或字符串列表。没有默认值。

  • option_groups:该选项指定从哪些选项组中读取。选项组被指定为方括号中的名称;在示例配置中,选项组是connector_python。该值是包含组名的字符串列表。默认是从clientconnector_python组中读取。

按照惯例,MySQL 配置文件在微软 Windows 上称为my.ini,在其他平台上称为my.cnf。从功能角度来看,对文件名或文件扩展名没有要求。

需要注意的一个重要特性是,option_groups选项不会平等对待所有组。具体来说,connector_python组是特殊的,因为该组中的所有选项都必须有效,否则将引发ValueError异常。对于其他组,未知选项将被忽略。忽略未知选项的原因是几个程序可能会读取相同的选项组。例如,客户端组也由mysql命令行客户端和其他 MySQL 客户端程序读取。

清单 2-3 展示了一个连接 MySQL 的例子,连接选项是从与程序位于同一目录的my.ini文件中读取的。

import mysql.connector

db = mysql.connector.connect(

  option_files="my.ini")

print(__file__ + " - single config file:")
print(
  "MySQL connection ID for db: {0}"
  .format(db.connection_id)
)

db.close()

Listing 2-3Using a MySQL Configuration File

输出类似于前面打印连接 ID 的示例,例如:

listing_2_3.py - single config file:
MySQL connection ID for db: 35

在某些情况下,您可能希望将 MySQL 配置分成几个文件。例如,假设几个应用需要连接到同一个 MySQL 后端,因此它们共享主机和端口信息,但是每个应用使用不同的凭据进行连接。继续这个例子,可以用以下内容创建两个文件my_shared.inimy_app_specific.ini:

my_shared.ini:

[client]
host     = 127.0.0.1
port     = 3306

my_app_specific.ini:

[connector_python]
user     = pyuser
password = Py@pp4Demo

测试程序所需的唯一改变是将option_ files的值改为一个列表。为了演示如何设置option_groups选项,它也被添加到程序中。产生的源代码可以在清单 2-4 中看到。

import mysql.connector

db = mysql.connector.connect(

  option_files = [
    "my_shared.ini",
    "my_app_specific.ini"
  ],
  option_groups = [
    "client",
    "connector_python"
  ]

)

print(__file__ + " - two config files:")
print(
  "MySQL connection ID for db: {0}"
  .format(db.connection_id)
)

db.close()

Listing 2-4Using Multiple Configuration Files

输出如下(ID 除外,它会随着执行的不同而变化):

listing_2_4.py - two config files:
MySQL connection ID for db: 42

最后要考虑的是路径名。如果指定了相对路径,则执行 Python 的目录将用作基本目录。例如,使用下面的命令来执行一个程序(ID 通常是不同的):

PS C:\MySQL> python Source/test.py
MySQL connection ID for db: 56

C:\MySQL是当前工作目录时执行。如果test.pyoption_files="my.ini",那么my.ini文件必须位于C:\MySQL

另一个观察结果是,对于 Microsoft Windows,使用反斜杠()还是正斜杠(/)来分隔路径组件(目录)是可选的。

配置文件的讨论到此结束。本章的最后一个主题是 MySQL Connector/Python 支持的连接的其余选项。

一般配置

到目前为止,已经讨论过的唯一配置选项是指定连接到哪里、以谁的身份连接以及是否使用 SSL 所需的选项。还有其他几个选项与应用的行为更相关。这些选项是本节的主题。

表 2-2 至表 2-5 总结了本章前面的连接选项列表中未包含的选项,以下选项类型各有一个表:连接、字符集、查询行为和警告。本书的其余部分将包括使用其中几个选项的例子。

关系

除了“创建连接”一节中讨论的选项之外,还有更多连接选项。它们并不常用,但在某些用例中可能是必需的。表 2-2 总结了这些选项。一些选项将在表后更详细地讨论。

表 2-2

不太常见的连接相关选项

|

名字

|

缺省值

|

描述

|
| --- | --- | --- |
| auth_plugin |   | 要使用哪个认证插件。例如,当使用 MySQL Connector/Python 2.1 连接到 MySQL Server 8.0 时,这是必需的,因为旧的 MySQL Connector/Python 版本不支持服务器的默认身份验证插件。 |
| client_flags |   | 通过标志配置几个选项的另一种方法。 |
| compress | False | 启用后,网络流量将被压缩。 |
| connection_timeout |   | 创建连接时等待多长时间后超时。 |
| converter_class |   | 指定用于将原始行数据转换为 Python 类型的自定义转换器类。 |
| failover |   | 字典元组,指定在主连接失败时要故障转移到的备用 MySQL 服务器实例。这仅在使用mysql.connector. connect()功能时受支持。 |
| force_ipv6 | False | 当True时,尽可能使用 IPv6。 |
| pool_name | 自动生成 | 连接池的名称。默认情况下,该名称是通过连接hostportuserdatabase连接选项的值生成的。名称最长为pooling.CNX_POOL_MAXNAMESIZE(默认为 64 个)字符,允许使用字母数字字符以及以下字符:。、_、:、-、*、$和#。只有使用mysql.connector. connect()函数或直接实例化pooling.MySQLConnectionPool构造函数类才能支持这一点。 |
| pool_reset_session | True | 当True时,当连接返回到池中时,会话变量被重置。只有使用mysql.connector.connect()函数或者直接实例化pooling.MySQLConnectionPool构造函数类才能支持这一点。 |
| pool_size | 5 | 池中容纳的连接数。该值必须至少为 1,最多为pooling.CNX_POOL_MAXSIZE(默认为 32)。只有使用mysql.connector. connect()函数或通过直接实例化pooling.MySQLConnectionPool构造函数类才支持这一点。 |
| use_pure | False | 当True时,使用连接器的纯 Python 实现。当False时,使用 C 扩展。如果未指定选项,默认情况下使用 C 扩展(如果已安装);否则,它会退回到纯 Python 实现。这仅支持使用mysql.connector.功能。在大多数情况下,建议使用 C 扩展。 |

通过压缩应用和 MySQL 服务器之间传输的数据(反之亦然),可以使用compress选项以额外的计算资源为代价来减少网络流量。如果将大型 SQL 语句发送到服务器,或者将大型查询结果返回到应用,并且应用安装在远程主机上,这可能特别有用。

值得多加注意的四个选项是failoverpool选项。failover选项可用于定义一个或多个 MySQL 服务器实例,如果与主实例的连接失败,MySQL Connector/Python 将故障转移到这些实例。每个可选的 MySQL 服务器实例都被指定为元组或列表中的一个字典。pool选项设置了一个连接池,应用可以向其请求连接。这些选项将在第五章中详细讨论。

client_flags选项可用于设置多个选项。可用选项列表可以使用ClientFlag常量的get_full_info()方法确定:

from mysql.connector.constants import ClientFlag

print("\n".join(
  sorted(ClientFlag.get_full_info())
))

连接器/Python 8.0.11 的输出可以在清单 2-5 中看到。首先列出客户端标志的名称,然后是对标志控制内容的描述。大多数标志也有专用选项,但有一些附加标志如INTERACTIVE只能通过client_flags选项设置。

CAN_HANDLE_EXPIRED_PASSWORDS : Don't close the connection for a connection with expired password
COMPRESS : Can use compression protocol
CONNECT_ARGS : Client supports connection attributes
CONNECT_WITH_DB : One can specify db on connect
DEPRECATE_EOF : Client no longer needs EOF packet
FOUND_ROWS : Found instead of affected rows
IGNORE_SIGPIPE : IGNORE sigpipes
IGNORE_SPACE : Ignore spaces before ''
INTERACTIVE : This is an interactive client
LOCAL_FILES : Can use LOAD DATA LOCAL
LONG_FLAG : Get all column flags
LONG_PASSWD : New more secure passwords
MULTI_RESULTS : Enable/disable multi-results
MULTI_STATEMENTS : Enable/disable multi-stmt support
NO_SCHEMA : Don't allow database.table.column
ODBC : ODBC client
PLUGIN_AUTH : Client supports plugin authentication
PLUGIN_AUTH_LENENC_CLIENT_DATA : Enable authentication response packet to be larger than 255 bytes
PROTOCOL_41 : New 4.1 protocol
PS_MULTI_RESULTS : Multi-results in PS-protocol
REMEMBER_OPTIONS :
RESERVED : Old flag for 4.1 protocol
SECURE_CONNECTION : New 4.1 authentication
SESION_TRACK : Capable of handling server state change information
SSL : Switch to SSL after handshake
SSL_VERIFY_SERVER_CERT :
TRANSACTIONS : Client knows about transactions
Listing 2-5List of Client Flags

为了配置client_flags,指定应启用或禁用的标志列表。要启用标志,只需指定标志的名称;要禁用该标志,请在前面加上一个减号。清单 2-6 展示了一个例子,告诉这个连接它是一个交互连接,但是它不能处理过期的密码。

import mysql.connector
from mysql.connector.constants import ClientFlag

connect_args = {
  "host": "127.0.0.1",
  "port": 3306,
  "user": "pyuser",
  "password": "Py@pp4Demo",
  "client_flags": [
    ClientFlag.INTERACTIVE,
    -ClientFlag.CAN_HANDLE_EXPIRED_PASSWORDS
  ]
};

db = mysql.connector.connect(
  **connect_args
)

print(__file__ + " - Client flags:")
print(
  "MySQL connection ID for db: {0}"
  .format(db.connection_id)
)

db.close()

Listing 2-6Using Client Flags in the Connection

这将给出以下输出(除了 ID 的值之外):

listing_2_6.py - Client flags:
MySQL connection ID for db: 60

use_pure选项可以用来指定是使用 C 扩展还是连接器的纯 Python 实现。C 扩展提供了比纯实现更好的性能,尤其是在处理大型结果集和准备好的语句时。另一方面,纯 Python 实现在更多的平台上得到支持,具有更多的特性,并且更容易修改源代码。在 8.0.11 和更高版本中,安装 C 扩展是默认的,而早期版本默认使用纯 Python 实现。

也可以通过导入_mysql_connector模块而不是通常的mysql.connector模块来使用 C 扩展。第四章中包含了一个使用 C 扩展的例子。

其他连接选项将不再详细讨论。相反,焦点将转移到字符集选项上。

字符集

字符集定义了字符的编码方式。在互联网的早期,经常使用 ASCII 字符集。ASCII 对每个字符使用 7 位,这样可以节省空间,但这意味着只有 128 个不同的字符可用。这对于英文的纯文本相当有效,但是对于其他语言却缺少字符。多年来,各种其他字符集被使用,如拉丁字符集。

特定于地区的字符集有助于支持所有语言,但缺点是不同的语言需要不同的编码。对此的一种回应是 Unicode 转换格式(UTF)编码;特别是 UTF-8 已经变得流行起来。UTF-8 使用可变数量的字节来存储字符。最初的 128 个 ASCII 字符在 UTF-8 中具有相同的编码;其他字符使用两到四个字节。

在 MySQL Server 5.7 之前,服务器端的默认字符集是 Latin1,但在 MySQL 8.0 中,当utf8mb4成为默认字符集时,这种情况发生了变化。mb4后缀表示每个字符最多使用四个字节(mb =多字节)。这是必需的,因为 MySQL 中的utf8以前意味着每个字符最多支持三个字节。然而,三字节的 UTF-8 实现错过了几个表情符号,它已经被否决,所以最好使用四字节的变体。在 8.0.12 版本之前,Connector/Python 的默认字符集是utf8,这是 UTF-8 的三字节实现(在 MySQL 服务器中称为utf8utf8mb3)。从 8.0.12 版本开始,默认设置为utf8mb4,就像 MySQL 服务器一样。

还有整理的概念要考虑。归类定义了如何对两个字符或字符序列进行相互比较,例如在比较中是否应该将“”和“”视为同一字符,以及是否应该将 ss 视为等于“”(德语尖音 s)。归类还定义了字符的排序顺序以及比较是否区分大小写。每个字符集都有一个默认的排序规则,但是也可以显式地请求排序规则。

小费

除非您有特定的国家要求,否则当选择utf8utf8mb4作为字符集时,MySQL Server 中的默认排序通常是一个不错的选择。

通常,MySQL 中可用的字符集和排序规则在不同版本之间变化不大。然而,MySQL Server 8.0 的主要变化之一是增加了一系列 UCA 9.0.0 排序规则。关于可用字符集及其默认排序规则的信息可以使用信息模式中的CHARACTER_SETS表找到,如清单 2-7 所示。

mysql> SELECT CHARACTER_SET_NAME AS Name,
              DEFAULT_COLLATE_NAME
         FROM information_schema.CHARACTER_SETS
        ORDER BY CHARACTER_SET_NAME;
+----------+----------------------+
| Name     | DEFAULT_COLLATE_NAME |
+----------+----------------------+
| armscii8 | armscii8_general_ci  |
| ascii    | ascii_general_ci     |
| big5     | big5_chinese_ci      |
| binary   | binary               |
...
| ujis     | ujis_japanese_ci     |
| utf16    | utf16_general_ci     |
| utf16le  | utf16le_general_ci   |
| utf32    | utf32_general_ci     |
| utf8     | utf8_general_ci      |
| utf8mb4  | utf8mb4_0900_ai_ci   |
+----------+----------------------+
41 rows in set (0.00 sec)
Listing 2-7Character Set Collations in MySQL 8.0.11

类似地,特定字符集可用的排序规则可以使用COLLATIONS表来确定。清单 2-8 显示了utf8mb4字符集的输出。

mysql> SELECT COLLATION_NAME, IS_DEFAULT
         FROM information_schema.COLLATIONS
        WHERE CHARACTER_SET_NAME = 'utf8mb4';
+----------------------------+------------+
| COLLATION_NAME             | IS_DEFAULT |
+----------------------------+------------+
| utf8mb4_general_ci         |            |
| utf8mb4_bin                |            |
| utf8mb4_unicode_ci         |            |
...
| utf8mb4_0900_ai_ci         | Yes        |
| utf8mb4_de_pb_0900_ai_ci   |            |
| utf8mb4_is_0900_ai_ci      |            |
| utf8mb4_lv_0900_ai_ci      |            |
...
| utf8mb4_vi_0900_as_cs      |            |
| utf8mb4_ja_0900_as_cs      |            |
| utf8mb4_ja_0900_as_cs_ks   |            |
| utf8mb4_0900_as_ci         |            |
| utf8mb4_ru_0900_ai_ci      |            |
| utf8mb4_ru_0900_as_cs      |            |
+----------------------------+------------+
73 rows in set (0.00 sec)
Listing 2-8The Collations Available for the utf8mb4 Character Set

输出显示了 MySQL Server 8.0.11 中可用于utf8mb4的 73 种排序规则。归类名称由几部分组成:

  • 字符集名称

  • 该校对适用于哪个国家(例如ja适用于日本),或者它是否具有更一般的性质

  • 修饰符(重音符号):并非所有的排序规则都有这些修饰符。例如,ai不区分重音,as区分重音,ci不区分大小写,cs区分大小写。

小费

MySQL 中字符集和排序规则的主题很大。更深入的讨论见 https://dev.mysql.com/doc/refman/en/charset.html 及其中的参考文献。

MySQL Connector/Python 有三个与字符集和排序规则相关的选项。这些总结在表 2-3 中。

表 2-3

字符集相关选项

|

名字

|

缺省值

|

描述

|
| --- | --- | --- |
| charset | utf8mb4 | 用于连接的字符集。在 MySQL Connector/Python 8.0.11 及更早版本中,默认为utf8。大多数情况下,建议使用utf8mb4。 |
| collation | utf8mb4_general_ci | 用于字符串比较和排序的排序规则。在许多情况下,可以使用默认值。MySQL Connector/Python 8.0.11 及更早版本的默认值为utf8_general_ci。在 MySQL Server 8.0 中,utf8mb4字符集的默认排序规则是utf8mb4_0900_ai_ci,这通常是一个不错的选择,除非存在特定的需求。 |
| use_unicode | True | 是否将查询结果中的字符串作为 Python Unicode 文本返回。默认值是True,这通常也是最好的值。 |

清单 2-9 展示了一个配置字符集相关选项的例子。

import mysql.connector

connect_args = {
  "host": "127.0.0.1",
  "port": 3306,
  "user": "pyuser",
  "password": "Py@pp4Demo",
  "charset": "utf8mb4",
  "collation": "utf8mb4_unicode_ci",
  "use_unicode": True
};

db = mysql.connector.connect(
  **connect_args)

print(__file__ + " - Setting character set:")
print(
  "MySQL connection ID for db: {0}"
  .format(db.connection_id)
)

db.close()

Listing 2-9Specifying the Character Set and Collation

可用的字符集和排序规则被编码到 MySQL 连接器/Python 源代码中。这意味着当您升级 MySQL Server 时,如果包含新的字符集或排序规则,您只能在 Python 程序中使用它们,前提是您将 MySQL Connector/Python 更新到支持新字符集和排序规则的版本。

小费

如果您升级 MySQL Server,您可能还需要升级 MySQL Connector/Python 以获得对所有新功能的支持。

在首次连接到 MySQL 服务器后,可以更改连接所使用的字符集和排序规则。最好的方法是使用清单 2-10 中展示的set_charset_collation()方法来改变连接的charsetcollation属性。注意,与其他示例不同,这个示例首先实例化了MySQLConnection类,以便能够在创建连接之前打印初始字符集和排序规则。

import mysql.connector

db = mysql.connector.MySQLConnection()

# Print banner and initial settings
print(
  "{0:<9s}   {1:<7s}   {2:<18s}".format(
    "Stage", "charset", "collation"
  )
)
print("-" * 40)
print(
  "{0:<9s}   {1:<7s}   {2:<18s}".format(
    "Initial", db.charset, db.collation
  )
)

# Create the connection
connect_args = {
  "host": "127.0.0.1",
  "port": 3306,
  "user": "pyuser",
  "password": "Py@pp4Demo"
};

db.connect(**connect_args)

# The connection does not change the
# settings
print(
  "{0:<9s}   {1:<7s}   {2:<18s}".format(
    "Connected",
    db.charset, db.collation
  )
)

# Change only the character set

db.set_charset_collation(

  charset = "utf8mb4"

)

print(
  "{0:<9s}   {1:<7s}   {2:<18s}".format(
    "Charset", db.charset, db.collation
  )
)

# Change only the collation

db.set_charset_collation(

  collation = "utf8mb4_unicode_ci"

)

print(
  "{0:<9s}   {1:<7s}   {2:<18s}".format(
    "Collation",
    db.charset, db.collation
  )
)

# Change both the character set and
# collation

db.set_charset_collation(

  charset   = "latin1",
  collation = "latin1_general_ci"

)

print(
  "{0:<9s}   {1:<7s}   {2:<18s}".format(
    "Both", db.charset, db.collation
  )
)

db.close()

Listing 2-10Changing the Character Set of a Connection

从示例中可以看出,字符集和排序规则属性甚至可以在连接建立之前使用。然而,在连接建立之前,不可能使用set_charset_collation()方法来改变字符集或排序规则。

注意

始终使用set_charset_collation()方法来更改连接的字符集和/或排序规则。与直接将SET NAMES作为 SQL 语句执行相比,它确保了 Connector/Python 知道哪些设置用于将 bytearrays 转换为 Python 字符串(参见下一章),字符集和排序规则选择根据 Connector/Python 已知的进行验证,并且 C 扩展设置保持同步。

建立连接不会改变charsetcollation属性的值。字符集可以自行更改,在这种情况下,归类设置为字符集的默认值。在这种情况下,字符集设置为utf8mb4,因此默认字符集为utf8mb4_general_ci

也可以单独设置归类,最后字符集和归类都设置好了。使用版本 8.0.11 执行清单 2-10 中的程序的输出是

Stage       charset   collation
----------------------------------------
Initial     utf8      utf8_general_ci
Connected   utf8      utf8_general_ci
Charset     utf8mb4   utf8mb4_general_ci
Collation   utf8mb4   utf8mb4_unicode_ci
Both        latin1    latin1_general_ci

如果您使用的是 MySQL Connector/Python 8.0.12 或更高版本,初始和连接的字符集和排序规则是utf8mb4utf8mb4_general_ci

查询行为

有几个选项可以控制查询的行为。从定义事务配置是否允许特性到定义 MySQL Connector/Python 如何处理结果。选项在表 2-4 中列出。

表 2-4

与查询相关的选项

|

名字

|

缺省值

|

描述

|
| --- | --- | --- |
| allow_local_infile | True | 是否允许使用LOAD DATA LOCAL INFILE语句。 |
| autocommit | False | 当True时,在每个查询后执行一个隐式的COMMIT。 |
| buffered | False | 当True时,结果集被立即提取并缓存在应用中。 |
| consume_results | False | 当True时,如果有未提取的行,则自动提取查询结果,并执行新的查询。 |
| database |   | 在没有为表显式给出数据库名称的情况下,哪个数据库(模式)用作查询的默认数据库。 |
| raw | False | 默认情况下,使用游标时,结果值会转换为 Python 类型。当将该选项设置为True时,返回的结果没有转换。 |
| sql_mode | (服务器默认值) | 执行查询时使用的 SQL 模式。参见https://dev.mysql.com/doc/refman/en/sql-mode.html。 |
| time_zone |   | 设置后,时间戳将转换为该时区,而不是使用服务器端时区。 |

第三章和第四章讨论了这些选项,包括使用这些选项的代码示例。

警告信息

以正确的方式处理警告和错误是非常重要的。否则会导致数据损坏或丢失。使用游标时,有两个选项控制 MySQL Connector/Python 如何处理警告(游标将在下一章讨论)。选项如表 2-5 所示。

表 2-5

游标的警告相关选项

|

名字

|

缺省值

|

描述

|
| --- | --- | --- |
| get_warnings | False | 当设置为True时,每次查询后自动提取警告。 |
| raise_on_warnings | False | 当设置为True时,警告会引发异常。 |

由于正确处理警告和错误非常重要,第九章专门讨论这个主题。

摘要

本章讲述了如何创建和配置从 Python 程序到 MySQL 服务器数据库的连接。讨论了以下主题:

  • 建立连接的四种不同方式,包括初始配置。mysql.connector. connect()功能是四种方法中最灵活的。

  • 配置选项。

  • 连接的最佳实践:关闭连接,使用 SSL/TLS 加密流量,不要在源代码中硬编码连接选项(尤其是密码)。

  • MySQL 配置文件。

  • 字符集。

能够创建到数据库的连接当然很好,但是除非您能够执行查询,否则它没有多大用处。接下来的两章将讨论查询执行,从更基本的用例开始。

三、基本查询执行

前一章讨论了如何从 Python 程序连接到 MySQL。然而,仅仅为了获得一个连接 ID 或什么都不做而创建一个连接没有多大意义。毕竟,MySQL Connector/Python 的全部意义在于执行查询。本章将着眼于查询执行的基础。

首先,您将学习如何使用连接对象的cmd_query()方法执行查询。然后,您将探索更高级的游标概念。最后,您将看到如何处理用户输入。

小费

本章中有许多示例程序。列表中出现的所有示例程序都可以下载。有关使用示例程序的更多信息,参见第一章中对示例程序的讨论。

简单执行

通过 MySQL Connector/Python 执行查询有几种不同的方法。最简单但也是最不强大的是连接对象的cmd_query()方法。我还将讨论获取SELECT语句结果的get_rows()get_row()方法。

在深入研究查询和获取结果的三种方法之前,考虑一下它们之间的关系是很有用的,所以请看一下图 3-1 。

img/463459_1_En_3_Fig1_HTML.jpg

图 3-1

通过连接对象执行查询的流程

图 3-1 显示了一旦创建了连接,就可以使用cmd_query()方法执行查询。如果有结果(返回行),可以使用get_rows()get_row()方法来读取行。该连接可以在更多的查询中重用。最后,当不再有查询时,使用close()方法关闭连接。与真实世界的程序相比,这有点简化;比如没有关于交易的考虑。但是,它是一个有用的高层次概述。

本节的主题是cmd_query()get_rows()get_row()方法,以及如何处理结果。对于更一般的用法,有必要使用光标;这将是下一节以及下一章的主题。

注意

在大多数情况下,最好按照下一节所述使用游标。然而,这一节很重要,因为它解释了游标是如何工作的。

执行查询:cmd_query()

cmd_query()方法很简单。它接受一个参数,即要执行的查询,并返回一个字典,其中包含有关已执行查询的信息。返回的字典的确切内容取决于查询。例如,对于一个SELECT查询,字典将包含关于所选列的信息。对于所有查询,还包括查询的状态。本节中的示例将包括结果字典的内容。

清单 3-1 显示了一个使用cmd_query()执行返回单行的SELECT查询的简单示例。

import mysql.connector
import pprint

printer = pprint.PrettyPrinter(indent=1)

# Create connection to MySQL
db = mysql.connector.connect(
  option_files="my.ini")

# Execute a query

result = db.cmd_query(

  """SELECT *
       FROM world.city
      WHERE ID = 130"""

)

# Print the result dictionary
print("Result Dictionary\n" + "="*17)
printer.pprint(result)

db.close()

Listing 3-1Executing a Simple SELECT Using cmd_query()

警告

这个例子(以及本章中的其他几个例子)有一个查询参数(在这个例子中,ID的值用130)。这是可以接受的,因为它是一个固定的查询。但是,不要将用户提交的数据内联到查询中。本章后面的“处理用户输入”部分将向您展示如何安全地处理用户提交的值。

这个程序创建了一个连接,正如你在第二章中看到的。建立连接后,使用cmd_query()方法执行查询,返回的字典存储在result变量中,使用 pretty 打印模块(pprint)打印出来:

Result Dictionary
=================
{'columns': [('ID', 3, None, None, None, None, 0, 49667),
             ('Name', 254, None, None, None, None, 0, 1),
             ('CountryCode', 254, None, None, None, None, 0, 16393),
             ('District', 254, None, None, None, None, 0, 1),
             ('Population', 3, None, None, None, None, 0, 32769)],
 'eof': {'status_flag': 16385, 'warning_count': 0}}

结果字典的列部分将在下一章详细讨论;现在,只需要知道列的元组的第一个元素是列名。结果字典的第二部分是eof元素,包括查询的一些细节;包含的字段取决于查询。您获得的列元组中最后一个整数的值和status_flag的值可能与示例输出不同,因为它们取决于是否使用了 C 扩展。

eof元素中常见的字段是status_flagwarning_count字段。状态标志远没有听起来那么有用;其实价值是没有记载的,不应该从它的价值中取任何意义。另一方面,警告计数显示查询期间发生的警告数量。第九章讲述了如何检查警告。

对于没有结果集的查询(即不返回行),eof信息是一个“OK 包”,其中包含关于查询的信息。例如,以下信息是使用纯 Python 实现更新 14 行的UPDATE语句的结果:

Result Dictionary
=================
{'affected_rows': 14,
 'field_count': 0,
 'info_msg': 'Rows matched: 14  Changed: 14  Warnings: 0',
 'insert_id': 0,
 'status_flag': 1,
 'warning_count': 0}

两个最重要的参数是

  • affected_rows:显示受影响的行数。在本例中,更新了 14 行。

  • insert_ id:对于INSERTREPLACE语句,将数据插入到带有自动递增列的表中,insert_id是该语句插入的第一行的 ID。

use_pure = False时,info_msg参数不存在,status_flag被替换为server_status

cmd_query()类似的是cmd_query_iter()方法,它可以用来向 MySQL 发送多个查询。在一个调用中执行多个查询和处理多个结果集是下一章的主题。

像刚才讨论的例子那样执行查询当然很好,但是如果不检索结果,像清单 3-1 中的SELECT语句这样的查询就没什么意思了。为了获取找到的行,使用了get_rows()get_row()方法。

正在检索行–get _ Rows()

有些查询,如CREATE TABLEALTER TABLEINSERTUPDATEDELETE语句,不返回任何结果,只需检查查询是否成功。然而,一般来说,程序中的大多数查询都是返回结果集的SELECT查询。对于返回结果集的查询,必须提取行。当使用cmd_query()执行查询时,相应的获取行的方法是get_rows(),它返回查询找到的所有行。

get_rows()的用法很简单。所需要做的就是调用它,行作为元组列表返回,如清单 3-2 所示。

import mysql.connector
import pprint

printer = pprint.PrettyPrinter(indent=1)

# Create connection to MySQL
db = mysql.connector.connect(
  option_files="my.ini", use_pure=True)

# Execute a query
result = db.cmd_query(
  """SELECT Name, CountryCode,
            Population
       FROM world.city
      WHERE Population > 9000000
      ORDER BY Population DESC"""
)

# Fetch the rows

result_set = db.get_rows()

# Print the result dictionary
print("Result Dictionary\n" + "="*17)
printer.pprint(result)

# Print the rows
print("\nResult Set\n" + "="*10)
printer.pprint(result_set)

db.close()

Listing 3-2Fetching Rows with get_rows()

清单 3-2 中的程序类似于清单 3-1 中的程序,除了在这种情况下,它通过使用use_pure = True强制使用纯 Python 实现。这次的查询查找人口超过 900 万的城市,并要求结果行按人口降序排列。输出看起来如清单 3-3 所示。输出是用 MySQL 连接器/Python 版本 8.0.11 生成的。

注意

MySQL 连接器 8.0.12 中的一个重要变化是在纯 Python 和 C 扩展实现之间调整了get_rows()get_row()的行为。这意味着在 MySQL Connector/Python 8.0.12 和更高版本中,get_rows()get_row()的纯 Python 实现不再以字节数组的形式返回结果。下面的讨论仍然有助于说明结果。

Result Dictionary
=================
{'columns': [('Name', 254, None, None, None, None, 0, 1),
             ('CountryCode', 254, None, None, None, None, 0, 16393),
             ('Population', 3, None, None, None, None, 0, 1)],
 'eof': {'status_flag': 33, 'warning_count': 0}}

Result Set
==========
([(bytearray(b'Mumbai (Bombay)'), bytearray(b'IND'), bytearray(b'10500000')),
  (bytearray(b'Seoul'), bytearray(b'KOR'), bytearray(b'9981619')),
  (bytearray(b'S\xc3\xa3o Paulo'), bytearray(b'BRA'), bytearray(b'9968485')),
  (bytearray(b'Shanghai'), bytearray(b'CHN'), bytearray(b'9696300')),
  (bytearray(b'Jakarta'), bytearray(b'IDN'), bytearray(b'9604900')),
  (bytearray(b'Karachi'), bytearray(b'PAK'), bytearray(b'9269265'))],
 {'status_flag': 33, 'warning_count': 0})

Listing 3-3The Output of Executing the Program in Listing 3-2

结果字典类似于前面的例子,有列信息和eof信息。更有趣的是get_rows()返回的结果集。这些值以二进制数据数组表示的字符串形式返回(bytearray)。虽然这在技术上是结果的正确表示,但它并不十分有用。例如,人口是一个整数,所以数据最好是整数而不是字符串。另一个问题是像圣保罗这样的城市,其字节序列是“S \ xc3 \ xa3o Paulo”;请注意,γ表示为\xc3\xa3。

注意

如果使用 C 扩展或 8.0.12 及更高版本,则这些值不会以字节数组的形式返回,而是以 Unicode 字符串的形式返回。这是一个在早期版本中两个实现不相同的例子。

为了让数据在程序中真正有用,有必要将字节数组转换为原生 Python 数据类型。具体的转换方式取决于数据,为每种数据类型实现显式转换超出了本书的范围。然而,它也不是必需的,因为 MySQL Connector/Python 已经包含了它的代码;稍后会有更多的介绍。现在,请看清单 3-4 中转换清单 3-2 结果中的字符串和整数的例子。

Note

这个示例和后面的输出包含非 ASCII 字符的示例展示了 Python 2 和 Python 3 在 Unicode 处理上的差异。示例假设 Python 3 和 MySQL Connector/Python 8.0.11。这些示例在 8.0.12 版和更高版本中不起作用。

import mysql.connector

# Create connection to MySQL
db = mysql.connector.connect(
  option_files="my.ini", use_pure=True)

# Execute a query
result = db.cmd_query(
  """SELECT Name, CountryCode,
            Population
       FROM world.city
      WHERE Population > 9000000
      ORDER BY Population DESC"""
)

# Fetch the rows
(cities, eof) = db.get_rows()

# Print the rows found
print(__file__ + " – Using decode:")
print("")
print(
  "{0:15s}   {1:7s}   {2:3s}".format(
    "City", "Country", "Pop"
  )
)
for city in cities:
  print(
    "{0:15s}   {1:⁷s}   {2:4.1f}".format(
      city[0].decode(db.python_charset),
      city[1].decode(db.python_charset),
      int(
        city[2].decode(db.python_charset)
      )/1000000.0
    )
  )

# Print the eof package
print("\nEnd-of-file:");
for key in eof:
  print("{0:15s} = {1:2d}".format(
    key, eof[key]
  ))

db.close()

Listing 3-4Converting the Result to Native Python Types

清单 3-2 和清单 3-4 的主要区别在于对结果集的处理。首先,结果集被分成返回的行(城市)和文件尾(eof)包。然后在将值转换为本地 Python 类型的同时打印城市。

使用bytearray类型的decode()方法转换字符串值。这需要解析连接的字符集。在这种情况下,字符集是utf8(使用默认);但是,为了确保可以处理任何字符集,连接的python_ charset属性用于设置转换中使用的字符集。由于utf8mb4是 MySQL 的发明,所以有必要抓住这一点,用utf8来代替;这就是charsetpython_charset属性的区别。可以使用int()函数转换人口,然后除以一百万,得到百万人口。

最后,打印结果集的文件结尾部分。这与由cmd_query()返回的结果的eof部分中可用的信息相同。该程序的输出是

listing_3_4.py – Using decode

City              Country   Pop
Mumbai (Bombay)     IND     10.5
Seoul               KOR     10.0
São Paulo           BRA     10.0
Shanghai            CHN      9.7
Jakarta             IDN      9.6
Karachi             PAK      9.3

End-of-file:
status_flag     = 33
warning_count   =  0

手动转换字节数组在一般情况下是不可行的,也是不必要的,这将在下面讨论行的自动转换时显示。

自动转换成原生 Python 类型

在前面的示例中,查询返回的行是手动处理的。这可能是理解正在发生的事情的一个很好的方法,但是在更真实的情况下,通常更倾向于将结果作为原生 Python 类型返回。

注意

和前面的例子一样,只有 MySQL Connector/Python 8.0.11 和更早的版本(包括 2.1 版)才需要这个讨论。在更高版本中,转换会自动发生;然而,调用row_to_python()是安全的,因为如果转换已经发生,它将只是一个空操作。

MySQL Connector/Python 包括转换模块,该模块提供了对 MySQL 服务器返回的结果进行转换的工具。具体来说,MySQLConverter类中的row_to_python()方法可以转换一行中的所有值。清单 3-5 展示了清单 3-4 中示例的等效物,但是这次使用row_to_python()来处理转换。

import mysql.connector

from mysql.connector.conversion import MySQLConverter

# Create connection to MySQL
db = mysql.connector.connect(
  option_files="my.ini", use_pure=True)

# Execute a query
result = db.cmd_query(
  """SELECT Name, CountryCode,
            Population
       FROM world.city
      WHERE Population > 9000000
      ORDER BY Population DESC"""
)

# Fetch the rows
(cities, eof) = db.get_rows()

# Initialize the converter

converter = MySQLConverter(

  db.charset, True)

# Print the rows found
print(__file__ + " - Using MySQLConverter:")
print("")
print(
  "{0:15s}   {1:7s}   {2:3s}".format(
    "City", "Country", "Pop"
  )
)
for city in cities:
  values = converter.row_to_python(
    city, result["columns"])
  print(
    "{0:15s}   {1:⁷s}   {2:4.1f}".format(
      values[0],
      values[1],
      values[2]/1000000.0
    )
  )

db.close()

Listing 3-5Converting Query Results Using MySQLConverter.row_to_python()

清单 3-5 中例子的重要部分是那些涉及MySQLConverter类的部分。第一,导入类;然后,当结果集准备好打印时,实例化该类;最后,使用row_to_python()方法转换行。

当实例化MySQLConverter类时,需要两个参数:字符集和 Python 中是否使用 Unicode。请记住第二章中的内容,在创建连接时可以配置两者。字符集是通过连接的charset属性公开的,因此,和前面一样,它用于确保在转换行时,连接字符集的改变不需要代码的改变。MySQLConverter类知道如何处理utf8mb4,所以没有必要明确地处理它。Python 中没有使用 Unicode 的属性,因此有必要显式指定它。

有了MySQLConverter类的实例,就可以一次转换一行。来自cmd_query()调用结果的列信息作为一个参数沿着要转换的值传递;这确保了 MySQL 连接器/Python 知道每一列的数据类型。输出与清单 3-4 中的示例相同,除了eof部分的信息已被删除:

listing_3_5.py - Using MySQLConverter

City              Country   Pop
Mumbai (Bombay)     IND     10.5
Seoul               KOR     10.0
São Paulo           BRA     10.0
Shanghai            CHN      9.7
Jakarta             IDN      9.6
Karachi             PAK      9.3

到目前为止,示例已经获取了结果集中的所有行,然后使用这些行。这对于小结果来说很好,但是对于大量具有大值的行来说效率不高。

正在检索行–get _ Rows()有限制

限制检索的行数的一个选项是指定要提取的行数作为get_rows()的参数。这可以通过两种方式之一来实现:要么直接给出行数作为参数,要么显式地作为count参数。指定的行数是批中要读取的最大行数。当有更多行要被读取时,eof将被设置为None。如果可用的行数少于请求的行数,get_rows()将返回剩余的行数,并设置eof包含文件结束信息。清单 3-6 对此进行了说明。

import mysql.connector
from mysql.connector.conversion import MySQLConverter

# Create connection to MySQL
db = mysql.connector.connect(
  option_files="my.ini", use_pure=True)

# Execute a query
result = db.cmd_query(
  """SELECT Name, CountryCode,
            Population
       FROM world.city
      WHERE Population > 9000000
      ORDER BY Population DESC"""
)

# Initialize the converter
converter = MySQLConverter(
  db.charset, True)

# Fetch and print the rows
print(__file__
      + " - Using get_rows with limit:")
print("")
count = 0

(cities, eof) = db.get_rows(4)

while (cities):
  count = count + 1
  print("count = {0}".format(count))

  # Print the rows found in this batch
  print(
    "{0:15s}   {1:7s}   {2:3s}".format(
      "City", "Country", "Pop"
    )
  )
  for city in cities:
    values = converter.row_to_python(
      city, result["columns"])
    print(
      "{0:15s}   {1:⁷s}   {2:4.1f}".format(
        values[0],
        values[1],
        values[2]/1000000.0
      )
    )
  print("")

  # Read the next batch of rows
  if (eof == None):
    (cities, eof) = db.get_rows(count=4)
  else:
    cities = []

db.close()

Listing 3-6Fetching a Limited Number of Rows at a Time Using get_rows()

提取前四行时,行数被指定为单独的参数:

(cities, eof) = db.get_rows(4)

其余的行是在循环中读取的:

  if (eof == None):
    (cities, eof) = db.get_rows(count=4)
  else:
    cities = []

有必要检查结果集的eof部分的值,因为先前的读取可能已经获取了最后的行。事实上,这里就是这样。第一次循环打印结果的前四行,第二次循环打印剩下的两行:

listing_3_6.py - Using get_rows with limit

count = 1
City              Country   Pop
Mumbai (Bombay)     IND     10.5
Seoul               KOR     10.0
São Paulo           BRA     10.0
Shanghai            CHN      9.7

count = 2
City              Country   Pop
Jakarta             IDN      9.6
Karachi             PAK      9.3

这种用法需要注意的一点是,get_rows()总共读取七个“行”:六行是查询的结果加上eof信息。

一次读取有限数量的行的特殊情况是获取一行。对于这种情况,read_row()方法可以作为用count=1包装对get_rows()的调用的包装器。

正在检索行–get _ row()

使用cmd_query()方法执行查询后,有两种不同的获取行的策略。可以使用get_rows()一次提取几行,正如到目前为止所展示的那样,也可以使用get_row()方法一次提取一行。

一次只获取一行的好处是应用一次只在内存中存储一行。对于大型结果集来说,这可能更有效,尽管它需要更多的对get_row()方法的调用和更多的从 MySQL 服务器读取数据的往返行程。

注意

这个有点简化了。正如您将在下一章中看到的,游标支持缓冲结果(即预取结果集)。然而,当直接使用cmd_query()方法时,这是不受支持的。

get_row()的另一个潜在优势是它提供了不同的代码流。使用get_rows(),首先获取行,然后代码遍历这些行。另一方面,当一次获取一行时,可以直接在循环中使用get_row(),如清单 3-7 所示。哪种代码流更好取决于程序的情况和一般风格。

import mysql.connector
from mysql.connector.conversion import MySQLConverter

# Create connection to MySQL
db = mysql.connector.connect(
  option_files="my.ini", use_pure=True)

# Execute a query
result = db.cmd_query(
  """SELECT Name, CountryCode,
            Population
       FROM world.city
      WHERE Population > 9000000
      ORDER BY Population DESC"""
)

# Print the rows found
print(__file__ + " - Using get_row:")
print("")
converter = MySQLConverter(
  db.charset, True)
print(
  "{0:15s}   {1:7s}   {2:3s}".format(
    "City", "Country", "Pop"
  )
)

(city, eof) = db.get_row()

while (not eof):
  values = converter.row_to_python(
    city, result["columns"])
  print(
    "{0:15s}   {1:⁷s}   {2:4.1f}".format(
      values[0],
      values[1],
      values[2]/1000000
    )
  )
  (city, eof) = db.get_row()

db.close()

Listing 3-7Using get_row() to Read the Rows One by One

清单 3-7 中的大部分代码与前面的例子相同。区别在于打印结果集的循环是如何完成的。这里每个城市的值和文件结束信息是使用get_row()方法获得的。当有更多行要读取时,eof变量是None。然后使用 while 循环继续获取行,直到eof被设置为与get_rows()相同的值。输出是

listing_3_7.py - Using get_row

City              Country   Pop
Mumbai (Bombay)     IND     10.5
Seoul               KOR     10.0
São Paulo           BRA     10.0
Shanghai            CHN      9.7
Jakarta             IDN      9.6
Karachi             PAK      9.3

在转向游标之前,有必要考虑一下 MySQL Connector/Python 中使用结果的一般性质。

消费结果

到目前为止,示例只是使用了get_rows()get_row()来获取由SELECT语句返回的行。这在测试时很好,但是值得更深入地研究消费结果。

每当查询返回结果集时,必须先消耗这些行,然后才能执行另一个查询。如果这些行尚未被消耗,将会出现异常:

mysql.connector.errors.InternalError: Unread result found

有两种方法可以避免这种错误:

  • get_rows()get_row()读取行。所有的行和eof包都必须被阅读。

  • 创建连接时启用can_consume连接属性。

警告

总是确保使用方法get_rows()get_row()之一或者在创建连接时启用can_consume来使用查询返回的所有行。

您可以通过使用连接的can_consume_results属性来检查can_consume选项是否已启用。当can_consume被启用时,如果一个新的查询即将被执行并且仍然有未读的行,MySQL 连接器/Python 将在内部调用get_rows()

程序如何知道是否还有未读的行?连接类通过unread_result属性对此进行跟踪。当读取结果集的最后一行时,unread_result被设置为False。该属性是公共可访问的,因此可以与get_rows()一起使用。

can_consume_results属性只是连接对象的许多属性之一。在前一章我讨论如何创建连接时,提到了几个属性。现在,随着对连接和查询执行如何工作有了更好的理解,您可以继续讨论游标了。

小费

如果有大量数据要使用,并且不需要这些数据,那么关闭连接并重新连接会比获取行更快。

光标

到目前为止,本章中的所有示例都专门使用了 connection 对象的方法和属性来执行查询并获取结果行。直接使用连接可以认为是低级方法。对于实际的程序,更常见的是选择高级游标,它提供了一种更好的处理查询的方式。

注意

虽然连接方法cmd_query()get_rows()get_row()很少被直接使用,但了解这些方法的工作原理仍然很有用。它有助于解释为什么游标以这样的方式工作,并且在调试问题时很有用。

在可以使用游标执行查询之前,必须对其进行实例化。这是使用游标之旅的第一个主题。

实例化

有两种方法可以实例化一个游标:要么使用 connection 对象的cursor()方法,要么直接使用MySQLCursor构造函数。下面的代码片段说明了这两种方法:

import mysql.connector
from mysql.connector.cursor import MySQLCursor

# Create connection to MySQL
db = mysql.connector.connect(
  option_files="my.ini", use_pure=True)

# Create a cursor using cursor()
cursor1 = db.cursor()
cursor1.close()

# Create a cursor using the constructor
cursor2 = MySQLCursor(db)
cursor2.close()

db.close()

注意

这个例子并不是故事的全部。有几个光标子类,由db.cursor()返回的那个依赖于光标设置。稍后会详细介绍。

与数据库连接本身一样,游标是使用 cursor 对象的close()方法关闭的。使用完游标后关闭它可以确保删除对连接对象的引用,从而避免内存泄漏。

有几种不同的游标类。使用哪一个取决于需求。这些类别是

  • MySQLCursor:这是一个“普通”游标类,用于将无缓冲输出转换为 Python 类型。这是默认的游标类。

  • MySQLCursorBuffered:这使用缓冲的结果集(参见第四章),但是仍然将结果集转换为 Python 类型。

  • MySQLCursorRaw:这将原始结果作为字节数组返回,类似于 8.0.11 和更早版本中的get_rows(),但不使用缓冲。

  • MySQLCursorBufferedRaw:返回原始结果集并启用缓冲。

  • MySQLCursorDict:与MySQLCursor相同,但行作为字典返回。

  • MySQLCursorBufferedDict:与MySQLCursorBuffered相同,但行作为字典返回。

  • MySQLCursorNamedTuple:与MySQLCursor相同,但行以命名元组的形式返回。

  • MySQLCursorBufferedNamedTuple:与MySQLCursorBuffered相同,但行以命名元组的形式返回。

  • MySQLCursorPrepared:与预准备语句一起使用。准备好的语句将在本章末尾讨论。

使用cursor()方法的一个优点是,您可以为游标提供参数,该方法将使用适当的游标类返回一个游标对象。支持的参数有

  • buffered:是否缓冲应用中的结果集。默认值来自连接的buffered选项。

  • raw:是否返回原始结果集,而不是将其转换为 Python 类型。默认值来自连接的raw选项。

  • prepared:光标是否将使用准备好的语句。“处理用户输入”一节将给出这样的例子。默认为None ( False)。

  • cursor_class:指定要使用的自定义光标类别。这个自定义类必须是CursorBase类的子类。默认是None。自定义类超出了本书的范围。

  • dictionary:是否将行作为字典返回。不能与rawnamed_tuple组合使用。默认为None ( False)。

  • named_tuple:是否将行作为命名元组返回。如果rawdictionary也被启用,则该选项不可用。默认为None ( False)。

表 3-1 总结了支持的选项组合和返回的光标类别。在标题中,dictionary选项被缩写为“dict”,而named_tuple选项被缩写为“tuple”。表格中留空的选项可以是FalseNone

表 3-1

游标对象的参数

|

减轻

|

生的

|

准备

|

词典

|

元组

|

班级

|
| --- | --- | --- | --- | --- | --- |
|   |   |   |   |   | MySQLCursor |
| True |   |   |   |   | MySQLCursorBuffered |
|   | True |   |   |   | MySQLCursorRaw |
| True | True |   |   |   | MySQLCursorBufferedRaw |
|   |   |   | True |   | MySQLCursorDict |
| True |   |   | True |   | MySQLCursorBufferedDict |
|   |   |   |   | True | MySQLCursorNamedTuple |
| True |   |   |   | True | MySQLCursorBufferedNamedTuple |
|   |   | True |   |   | MySQLCursorPrepared |

如果使用了不受支持的选项组合,则会引发ValueError异常,例如:

ValueError: Cursor not available with given criteria: dictionary, named_tuple

本节的其余部分将涵盖游标执行流以及实例化和使用游标的示例,从执行流开始。

mysql 游标

MySQLCursor类的用法类似于直接从连接类执行查询时的用法:执行查询,然后获取行。

执行查询的主要方法是execute()方法,而读取查询返回的行有三种不同的方法。图 3-2 总结了execute()和行提取方法及其关系。此外,还有用于执行查询的executemany()callproc()方法。它们在第四章与stored_results()一起讨论,后者与callproc()方法一起使用。

img/463459_1_En_3_Fig2_HTML.jpg

图 3-2

使用游标的典型代码流

流程从应用创建连接开始。然后使用cursor()方法创建一个光标。不止有一个游标类;更确切地说,它是一系列依赖于游标的确切性质的类。本章讨论的单个查询是使用execute()方法执行的。

游标类有一个名为with_rows的属性,它指定是否有要处理的结果集。可以使用以下三种方法之一获取行:fetchone()fetchmany()fetchall()。一旦获取了所有行,fetch 方法将返回None或一个空结果。可以重用该游标来执行更多的查询。一旦执行完所有查询,游标和连接都将关闭。

与显示如何使用连接方法的流程图一样,这是一个简化的示例。在这一节的最后,将会更清楚光标是如何工作的,下一章将会添加更多的细节。

mysql 游标

对于存储过程以外的单个查询,使用execute()方法;这包括支持在一次调用中执行多个不同的查询。executemany()方法可用于使用不同的参数集执行相同的查询。

execute()方法接受一个必需的参数,即要执行的查询,以及两个可选参数:

  • operation:要执行的查询。这个参数是强制性的。

  • params:与查询一起使用的参数的字典、列表或元组。“处理用户输入”一节讨论了参数化查询的使用。默认为None

  • multi:当True时,操作被认为是由分号分隔的多个查询,并且execute()返回一个迭代器,使得可以迭代每个查询的结果。第四章包含了这样的例子。默认为False

可以使用下列方法之一提取查询返回的行:

  • fetchall():获取所有剩余的行。这类似于没有任何参数的get_rows()方法。fetchall()使用get_rows()进行连接,以便在一次调用中获得无缓冲游标的所有行。

  • fetchmany():获取一批行,可以设置该批中包含的最大行数。这类似于使用带有参数的get_rows()fetchmany()使用fetchone()实现。默认是一次读取一行。

  • fetchone():一次读取一行。这相当于get_row()方法,也用于无缓冲的结果。

可以使用callproc()方法执行存储过程。stored_results()是一个相关的方法,可以在存储过程返回一个或多个结果集时使用。执行多重查询和使用存储过程将在第四章中讨论。

注意

所有行都必须显式获取,或者通过为连接启用consume_results来获取。如果发现未读的行,使用相同的游标执行新的查询,或者关闭游标,除非启用consume_results,否则将引发异常。一次只能有一个光标用于未读结果的连接。

清单 3-8 展示了一个使用光标查找人口超过 900 万的城市的简单示例(在几个cmd_query()示例中使用了相同的查询)。

import mysql.connector

# Create connection to MySQL
db = mysql.connector.connect(
  option_files="my.ini")

# Instantiate the cursor

cursor = db.cursor()

# Execute the query
cursor.execute(
  """SELECT Name, CountryCode,
            Population
       FROM world.city
      WHERE Population > 9000000
      ORDER BY Population DESC"""
)

print(__file__
      + " - Using the default cursor:")
print("")

if (cursor.with_rows):

  # Print the rows found
  print(
    "{0:15s}   {1:7s}   {2:3s}".format(
      "City", "Country", "Pop"
    )
  )
  city = cursor.fetchone()
  while (city):
    print(
      "{0:15s}   {1:⁷s}   {2:4.1f}".format(
        city[0],
        city[1],
        city[2]/1000000.0
      )
    )
    city = cursor.fetchone()

cursor.close()

db.close()

Listing 3-8Using a Cursor to Execute a SELECT Statement

关于这个程序首先要注意的是,与使用get_row()相比,打印结果的循环更加紧凑。在清单 3-7 中的例子,本质上是使用cmd_query()get_row()的相同例子,循环是 13 行代码(包括读取第一行);在游标示例中,循环是 11 行。这样做的原因是MySQLCursor类自动处理从原始数据到 Python 类型的转换,而不管是使用纯 Python 还是 C 扩展实现,这使得循环遍历行并打印它们变得更加简单。

第二点是fetchone()的使用和循环条件与使用get_rows()的例子相比略有不同。fetchone()的返回值只是该行值的一个元组,而get_rows()还包括eof信息。这意味着当fetchone()返回None时,循环必须终止。

第三点是在获取行之前检查游标的with_rows属性。当查询返回行时,with_rows属性是True。即使提取了所有行,该值也不会改变;这不同于前面检查的 connection 对象的unread_result属性。

除了标题之外,输出与前面的示例相同:

listing_3_8.py - Using the default cursor

City              Country   Pop
Mumbai (Bombay)     IND     10.5
Seoul               KOR     10.0
São Paulo           BRA     10.0
Shanghai            CHN      9.7
Jakarta             IDN      9.6
Karachi             PAK      9.3

既然execute()和获取方法都不包含eof信息,那么如何获得该信息呢?我们来看看。

mysql 游标

使用游标的一个优点是不再需要考虑cmd_query()get_rows()get_row()返回的eof信息。相反,相关信息可通过光标的属性获得。

可用的属性有

  • column_names

  • description

  • lastrowid

  • rowcount

  • statement

  • with_rows

所有属性都是只读的,并且包含与最近执行的查询相关的信息。下面几节将简要讨论每个属性。

列名

column_names属性包括每一列的名称,其顺序与它们的值相同。它与由cmd_query()方法返回的结果字典中的列的列表中的第一个元素相同。

例如,如果应该使用列名作为键将行转换为字典,则列名会很有用:

row = cursor.fetchone()
row_dict = dict(
  zip(cursor.column_names, row)
)

小费

如果您想将所有结果转换成字典,那么使用MySQLCursorDict游标类。“字典和命名元组游标子类”一节中提供了一个示例。

描述

description属性相当于cmd_query()结果字典中的整个 columns 元素。属性的值是一个元组列表,如下所示(使用pprint模块打印):

[('Name', 254, None, None, None, None, 0, 1),
 ('CountryCode', 254, None, None, None, None, 0, 16393),
 ('Population', 3, None, None, None, None, 0, 1)]

元组中包含的值的详细信息可以在第四章的“查询元数据”一节中找到。

lastrawid

在将最后一个分配的 ID 插入到具有自动递增列的表中之后,可以使用lastrowid来获取该 ID。这与cmd_query()INSERT语句返回的 OK 包的insert_id元素相同。如果该语句插入多行,则第一行的 ID 被分配给lastrowid。如果没有 ID 可用,则lastrowid的值为None

行数

属性的含义取决于所执行的语句。对于SELECT语句,它是返回的行数。对于数据修改语言(DML)语句,如INSERTUPDATEDELETE,它是受影响的行数。

对于无缓冲的SELECT查询(默认),行数只有在所有行都被提取后才知道。在这些情况下,rowcount被初始化为-1,在读取第一行时被设置为 1,然后在读取行时递增。也就是说,rowcount将为-1,直到提取了第一行,之后将反映直到读取该属性时提取的行数。

声明

statement属性保存要执行的最后一个或多个查询。当使用参数替换时(参见“处理用户输入”一节),statement属性被设置为结果查询,这对于调试非常有用。

包含 _ 行

with_rows属性是一个布尔值,当查询返回一个结果集时为True。与连接的unread_result属性不同,当所有行都被读取后,with_rows不会被设置为False

字典和命名元组游标子类

除了MySQLCursor之外,其他可用的光标类,比如MySQLCursorDict,都是MySQLCursor类的子类。这意味着所有游标类的行为通常是相同的;区别在于它们如何处理SELECT语句结果的细节,以及对于MySQLCursorPrepared类,查询是如何执行的。

经常出现的一种情况是,需要以字典的形式获取查询结果,而不是每一行都是一个(匿名)元组。例如,在本章的几个示例中使用的循环查询中,城市名称被发现为city[0]或类似名称。按序号位置引用列会使代码难以理解,并且容易出错。使用错误的列号或向查询中添加列很容易导致错误。

更好的解决方案是通过列名来引用它。MySQLCursorDict子类可以自动完成从一组值到一个字典的转换。清单 3-9 展示了一个例子,展示了如何使用设置为Truedictionary参数创建光标。

import mysql.connector

# Create connection to MySQL
db = mysql.connector.connect(
  option_files="my.ini")

# Instantiate the cursor

cursor = db.cursor(dictionary=True)

# Execute the query
cursor.execute(
  """SELECT Name, CountryCode,
            Population
       FROM world.city
      WHERE Population > 9000000
      ORDER BY Population DESC"""
)

print(__file__
      + " - Using the dictionary cursor:")
print("")
if (cursor.with_rows):
  # Print the rows found
  print(
    "{0:15s}   {1:7s}   {2:3s}".format(
      "City", "Country", "Pop"
    )
  )
  city = cursor.fetchone()
  while (city):
    print(
      "{0:15s}   {1:⁷s}   {2:4.1f}".format(
        city["Name"],
        city["CountryCode"],
        city["Population"]/1000000
      )
    )
    city = cursor.fetchone()

cursor.close()
db.close()

Listing 3-9Using the MySQLCursorDict cursor Subclass

与前一个例子的唯一区别是,dictionary=True作为参数提供给db.cursor(),当打印值时,列值由列名引用,例如city["Name"]

MySQLCursorNamedTuple子类的工作方式类似:

...

cursor = db.cursor(named_tuple=True)

...
  city = cursor.fetchone()
  while (city):
    print(
      "{0:15s}   {1:⁷s}   {2:4.1f}".format(
        city.Name,
        city.CountryCode,
        city.Population/1000000
      )
    )
    city = cursor.fetchone()
...

关于游标的讨论到此结束。下一章是关于高级查询用法,它将包括更多关于游标的讨论。然而,在此之前,必须解决一个非常重要的问题:如何处理用户提供的输入。

处理用户输入

程序中的一个常见场景是基于用户或其他外部来源的输入生成查询。毕竟,一个全是静态查询的程序很少让人感兴趣。如何处理这种输入至关重要。如果处理不当,在最好的情况下,会导致神秘的错误;在最坏的情况下,它会导致数据失窃、数据丢失和数据损坏。本节讨论如何正确处理外部提供的数据。

警告

在没有确保信息得到处理以使其不会改变查询含义的情况下,不要将信息输入数据库。例如,如果不这样做,应用就会受到 SQL 注入攻击。

有几种方法可以保护程序。将要讨论的三种方法是

  • 验证输入

  • 参数化查询

  • 使用准备好的语句

这三种方法是本章剩余部分的主题。

验证输入

每当应用读取数据时,验证输入是很重要的。例如,如果应用要求以年为单位的年龄,请验证输入的数据是否为正整数,还可以检查指定的年龄是否在预期范围内。验证不仅有助于使应用更安全,还可以更容易地向用户提供有用的反馈,从而增强用户体验。

注意

客户端数据验证,比如在网页中使用 JavaScript,对于改善用户体验很有帮助,但是不能算作应用的数据验证。原因是用户可以覆盖他们这边执行的验证。

就数据验证而言,Python 编程没有什么独特之处。无论使用何种编程语言,这都是一个常见的要求。然而,如何进行查询参数化是特定于 MySQL Connector/Python 的,这是第二道防线。

查询参数化

防止数据库遭受 SQL 注入攻击的一个好方法是使用参数化查询。这样就把转义和引用数据的任务交给了 MySQL Connector/Python。

使用光标execute()方法进行参数替换有两种方式。第一种是提供一个列表或元组,其中的值与它们在查询中出现的顺序相同。在这种情况下,每个参数在查询文本中都用一个%s表示。如果只有几个参数,或者重复使用,例如对于一个INSERT语句,这是一个提供参数的有用方法。

小费

在参数中指定单个参数,如(“John Doe”),不会创建元组;最终结果是一个标量字符串。如果只有一个参数,可以使用一个列表或者在值后添加一个逗号,比如(" John Doe ",),将值强制转换成一个元组。

另一种方法是提供一个字典,其中每个参数都有一个名称(字典的键,值是参数值)。这更加冗长,但是从好的方面来说,这也使得源代码更容易阅读。如果查询包括几个参数,情况尤其如此。参数在查询中指定,如%(name_of_parameter)s

例如,考虑以下查询:

SELECT *
  FROM world.city
 WHERE Name = ?

问号表示应用用户将提供的数据。假设用户将城市名称指定为'Sydney' OR True。列表 3-10 显示了使用参数化查询的字典处理输入的两种不同方式。

import mysql.connector

input = "'Sydney' OR True"

# Create connection to MySQL
db = mysql.connector.connect(
  option_files="my.ini")

# Instantiate the cursor
cursor = db.cursor(dictionary=True)

# Execute the query without parameter

sql = """SELECT *

           FROM world.city
          WHERE Name = {0}""".format(input)

cursor.execute(sql)

cursor.fetchall()
print("1: Statement: {0}".format(
  cursor.statement))
print("1: Row count: {0}\n".format(
  cursor.rowcount))

# Execute the query with parameter

sql = """SELECT *

           FROM world.city
          WHERE Name = %(name)s"""

params = {'name': input}

cursor.execute(

  sql,
  params=params

)

cursor.fetchall()
print("2: Statement: {0}".format(
  cursor.statement))
print("2: Row count: {0}".format(
  cursor.rowcount))

cursor.close()
db.close()

Listing 3-10Handling User-Provided Data

该输入首先在程序的input变量中设置。然后查询执行两次。在第一次执行中,使用format()字符串方法将输入简单地添加到查询中。在第二次执行中,通过在调用execute()光标方法时设置params选项来添加输入。每次执行后,打印执行的语句和找到的行数。输出是

1: Statement: SELECT *
           FROM world.city
          WHERE Name = 'Sydney' OR True
1: Row count: 4079

2: Statement: SELECT *
           FROM world.city
          WHERE Name = '\'Sydney\' OR True'
2: Row count: 0

注意第一次执行是如何在world.city表中找到所有 4079 行的。原因是WHERE子句最终由两部分组成:Name = 'Sydney'True。由于两个条件之间的OR,所有的城市将最终匹配,因为True匹配所有的东西。

另一方面,第二次执行会转义单引号,并在整个字符串周围添加引号。因此,找不到行,因为没有城市被命名为“‘悉尼’或 True。”

警告

MySQL 连接器/Python 使用 Python 数据类型来确定如何将参数插入到查询中。因此,它不是针对用户提供错误类型数据的防御措施。为了防止使用错误的数据类型,必须使用数据验证和/或准备好的语句。

使用参数化不仅有利于确保数据被正确引用和转义。它还使得重用查询变得容易,并且可以在应用中使用 Python 数据类型,并让 MySQL Connector/Python 处理到 MySQL 数据类型的正确转换。日期就是一个例子。在清单 3-11 中,首先创建一个临时表,然后插入一个包含日期的行,然后打印实际执行的查询。这次参数是在一个元组中提供的。

import mysql.connector
import datetime

# Create connection to MySQL
db = mysql.connector.connect(
  option_files="my.ini")

# Instantiate the cursor
cursor = db.cursor()

# Create a temporary table
sql = """
CREATE TEMPORARY TABLE world.tmp_person (
  Name varchar(50) NOT NULL,
  Birthday date NOT NULL,
  PRIMARY KEY (Name)
)"""
cursor.execute(sql)

sql = """
INSERT INTO world.tmp_person
VALUES (%s, %s)
"""

params = (

  "John Doe",
  datetime.date(1970, 10, 31)

)

cursor.execute(sql,params=params)

print("Statement:\n{0}".format(
  cursor.statement))

cursor.close()
db.close()

Listing 13-11Using Parameters with a datetime Value

print 语句的输出是:

Statement:
INSERT INTO world.tmp_person
VALUES ('John Doe', '1970-10-31')

所以参数替换确保了 1970 年 10 月 31 日的日期在发送给 MySQL 的查询中被正确地表示为'1970-10-31'

与参数优化相关的一种方法是准备语句。这是将要讨论的最后一种防御方法。

准备好的陈述

在处理数据库时,预准备语句非常有用,因为它们比迄今为止使用的更直接的查询方式有一些优势。其中的两个优点是重用查询时性能的提高和防止 SQL 注入。

从 MySQL Connector/Python 的角度来看,使用参数化或预处理语句差别不大。事实上,除了创建不同的游标子类之外,从应用的角度来看,用法是相同的。

然而,在幕后,还是有一些微妙的不同。第一次执行查询时,准备语句;也就是将语句提交给 MySQL 服务器,并带有占位符,MySQL 服务器准备语句以备将来使用。然后,游标发送一个命令,告诉 MySQL 服务器执行准备好的语句以及用于查询的参数。这种方法有两个优点:

  • MySQL Server 在准备阶段尽可能多地准备查询。这意味着对于后续的执行,需要做的工作更少,并且只需要通过网络发送参数,因此性能得到了提高。

  • MySQL 服务器解析查询需要哪些表和列,因此它能够确保根据列的数据类型处理提交的参数。这阻止了 SQL 注入。

注意

关于性能,需要注意的一件事是,如果查询只执行一次,性能不会提高。另一方面,会有到 MySQL 服务器的额外往返,所以使用准备好的语句的性能会比直接执行查询差。重用预处理语句的次数越多,性能就越好。

用于准备和执行准备好的语句的确切方法取决于使用的是纯 Python 还是 MySQL Connector/Python 的 C 扩展实现。纯 Python 实现使用了PREPAREEXECUTE语句(参见 https://dev.mysql.com/doc/refman/en/sql-syntax-prepared-statements.html )。C 扩展使用二进制协议,效率更高。使用 C 扩展和预准备语句需要使用_mysql_connector模块,这将在第四章中讨论。

清单 3-12 展示了一个使用相同查询(除了国家代码之外)来查找美国和印度人口最多的三个城市的例子。

import mysql.connector

# Format strings
FMT_QUERY = "Query {0}:\n" + "-"*8
FMT_HEADER = "{0:18s}   {1:7s}   {2:3s}"
FMT_ROW = "{0:18s}   {1:⁷s}   {2:4.1f}"

# Define the queries
SQL = """
SELECT Name, CountryCode, Population
  FROM world.city
 WHERE CountryCode = %s
 ORDER BY Population DESC
 LIMIT 3"""

# Create connection to MySQL
db = mysql.connector.connect(
  option_files="my.ini", use_pure=True)

cursor = db.cursor(prepared=True)

# Execute the query finding the top
# three populous cities in the USA and
# India.
count = 0
for country in ("USA", "IND"):
  count = count + 1;
  print(FMT_QUERY.format(count))

  cursor.execute(SQL, (country,))

  if (cursor.with_rows):
    # Print the result.
    print(FMT_HEADER.format(
      "City", "Country", "Pop"))
    city = cursor.fetchone()
    while (city):
      print(FMT_ROW.format(
        city[0],
        city[1],
        city[2]/1000000
      ))
      city = cursor.fetchone()

  print("")

cursor.close()
db.close()

Listing 13-12Using Prepared Statements

这个例子与前面的例子非常相似,除了光标是用prepared=True创建的。主要区别是不支持命名参数,所以使用了%s。该程序的输出是

Query 1:
--------
City                 Country    Pop
New York               USA      8.0
Los Angeles            USA      3.7
Chicago                USA      2.9

Query 2:
--------
City                 Country    Pop
Mumbai (Bombay)        IND     10.5
Delhi                  IND      7.2
Calcutta [Kolkata]     IND      4.4

使用准备好的语句将行转换为字典或命名元组时,不支持。这使得使用预准备语句有些困难。最后,callproc()stored_results()方法(参见第四章中的“存储过程”一节)没有实现。好的一面是提高了对 SQL 注入的保护,所以值得做额外的工作。

注意

预准备语句游标比其他游标子类更基本。不支持字符串、字典、命名元组和存储过程方法的数据转换。如果经常使用预准备语句,那么值得考虑添加对这些特性的支持的自定义游标类。

摘要

本章讲述了使用 MySQL Connector/Python 执行查询的基础知识。您开始时使用了可用于连接对象的方法:

  • cmd_query()执行查询

  • get_rows()在查询生成结果集时获取多行(默认为所有行)

  • get_row()一次提取一行

这些方法可以被认为是低级方法。在更高层次上,游标类支持执行查询,同时支持将结果自动转换为 Python 类型和其他特性。讨论的光标方法有

  • execute()执行查询

  • 用于读取结果集的fetchone()fetchmany()fetchall()

最后,您学习了如何处理用户输入。非常重要的一点是,所有的输入都要经过验证,并且使用参数化来防止 SQL 注入。可以使用游标执行参数化。在游标中启用预准备语句提供了额外的保护,因为是 MySQL 服务器在了解目标数据类型的情况下处理参数。当重复执行相同的基本查询时,预处理语句也可以提高性能。

MySQL Connector/Python 中的查询执行还有很多工作要做,所以下一章将继续介绍更高级的例子。

四、高级查询执行

在前一章中,您已经了解了执行查询的基础知识。本章探讨了与查询执行相关的附加功能。它从研究在一个 API 调用中执行多个查询的选项开始,然后转向诸如缓冲结果、调用存储过程和在 CSV 文件中加载数据等特性。

本章的后半部分重点介绍连接属性、如何执行事务、使用默认数据库属性以避免为每个表显式指定数据库名称,以及使用时区。它还概述了如何使用查询后可用的列信息。本章最后讨论了 C 扩展。

小费

本章中有许多示例程序。列表中出现的所有示例程序都可以下载。有关使用示例程序的更多信息,参见第一章中对示例程序的讨论。

多查询执行

在前一章中,对cmd_query()execute()方法的所有调用都涉及到一个查询。然而,也可以通过一次调用 MySQL Connector/Python 来执行多个查询。

注意

虽然一次提交多个查询似乎是一种简化,并且在某些情况下可以提高性能,但也有不利的一面。特别是,当阅读程序的源代码时,可能更难理解正在发生的事情。因此,一定要小心使用对多查询执行的支持。

支持使用连接对象和游标的多个查询。假设连接对象名为db,光标名为cursor,支持处理多个查询的方法有

  • db.cmd_query_ iter ():这与cmd_query()方法类似,只是它返回一个生成器,可以用来获取每个查询的结果。该方法仅在纯 Python 实现中可用。

  • cursor. execute():当multi参数启用时,游标的execute()方法也可以执行多个查询。这相当于cmd_query_iter(),返回结果的生成器。

  • cursor. executemany():该方法采用一个模板(带有参数占位符的查询)和一组参数列表。不支持返回结果。

这三种方法将是本节剩余部分的主题,首先使用cmd_query_iter()execute()方法执行几个查询,然后使用executemany()继续执行基于模板的查询,最后使用executemany()将多行插入到一个表中。

支持结果的多重查询

与前一章中使用的方法等效但支持执行多个查询的方法是 connection 对象的cmd_query_iter()方法和 cursor 对象的execute()方法(与用于单个查询的方法相同)。在这两种情况下,结果的生成器(不是行!)被返回。

以这种方式执行查询的流程如图 4-1 所示。

img/463459_1_En_4_Fig1_HTML.jpg

图 4-1

一次执行多个查询的流程

图 4-1 显示查询被一个接一个地发送到 MySQL 服务器。当 MySQL 服务器执行完查询后,结果被发送回应用。一旦读取了所有行,MySQL Connector/Python 会自动发送下一个查询。这意味着启用缓冲(将在本章后面的“缓冲结果”一节中讨论)对于多语句执行非常有用。

cmd_query_iter()execute()multi = True的细节将在接下来的小节中依次讨论。

连接- cmd_query_iter()

cmd_query_iter()方法的工作方式类似于cmd_query()方法。主要区别在于cmd_query_iter()返回一个可以用来获取结果的生成器,而不是直接返回结果。

清单 4-1 展示了一个例子,其中cmd_query_iter()用于选择美国和印度人口最多的三个城市。

注意

cmd_query()一样,在 MySQL Connector/Python 8.0.12 和更高版本中不需要使用 MySQLConverter 类。

import mysql.connector
from mysql.connector.conversion import MySQLConverter
from datetime import datetime
from time import sleep

# Format strings
FMT_QUERY = "Query {0} - {1}:\n" + "-"*19
FMT_HEADER = "{0:18s}   {1:7s}   {2:3s}"
FMT_ROW = "{0:18s}   {1:⁷s}   {2:4.1f}"

# Create connection to MySQL
db = mysql.connector.connect(
  option_files="my.ini", use_pure=True)

# Prepare the converter
converter = MySQLConverter(db.charset, True)

# Define the queries
sql1 = """
SELECT Name, CountryCode, Population
  FROM world.city
 WHERE CountryCode = 'USA'
 ORDER BY Population DESC
 LIMIT 3"""

sql2 = "DO SLEEP(3)"

sql3 = """
SELECT Name, CountryCode, Population
  FROM world.city
 WHERE CountryCode = 'IND'
 ORDER BY Population DESC
 LIMIT 3"""
queries = [sql1, sql2, sql3]

# Execute the queries and obtain the
# iterator

results = db.cmd_query_iter(";".join(queries))

# Iterate through the results
count = 0

for result in results:

  count = count + 1;
  time = datetime.now().strftime('%H:%M:%S')
  print(FMT_QUERY.format(count, time))
  if ('columns' in result):
    # It is one of the SELECT statements
    # as it has column definitions.
    # Print the result.
    print(FMT_HEADER.format(
      "City", "Country", "Pop"))
    (city, eof) = db.get_row()
    while (not eof):
      values = converter.row_to_python(
        city, result["columns"])
      print(FMT_ROW.format(
        values[0],
        values[1],
        values[2]/1000000.0
      ))
      (city, eof) = db.get_row()
  else:
    # Not a SELECT statement
    print("No result to print")

  sleep(2)
  print("")

db.close()

Listing 4-1Executing Multiple Queries with cmd_query_iter()

首先要注意的是,连接是使用pure_python = True建立的。cmd_query_iter()方法是需要使用纯 Python 实现的情况之一,因为该方法在 C 扩展实现中不可用。

建立连接后,定义了后面输出的格式字符串,并定义了将要执行的三个查询。执行查询后,每个结果都有一个循环。如果结果字典包含列信息,那么就有要提取的行。否则,它是另一种类型的查询(例如INSERT或这里的DO语句)。

注意

游标的with_rows属性也是通过检查结果字典中是否有列信息来工作的。

插入了两次休眠:第二个查询在 MySQL 服务器端执行三秒钟的休眠,在处理结果的每个循环结束时,Python 代码中有两秒钟的休眠。这使您可以从输出中看到查询和获取结果的流程。输出是

Query 1 - 16:24:58:
-------------------
City                 Country    Pop
New York               USA      8.0
Los Angeles            USA      3.7
Chicago                USA      2.9

Query 2 - 16:25:01:
-------------------
No result to print

Query 3 - 16:25:03:
-------------------
City                 Country    Pop
Mumbai (Bombay)        IND     10.5
Delhi                  IND      7.2
Calcutta [Kolkata]     IND      4.4

第一个查询在 16:24:58 完成执行。第二个查询是三秒钟的睡眠,这也是查询结果准备好之前的延迟。由于在处理第一个查询的结果后,Python 代码中还有两秒钟的休眠,这表明在读取第一个查询的结果后,当应用“工作”时,第二个查询正在执行。

这也显示了使用多查询方法的优势之一:如果查询很慢,应用可能也需要一些时间来处理结果,这可以提高整体性能,因为应用可以在执行下一个查询时继续处理查询结果。

接下来是如何使用光标来执行相同的任务。

光标–执行()

使用游标一次执行多个查询与执行单个查询非常相似。在这两种情况下,都使用了execute()方法。主要区别在于multi参数被设置为True,并且返回一个生成器。查询在一个字符串中提交,查询之间用分号分隔,就像使用cmd_query_iter()一样。

支持向查询传递参数;但是,所有参数都必须在一个元组、列表或字典中。这使得在需要参数时使用多查询执行变得不太有用;相反,在这些情况下,建议执行几次单个查询。

小费

如果所有的查询都使用相同的查询模板,并且没有要处理的结果集,那么接下来讨论的executemany()方法是用multi = True代替execute()的一个有用的替代方法。

清单 4-2 显示了对应于前一个cmd_query_iter()的例子,但是这次使用了一个光标。Python 代码中的休眠和时间戳的打印已经被删除,因为它们的工作方式是一样的。

import mysql.connector

# Format strings
FMT_QUERY = "Query {0}:\n" + "-"*8
FMT_HEADER = "{0:18s}   {1:7s}   {2:3s}"
FMT_ROW = "{0:18s}   {1:⁷s}   {2:4.1f}"

# Create connection to MySQL
db = mysql.connector.connect(
  option_files="my.ini")

# Define the queries
sql_select = """
SELECT Name, CountryCode, Population
  FROM world.city
 WHERE CountryCode = %s
 ORDER BY Population DESC
 LIMIT 3"""
sql_do = "DO SLEEP(3)"
queries = [sql_select, sql_do, sql_select]

# Execute the queries and obtain the
# iterator
cursor = db.cursor()

results = cursor.execute(

  ";".join(queries),
  params=("USA", "IND"),
  multi=True

)

# Iterate through the results
count = 0
for result in results:
  count = count + 1;
  print(FMT_QUERY.format(count))
  if (result.with_rows):
    # It is one of the SELECT statements
    # as it has column definitions.
    # Print the result.
    print(FMT_HEADER.format(
      "City", "Country", "Pop"))
    city = cursor.fetchone()
    while (city):
      print(FMT_ROW.format(
        city[0],
        city[1],
        city[2]/1000000
      ))
      city = cursor.fetchone()
  else:
    # Not a SELECT statement
    print("No result to print")

  print("")

cursor.close()
db.close()

Listing 4-2Using a Cursor to Execute Multiple Queries

代码中只有很少的惊喜。主要的是,如前所述,params参数是所有查询共享的一个元组。在这种情况下,跟踪查询中的参数是相当简单的,但是一般来说,这可能会变得很困难并且容易出错。因此,如果需要参数替换,在大多数情况下最好使用单个查询的多次执行,或者如果用例允许,使用 cursor executemany()方法。

警告

在多查询执行中使用参数替换容易出错。考虑逐个执行查询,或者如果没有返回结果集,并且所有查询都使用相同的模板,则使用 cursor executemany()方法。

该脚本的输出类似于前面的示例:

Query 1:
--------
City                 Country    Pop
New York               USA      8.0
Los Angeles            USA      3.7
Chicago                USA      2.9

Query 2:
--------
No result to print

Query 3:
--------
City                 Country    Pop
Mumbai (Bombay)        IND     10.5
Delhi                  IND      7.2
Calcutta [Kolkata]     IND      4.4

基于一个模板的多个查询

在某些情况下,有必要反复执行相同的查询,但使用不同的参数。对于那个用例,executemany()方法是存在的。

executemany()的主要缺点是不支持返回结果集。在每次查询执行之后,检查是否有任何要提取的行;如果是这样的话,所有的行都会被提取,但不会被保存。甚至返回值永远是None

清单 4-3 展示了一个简单的例子,其中几个城市都有人口变化。因为基本查询是相同的,executemany()是这个任务的一个很好的候选。为了便于查看更新的城市,城市名称、地区和国家代码被拼写出来。然而,在实际的应用中,如果主键(city表的ID列)是已知的,那么它是要更新的行的更好的标识符,因为它需要更少的锁和更好的性能。

小费

尽可能使用主键或至少另一个索引来查找行。索引越具体,搜索的行数就越少,持有的锁也就越少。这适用于所有选择、更新或删除行的查询。

import mysql.connector

# Create connection to MySQL
db = mysql.connector.connect(
  option_files="my.ini", use_pure=True)
cursor = db.cursor()

# Definte the query template and the
# parameters to submit with it.
sql = """
UPDATE world.city
   SET Population = %(population)s
 WHERE Name = %(name)s
       AND CountryCode = %(country)s
       AND District = %(district)s"""

params = (
  {
    "name": "Dimitrovgrad",
    "country": "RUS",
    "district": "Uljanovsk",
    "population": 150000
  },
  {
    "name": "Lower Hutt",
    "country": "NZL",
    "district": "Wellington",
    "population": 100000
  },
  {
    "name": "Wuhan",
    "country": "CHN",
    "district": "Hubei",
    "population": 5000000
  },
)

# Get the previous number of questions
# asked to MySQL by the session
cursor.execute("""
  SELECT VARIABLE_VALUE
    FROM performance_schema.session_status
   WHERE VARIABLE_NAME = 'Questions'""")
tmp = cursor.fetchone()
questions_before = int(tmp[0])

# Execute the queries

cursor.executemany(sql, params)

print("Row count: {0}".format(
  cursor.rowcount))

print("Last statement: {0}".format(

  cursor.statement))

# Get the previous number of questions
# asked to MySQL by the session
cursor.execute("""
  SELECT VARIABLE_VALUE
    FROM performance_schema.session_status
   WHERE VARIABLE_NAME = 'Questions'""")
tmp = cursor.fetchone()
questions_after = int(tmp[0])

print("Difference in number of"
  + " questions: {0}".format(
    questions_after-questions_before
))

db.rollback()
cursor.close()
db.close()

Listing 4-3Using 
executemany()

to Update Several Rows

首先,定义模板。在这种情况下,命名参数用于使使用参数字典序列成为可能。它产生了更冗长的代码,但也产生了更易于阅读和理解的代码。

然后调用executemany()方法并打印修改的行数。在这种情况下,更新了三行。在executemany()调用之前和之后,连接询问的问题数量从performance_schema.session_status表中获取。这用于显示在executemany()调用期间有多少查询被发送到 MySQL 服务器。光标的rowcountstatement属性用于获取关于调用executemany()的一些信息。创建连接时,使用statement属性是使用use_pure = True的原因;当使用 C 扩展时,除了扩展插入之外,使用executemany()执行查询时不支持statement属性。

小费

在 MySQL Server 5.6 及更早版本中,将performance_schema.session_status表更改为information_schema.session_status表。

最后,事务回滚到示例开始之前的状态,离开city表。这样做只是为了让所有的例子都使用相同的已知数据状态;另外,这意味着您可以重新执行该示例,并获得相同的结果。事务将在本章后面的“事务”一节中讨论。该程序的输出是

Row count: 3
Last statement: UPDATE world.city
   SET Population = 5000000
 WHERE Name = 'Wuhan'
       AND CountryCode = 'CHN'
       AND District = 'Hubei'
Difference in number of questions: 4

输出显示executemany()更新了三行(然后回滚),最后执行的语句更新了武汉市的人口。前后问的问题数量相差四个:三个用于三个 update 语句,一个用于查询问的问题数量。

如图所示,通过executemany()调用execute()方法来逐个执行查询。因此,与在应用本身中循环查询相比,没有性能优势。然而,有一个例外:INSERT声明。

扩展插件

MySQL 支持一个叫做扩展插入的特性。通常,当向表中插入多行时,这是通过一系列INSERT语句完成的:

CREATE TEMPORARY TABLE world.t1 (
  id int unsigned NOT NULL,
  val varchar(10),
  PRIMARY KEY (id)
);

INSERT INTO world.t1 VALUES (1, 'abc');

INSERT INTO world.t1 VALUES (2, 'def');

INSERT INTO world.t1 VALUES (3, 'ghi');

SELECT * FROM world.t1;
+----+------+
| id | val  |
+----+------+
|  1 | abc  |
|  2 | def  |
|  3 | ghi  |
+----+------+

这可以转换成使用扩展插入的单个语句:

DELETE FROM world.t1;

INSERT INTO world.t1

VALUES (1, 'abc'),

       (2, 'def'),
       (3, 'ghi');
SELECT * FROM world.t1;
+----+------+
| id | val  |
+----+------+
|  1 | abc  |
|  2 | def  |
|  3 | ghi  |
+----+------+

与单次插入相比,扩展插入可以大大提高批量插入的性能,尤其是在启用了自动提交的情况下(有关自动提交的更多信息,请参见“事务”一节)。

MySQL Connector/Python 内置了对使用 executemany()方法生成扩展插入语句的支持。当检测到模板与 INSERT 语句匹配时,将生成一条插入所有所需行的语句。清单 4-4 展示了在world.t1临时表中插入三行的例子。

import mysql.connector

# Create connection to MySQL
db = mysql.connector.connect(
  option_files="my.ini"
)
cursor = db.cursor()

# Create a temporary table for this
# example
cursor.execute("""
  CREATE TEMPORARY TABLE world.t1 (
    id int unsigned NOT NULL,
    val varchar(10),
    PRIMARY KEY (id)
  )""")
# Definte the query template and the
# parameters to submit with it.
sql = """
INSERT INTO world.t1 VALUES (%s, %s)"""
params = (
  (1, "abc"),
  (2, "def"),
  (3, "ghi")
)
# Get the previous number of questions
# asked to MySQL by the session
cursor.execute("""
  SELECT VARIABLE_VALUE
    FROM performance_schema.session_status
   WHERE VARIABLE_NAME = 'Questions'""")
tmp = cursor.fetchone()
questions_before = int(tmp[0])

# Execute the query

cursor.executemany(sql, params)

print("Row count = {0}".format(
  cursor.rowcount))
print("Last statement: {0}".format(
  cursor.statement
))

# Get the previous number of questions
# asked to MySQL by the session
cursor.execute("""
  SELECT VARIABLE_VALUE
    FROM performance_schema.session_status
   WHERE VARIABLE_NAME = 'Questions'""")
tmp = cursor.fetchone()
questions_after = int(tmp[0])

print("Difference in number of"
  + " questions: {0}".format(
    questions_after-questions_before
))

cursor.close()
db.close()

Listing 4-4Using 
executemany()

to Insert Several Rows

除了是一个INSERT语句和为要插入的数据创建一个临时表之外,这个例子基本上与前一个一样。这次的输出是

Row count = 3
Last statement: INSERT INTO world.t1 VALUES (1, 'abc'),(2, 'def'),(3, 'ghi')
Difference in number of questions: 2

行数仍然是三,但是现在最后一条语句包含了所有三行:三条INSERT语句被重写为一条插入所有三行的语句。这也反映在问题的数量上,比UPDATE示例少了两个。

现在多重查询的主题到此结束。在“交易”部分将有一个简短的附加讨论。现在是时候看看游标的其他一些特性了;首先是缓冲结果。

缓冲结果

游标的一个特殊特性是,可以让 MySQL Connector/Python 在查询后自动获取结果集并进行缓冲,以便以后使用。缓冲结果会尽可能快地释放 MySQL 服务器的资源,但反而会增加应用的需求。这使得缓冲在应用处理小结果集时最有用。缓冲光标可与dictionarynamed_tuple选项结合使用。

与非缓冲游标相比,缓冲游标的一个优点是,即使两个游标都包含结果集,也可以同时为同一连接激活两个游标。对于非缓冲游标,试图在获取所有行之前通过同一连接执行查询会导致异常。由于缓冲游标会自动获取行,并使获取方法从缓冲区中读取,就 MySQL 服务器而言,连接可以自由地再次使用。

这个特性有用的两个例子是

  • 在并排处理返回的行有意义的情况下,执行两个或更多的SELECT语句。通常,使用JOIN将这些查询重写为一个查询会更好,但偶尔也有理由使用更多但更简单的查询。

  • 从一个查询中读取行,然后在另一个查询中使用这些行。同样,组合查询可能更好,但是,例如,如果读取了一行以便可以更新它,而业务逻辑在数据库中不可用,那么使用两个游标可能会很有用。

注意

缓冲允许在处理结果集之前在同一个游标中执行新的查询。但是,这将导致旧的结果集被丢弃。

清单 4-5 展示了一个例子,其中缓冲和到字典的转换都被启用。有两个游标:cursor1读取澳大利亚的城市,cursor2通过增加 10%来更新人口。这种情况下的业务逻辑非常简单,最好在一个查询中完成,但是在更新行的逻辑比较复杂的情况下,也可以使用类似的方法。

import mysql.connector
from math import ceil

# The SQL UPDATE statement that will be
# used in cursor2.
SQL_UPDATE = """
  UPDATE world.city
     SET Population = %(new_population)s
   WHERE ID = %(city_id)s"""

# Function to increase the population
# with 10%

def new_population(old_population):

  return int(ceil(old_population * 1.10))

# Create connection to MySQL
db = mysql.connector.connect(
  option_files="my.ini")

# Instantiate the cursors

cursor1 = db.cursor(

  buffered=True, dictionary=True)

cursor2 = db.cursor()

# Execute the query to get the
# Australian cities
cursor1.execute(
  """SELECT ID, Population
       FROM world.city
      WHERE CountryCode = %s""",
  params=("AUS",)
)

city = cursor1.fetchone()
while (city):
  old_pop = city["Population"]
  new_pop = new_population(old_pop)
  print("ID, Old => New: "
    + "{0}, {1} => {2}".format(
    city["ID"], old_pop, new_pop
  ))
  cursor2.execute(
    SQL_UPDATE,
    params={
      "city_id": city["ID"],
      "new_population": new_pop
    }
  )
  print("Statement: {0}".format(
    cursor2.statement))
  city = cursor1.fetchone()

db.rollback()
cursor1.close()
cursor2.close()
db.close()

Listing 4-5Using a Buffering Cursor to Update Rows

首先,定义更新群体的逻辑。在这种情况下,这是一个简单的函数,它将参数增加 10%,然后将结果向上舍入到最接近的整数。

定义了两个游标。cursor1读取将要更新的行,因此它必须是一个缓冲游标。在这种情况下,还决定将行作为字典返回。cursor2在读取来自cursor1的行时执行更新。在本例中,cursor2是缓冲还是非缓冲游标并不重要,因为它不返回任何行。

在执行了用于查找澳大利亚城市的 ID 和现有人口的SELECT查询之后,会有一个遍历城市的循环。对于每个城市,计算新的人口并执行UPDATE语句。城市 ID、前后人口以及UPDATE语句都作为输出打印出来。前三个城市的产量是

ID, Old => New: 130, 3276207 => 3603828
Statement: UPDATE world.city
     SET Population = 3603828
   WHERE ID = 130
ID, Old => New: 131, 2865329 => 3151862
Statement: UPDATE world.city
     SET Population = 3151862
   WHERE ID = 131
ID, Old => New: 132, 1291117 => 1420229
Statement: UPDATE world.city
     SET Population = 1420229
   WHERE ID = 132
...

警告

两个游标的查询必须在同一个事务中执行(默认情况下发生),以确保正确的结果。否则,另一个连接可能会更新SELECTUPDATE之间的群体。但是,只要事务处于活动状态,这些行就会被锁定,因此要注意循环的执行时间不要太长,否则可能会导致其他查询超时或发生死锁。

最后要讨论的特定于游标的特性是对存储过程的支持。

存储过程

存储过程的特性意味着对于某些用例,它们必须被区别对待。具体来说,它们可以通过参数列表返回值,并且调用存储过程的单个查询可以返回多个结果集。在幕后,连接的cmd_query_iter()方法与每个结果集的一个内部缓冲游标一起使用。

警告

因为缓冲游标用于处理存储过程的结果集,所以应用端的内存使用率可能会高于预期。如果返回大量结果,请小心使用存储过程支持。

可用于执行存储过程的游标方法有

  • callproc():这是用来执行存储过程的方法。返回值是一个带有传递给过程的参数的元组。

  • stored_results():这个方法是一个生成器,用于迭代由callproc()调用的存储过程返回的结果集。

理解这两个过程如何工作的最简单的方法是考虑一个例子。为此,将使用清单 4-6 中的min_max_cities()程序。

DELIMITER $$
CREATE PROCEDURE world.min_max_cities(
    IN in_country char(3),
    INOUT inout_min int,
    OUT out_max int
)
SQL SECURITY INVOKER
BEGIN
  SELECT MIN(Population),
         MAX(Population)
    INTO inout_min, out_max
    FROM world.city
   WHERE CountryCode = in_country
         AND Population >= inout_min;

  SELECT *
    FROM world.city
   WHERE CountryCode = in_country
         AND Population >= inout_min
   ORDER BY Population ASC
   LIMIT 3;

  SELECT *
    FROM world.city
   WHERE CountryCode = in_country
         AND Population >= inout_min
   ORDER BY Population DESC
   LIMIT 3;
END$$
DELIMITER ;

Listing 4-6The min_max_cities() Procedure

该过程查找给定国家的城市的最小和最大人口,其中城市人口必须至少达到一定数量。然后选择满足最低人口要求的三个城市的所有数据,最后选择人口最多的三个城市的所有数据。该过程有三个参数:

  • in_country:过滤城市所依据的国家代码。该参数在过程中是只读的。

  • 在投入上,这是城市必须拥有的最低人口。就产出而言,它是满足需求的城市的最小人口。

  • out_max:在输出中,它包含至少有inout_min名居民的人口最多的城市的人口。输入值被丢弃。

总之,这个过程使用了 MySQL 连接器/Python 游标中存储过程实现的所有特性。该程序的安装方式与安装world样本数据库的方式相似:

shell$ mysql --user=pyuser --password \
             --host=127.0.0.1 --port=3306 \
             --execute="SOURCE listing_4_6.sql"

该命令假定mysql命令行客户端是执行搜索路径,并且带有过程定义的文件listing_4_6.sql位于当前工作目录中。在 Windows 上,可以使用相同的命令,但所有参数必须在同一行上。清单 4-7 中显示了在mysql命令行客户端中使用该过程的示例。

mysql> SET @MIN = 500000;
Query OK, 0 rows affected (0.00 sec)

mysql> CALL world.min_max_cities('AUS', @MIN, @MAX);
+-----+----------+-------------+-----------------+------------+
| ID  | Name     | CountryCode | District        | Population |
+-----+----------+-------------+-----------------+------------+
| 134 | Adelaide | AUS         | South Australia |     978100 |
| 133 | Perth    | AUS         | West Australia  |    1096829 |
| 132 | Brisbane | AUS         | Queensland      |    1291117 |
+-----+----------+-------------+-----------------+------------+
3 rows in set (0.01 sec)

+-----+-----------+-------------+-----------------+-----------+
| ID  | Name      | CountryCode | District        |Population |
+-----+-----------+-------------+-----------------+-----------+
| 130 | Sydney    | AUS         | New South Wales |   3276207 |
| 131 | Melbourne | AUS         | Victoria        |   2865329 |
| 132 | Brisbane  | AUS         | Queensland      |   1291117 |
+-----+-----------+-------------+-----------------+-----------+
3 rows in set (0.01 sec)

Query OK, 0 rows affected (0.02 sec)

mysql> SELECT @MIN, @MAX;
+--------+---------+
| @MIN   | @MAX    |
+--------+---------+
| 978100 | 3276207 |
+--------+---------+
1 row in set (0.00 sec)

Listing 4-7Using the world.min_max_cities Procedure

清单 4-8 显示了相应的 Python 程序,该程序使用callproc()方法调用过程,然后使用stored_results()方法读取结果集。

import mysql.connector

# Format strings
FMT_QUERY = "Query {0}:\n" + "-"*8
FMT_HEADER = "{0:18s}   {1:3s}"
FMT_ROW = "{0:18s}   {1:4.1f}"

# Create connection to MySQL
db = mysql.connector.connect(
  option_files="my.ini")
cursor = db.cursor()

# Execute the procedure

return_args = cursor.callproc(

  "world.min_max_cities",
  ("AUS", 500000, None)

)

# Print the returned arguments
print("""Country ..........: {0}
Min Population ...: {1:8d}
Max Population ...: {2:8d}
""".format(*return_args))

# Iterate over the result sets and print
# the cities and their population
# Convert the rows to dictionaries to
# avoid referencing the columns by
# ordinal position.
count = 0

for result in cursor.

stored_results()

:

  count = count + 1;
  print(FMT_QUERY.format(count))
  if (result.with_rows):
    # It is one of the SELECT statements
    # as it has column definitions.
    # Print the result.
    print(FMT_HEADER.format("City", "Pop"))
    city = result.fetchone()
    while (city):
      city_dict = dict(
        zip(result.column_names, city))

      print(FMT_ROW.format(
        city_dict["Name"],
        city_dict["Population"]/1000000
      ))
      city = result.fetchone()
  print("")

cursor.close()
db.close()

Listing 4-8Using the Cursor Stored Procedure Methods

建立连接并设置打印输出的格式字符串后,使用callproc()方法调用该过程。args参数(第二个参数)必须为过程采用的每个参数包含一个元素,即使有些参数只用作 out 参数。返回值是一个元组,传递给过程的每个参数有一个元素。对于只发送到过程的参数,在返回元组中使用原始值。

小费

默认情况下,返回的参数将保留过程中定义的数据类型(除非对于游标为raw=True,在这种情况下,它们将作为arraybytes返回)。然而,也可以显式指定 MySQL 数据类型,例如(0, 'CHAR')。可用类型见 https://dev.mysql.com/doc/refman/en/cast-functions.html

程序的最后一部分迭代由stored_results()返回的结果。该循环类似于其他多结果集方法。输出如下所示:

Country ..........: AUS
Min Population ...:   978100
Max Population ...:  3276207

Query 1:
--------
City                  Pop
Adelaide              1.0
Perth                 1.1
Brisbane              1.3

Query 2:
--------
City                  Pop
Sydney                3.3
Melbourne             2.9
Brisbane              1.3

有了存储过程,就只剩下一种类型的查询需要讨论了:加载存储为逗号分隔值(CSV)的数据。

使用 CSV 文件加载数据

在系统之间传输数据的一种流行方式是使用逗号分隔值的文件(CSV)。这是存储数据的标准方式,从导出电子表格到数据库备份,这种方式得到了广泛的支持。加载存储在 CSV 文件中的数据也是一种相对高效的大容量加载数据的方式。

注意

虽然 CSV 中的 C 表示数据用逗号分隔,但通常使用制表符、空格、分号或其他字符作为分隔符。事实上,MySQL 使用制表符作为默认分隔符。

加载数据的 MySQL 语句是LOAD DATA INFILE命令。MySQL Connector/Python 中没有对此命令的原生支持,但仍有一些特殊的考虑。使用LOAD DATA INFILE主要有两种方式:加载位于安装 MySQL 服务器的主机上的文件或者从应用端加载文件。在任何一种情况下,该语句都像任何其他使用连接cmd_query()方法或游标execute()方法的单个语句一样执行。

加载服务器端文件

加载一个位于安装 MySQL 服务器的主机上的文件是在没有任何修饰符的情况下执行LOAD DATA INFILE时使用的方法。需要注意的主要事情是 MySQL 用户必须拥有FILE特权,并且 CSV 文件不能位于任何随机位置。

MySQL 服务器的secure_file_priv选项( https://dev.mysql.com/doc/refman/en/server-system-variables.html#sysvar_secure_file_priv )限制了允许LOAD DATA INFILE读取文件的路径。只能使用secure_file_priv指定的路径或其下的路径。secure_file_priv选项还指定了SELECT … INTO OUTFILE语句可以将数据导出到哪里。例如,secure_file_priv的当前值可以使用以下查询找到:

mysql> SELECT @@global.secure_file_priv;
+---------------------------+
| @@global.secure_file_priv |
+---------------------------+
| C:\MySQL\Files\           |
+---------------------------+
1 row in set (0.00 sec)

在最近的 MySQL 服务器版本中,secure_file_priv默认为NULL,这将禁用导入和导出,除非在 Linux 上使用原生包安装(默认为/var/lib/mysql-files)或在 Windows 上使用 MySQL 安装程序(默认为C:\ProgramData\MySQL\MySQL Server 8.0\Uploads\或类似)。

只能通过更新 MySQL 配置文件来更改secure_file_priv选项(按照惯例,Windows 上的my.ini和其他平台上的my.cnf)。例如:

[mysqld]
secure_file_priv = C:\MySQL\Files

MySQL 配置文件更新后,需要重新启动 MySQL 服务器以使更改生效。此时,可以使用LOAD DATA INFILE命令加载数据。在讨论加载数据的例子之前,让我们看一下从 MySQL 服务器端加载文件的替代方法:加载应用本地的文件。

加载应用端文件

LOCAL关键字被添加到命令:LOAD DATA LOCAL INFILE时,使用本地版本。在 MySQL 服务器端,选项local_infile ( https://dev.mysql.com/doc/refman/en/server-system-variables.html#sysvar_local_infile )指定是否允许该特性。在 MySQL Server 5.7 及更早版本中,默认启用;在 8.0 及更高版本中,默认情况下禁用该功能。

在 MySQL Connector/Python 中,选项allow_local_infile指定是否允许加载本地文件。在所有最近的 MySQL Connector/Python 版本中,allow_local_infile是默认启用的。该选项在创建连接时设置,或者通过连接config()方法设置。

小费

从安全角度来看,最好禁用从应用端读取数据文件的支持。也就是建议在 MySQL 服务器端设置local_infile = 0(MySQL Server 8.0 及以后版本默认),在 MySQL Connector/Python 程序中设置allow_local_infile = False,除非确实需要该特性。允许读取本地文件的一个潜在问题是,web 应用中的一个错误最终可能会允许用户检索应用可以读取的任何文件。参见 https://dev.mysql.com/doc/refman/en/load-data-local.html 了解更多关于加载本地文件的安全隐患。

这一节的其余部分将通过一个例子,主要关注本地变体,但也有一些关于如何将它转化为服务器端的例子的说明。

加载数据示例

LOAD DATA INFILE语句非常通用,因为它可以处理不同的分隔符、引用样式、行尾等。该语句的完整指南超出了本书的范围,但是值得看一看一个例子。代码清单将使用应用端变体;然而,服务器端非常相似,从服务器端加载文件只是一个练习。

小费

LOAD DATA INFILE的完整文档见 https://dev.mysql.com/doc/refman/en/load-data.html

该示例将加载文件testdata.txt,该文件位于执行 Python 程序的同一目录中。数据将被加载到world.loadtest表中。该文件的内容是

# ID, Value
1,"abcdef..."
2,"MySQL Connector/Python is fun"
3,"Smileys require utf8mb4"

4, img/463459_1_En_4_Figa_HTML.gif

最后一个值是海豚表情符号(U+1F42C)。由于海豚是 UTF-8 (0xF09F90AC)中需要四个字节的表情符号之一,所以需要使用 MySQL 中的utf8mb4字符集(MySQL 8.0 及以后版本中的默认)。可以用下面的语句创建world.loadtest表:

CREATE TABLE world.loadtest (
  id int unsigned NOT NULL PRIMARY KEY,
  val varchar(30)
) DEFAULT CHARACTER SET=utf8mb4;

由于这是加载一个本地文件,您必须在 MySQL 服务器中启用local_infile选项。这可以使用下面的语句来完成:

mysql> SET GLOBAL local_infile = ON;
Query OK, 0 rows affected (0.00 sec)

这使得设置不需要重启 MySQL 但是,它不会保持更改。

准备好数据、表格和服务器端设置后,清单 4-9 中的程序可以用来将数据加载到表格中。示例中用于文件的文件结尾被假定为 Unix 换行符。如果使用 Windows 换行符,则必须将LOAD DATA LOCAL INFILE语句改为\r\n\r(取决于编写文件的应用),而不是将LINES TERMINATED BY参数改为\n

import mysql.connector

FMT_HEADER = "{0:2s}   {1:30s}   {2:8s}"
FMT_ROW = "{0:2d}   {1:30s}   ({2:8s})"

# Create connection to MySQL
db = mysql.connector.connect(
  option_files="my.ini",
  allow_local_infile=True
)
cursor = db.cursor(dictionary=True)

# Clear the table of any existing rows
cursor.execute("DELETE FROM world.loadtest")

# Define the statement and execute it.

sql = """

LOAD DATA LOCAL INFILE 'testdata.txt'

     INTO TABLE world.loadtest

CHARACTER SET utf8mb4

   FIELDS TERMINATED BY ','
          OPTIONALLY ENCLOSED BY '"'
    LINES TERMINATED BY '\n'
   IGNORE 1 LINES"""
cursor.execute(sql)

print(
  "Number of rows inserted: {0}".format(
  cursor.rowcount
))
print("")

sql = """
SELECT id, val, LEFT(HEX(val), 8) AS hex
  FROM world.loadtest
 ORDER BY id"""
cursor.execute(sql)

if (cursor.with_rows):
  # Print the rows found
  print(
    FMT_HEADER.format(
      "ID", "Value", "Hex"
    )
  )
  row = cursor.fetchone()
  while (row):
    print(
      FMT_ROW.format(
        row["id"],
        row["val"],
        row["hex"]
      )
    )
    row = cursor.fetchone()

# Commit the transaction
db.commit()
cursor.close()
db.close()

Listing 4-9Loading Data with LOAD DATA LOCAL INFILE

创建连接时将allow_local_infile选项明确设置为True。这似乎是不必要的;但是,它清楚地表明了加载本地文件并将内容插入表中的意图。请注意,MySQL 客户端程序,如mysql命令行客户端,在 MySQL Server 8.0 中默认禁用了对应于allow_local_infile的选项。可能包括 MySQL Connector/Python 在内的连接器会在未来的某个时候做出同样的改变,所以显式启用allow_local_infile会让未来的升级更容易。

创建连接和光标后要做的第一件事是删除world.loadtest表中的所有现有数据。这是在程序运行多次的情况下完成的;DELETE语句确保在加载数据之前表总是空的。

然后使用LOAD DATA LOCAL INFILE语句加载数据。要使用的确切参数取决于正在加载的 CSV 文件。建议始终指定文件的字符集,以便正确读取数据。

小费

始终指定保存数据时使用的字符集,以便 MySQL 能够正确解释数据。

加载数据后,使用cursor.rowcount属性打印插入的行数,并选择和打印表的内容。要知道,不是所有的终端程序都可以打印海豚表情符号,所以它可能看起来像一个问号,一些其他的占位符,甚至根本没有字符。这就是该值的前四个字节也以十六进制表示法打印的原因,因此可以验证该值是否正确。输出如下所示

Number of rows inserted: 4

ID   Value                            Hex
 1   abcdef...                        (61626364)
 2   MySQL Connector/Python is fun    (4D795351)
 3   Smileys require utf8mb4          (536D696C)
 4   ?                                (F09F90AC)

如果您想尝试服务器端的变体,那么您需要确保用户拥有FILE特权:

mysql> GRANT FILE ON *.* TO pyuser@localhost;
Query OK, 0 rows affected (0.40 sec)

另一方面,您不再需要启用allow_local_infile选项。此外,您必须删除LOAD DATA INFILE语句中的LOCAL关键字,并将路径改为指向文件服务器端的位置,例如:

LOAD DATA INFILE 'C:/MySQL/Files/testdata.txt'
     INTO TABLE world.loadtest
CHARACTER SET utf8mb4
   FIELDS TERMINATED BY ','
          OPTIONALLY ENCLOSED BY '"'
    LINES TERMINATED BY '\n'
   IGNORE 1 LINES

这就结束了各种执行查询的重要方法的旅程。是时候看看连接属性了。

连接属性

有几个属性提供有关连接状态或执行查询时的行为的信息。这些属性包括为查询设置默认数据库(模式)和字符集,以及关于是否有行要被消费的信息,如前一章所述。这些属性是连接对象的一部分,总结在表 4-1 中。

表 4-1

连接属性

|

财产

|

类型

|

数据类型

|

描述

|
| --- | --- | --- | --- |
| autocommit | RW | 布尔代数学体系的 | 是否为连接启用了自动提交模式。使用该属性会导致在 MySQL 服务器上执行SELECT @@session.autocommit查询。 |
| can_consume_results | RO | 布尔代数学体系的 | 如果在获取之前的行之前执行了新的查询,则结果会被自动消耗。对应的连接配置选项是consume_results。 |
| charset | RO | 线 | 用于连接的字符集。使用set_charset_collation()方法改变所使用的字符集和/或集合。 |
| collation | RO | 线 | 用于连接的集合。使用set_charset_collation()方法改变所使用的字符集和/或集合。 |
| connection_id | RO | 整数 | MySQL 服务器分配给连接的连接 ID。该属性在第二章中被用来验证连接已经被创建。 |
| database | RW | 线 | 连接的当前默认数据库(模式),如果没有设置默认数据库,则为None。引用属性在服务器上执行SELECT DATABASE()。设置属性执行USE语句。或者,可以使用cmd_init_db()方法更改默认数据库。 |
| get_warnings | RW | 布尔代数学体系的 | 使用游标时是否自动检索警告。 |
| in_transaction | RO | 布尔代数学体系的 | 连接当前是否在事务中。 |
| python_charset | RO | 线 | MySQL 字符集的 Python 等价物。两者的区别在于,在 Python 中,utf8mb4binary字符集作为utf8返回。 |
| raise_on_warnings | RW | 布尔代数学体系的 | 使用游标时,警告是否转换为异常。当值改变时,get_warnings的值自动设置为与raise_on_warnings相同的值。 |
| server_host | RO | 线 | 用于连接 MySQL 服务器的主机名。对应的连接配置选项是host。 |
| server_port | RO | 整数 | 用于连接 MySQL 服务器的 TCP/IP 端口。对应的连接配置选项是port。 |
| sql_mode | RW | 线 | 当前使用的 SQL 模式。读取该属性会导致执行以下查询:SELECT @@session.sql_mode。设置 SQL 模式会执行一个SET语句。建议使用来自SQLMode常量类的模式列表指定一个新的 SQL 模式。返回值是用逗号分隔的字符串。 |
| time_zone | RW | 线 | 用于连接的时区。这会影响为时间戳数据类型返回的值。读取属性执行查询SELECT @@session. time_zone。分配一个新时区会执行一个SET语句。 |
| unix_socket | RO | 线 | 用于连接 MySQL 服务器的 Unix 套接字的路径。 |
| unread_result | RW | 布尔代数学体系的 | 是否有要从上一个查询中读取的行。警告:不要设置该属性。写支持仅适用于游标。 |
| user | RO | 线 | 当前用于连接的用户。连接的cmd_change_user()方法可用于更改用户、默认模式和字符集。 |

Type列指定属性是只读的(RO)还是可以读写的属性(RW)。一些只读选项有一个特殊的方法来改变它们的值;在这种情况下,描述中会提到该方法。例如,字符集和与归类相关的属性可以通过set_charset_collation()方法更新,如第二章所示。除了connection_idin_transactionpython_charsetunread_result属性外,这些属性都可以在创建配置时进行设置。配置选项的名称与属性名称相同,除非在描述中有所提及。

以下部分将包括一些属性的示例。首先,将讨论事务,包括autocommitin_transaction属性之间的关系。后面的部分将讨论指定默认数据库和使用时区。

处理

使用数据库时,事务是一个非常重要的概念。事务将几个查询组合在一起,并确保所有查询都被提交或回滚。它们还允许隔离,例如,在提交更改之前,一个事务所做的更改不会被其他事务看到。

交易——什么是 Acid?

酸代表原子性、一致性、隔离性和持久性。也许是数据库理论中最重要的概念之一,它定义了数据库系统必须表现出的行为,才能被认为是可靠的事务处理。

原子性意味着对于包含多个命令的事务,数据库必须允许在“全有或全无”的基础上修改数据。也就是说,每个事务都是原子的。如果命令失败,则整个事务失败,并且事务中到该点为止的所有更改都将被丢弃。这对于在高交易环境(如金融市场)中运行的系统尤其重要。考虑一下资金转移的后果。通常,借记一个账户和贷记另一个账户需要多个步骤。如果在借记步骤后交易失败,并且没有将钱贷记回第一个帐户,该帐户的所有者将会非常生气。在这种情况下,从借方到贷方的整个交易必须成功,否则都不会成功。

一致性意味着只有有效的数据才会存储在数据库中。也就是说,如果事务中的命令违反了一致性规则之一,则整个事务将被丢弃,数据将返回到事务开始之前的状态。相反,如果事务成功完成,它将以遵守数据库一致性规则的方式更改数据。

隔离意味着同时执行的多个事务不会互相干扰。这是并发性的真正挑战最明显的地方。数据库系统必须处理事务不能违反数据的情况(更改、删除等)。)正在另一个事务中使用。有很多方法可以解决这个问题。大多数系统使用一种称为锁定的机制,在第一个事务完成之前,防止数据被另一个事务使用。尽管隔离属性没有规定先执行哪个事务,但它确实确保了它们不会相互干扰。

持久性意味着任何交易都不会导致数据丢失,在交易过程中创建或更改的任何数据也不会丢失。耐用性通常由强大的备份和恢复维护功能提供。一些数据库系统使用日志记录来确保任何未提交的数据可以在重启时恢复。 1

有两个与事务相关的连接属性。autocommit选项指定是否自动提交事务;in_transaction属性反映连接是否在事务中。

注意

MySQL 有两个事务存储引擎:InnoDB,这是 MySQL Server 中的默认设置,以及NDBCluster,它包含在 MySQL Cluster 产品中。

从清单 4-10 中的例子可以看出autocommit选项的效果,其中先禁用autocommit属性,然后启用in_transaction属性,检查属性的值。

import mysql.connector

# Create connection to MySQL
db = mysql.connector.connect(
  option_files="my.ini")
cursor = db.cursor()

# Initialize the stages (ordered)
stages = [
  "Initial",
  "After CREATE TABLE",
  "After INSERT",
  "After commit()",
  "After SELECT",
]

# Initialize dictionary with one list
# per stage to keep track of whether
# db.in_transaction is True or False
# at each stage.
in_trx = {stage: [] for stage in stages}

for autocommit in [False, True]:
  db.autocommit = autocommit;

  in_trx["Initial"].insert(
    autocommit, db.in_transaction)

  # Create a test table
  cursor.execute("""

CREATE TABLE world.t1 (

  id int unsigned NOT NULL PRIMARY KEY,
  val varchar(10)

)"""

  )

  in_trx["After CREATE TABLE"].insert(
    autocommit, db.in_transaction)

  # Insert a row
  cursor.execute("""
INSERT INTO world.t1
VALUES (1, 'abc')"""
  )

  in_trx["After INSERT"].insert(
    autocommit, db.in_transaction)

  # Commit the transaction
  db.commit()

  in_trx["After commit()"].insert(
    autocommit, db.in_transaction)

  # Select the row
  cursor.execute("SELECT * FROM world.t1")
  cursor.fetchall()

  in_trx["After SELECT"].insert(
    autocommit, db.in_transaction)

  # Commit the transaction
  db.commit()

  # Drop the test table
  cursor.execute("DROP TABLE world.t1")

cursor.close()
db.close()

fmt = "{0:18s}   {1:⁸s}   {2:⁷s}"
print("{0:18s}   {1:¹⁸s}".format(
  "", "in_transaction"))
print(fmt.format(
  "Stage", "Disabled", "Enabled"))
print("-"*39)
for stage in stages:
  print(fmt.format(
    stage,
    "True" if in_trx[stage][0] else "False",
    "True" if in_trx[stage][1] else "False",
  ))

Listing 4-10The Effect of the 
autocommit

Property

该示例的主要部分由一个循环组成,在该循环中,autocommit属性首先被禁用(默认),然后被启用。在循环中,执行一系列语句:

  1. 创建一个测试表。

  2. 测试表中会插入一行。

  3. 调用了commit()方法。

  4. 从测试表中选择该行。

  5. 调用了commit()方法。

  6. 测试表被删除。

在第一步之前和前四步的每一步之后,捕捉in_transaction的值。该程序的输出是

                       in_transaction
Stage                Disabled   Enabled
---------------------------------------
Initial               False       False
After CREATE TABLE    False       False
After INSERT           True       False
After commit()        False       False
After SELECT           True       False

在两次迭代中,in_transaction的初始值都是False。创建表后,该值保持为False;这是因为 MySQL 不支持事务内部的模式更改。在INSERTSELECT声明之后,事情变得更加有趣。当autocommit = False时,in_transactionTrue,直到commit()被调用。当autocommit被启用时,在语句执行完毕后,不会有正在进行的事务。

在编写使用 MySQL 进行数据存储的应用时,了解这种行为差异非常重要。如果autocommit被禁用,您必须确保在完成事务后提交或回滚事务。否则,其他连接将看不到这些更改,锁将阻止其他连接对行进行更改,并且由于存储引擎必须跟踪各种版本的数据,所有连接都将有(潜在的巨大)开销。如果启用了autocommit,当您需要对多个语句进行分组时,您必须启动一个显式的事务,因此它们表现为一个原子变化。

注意

如果有正在进行的事务,像CREATE TABLE这样的数据定义语言(DDL)语句总是执行隐式提交。

即使启用了autocommit,仍然可以使用多语句交易。在这种情况下,有必要显式启动事务,并在事务完成时提交或回滚事务。没有对保存点的内置支持(除非使用第六章中讨论的 X DevAPI)。控制交易有三种方法:

  • start_transaction():开始交易。这仅在autocommit启用时需要。可以设置是否使用一致的快照启动事务、事务隔离级别以及事务是否为只读。这些论点将在后面讨论。

  • commit():提交正在进行的事务。该方法不接受任何参数。

  • rollback():回滚正在进行的事务。像commit()方法一样,rollback()不接受任何参数。

无论autocommit设置的值如何,commit()rollback()方法的工作方式相同。start_transaction()方法主要在autocommit启用时使用;然而,它也可以在禁用autocommit的情况下使用,以更好地控制事务的行为。

启动事务时有三个可选参数:

  • consistent_snapshot:获取一个布尔值,并指定在调用start_transaction()方法时是否创建一致的快照。缺省值是False,这意味着快照(如果事务隔离级别是REPEATABLE READ)将在事务开始后执行第一个查询时创建。启用consistent_snapshot与使用WITH CONSISTENT SNAPSHOTSTART TRANSACTION语句是一样的。只有使用InnoDB存储引擎的表才支持一致的快照。

  • isolation_level:用于事务的事务隔离级别。默认为REPEATABLE READ。只有InnoDB表支持设置事务隔离级别。注意,对于使用NDBCluster存储引擎的表,指定的隔离级别将被忽略,并且将始终使用READ COMMITTED事务隔离级别。

  • readonly:如果知道事务永远不会修改任何数据,可以设置readonly参数,允许 InnoDB 优化事务。默认是False

小费

有关事务设置的更多信息,请参见 MySQL Server 手册中对SET TRANSACTION语句的描述以及其中的引用: https://dev.mysql.com/doc/refman/en/set-transaction.html 。事务隔离级别的详细描述可以在 https://dev.mysql.com/doc/refman/en/innodb-transaction-isolation-levels.html 中找到。

因此,要么通过禁用autocommit来隐式启动事务,要么通过显式调用start_transaction()来启动事务。在这两种情况下,要么使用commit()保存更改并使其对其他连接可见,要么使用rollback()放弃更改,从而完成事务。典型交易的流程如图 4-2 所示。

img/463459_1_En_4_Fig2_HTML.jpg

图 4-2

典型交易的流程

连接左侧黄色(浅灰色)椭圆体中的布尔值是不同阶段的in_transaction属性的值。事务开始后,使用连接对象或游标(或两者的组合)执行一个或多个查询。注意,事务的状态总是与连接有关,但是查询既可以在连接本身中执行,也可以在游标中执行。

在清单 4-11 中可以看到一个使用显式事务的例子。在本例中,一行被插入到world.city表中,然后在事务回滚之前再次被选中。为了说明隐式提交的事务和使用显式事务之间的区别,为连接启用了autocommit,并且在INSERTSELECT语句之间检查了in_transaction属性。

import mysql.connector
import pprint

printer = pprint.PrettyPrinter(indent=1)

# Create two connections to MySQL
db1 = mysql.connector.connect(
  option_files="my.ini",
  autocommit=True
)
db2 = mysql.connector.connect(
  option_files="my.ini",
  autocommit=True
)
cursor1 = db1.cursor(dictionary=True)
cursor2 = db2.cursor(dictionary=True)

# Start a transaction

db1.start_transaction()

# Insert a row

cursor1.execute("""

INSERT INTO world.city

VALUES (DEFAULT, 'Camelot', 'GBR',

        'King Arthur County', 2000)"""

)

print("\nin_transaction = {0}".format(
  db1.in_transaction))

id = cursor1.lastrowid
sql = """SELECT *
           FROM world.city
          WHERE id = {0}""".format(id)

cursor1.execute(sql)

cursor2.execute(sql)

# Fetch and print the rows
print("\nResult Set in Connection 1")
print("="*26)
result_set1 = cursor1.fetchall()
printer.pprint(result_set1)

print("\nResult Set in Connection 2")
print("="*26)
result_set2 = cursor2.fetchall()
printer.pprint(result_set2)

db1.rollback()
cursor1.close()
db1.close()

cursor2.close()
db2.close()

Listing 4-11Using an Explicit Transaction

这个程序非常简单。创建了两个连接,然后使用第一个连接将亚瑟王的城堡作为新城市插入。world.city表有一个自动递增的列作为主键,所以第一个值被设置为使用默认值(下一个可用的 ID)。ID 是从游标的lastrowid属性中检索的(如果使用cmd_query()插入行,则是从INSERT语句的结果字典的insert_id元素中检索的),因此可以使用主键再次检索行。

在两个语句之间,检查in_transaction属性的值。最后,事务被回滚(插入的行再次被删除)。第二个连接和游标用于从单独的连接中查询同一行。输出类似于下面的例子(这个ID将取决于有多少行被插入到表中,即使后来被回滚):

Result Set in Connection 1
==========================
[{'CountryCode': 'GBR',
  'District': 'King Arthur County',
  'ID': 4080,
  'Name': 'Camelot',
  'Population': 2000}]

Result Set in Connection 2
==========================
[]

注意第二个连接是如何看不到行的。这是一个事务如何提供变更隔离的例子。

小费

除非特别要求禁用autocommit,否则启用它通常更好。它可以更容易地在源代码中看到需要多语句事务的地方,节省了应用与许多单语句事务之间的往返行程,并允许 InnoDB 自动为不使用存储函数的SELECT语句启用只读优化。

在本例中,该表被称为world.city。也就是说,数据库名(world)和表名(city)都是显式指定的。通过设置默认数据库,可以避免每次都指定数据库名称。接下来将讨论如何做到这一点。

多个查询执行和事务

autocommit启用时,重要的是要考虑在一个cmd_query_iter()execute()executemany()调用中执行多个语句的影响。除了当多个INSERT语句被重写为单个扩展的INSERT语句时,每个语句都将在自己的事务中执行,除非创建了一个显式事务。这是一个很容易被忘记的事实,因为它只是一行代码。

要在单个事务中执行所有查询,请使用连接的start_transaction()方法显式启动一个事务:

import mysql.connector

# Create connection to MySQL
db = mysql.connector.connect(
  option_files="my.ini", autocommit=True)
cursor = db.cursor()

queries = [
  """UPDATE world.city
        SET Population = Population + 1
      WHERE ID = 130""",
  """UPDATE world.country
        SET Population = Population + 1
      WHERE Code = 'AUS'""",
]

db.start_transaction()

tests = cursor.execute(
  ";".join(queries), multi=True)
for test in tests:
  # Do something or pass if no action
  # is required.
  pass
db.rollback();

cursor.close()
db.close()

本书源代码下载中的Chapter_04/multi_stmt_transaction.py文件中也有这个例子。

默认数据库

在许多应用中,大多数或所有查询都是针对同一个数据库中的表执行的。根据最终用户的不同,应用也可以使用多个数据库名称。一个例子是允许用户写博客的在线应用,用户可以指定用于安装的表所在的数据库的名称。在这种情况下,能够在应用的配置部分指定数据库名称是很方便的,这样所有查询都将针对配置的数据库自动执行。

注意

在 MySQL 中,数据库模式是同义词。

这可以通过设置连接的database选项来实现,可以在创建连接时设置,也可以直接操作属性。清单 4-12 显示了默认数据库的使用示例。

import mysql.connector
from mysql.connector import errors

# Create connection to MySQL
db = mysql.connector.connect(
  option_files="my.ini",
  consume_results=True
)

# First query the city table without
# a default database
try:
  result = db.cmd_query(
    """SELECT *
         FROM city
        WHERE id = 130"""
  )
except errors.ProgrammingError as err:
  print(
    "1: Failed to execute query with "
    + "the error:\n   {0}".format(err)
  )
else:
  print("1: Query executed successfully")

# Then query the city table with
# a default database

db.database = "world"

try:
  result = db.cmd_query(
    """SELECT *
         FROM city
        WHERE id = 130"""
  )
except errors.ProgrammingError as err:
  print(
    "2: Failed to execute query with "
    + "the error:\n   {0}".format(err)
  )
else:
  print("2: Query executed successfully")

db.close()

Listing 4-12Using a Default Database

第一个查询在没有配置默认数据库的情况下执行。结果是发生了异常。将默认数据库设置为world后,第二个查询成功。执行查询的输出是

1: Failed to execute query with the error:
   1046 (3D000): No database selected
2: Query executed successfully

小费

也可以使用cmd_init_db()连接方法更改默认数据库,例如cmd_init_db("world")。不同之处在于,设置属性会执行一个USE <database name>,而cmd_init_db()会向 MySQL 服务器发送一个COM_INIT_DB命令。如果您使用性能模式来监控 MySQL,那么使用哪种方法会有所不同。USE的事件名为statement/sql/change_db,语句可见,而COM_INIT_DB命令的事件名为statement/com/Init DB

关于默认数据库的讨论到此结束,剩下最后一个与连接属性相关的主题需要讨论:时区。

时区

时区是当今全球的一个重要概念。通常希望以用户的本地时区显示事件的时间,但是这对于每个用户来说是不同的。本节将讨论如何在 MySQL Connector/Python 程序中使用时区。

在深入研究 MySQL Connector/Python 处理时区的细节之前,有必要回顾一下 MySQL Server 处理时区的方式。MySQL 服务器有两种数据类型来处理由日期和时间组成的值:datetimetimestampdatetime数据类型用于直接存储给 MySQL 的日期和时间;也就是说,该值与时区无关,并且无论时区如何,总是返回相同的值。timestamp数据类型提供了一种更紧凑的存储格式,它总是以 UTC 格式存储值,并根据为会话设置的时区返回值。在这两种情况下,自定义时区都不会与存储值相关联。表 4-2 总结了这两种数据类型。

表 4-2

存储日期和时间的 MySQL 服务器数据类型

|

数据类型

|

时区支持

|

日期范围

|

描述

|
| --- | --- | --- | --- |
| datetime | 不 | 1000-01-019999-12-31 | 数据按原样存储。缺少时区支持的一个解决方法是在存储和读取时间时,显式地将时间转换为 UTC 时间。 |
| timestamp | 有限的 | 1970-01-012038-01-19 | 自 Unix 纪元时间开始以来,日期和时间(小数秒除外)存储为无符号的四字节整数。该值始终以 UTC 格式存储,使用会话的时区进行转换。 |

除了对timestamp数据类型的有限时区支持,还有CONVERT_TZ()函数(在 MySQL 服务器端; https://dev.mysql.com/doc/refman/en/date-and-time-functions.html#function_convert-tz )。它接受一个datetime值,并在两个时区之间进行转换。默认情况下,支持明确指定了相对于 UTC 的时差的时区(例如+10:00)。可选地,可以填充mysql数据库中的时区表,以添加对命名时区的支持(例如Australia/Sydney)。命名时区还包括有关夏令时更改的信息。

小费

使用datetime列时,以 UTC 时区存储数据,并在使用数据时转换为所需的时区。通过始终以 UTC 存储该值,如果操作系统时区或 MySQL 服务器时区发生变化,出现问题的可能性会更小。

要理解时区可能有点困难,因此值得考虑一个例子。清单 4-13 是一个例子,其中相同的值被插入到datetimetimestamp列中,然后使用不同的时区值再次被选择。连接的time_zone属性用于更改时区。

import mysql.connector
from datetime import datetime

# Create connection to MySQL
db = mysql.connector.connect(
  option_files="my.ini", use_pure=True)
cursor = db.cursor(named_tuple=True)

# Create a temporary table for this
# example
cursor.execute("""
  CREATE TEMPORARY TABLE world.t1 (
    id int unsigned NOT NULL,
    val_datetime datetime,
    val_timestamp timestamp,
    PRIMARY KEY (id)
  )""")

# Set the time zone to UTC

db.time_zone = "+00:00"

# Insert a date and time value:
#    2018-05-06 21:10:12
#    (May 6th 2018 09:10:12pm)
time = datetime(2018, 5, 6, 21, 10, 12)

# Definte the query template and the
# parameters to submit with it.
sql = """
INSERT INTO world.t1
VALUES (%s, %s, %s)"""

params = (1, time, time)

# Insert the row
cursor.execute(sql, params)

# Define output formats
# and print output header
fmt = "{0:9s}   {1:¹⁹s}   {2:¹⁹s}"
print(fmt.format(
  "Time Zone", "Datetime", "Timestamp"))
print("-"*53)

# Retrieve the values using thee
# different time zones
sql = """
SELECT val_datetime, val_timestamp
  FROM world.t1
 WHERE id = 1"""

for tz in ("+00:00", "-05:00", "+10:00"):

  db.time_zone = tz
  cursor.execute(sql)
  row = cursor.fetchone()
  print(fmt.format(
    "UTC" + ("" if tz == "+00:00" else tz),
    row.val_datetime.isoformat(" "),
    row.val_timestamp.isoformat(" ")
  ))

# Use the CONVERT_TZ() function to
# convert the time zone of the datetime
# value

sql = """

SELECT CONVERT_TZ(

         val_datetime,
         '+00:00',
         '+10:00'
       ) val_utc
  FROM world.t1
 WHERE id = 1"""
cursor.execute(sql)
row = cursor.fetchone()
print("\ndatetime in UTC+10:00: {0}".format(
  row.val_utc.isoformat(" ")))

cursor.close()
db.close()

Listing 4-13The Effect of the Time Zone

2018 年 5 月 5 日下午 09:10:12 的日期时间值被插入到一个带有datetimetimestamp列的临时表中。当插入行时,使用连接time_zone属性将时区设置为 UTC。然后通过设置三个不同的时区来选择datetimetimestamp值,并使用CONVERT_TZ()函数将datetime值转换为 UTC+10。输出是

Time Zone        Datetime              Timestamp
-----------------------------------------------------
UTC         2018-05-06 21:10:12   2018-05-06 21:10:12
UTC-05:00   2018-05-06 21:10:12   2018-05-06 16:10:12
UTC+10:00   2018-05-06 21:10:12   2018-05-07 07:10:12

datetime in UTC+10:00: 2018-05-07 07:10:12

不管时区如何,为datetime列打印的值总是相同的。但是,对于timestamp列,返回值取决于时区。只有当选择数据时的时区与插入数据时的时区相同时,返回的timestamp值才与插入的值相同。所以,当使用timestamp列时,记住时区是很重要的。

小费

有关 MySQL 时区支持以及如何添加命名时区的更多信息,请参见 https://dev.mysql.com/doc/refman/en/time-zone-support.html

这是涉及连接属性的最后一个示例。还有其他几种实用方法,其中一些将在下一节讨论。

其他连接实用程序方法

connection 对象有几种方法可用于一系列任务,如检查连接是否仍然可用、重置连接以及获取应用所连接的服务器的信息。本节将简要讨论最有用的实用方法。

注意

有关方法的完整列表,请参见 https://dev.mysql.com/doc/connector-python/en/connector-python-api-mysqlconnection.html

表 4-3 总结了将要讨论的实用方法。这些方法的范围是连接和服务器。连接方法会影响连接或执行连接测试。服务器方法可以用来获取关于 MySQL 服务器的信息。

表 4-3

一些有用的连接实用程序方法

|

方法

|

范围

|

描述

|
| --- | --- | --- |
| cmd_change_user() | 关系 | 更改用户以及数据库和字符集相关选项。 |
| cmd_reset_connection() | 关系 | 重置连接的用户变量和会话变量。仅适用于 MySQL Server 5.7 和更高版本,并且仅在使用纯 Python 实现时可用。 |
| is_connected() | 关系 | 如果连接仍然连接到 MySQL 服务器,则返回True。 |
| ping() | 关系 | 通过 pinging MySQL 服务器来验证连接是否仍然可用。可以选择尝试重新连接。 |
| reset_session() | 关系 | 重置连接的用户变量和会话变量。适用于所有 MySQL 服务器版本,并允许在重置后设置用户和会话变量。 |
| cmd_statistics() | 计算机网络服务器 | 返回包含 MySQL 服务器统计信息的字典。 |
| get_server_info() | 计算机网络服务器 | 以字符串形式返回 MySQL 服务器版本。这包括任何可能适用的版本后缀,如“rc”。如果没有后缀,则表示正式发布。例如“8.0.4-rc-log”和“8.0.11”。 |
| get_server_version() | 计算机网络服务器 | 以整数元组的形式返回 MySQL 服务器版本。不包括版本后缀。 |

这些方法将在下面的小节中讨论。

连接方法

连接方法可用于执行各种操作,以影响连接或检查连接是否仍处于活动状态。这些方法将按字母顺序进行讨论,除了reset_session()方法,它将与cmd_reset_connection()一起讨论。

cmd_change_user()

cmd_change_user()方法可用于改变哪个用户用于连接、默认数据库、字符集和排序规则。所有的参数都是可选的;未设置的参数使用其默认值。参数及其默认值总结在表 4-4 中。

表 4-4

cmd_change_user()的论据

|

争吵

|

缺省值

|

描述

|
| --- | --- | --- |
| username | (空字符串) | 要连接的用户名。 |
| password | (空字符串) | 用于验证的密码。 |
| database | (空字符串) | 新的默认数据库。 |
| charset | Forty-five | 字符集和排序规则。值 33 对应于带有utf8mb4_general_ci排序规则的utf8mb4(4 字节实现)。在 MySQL Connector/Python 8.0.11 和更早的版本中,缺省值是 33(utf8-3 字节实现-带有utf8_general_ci排序)。 |

注意,用户名是用username参数指定的,而不是通常的user参数。此外,字符集是使用内部字符集 ID 设置的,它是一个整数,还包括使用的归类。字符集 ID 是由mysql.connector.constants模块中的CharacterSet.get_charset_info()方法返回的元组中的第一个元素。

小费

如果目标只是更改默认数据库和/或字符集和排序规则,请使用这些任务的专用方法。默认数据库可以通过设置database属性或调用cmd_init_db()方法来更改,如本章前面所讨论的。如第二章所述,可以使用set_charset_collation()方法更改字符集和排序规则。

将用户更改为root(管理员)用户,同时将默认数据库设置为world,将字符集设置为utf8mb4,并将排序规则设置为utf8mb4_0900_ai_ci的示例如下

import mysql.connector
from mysql.connector.constants import CharacterSet

db = mysql.connector.connect(
  option_files="my.ini")

charset = CharacterSet.get_charset_info(

  "utf8mb4", "utf8mb4_0900_ai_ci")

db.cmd_change_user(

  username="root",
  password="password",
  database="world",
  charset=charset[0]
)

db.close()

警告

此示例对密码进行了硬编码,以保持示例的简单性。不要在实际的程序中这样做,因为这样会让太多人知道密码,使代码更难维护。

命令重置连接()和重置会话()

cmd_reset_connection()是一个轻量级方法,用于取消设置连接的所有用户变量(如@my_user_variable)并确保所有会话变量(如@@session.sort_buffer_size)被重置为全局默认值。该方法是轻量级的,因为它不需要重新认证。该方法不带任何参数,仅在 MySQL 5.7 和更高版本中使用纯 Python 实现时有效。一个例子是

import mysql.connector

db = mysql.connector.connect(
  option_files="my.ini", use_pure=True)

db.cmd_reset_connection()

db.close()

reset_session()方法是相关的(并在幕后使用cmd_reset_connection()),但允许您在重置后设置用户和会话变量。reset_connection()的另一个优势是它可以与所有版本的 MySQL 服务器和 C 扩展实现一起工作。对于支持cmd_reset_connection()的服务器版本,这用于避免重新认证;对于旧的服务器版本,reset_session()依赖于更昂贵的重新认证方法。使用reset_connection()的一个例子是

import mysql.connector

db = mysql.connector.connect(
  option_files="my.ini")

user_variables = {
  "employee_id": 1,
  "name": "Jane Doe",
}
session_variables = {
  "sort_buffer_size": 32*1024,
  "max_execution_time": 2,
}
db.reset_session(
  user_variables=user_variables,
  session_variables=session_variables
)

db.close()

这将把用户变量@employee_id@name分别设置为1Jane Doe的值。该会话使用最大 32kiB 的排序缓冲区,并且SELECT查询不允许超过两秒钟。这两个参数都是可选的,默认情况下不设置任何变量。

is_connected()

is_connected()方法检查连接是否仍然连接到数据库。它返回TrueFalse,其中True表示连接仍在工作。使用方法的一个简单示例是

import mysql.connector

db = mysql.connector.connect(
  option_files="my.ini")

if (db.is_connected()):
  print("Is connected")
else:
  print("Connection lost")

db.close()

一个相关的方法是ping()

ping()

ping()方法类似于is_connected()。事实上,ping()is_connected()方法都使用相同的底层(内部)方法来验证连接是否可用。然而,还是有一些不同之处。

如果连接不可用,is_connected()方法返回Falseping()触发InterfaceError异常。另一个区别是ping()方法支持等待连接变得可用。它支持表 4-5 中的参数。

表 4-5

ping()支持的论点

|

争吵

|

缺省值

|

描述

|
| --- | --- | --- |
| reconnect | False | 如果连接不可用,是否尝试重新连接。 |
| attempts | 1 | 尝试重新连接的最大次数。使用负值可以尝试无限次。 |
| Delay | 0 | 完成前一次重新连接尝试和尝试下一次重新连接之间的延迟时间(秒)。由于连接尝试本身需要时间,每次尝试的总时间将大于指定的值。 |

ping 操作的一个示例是,最多尝试五次重新连接,每次尝试间隔一秒钟,然后连接再次可用

import mysql.connector
from mysql.connector import errors

db = mysql.connector.connect(
  option_files="my.ini")

try:
  input("Hit Enter to continue.")
except SyntaxError:
  pass

try:

  db.ping(reconnect=True, attempts=5, delay=1)

except errors.InterfaceError as err:

  print(err)
else:
  print("Reconnected")
db.close()

input()函数允许您在 pinging 服务器之前关闭 MySQL。如果在尝试用尽之前,MySQL 服务器再次变得可用,则打印出重新连接。否则,一段时间后,当五次尝试重新连接失败后,会出现InterfaceError异常,例如:

Can not reconnect to MySQL after 5 attempt(s): 2003 (HY000): Can't connect to MySQL server on '127.0.0.1' (10061)

该消息告知尝试的次数以及连接失败的原因。细节将取决于平台,是否使用 C 扩展,以及 MySQL 连接器/Python 无法连接的原因。

这是将讨论的最后一个与连接相关的实用方法。但是,有一些与服务器相关的方法值得讨论。

服务器信息方法

有三种方法可以获得有关服务器或服务器版本的统计信息。这些信息也可以通过普通的 SQL 语句获得,但是专用的方法非常有用,因为它们需要较少的解析。

cmd_statistics()方法返回一个字典,其中包含一些关于服务器操作的指标,例如表被刷新的次数、提出的问题(查询)的数量以及正常运行时间。

get_server_info()方法以字符串形式返回服务器版本。如果应用记录它所连接的数据库的版本,这可能会很有用。

最后一个方法是get_server_version(),它以元组的形式返回服务器版本,其中三个组件中的每一个都是一个元素。例如,当验证服务器是否足够新以具有某个特性时,这可能是有用的。

下面的代码示例演示了如何使用这三种方法:

import mysql.connector
import pprint

# Print the result dictionary
printer = pprint.PrettyPrinter(indent=1)

# Create connection to MySQL
db = mysql.connector.connect(
  option_files="my.ini")

print("cmd_statistics\n" + "="*14)

statistics = db.cmd_statistics()

printer.pprint(statistics)

print("\nget_server_info\n" + "="*15)

server_info = db.get_server_info()

printer.pprint(server_info)

print("\nget_server_version\n" + "="*18)

server_version = db.get_server_version()

printer.pprint(server_version)
if (server_version >= (8, 0, 2)):
  print("Supports window functions")

db.close()

输出取决于 MySQL 服务器启动后的时间、实例上的工作负载以及 MySQL 服务器的版本。代码生成的输出示例如下

cmd_statistics
==============
{'Flush tables': 2,
 'Open tables': 66,
 'Opens': 90,
 'Queries per second avg': Decimal('0.034'),
 'Questions': 71,
 'Slow queries': 0,
 'Threads': 2,
 'Uptime': 2046}

get_server_info
===============
'8.0.11'

get_server_version
==================
(8, 0, 11)
Supports window functions

一个相关的主题是可用于在SELECTSHOW语句中返回的列的元数据。这是下一个要探讨的话题。

列信息

当执行一个要求返回数据的查询(通常是一个SELECT语句)时,由连接cmd_query()方法返回的字典包括结果集中每一列的详细信息。当使用光标时,description属性包含相同的信息。您已经看到了在将结果转换为 Python 类型时使用列信息的例子。许多信息使用起来并不简单,因此本节将研究如何将这些信息轻松地转换成更容易理解的格式。

使用纯 Python 实现的world.city表中一行的列信息是

[('ID', 3, None, None, None, None, 0, 16899),
 ('Name', 254, None, None, None, None, 0, 1),
 ('CountryCode', 254, None, None, None, None, 0, 16393),
 ('District', 254, None, None, None, None, 0, 1),
 ('Population', 3, None, None, None, None, 0, 1)]

信息是一个列表,每列有一个元组。每个元组有八个元素:

  • 列的名称

  • 字段类型(这是一个整数)

  • 显示尺寸

  • 内部尺寸

  • 列的精度

  • 列的比例

  • 列值是否可以是NULL (0 表示False,1 表示True)

  • 指定为整数的 MySQL 特定标志

显示尺寸、内部尺寸、列的精度和列的比例始终设置为None。从示例输出中可以看出,列名很容易使用,但是字段类型和 MySQL 特有的标志需要映射。本节的其余部分将讨论如何将字段类型和标志转换成名称。

字段类型

字段类型 integers 源自 MySQL Server,在源代码中定义(MySQL Server 8.0 源代码中的include/mysql.h.pp文件)。MySQL Connector/Python 包含了FieldType. get_info()函数(在constants.py文件中),用于将类型转换为人类可读的名称。清单 4-14 中的例子展示了如何将整型字段类型映射成名字。

import mysql.connector
from mysql.connector import FieldType

# Create connection to MySQL
db = mysql.connector.connect(
  option_files="my.ini")
cursor = db.cursor()

# Create a test table

cursor.execute(

  """CREATE TEMPORARY TABLE world.t1 (
    id int unsigned NOT NULL PRIMARY KEY,
    val1 tinyint,
    val2 bigint,
    val3 decimal(10,3),
    val4 text,
    val5 varchar(10),
    val6 char(10)
  )"""

)

# Select all columns (no rows returned)
cursor.execute("SELECT * FROM world.t1")

# Print the field type for each column
print("{0:6s}   {1}".format(
  "Column", "Field Type"))
print("=" * 25);

for column in cursor.description:

  print("{0:6s}   {1:3d} - {2}".format(
    column[0],
    column[1],
    FieldType.get_info(column[1])
  ))

# Consume the (non-existing) rows
cursor.fetchall()

cursor.close
db.close()

Listing 4-14Mapping the Field Types

连接完成后,创建临时表world.t1。该表有七列不同的数据类型。接下来,执行一个SELECT查询来获得包含列信息的结果字典。字典用于以整数和字符串的形式打印字段类型。执行代码的输出是

Column   Field Type
=========================
id         3 - LONG
val1       1 - TINY
val2       8 - LONGLONG
val3     246 - NEWDECIMAL
val4     252 - BLOB
val5     253 - VAR_STRING
val6     254 - STRING

MySQL 列标志

可以转换成名称的另一条可用信息是 MySQL 列标志(也称为字段标志)。列标志是在 MySQL 服务器的源文件的include/mysql_com.h头文件中定义的。标志中包含的信息包括列是否是主键,是否允许NULL值等。后者是列描述中的“允许NULL”值是如何导出的(从 MySQL 连接器/Python 安装中的protocol.py):

~flags & FieldFlag.NOT_NULL,  # null_ok

如定义所示,列标志在constants.pyFieldFlag类中定义,它们可以与按位 and 运算符(&)一起使用,以检查给定的标志是否已设置。与列类型不同,没有现成的函数来获取给定列的标志,因此有必要自己确定标志。清单 4-15 展示了一个如何做到这一点的例子。

def get_column_flags(column_info):

  """Returns a dictionary with a
  dictionary for each flag set for a
  column. The dictionary key is the
  flag name. The flag name, the flag
  numeric value and the description of
  the flag is included in the flag
  dictionary.
  """
  from mysql.connector import FieldFlag

  flags = {}
  desc = FieldFlag.desc
  for name in FieldFlag.desc:
    (value, description) = desc[name]
    if (column_info[7] & value):
      flags[name] = {
        "name": name,
        "value": value,
        "description": description
      }

  return flags

# Main program
import mysql.connector

# Create connection to MySQL
db = mysql.connector.connect(
  option_files="my.ini")
cursor = db.cursor()

# Create a test table
cursor.execute("""
CREATE TEMPORARY TABLE world.t1 (
  id int unsigned NOT NULL auto_increment,
  val1 bigint,
  val2 varchar(10),
  val3 varchar(10) NOT NULL,
  val4 varchar(10),
  val5 varchar(10),
  PRIMARY KEY(id),
  UNIQUE KEY (val1),
  INDEX (val2),
  INDEX (val3, val4)
)"""
)

# Select all columns (no rows returned)
cursor.execute("SELECT * FROM world.t1")

# Print the field type for each column
print("{0:6s}   {1}".format(
  "Column", "Field Flags"))
print("=" * 74);
all_flags = {}
for column in cursor.description:
  flags = get_column_flags(column)

  # Add the flags to the list of
  # all flags, so the description
  # can be printed later
  # for flag_name in flags:
  all_flags.update(flags)

  # Print the flag names sorted
  # alphabetically
  print("{0:6s}   {1}".format(
    column[0],
    ", ".join(sorted(flags))
  ))

print("")

# Print description of the flags that
# were found
print("{0:18s}   {1}".format(
  "Flag Name", "Description"))
print("=" * 53);
for flag_name in sorted(all_flags):
  print("{0:18s}   {1}".format(
    flag_name,
    all_flags[flag_name]["description"]
  ))

# Consume the (non-existing) rows
cursor.fetchall()

cursor.close
db.close()

Listing 4-15Checking Whether Field Flags Are Set for a Column

程序中最有趣的部分是get_column_flags()函数。该函数循环遍历所有已知标志,并使用按位 and 运算符来检查标志是否已设置。FieldFlag .desc是以旗名为关键字的字典。对于每个标志,有一个元组,第一个元素是数值,第二个元素是描述。FieldFlag类也有一个与标志名同名的常量,例如FieldFlag.PRI_KEY代表“是主键的一部分”标志。

每列的标志名称按字母顺序打印,在最后,打印每个已使用的标志的说明。使用 C 扩展的 MySQL 8.0.11 的输出是

Column   Field Flags
===============================================================
id       AUTO_INCREMENT, GROUP, NOT_NULL, NUM, PART_KEY, PRI_KEY, UNSIGNED
val1     GROUP, NUM, PART_KEY, UNIQUE_KEY
val2     GROUP, MULTIPLE_KEY, NUM
val3     GROUP, MULTIPLE_KEY, NOT_NULL, NO_DEFAULT_VALUE, NUM
val4     GROUP, NUM
val5

Flag Name            Description
===============================================================
AUTO_INCREMENT       field is a autoincrement field
GROUP                Intern: Group field
MULTIPLE_KEY         Field is part of a key
NOT_NULL             Field can't be NULL
NO_DEFAULT_VALUE     Field doesn't have default value
NUM                  Field is num (for clients)
PART_KEY             Intern; Part of some key
PRI_KEY              Field is part of a primary key
UNIQUE_KEY           Field is part of a unique key
UNSIGNED             Field is unsigned

只有在使用 C 扩展实现时,才会包含PART_KEY标志。

对列信息的讨论到此结束。还有一个主题:MySQL 连接器/Python C 扩展。

C 扩展

到目前为止,大多数示例都没有指定是使用纯 Python 编写的 MySQL Connector/Python 的实现,还是使用 C 扩展的实现。虽然纯 Python 实现有一些优点,比如能够轻松查看连接器执行的代码,但是它在性能方面有一些缺点。为了克服这一点,有了 MySQL 连接器/Python C 扩展。

根据平台和 MySQL Connector/Python 的安装方式,C 扩展可能会也可能不会自动包含在内。例如,在使用 MySQL 安装程序和最新支持的 Python 版本的 Windows 上,它是包括在内的,但是在 Red Hat Enterprise Linux (RHEL)或 Oracle Linux 上使用 RPM 包需要安装额外的 RPM 包。

使用 C 扩展的主要好处是性能。与纯 Python 实现相比,C 扩展在两个用例中特别有用:

  • 处理大型结果集

  • 使用准备好的语句,尤其是当需要传输大量数据时

C 扩展提供了从连接器的 Python 部分到 MySQL C 客户端库的接口。对于返回大型结果集的查询,在 C 库中处理内存密集型部分是一个优势。此外,MySQL C 客户端库具有支持使用二进制协议实现的预准备语句的优势。

小费

在大多数情况下,除了简单的脚本之外,建议启用 C 扩展。也就是说,纯 Python 实现对于调试程序非常有用。

有两种方法可以切换到 C 扩展:

  • mysql.connector.connect()函数:调用use_pure连接选项设置为False的函数。这是 MySQL Connector/Python 8.0.11 及更高版本中的默认设置。一个优点是 API 保持不变。

*** _mysql_connector模块:导入_mysql_connector模块代替mysql.connector。优点是可以直接使用 C 扩展 API,从而消除了包装方法的开销。缺点是 API 不一样。**

**### 小费

使用mysql.connector.connect()函数是使用 C 扩展的最简单的方法。另一方面,使用_ mysql_connector模块可以给出更好的性能。

本节的其余部分提供了一个使用这两种方法中的每一种来访问 C 扩展的示例。

mysql.connector.connect()函数

使用 C 扩展的最简单方法是使用mysql.connector.connect()函数。如果您有一个现有的 MySQL Connector/Python 应用,那么需要做的就是改变连接的创建方式。

警告

虽然在使用mysql.connector模块时,纯 Python 和 C 扩展实现之间通常只有很小的差异,但是如果您更改了用于现有应用的实现,您必须进行详尽的测试。这包括从早期版本升级到 MySQL Connector/Python 8.0 版。

清单 4-16 显示了如何使用 C 扩展。一旦创建了连接,就执行查询并打印结果。

import mysql.connector

# Create connection to MySQL

db = mysql.connector.connect(

  option_files="my.ini",
  use_pure=False
)

# Instantiate the cursor
cursor = db.cursor(dictionary=True)

# Execute the query
cursor.execute(
  """SELECT Name, CountryCode,
            Population
       FROM world.city
      WHERE Population > 9000000
      ORDER BY Population DESC"""
)

print(__file__ + " - Setting use_pure = False:")
print("")
if (cursor.with_rows):
  # Print the rows found
  print(
    "{0:15s}   {1:7s}   {2:3s}".format(
      "City", "Country", "Pop"
    )
  )
  city = cursor.fetchone()
  while (city):
    print(
      "{0:15s}   {1:⁷s}   {2:4.1f}".format(
        city["Name"],
        city["CountryCode"],
        city["Population"]/1000000.0
      )
    )
    city = cursor.fetchone()

cursor.close()
db.close()

Listing 4-16Using the C Extension by Setting use_pure = False

与之前使用默认实现的类似程序相比,只有一个不同之处:变量use_pure被设置为False以请求 C 扩展。该程序的输出是

listing_4_16.py - Setting use_pure = False:

City              Country   Pop
Mumbai (Bombay)     IND     10.5
Seoul               KOR     10.0
São Paulo           BRA     10.0
Shanghai            CHN      9.7
Jakarta             IDN      9.6
Karachi             PAK      9.3

注意

记住,当使用 C 扩展时,必须使用mysql.connector.connect()函数来创建连接。原因是是否使用 C 扩展的决定决定了实例化哪个连接类。

如果试图将use_pure作为一个选项传递给CMySQLConnection()MySQLConnection()类或它们的connect()方法,就会出现一个属性错误:

AttributeError: Unsupported argument 'use_pure'

_mysql_connector 模块

使用 C 扩展的另一种方法是显式导入_mysql_connector模块。直接使用_mysql_connector模块时,用法类似于使用 C 客户端库。因此,如果您习惯于编写使用 MySQL 的 C 程序,这将是熟悉的,尽管不完全相同。清单 4-17 显示了前面例子的等效物,但是这次使用了_mysql_connector模块。

import _mysql_connector

# Create connection to MySQL
connect_args = {
  "host": "127.0.0.1",
  "port": 3306,
  "user": "pyuser",
  "password": "Py@pp4Demo",
};

db = _mysql_connector.MySQL()
db.connect(**connect_args)

charset_mysql = "utf8mb4"

charset_python = "utf-8"

db.set_character_set(charset_mysql)

# Execute the query

db.query(

  """SELECT Name, CountryCode,
            Population
       FROM world.city
      WHERE Population > 9000000
      ORDER BY Population DESC"""

)

print(__file__ + " - Using _mysql_connector:")
print("")

if (db.have_result_set):

  # Print the rows found
  print(
    "{0:15s}   {1:7s}   {2:3s}".format(
      "City", "Country", "Pop"
    )
  )
  city = db.fetch_row()
  while (city):
    print(
      "{0:15s}   {1:⁷s}   {2:4.1f}".format(
        city[0].decode(charset_python),
        city[1].decode(charset_python),
        city[2]/1000000.0
      )
    )
    city = db.fetch_row()

db.free_result()

db.close()

Listing 4-17Using the C Extension by Importing the _mysql_connector Module

首先要注意的是,与本章中的其他示例不同,连接参数不是从配置文件中读取的。支持从配置文件中读取选项是 MySQL Connector/Python 的 Python 部分的一个特性,因此当直接使用_mysql_connector模块时不支持。

第二件事是显式设置字符集,需要使用set_character_set()方法设置字符集。原因是connect()方法只支持 MySQL Connector/Python 支持的连接选项的子集。其余的选项必须使用专用方法设置,如set_character_set()方法。

第三件事是,使用_mysql_connector.MySQL类的方法类似于使用连接方法(如cmd_query())来执行查询和处理结果集。但是,方法名称是不同的。这样,显式处理结果值也是必要的。字符串值以字节形式返回,但总体以整数形式返回。

第四件事是,当程序处理完结果后,有必要使用free_result()方法释放结果。这也类似于使用 C 客户端库。

该程序的输出是

listing_4_17.py - Using _mysql_connector:

City              Country    Pop
Mumbai (Bombay)     IND     10.5
Seoul               KOR     10.0
São Paulo           BRA     10.0
Shanghai            CHN      9.7
Jakarta             IDN      9.6
Karachi             PAK      9.3

小费

不再详细讨论_mysql_connector模块。关于 C 扩展 API 的完整文档,请参见 https://dev.mysql.com/doc/connector-python/en/connector-python-cext-reference.html

摘要

本章讲述了 MySQL Connector/Python 中查询执行和连接对象的几个特性。它从在单个 API 调用中执行多个查询开始,包括处理多个结果集和使用扩展插入。此外,还讨论了缓冲结果的使用、调用存储过程以及从 CSV 文件加载数据。

本章的后半部分重点介绍连接属性、事务、设置默认数据库和时区。这一章以对 C 扩展的讨论结束。大多数情况下建议使用 C 扩展。当通过在mysql.connector.connect()函数中设置use_pure = False来启用 C 扩展时,API 与纯 Python 实现相同,这使得在两种实现之间进行切换相对简单。

是时候放下对查询的关注,看看高级连接特性,比如连接池和故障转移配置。

**

五、连接池和故障转移

在前两章中,您从查询的角度了解了 MySQL Connector/Python 的工作原理。是时候稍微改变一下话题,看看一些更高级的连接特性:连接池和故障转移。

小费

本章有几个示例程序。列表中出现的所有示例程序都可以下载。有关使用示例程序的更多信息,参见第一章中对示例程序的讨论。

连接池–背景

连接池使得应用可以利用大量的连接进行查询。这在多线程应用中很有用,在多线程应用中,查询最终是并行执行的。通过使用池,可以控制并发连接的数量,并减少开销,因为不必为每个任务创建新的连接。

虽然在 MySQL 中创建连接相对较快,特别是对于执行许多快速查询的应用,并且如果 MySQL 的网络稳定,使用持久连接可以节省足够的时间,从而值得实现连接池。另一方面,如果您正在为网络连接不稳定的物联网(IoT)编写一个程序,并且该程序每分钟只会执行几个查询,那么每次创建一个新连接会更好。

连接池使用两个类。这些类别是

  • pooling. MySQLConnectionPool

  • pooling.PooledMySQLConnection

本节将介绍这两个类、它们的方法和属性。下一节将讨论连接池更实际的一面。

注意

不能对连接池使用 C 扩展。结合连接池设置和use_pure调用mysql.connector.connect()函数时,将忽略use_pure选项。

联营。MySQLConnectionPool 类

pooling.MySQLConnectionPool类是定义池的主类。当应用需要执行查询时,在这个类中添加、配置和检索连接。

除了构造函数,还有三个方法和一个属性用于pooling.MySQLConnectionPool类。它们在表 5-1 中进行了总结。

表 5-1

pooling.MySQLConnectionPool课小结

|

名字

|

类型

|

描述

|
| --- | --- | --- |
| MySQLConnectionPool | 构造器 | 创建连接池的构造函数。 |
| add_connection( ) | 方法 | 向池中添加或返回一个连接(即池中的连接数增加 1)。 |
| get_connection( ) | 方法 | 从池中获取连接。 |
| set_config( ) | 方法 | 配置池中的连接。 |
| pool_``nam | 财产 | 池的名称。这可以在实例化池时设置。 |

首先通过调用构造函数来创建连接池。这可以直接发生,也可以通过mysql.connector.connect()函数间接发生。当应用需要连接时,它可以通过调用get_connection()方法来获取一个连接。

警告

在内部使用add_connection()方法来返回到池的连接。也可以使用MySQLConnection类的连接从外部调用它。(不支持使用 C 扩展名的连接。)但是,向池中添加新连接实际上不会增加池的大小。因此,结果是所有的连接都不能再返回到池中,并且出现一个PoolError异常,错误为“添加连接失败;队列已满”。

如有必要,可使用set_config()方法重新配置配置。与前面章节中使用的独立连接不同,不能直接更改连接的配置。如果可能的话,将不再保证池中的所有连接都配置相同。由于应用不知道哪个连接被返回,如果连接与连接之间的配置不同,那将是非常不幸的。如果需要不同配置的连接,请为每个配置创建一个池。

小费

可以有一个以上的池。例如,这可以用于提供不同的连接配置。一种使用情形是读写拆分,即写入到复制主机(源),读取到复制从机(副本)。

没有官方方法来断开池中的连接。构造函数和方法将在本节后面的示例中更详细地讨论。然而,首先让我们看看使用连接池的另一半:连接。

联营。PooledMySQLConnection 类

从连接池中检索的连接是pooling.PooledMySQLConnection类的实例,而不是MySQLConnectionCMySQLConnection类的实例。在大多数方面,池连接的行为方式与独立连接相同,但也有一些不同。

两个最重要的区别是close()config()方法被改变了。close()方法实际上并不关闭池化的连接,而是将它返回到池中。因为池中的所有连接必须具有相同的配置,所以config()方法将返回一个PoolError异常。

除了close()config()方法的行为改变之外,还有pool_name属性。这与pooling. MySQLConnectionPool类相同,可用于确认连接来自哪个池。这在将连接传递给另一个函数或方法时非常有用。

配置选项

连接池的配置由三个选项控制,它们都带有前缀pool_。这些选项允许您设置名称和大小,并控制连接返回池时是否重置。表 5-2 总结了这些选项。创建池后,无法更改任何设置。

表 5-2

配置连接池的选项

|

名字

|

缺省值

|

描述

|
| --- | --- | --- |
| pool_``nam | 自动生成 | 连接池的名称。默认情况下,该名称是通过连接hostportuserdatabase连接选项的值生成的。名称最长为pooling.CNX_POOL_MAXNAMESIZE(默认为 64 个)字符,允许使用字母数字字符以及以下字符:。、_、:、-、*、$和#。 |
| pool_reset_``sessio | True | 当True时,当连接返回到池中时,会话变量被重置。 |
| pool_``siz | 5 | 池中容纳的连接数。该值必须至少为 1,最多为pooling.CNX_POOL_MAXSIZE(默认为 32)。 |

所有连接池都有一个名称。如果在创建池时没有明确设置名称,将通过连接hostportuserdatabase连接选项的值自动生成名称。如果在关键字参数中没有设置这两个选项,就会引发一个PoolError异常。生成名称时,不考虑通过选项文件设置的选项。

小费

建议显式配置池名称。这确保了对配置的更改不会更改池名称。如果您有多个池,请为它们指定唯一的名称以避免混淆。请记住,即使在您当前的代码中没有使用池名称,以后也可能需要它。

pool_reset_session选项控制当连接返回到池中时是否重置会话变量。重置意味着取消设置所有用户变量(例如@my_user_variable)并确保所有会话变量(例如@@session.sort_buffer_size)具有与全局默认值相同的值。在 MySQL Server 5.6 和更低版本中重置连接有两个限制:

  • 通过重新连接完成重置。

  • 不支持压缩(compress选项)。

在大多数情况下,建议重置连接,因为这样可以确保从池中提取连接时,连接的状态始终相同。

小费

除非明确要求保持连接的状态,否则总是使用pool_reset_session = True(缺省值)来确保在从池中获取连接时知道它们的状态。

使用pool_size选项指定池中的连接数。默认情况下,创建具有五个连接的池,但是最多可以有pooling.CNX_POOL_MAXSIZE个连接。pooling.CNX_POOL_MAXSIZE属性默认为 32。

除了这三个连接池选项之外,必须以与独立连接相同的方式指定连接所需的其他连接选项。还可以使用set_config()方法为现有池中的连接设置与池无关的选项,该方法的工作方式与用于独立连接的config()方法相同。下一节包括一个使用set_config()方法的例子。

既然已经讨论了两个连接池类和配置的基础知识,让我们看看如何使用它们。

使用连接池

终于是时候更实际地开始使用连接池了。本节将首先展示如何创建连接池,然后展示获取和返回连接的示例。本节的后半部分将讨论使用连接池时的查询执行和连接的重新配置。

创建连接池

使用连接池时,第一步是创建池。如前所述,创建池有两种不同的方式:隐式或显式。

要隐式创建一个连接池,使用mysql.connector.connect()函数创建独立连接。只要存在至少一个连接池选项,如果不存在同名的池,就会创建一个池,并返回一个pooling.PooledMySQLConnection类的连接。如果存在具有相同池名称的池,则返回来自该池的连接。一个例子是

import mysql.connector

db = mysql.connector.connect(
  option_files="my.ini",
  pool_name="test_connect",
)

print("Pool name: {0}".format(db.pool_name))

间接方法的优点是它更类似于创建独立连接。缺点是与使用pooling. MySQLConnectionPool类相比,您对池的控制较少。

另一种方法是通过实例化pooling.MySQLConnectionPool类显式地创建一个连接池,例如:

from mysql.connector import pooling

pool = pooling.MySQLConnectionPool(
  option_files="my.ini",
  pool_name="test_constructor",
)

print("Pool name: {0}".format(pool.pool_name))

以这种方式创建池的优点是它允许重新配置连接。

当直接调用pooling. MySQLConnectionPool构造函数时,所有的连接池选项都是可选的。如果池是使用mysql.connector.connect()函数创建的,则必须指定至少一个选项。

一旦创建了连接池,就可以从池中检索连接并将其返回到池中。让我们看看这是如何做到的。

使用连接池连接

显然,连接池的主要目的是让连接可供使用。因此,让我们更深入地了解检索和返回连接以及如何使用它们。

获取连接的方式取决于池的创建方式。对于使用mysql.connector.connect()函数创建的池,使用与创建池时相同的池名再次使用mysql.connector.connect()函数检索连接。另一方面,对于通过调用pooling. MySQLConnectionPool构造函数显式创建的池,使用get_connection()方法获取连接。

警告

不要试图混合这两种获取连接的方式。如果使用与使用构造函数创建的池相同的池名使用mysql.connector.connect()函数,则创建第二个池。

使用 mysql.connector.connect()函数

当使用mysql.connector. connect()函数创建池时,立即返回第一个连接,并使用相同的池名再次调用该函数获取其他连接。除了pool_name选项,传递给mysql.connector.connect()的任何选项都被忽略。

图 5-1 显示了使用带有两个连接的连接池和通过mysql.connector. connect()函数创建的池时的一般工作流程。

img/463459_1_En_5_Fig1_HTML.jpg

图 5-1

使用mysql.connector.connect()功能时的工作流程

图的中间是应用中执行的命令。开始时会创建一个有两个连接的连接池。一个连接可以在池中等待应用获取它(左边的连接),也可以在应用中使用(右边的连接)。

您可以看到,创建池和从池中取出第一个连接的步骤被合并到一个对mysql.connector.connect()函数的调用中。后续的连接可以用同样的方式获取。一旦应用完成了连接,就通过关闭它来将其返回到池中。如果需要,可以通过再次从池中获取连接来重用它(图中未显示)。

清单 5-1 展示了一个使用mysql.connector.connect()函数管理连接池的例子。

import mysql.connector
from mysql.connector.errors import PoolError

print(__file__ + " - connect():")
print("")

# Create a pool and return the first
# connection

db1 = mysql.connector.connect(

  option_files="my.ini",
  pool_size=2,
  pool_name="test",

)

# Get a second connection in the same pool

db2 = mysql.connector.connect(

  pool_name="test")

# Attempt to get a third one
try:
  db3 = mysql.connector.connect(
    pool_name="test")

except PoolError as err:

  print("Unable to fetch connection:\n{0}\n"
    .format(err))

# Save the connection id of db1 and
# return it to the pool, then try
# fetching db3 again.
db1_connection_id = db1.connection_id

db1.close()

db3 = mysql.connector.connect(
  pool_name="test")

print("Connection IDs:\n")
print("db1   db2   db3")
print("-"*15)
print("{0:3d}   {1:3d}   {2:3d}".format(
    db1_connection_id,
    db2.connection_id,
    db3.connection_id
  )
)

db2.close()
db3.close()

Listing 5-1Managing a Connection Pool with mysql.connector.connect()

最初,获取连接的方式与获取独立连接的方式相同。唯一的区别是连接池是通过设置至少一个连接池选项来启用的;在这种情况下,pool_sizepool_name选项都被设置。这个连接是pooling.PooledMySQLConnection类的一个实例。

以类似的方式获取db2连接。pool_name选项是这里唯一设置的,也是唯一必需的选项。然而,如果保留原来的选项可以使代码更容易编写,那么这样做是很好的;当从池中获取额外的连接时,任何额外的选项都会被忽略,只要它们是有效的选项。

当尝试第三次连接时,会出现PoolError异常。该异常已经从示例顶部附近的mysql.connector.errors导入。发生异常是因为池已耗尽。将db1连接返回到池中可以让你得到db3。最后,打印三个连接 id:

listing_5_1.py - connect():

Unable to fetch connection:
Failed getting connection; pool exhausted

Connection IDs:

db1   db2   db3
---------------
324   325   324

实际的 id 将与示例输出不同,因为它们取决于自 MySQL 上次重启以来建立了多少个连接。重要的是,输出确认了db3以之前由db1使用的连接 ID 结束。

使用 get_connection()方法

直接使用连接池时使用的代码与使用mysql.connector.connect()函数有些不同;然而,功能本质上是相同的。用get_connection()方法获取一个连接。正如使用mysql.connector.connect()函数一样,返回的连接是pooling.PooledMySQLConnection类的一个实例。图 5-2 显示了具有两个连接的池的基本工作流程。

img/463459_1_En_5_Fig2_HTML.jpg

图 5-2

使用pooling.MySQLConnectionPool类时的工作流

具有两个连接的连接池是使用pooling. MySQLConnectionPool构造函数池显式创建的。MySQLConnectionPool:构造函数。最初,两个连接都在池中。每当应用需要一个连接时,就使用池对象的get_connection()方法获取它。一旦应用完成了连接,就通过关闭它来将其返回到池中。在清单 5-2 中可以看到前一个例子的对等物。

from mysql.connector import pooling

from mysql.connector import errors

print(__file__ + " - MySQLConnectionPool():")
print("")

pool = pooling.MySQLConnectionPool(

  option_files="my.ini",
  pool_name="test",
  pool_size=2,

)

# Fetch the first connection

db1 = pool.get_connection()

# Get a second connection in the same pool
db2 = pool.get_connection()

# Attempt to get a third one
try:
  db3 = pool.get_connection()

except errors.PoolError as err:

  print("Unable to fetch connection:\n{0}\n"
    .format(err))

# Save the connection id of db1 and
# return it to the pool, then try
# fetching db3 again.
db1_connection_id = db1.connection_id

db1.close()

db3 = pool.get_connection()

print("Connection IDs:\n")
print("db1   db2   db3")
print("-"*15)
print("{0:3d}   {1:3d}   {2:3d}".format(
    db1_connection_id,
    db2.connection_id,
    db3.connection_id
  )
)

db2.close()
db3.close()

Listing 5-2Managing a Connection Pool Using the pool Object Directly

连接池是显式创建的,使用 pool 对象的get_connection()方法检索连接,但除此之外,该示例与使用mysql.connector.connect()函数的示例相同。当连接池耗尽并尝试连接时,再次出现PoolError异常,使用 connection 对象的close()方法将连接返回到连接池。

输出类似于使用mysql.connector.connect()函数创建池时的输出。同样,实际的 id 会有所不同。输出示例如下

listing_5_2.py - MySQLConnectionPool():

Unable to fetch connection:
Failed getting connection; pool exhausted

Connection IDs:

db1   db2   db3
---------------
350   351   350

现在您已经知道了如何创建和返回连接,让我们继续讨论连接最终最重要的目的:执行查询。

执行查询

创建连接池并获取和返回连接可能很有趣,但是不用于查询的连接没有多大价值。当连接不在池中时,它可以像任何常规连接一样使用,只是config()方法不起作用,而close()方法将连接返回到池中,而不是关闭连接。

为了执行查询,可以使用第三章和第四章中讨论的所有其他特性(除了 C 扩展)。清单 5-3 展示了一个使用游标执行简单SELECT查询的例子。

from mysql.connector import pooling

pool = pooling.MySQLConnectionPool(
  option_files="my.ini",
  pool_name="test",
)

db = pool.get_connection()

cursor = db.cursor(named_tuple=True)
cursor.execute("""
SELECT Name, CountryCode, Population
  FROM world.city
 WHERE CountryCode = %s""", ("AUS",))

if (cursor.with_rows):
  # Print the rows found
  print(
    "{0:15s}   {1:7s}   {2:10s}".format(
      "City", "Country", "Population"
    )
  )
  city = cursor.fetchone()
  while (city):
    print(
      "{0:15s}   {1:⁷s}    {2:8d}".format(
        city.Name,
        city.CountryCode,
        city.Population
      )
    )
    city = cursor.fetchone()

cursor.close()
db.close()

Listing 5-3Executing a Query in a Connection Pool Connection

这个程序很简单。创建连接后,会获取一个连接。该查询正在使用命名元组的游标中执行。处理完查询结果后,游标关闭,连接返回到池中。正如您所看到的,与前几章中的查询相比,本例中没有什么特别之处,除了连接来自池的事实。执行程序的输出是

City              Country   Population
Sydney              AUS       3276207
Melbourne           AUS       2865329
Brisbane            AUS       1291117
Perth               AUS       1096829
Adelaide            AUS        978100
Canberra            AUS        322723
Gold Coast          AUS        311932
Newcastle           AUS        270324
Central Coast       AUS        227657
Wollongong          AUS        219761
Hobart              AUS        126118
Geelong             AUS        125382
Townsville          AUS        109914
Cairns              AUS         92273

对于连接池,最后要考虑的是如何重新配置池中的连接以及它对连接的影响。

重新配置连接

当你像第三章和第四章那样使用独立连接时,重新配置的概念很简单。重新配置发生在与连接用于查询相同的执行流中。然而,对于池连接来说,情况就不同了,因为有些连接将在池中,而其他连接将在池外工作。更改应用中其他地方使用的连接的配置可能会导致未定义的行为,并可能导致查询突然作为另一个用户或在另一个 MySQL 服务器实例上执行。

MySQL Connector/Python 处理重新配置请求的方式是,对于给定的连接,只有当它在池中时,它才被重新配置。对于在重新配置请求时正在使用的连接,对配置的改变被推迟,直到它被返回到池中。

清单 5-4 展示了一个在有两个连接的连接池中重新配置连接的例子。其中一个连接(db1)在调用set_config()时在池外,而另一个(db2)在池内。

from mysql.connector import pooling

pool = pooling.MySQLConnectionPool(
  option_files="my.ini",
  pool_name="test",
  pool_size=2,
)

print("{0:18s}: {1:3s}   {2:3s}".format(
  "Stage", "db1", "db2"
))
print("-"*29)
fmt = "{0:18s}: {1:3d}   {2:3d}"
db1 = pool.get_connection()
db2 = pool.get_connection()
print(
  fmt.format(
    "Initially",
    db1.connection_id,
    db2.connection_id
  )
)

# Return one of the connections before
# the reconfiguration
db2.close()

# Reconfigure the connections
pool.set_config(user="pyuser")

# Fetch db2 again
db2 = pool.get_connection()
print(
  fmt.format(
    "After set_config()",
    db1.connection_id,
    db2.connection_id
  )
)

# Return the db1 connection to the pool
# and refetch it.
db1.close()
db1 = pool.get_connection()
print(
  fmt.format(
    "After refetching",
    db1.connection_id,
    db2.connection_id
  )
)

db1.close()
db2.close()

Listing 5-4Using the 
set_config()

Method

首先创建一个连接池。然后检索两个连接(耗尽池)并打印连接 id。在重新配置之前,db2连接被恢复,而db1保持使用。重新配置连接后,将再次打印连接 id。在这种情况下,配置实际上没有任何变化,但这并不影响 MySQL Connector/Python 的行为。最后,db1连接被返回到池中并再次被检索,最后一次打印连接 ID。输出类似于

Stage             : db1   db2
-----------------------------
Initially         : 369   370
After set_config(): 369   371
After refetching  : 372   371

连接 ID 的改变意味着旧的连接被关闭,连接配置被更新,并且连接被重新建立。从输出中可以看到,db1连接的连接 ID 并没有因为调用set_config()而改变。已经从池中提取的连接在返回池之前不会更新配置。位于池中的连接,就像与db2一起使用的,将会立即更新。在db1连接回到池中并被再次获取后,连接 ID 被更改,反映了配置更新时发生的重新连接。

连接池的讨论到此结束。关于连接还有另一个高级主题:故障转移配置。这将是本章的最后一个主题。

连接故障转移

如今,许多应用需要全天候可用。但是,仍然需要能够在数据库后端执行维护,例如为了升级操作系统或 MySQL 服务器。也可能是由于硬件问题或数据库问题造成的中断。当数据库实例不可用时,应用如何保持在线?答案是执行故障转移到具有相同数据的另一个 MySQL 服务器实例。

有几种方法可以实现应用的高可用性。这是一个大而有趣的话题,许多书都是关于它的。所以,在本书中不可能详细讨论。然而,有一个选项与 MySQL 连接器/Python 直接相关:当主数据库不可用时,连接器可以自动进行故障转移。

本节将介绍 MySQL Connector/Python 中内置的故障转移是如何工作的。第一个主题是配置,然后是如何在应用代码中使用故障转移,最后会有一个例子。

注意

很容易认为实现故障转移所需要的只是配置它。但是,为了让故障转移正常工作,在编写应用时必须考虑到故障转移。“故障转移编码”一节将提供更多信息。

故障转移配置

配置应用以使用 MySQL Connector/Python 中的故障转移特性是使用故障转移最简单的部分。只有一个选项可以考虑:第一个选项。

failover选项为每个 MySQL 服务器实例获取一个带有字典的元组(或列表),以便在创建连接时考虑。字典必须具有对于该实例唯一的连接选项。常见的连接选项可以设置为正常。如果在mysql.connector.connect()函数的参数列表和failover字典中都指定了一个选项,则failover字典中的值优先。

failover选项支持连接选项的子集。只允许与指定连接到哪里、哪个用户和连接池选项直接相关的选项。受支持选项的完整列表如下

  • user

  • password

  • host

  • port

  • unix_socket

  • database

  • pool_name

  • pool_size

一般来说,最好让所有 MySQL 服务器实例的选项尽可能相似,因为这样可以减少出现难以调试的错误的几率。例如,如果用户名不同,那么对特权的更改在不同的实例之间结束的可能性就会增加。

创建带故障转移的连接的一个示例是

import mysql.connector

primary_args = {
  "host": "192.168.56.10",
}
failover_args = {
  "host": "192.168.56.11",
}
db = mysql.connector.connect(
  option_files="my.ini",
  failover=(
    primary_args,
    failover_args,
  )
)

在本例中,标准的my.ini文件用于设置两个实例的公共选项。在failover选项中设置的唯一选项是每个实例的host。MySQL Connector/Python 将尝试按照列出的顺序连接到实例,因此列出的第一个实例将是主实例,第二个实例是故障转移实例。如果需要,可以添加更多实例。

注意

MySQL 服务器实例添加到故障转移元组的顺序很重要。MySQL Connector/Python 将尝试从列出的第一个实例开始按顺序连接到实例。

只有在请求新连接时,才会考虑failover选项中列出的实例。也就是说,如果首先成功创建了一个连接,但后来失败了,MySQL Connector/Python 不会自动重新连接,既不会连接到旧实例,也不会连接到其他实例。失败连接的检测和新连接的建立必须在应用中明确编码。类似地,应用必须在用完要连接的实例时处理这种情况。

故障转移编码

如前所述,使用故障转移的困难部分是让应用与它们一起工作。MySQL Connector/Python 提供了连接到第一个可用实例的框架,但是由应用来确保它用于提高可用性。

当连接失败时,MySQL Connector/Python 永远不会自动重新连接。无论是否使用failover选项进行连接,情况都是如此。这样做的原因是,如果什么都没发生,只是重新连接并继续通常是不安全的。例如,当断开连接发生时,应用可能正在处理事务,在这种情况下,有必要返回到事务的开始。

这意味着开发人员在使用连接器时必须检查错误。一般来说,错误处理是第九章的主题。关于故障转移,重要的是检查它是否真的是连接错误;否则,初始化故障转移就没有什么意义了。表 5-3 中列出了创建连接后可能出现的一些常见连接错误。

表 5-3

常见的连接相关错误

|

全局变量

|

errno–定义符号

|

出错信息

|
| --- | --- | --- |
| (无) | (无) | MySQL 连接不可用 |
| One thousand and fifty-three | ER_SERVER_SHUTDOWN | 服务器正在关闭 |
| Two thousand and five | CR_SERVER_LOST_EXTENDED | 与位于“…”的 MySQL 服务器失去连接,系统错误:… |
| Two thousand and six | CR_SERVER_GONE_ERROR | MySQL 服务器已经消失了 |
| Two thousand and thirteen | CR_SERVER_LOST | 查询期间失去了与 MySQL 服务器的连接 |

当试图以非查询方式使用连接时,会出现“MySQL 连接不可用”错误,例如在连接丢失后创建游标时。当错误号可用时,可以在异常的errno属性中找到。定义符号在mysql.connector.errorcode模块中可用,可用于更容易地查看错误号与哪个错误进行比较。

如果有几个应用实例都使用相同的 MySQL 服务器实例,并且它们写入数据库,那么确保没有应用实例进行故障转移或者所有应用实例都进行故障转移也很重要。如果一些应用实例最终写入一个数据库实例,而其他应用实例写入另一个数据库实例,则数据可能会不一致。在有多个应用实例的情况下,最好使用一个代理来实现故障转移,比如 MySQL 路由器或 ProxySQL,它将连接指向正确的 MySQL 服务器实例。

小费

为了避免不一致的数据,确保故障转移 MySQL 实例设置了super_read_only选项,直到它们打算接受写入。MySQL Server 5.7 和更高版本中提供了super_read_only选项。早期版本只提供了较弱的read_only选项,它不会阻止拥有SUPER特权的用户写入实例。

当涉及到故障转移时,测试也比平常更重要。确保您测试了各种故障条件,包括在应用执行查询和引入网络故障时强行终止 MySQL 服务器。此外,添加一些不会导致故障转移的故障,例如锁定等待超时。这是验证应用能够正确处理故障的唯一方法。

为了总结关于使用故障转移特性的讨论,让我们看一个例子,这个例子包含了到目前为止所讨论的一些内容。

故障转移示例

在使用故障转移的应用中,很难考虑所有必须考虑的事情。希望一个例子将有助于使事情更清楚。

为了让这个例子正常工作,必须有两个 MySQL 服务器实例。在真实的应用中,数据库实例通常位于不同的主机上,因此即使整个主机都关闭了,也有可能进行故障转移。然而,对于这个例子,在同一个主机上有两个实例使用不同的 TCP 端口、到数据目录的路径(datadir选项)和其他特定于数据库的文件,并且在 Linux 和 Unix 上有不同的 Unix 套接字路径(socket选项)是没有问题的。

小费

根据您的操作系统和 MySQL 的安装方式,在一台机器上运行多个实例有不同的选择。参见 https://dev.mysql.com/doc/refman/en/multiple-servers.html 和其中的参考资料,了解微软视窗和 Unix/Linux 的操作说明。如果你在 Linux 上使用 systemd 管理 MySQL,参见 https://dev.mysql.com/doc/refman/en/using-systemd.html

该示例假设两个实例都在本地主机(127.0.0.1)上,主实例使用端口 3306(与前面所有示例一样),故障切换实例使用端口 3307。见清单 5-5 。

import mysql.connector
from mysql.connector import errorcode
from mysql.connector import errors

def connect():

  """Connect to MySQL Server and return
  the connection object."""
  primary_args = {
    "host": "127.0.0.1",
    "port": 3306,
  }
  failover_args = {
    "host": "127.0.0.1",
    "port": 3307,
  }
  db = mysql.connector.connect(
    option_files="my.ini",
    use_pure=True,
    failover=(
      primary_args,
      failover_args,
    )
  )

  return db

def execute(db, wait_for_failure=False):

  """Execute the query and print
  the result."""
  sql = """
SELECT @@global.hostname AS Hostname,
       @@global.port AS Port"""

  retry = False
  try:
    cursor = db.cursor(named_tuple=True)
  except errors.OperationalError as err:
    print("Failed to create the cursor."
      + " Error:\n{0}\n".format(err))
    retry = True
  else:
    if (wait_for_failure):
      try:
        input("Shut down primary now to"
          + " fail when executing query."
          + "\nHit Enter to continue.")
      except SyntaxError:
        pass
      print("")

    try:
      cursor.execute(sql)
    except errors.InterfaceError as err:
      print("Failed to execute query"
        + " (InterfaceError)."
        + " Error:\n{0}\n".format(err))
      retry = (err.errno == errorcode.CR_SERVER_LOST)
    except errors.OperationalError as err:
      print("Failed to execute query"
        + " (OperationalError)."
        + " Error:\n{0}\n".format(err))
      retry = (err.errno == errorcode.CR_SERVER_LOST_EXTENDED)
    else:
      print("Result of query:")
      print(cursor.fetchall())
    finally:
      cursor.close()

  return retry

# Execute for the first time This should
# be against the primary instance
db = connect()
retry = True
while retry:
  retry = execute(db)
  if retry:
    # Reconnect
    db = connect()
print("")

# Wait for the primary instance to
# shut down.
try:
  input("Shut down primary now to fail"
      + " when creating cursor."
      + "\nHit Enter to continue.")
except SyntaxError:
  pass
print("")

# Attempt to execute again
retry = True
allow_failure = True
while retry:
  retry = execute(db, allow_failure)
  allow_failure = False
  if retry:
    # Reconnect
    db = connect()

db.close()

Listing 5-5Using the Failover Feature

该连接是在connect()功能中创建的。将它放入自己的函数中的主要原因是,当故障发生时,有必要显式地重新连接,因此将与连接相关的代码隔离并可重用是很方便的。

这也是使用execute()函数的原因,该函数创建一个游标并执行一个查询来获取程序所连接的 MySQL 服务器实例的主机名和端口。执行代码包括try语句,用于测试操作是否成功,如果不成功,则在重新连接后是否应该重试查询(以及可能的故障转移)。

该示例假设主 MySQL 服务器实例和故障转移 MySQL 服务器实例在开始时都可用。当第一次创建连接时,它将针对主实例,因为它在failover选项中首先列出。一旦针对主实例的查询完成,执行将暂停,这样,如果在创建下一个游标时出现故障,就可以关闭主实例。

当执行继续时(按下输入后),将再次尝试查询。如果主实例已关闭,创建游标将失败,并将打印错误。否则,将会创建一个新的暂停,因为第二轮第一次调用execute()函数时wait_for_failover被设置为True。如果主实例此时关闭,则在尝试执行实际查询时会出现错误。在这种情况下,将错误号与预期值进行比较,以确保确实是连接问题导致了失败。

当检测到连接失败时,代码将尝试重新连接。这次mysql.connector.connect()将故障转移到故障转移实例。然后就可以执行查询了。

当游标创建失败时的输出是

Result of query:
[Row(Hostname='MY-COMPUTER', Port=3306)]

Shut down primary now to fail when creating cursor.
Hit Enter to continue.

Failed to create the cursor. Error:
MySQL Connection not available.

Result of query:
[Row(Hostname='MY-COMPUTER', Port=3307)]

收到的错误是一个没有错误号的OperationalError异常。请注意连接失败后端口号是如何变化的,这表明程序现在已连接到故障转移实例。

第二种情况是在尝试执行查询时发生错误,根据平台的不同,这种情况有不同的异常和错误。在 Microsoft Windows 上,输出是

Result of query:
[Row(Hostname='MY-COMPUTER', Port=3306)]

Shut down primary now to fail when creating cursor.
Hit Enter to continue.

Shut down primary now to fail when executing query.
Hit Enter to continue.

Failed to execute query (OperationalError). Error:
2055: Lost connection to MySQL server at '127.0.0.1:3306', system error: 10053 An established connection was aborted by the software in your host machine

Result of query:
[Row(Hostname='MY-COMPUTER', Port=3307)]

这里是另一个OperationalError异常,但是错误号设置为 2055。在 Linux 上,错误是

Failed to execute query (InterfaceError). Error:
2013: Lost connection to MySQL server during query

因此,在 Linux 上,这是一个错误号为 2013 的InterfaceError异常。这表明失败的细节也可能取决于平台。它还取决于是使用纯 Python 实现还是 C 扩展,因此在编码时也必须考虑到这一点。

摘要

在本章中,您了解了两个高级连接特性:连接池和故障转移。它们不常用,但在某些应用中会很有用。

连接池特性使应用可以从池中检索连接。这对于多线程应用特别有用,在多线程应用中,可以使用池来减少开销并限制查询执行的并发性。

故障转移功能使 MySQL Connector/Python 依次检查每个已配置的连接,以找到第一个可用的连接。这有助于提高可用性,但也需要在应用中做额外的工作。可以将连接池和故障转移功能结合起来。

除了错误处理和故障排除(第九章和第十章),传统 MySQL 连接器/Python 的讨论到此结束。在接下来的三章中,您将看到 MySQL Connector/Python 8.0 独有的 X DevAPI 是如何作为文档存储与 MySQL Server 一起使用的。

六、X DevAPI

MySQL Server 最初是在 1995 年作为 SQL 数据库发布的。如第 2 和第三章所示执行的 SQL 语句仍然是 MySQL 中最常见的执行查询的方式,mysql.connector模块使用传统的协议。然而,还有另一种方法:新的 X 协议。

本章将首先简要介绍 X 插件(后端)和 X DevAPI(应用使用的 API)以及它们之间的特性。本章的其余部分将集中在 X DevAPI 的 MySQL 连接器/Python 实现的部分,这些部分在 API 的三个主要部分中是通用的:MySQL 文档存储、SQL 表的创建-读取-更新-删除(CRUD)接口和 SQL 语句。这包括如何创建连接、常见的参数类型、语句类和结果对象。

接下来的两章将详细介绍 API 的其余部分。第七章将展示如何使用 MySQL 文档库。第八章将展示如何通过 CRUD NoSQL 方法和 SQL 语句将 X DevAPI 用于 SQL 表。错误处理和故障排除被推迟到第 9 和 10 章。

小费

MySQL X DevAPI 非常新;到了 MySQL 8.0 就变成 GA 了。这意味着新功能仍在以相对较快的速度开发。如果您找不到某个特性,请在 https://dev.mysql.com/doc/dev/connector-python/8.0/ 查看在线 API 文档,查看该特性是否已被添加。您也可以在 https://bugs.mysql.com/ 请求新功能。

什么是 NoSQL?

对于 NoSQL,没有一个大家都认同的定义。“不”是指“不,SQL 根本不用于定义查询”还是“不”是指“不仅如此?”“SQL”是指用于编写查询的语言还是指关系数据库?即使在 NoSQL,现有产品之间也有很大差异。有些是键值存储,有些存储 JSON 文档等文档。换句话说,还不清楚 NoSQL 是什么,但一个共同点是使用 API 方法而不是 SQL 语句来查询数据。

从 MySQL 的角度考虑 NoSQL,使用 MySQL 作为关系数据库,并使用结构化查询语言(SQL)如SELET * FROM world.city编写查询,这意味着它显然属于 SQL 范畴。另一方面,使用 MySQL 文档存储(将数据存储在 JSON 文档中)并使用 X DevAPI(编程语言特性)的方法来定义查询来查询数据,这意味着它处于 NoSQL 体系中。

然而,两者之间有一个灰色地带。X DevAPI 还支持查询 SQL(关系)表,而无需编写 SQL 查询,并且您可以使用 SQL 语句查询文档存储中的文档。这些用途是否应被视为 NoSQL 可以讨论。在这个意义上,你可以说 MySQL 8.0 和文档存储以及 X DevAPI 是一个“不仅仅是 SQL”的数据库。

MySQL X 插件

在 MySQL Server 5.7.12 中,MySQL X 插件是作为测试版特性引入的。此后,它被赋予了成熟的时间,并在 MySQL Server 8.0 中正式发布。除了传统的 SQL 语句之外,X 插件允许您以类似于其他文档存储的方式使用 NoSQL 来使用 MySQL。

X 插件有几个部分来处理它使用的各个级别。这些零件是

  • X 插件:这是特性的服务器端实现。在 MySQL 8.0.11 和更高版本中,它是一个内置插件,因此不需要任何操作就可以启用它。

  • ****X 协议:应用用来与 X 插件通信的新协议。X 协议的默认 TCP 端口是端口 33060。

*** X DevAPI :这是用于 X 协议的新 API。

*   **mysqlx 模块**:MySQL 连接器/Python 模块,实现了 X DevAPI。** 

**此外,还有 MySQL 文档库的概念,它是 X 插件、X 协议、X DevAPI 和 MySQL Shell(稍后讨论)的集合。

为了能够使用 X 插件,开发了一个名为 X DevAPI 的新 API。它可用于多种编程语言,包括 Python、JavaScript (Node.js)、PHP、.Net、C++和 Java。MySQL Shell 是一个新的命令行客户端,在某种程度上可以取代传统的mysql命令行客户端。它支持 X DevAPI,可用于使用 SQL 语句、Python 和 JavaScript 执行查询。此外,MySQL Shell 可用于使用 Python 或 JavaScript 管理 MySQL InnoDB 集群。

应用和文档存储之间的通信是使用 X 协议完成的。因为与传统的 MySQL 协议相比,它是一个新的协议,所以文档存储使用自己的端口。默认端口号是 33060;这可以使用mysqlx_port选项进行更改。

X DevAPI 的服务器端部分是作为 MySQL 服务器的插件实现的。这个插件被命名为 X 插件。在 MySQL Server 8.0.11 及更高版本中默认启用;事实上,它已经成为一个内置的插件,无法删除,所以它会一直存在。X 插件也适用于 MySQL 的旧版本;然而,直到 8 . 0 . 11 MySQL 8.0 普遍上市时才发生了变化。所以,确保你用的是 MySQL 8.0.11 或者更高版本。

您可以通过查询information_schema数据库中的PLUGINS视图来确认插件是否处于活动状态:

mysql> SELECT *
         FROM information_schema.PLUGINS
        WHERE PLUGIN_NAME = 'mysqlx'\G
*************************** 1\. row ***************************
           PLUGIN_NAME: mysqlx
        PLUGIN_VERSION: 1.0
         PLUGIN_STATUS: ACTIVE
           PLUGIN_TYPE: DAEMON
   PLUGIN_TYPE_VERSION: 80011.0
        PLUGIN_LIBRARY: NULL
PLUGIN_LIBRARY_VERSION: NULL
         PLUGIN_AUTHOR: Oracle Corp
    PLUGIN_DESCRIPTION: X Plugin for MySQL
        PLUGIN_LICENSE: GPL
           LOAD_OPTION: ON
1 row in set (0.00 sec)

注意这里的PLUGIN_STATUSACTIVE。如果不是这样,最可能的原因是 X 插件在 MySQL 配置文件中被显式禁用(在微软 Windows 上是my.ini,在其他平台上是my.cnf)。寻找一个选项,如

[mysqld]
mysqlx = 0

代替mysqlx = 0,你也可能看到skip-mysqlx。去掉这个选项,注释掉,或者改成mysqlx = 1。因为 X 插件是默认启用的,所以推荐的方法是移除或注释掉它。

在这一章和接下来的两章中,你会学到 X 特性的一些特征。然而,在开始使用 X DevAPI 之前,您需要对mysqlx模块有一个高层次的概述。

mysqlx 模块

与 MySQL Connector/Python 的其他部分相比,Connector/Python 中的 X DevAPI 支持在它自己的独立模块中。这个模块叫做mysqlxmysqlx模块和 mysql.connector 模块的名称和一般用法有很大的不同。这可能看起来很奇怪,但是 X DevAPI 的部分思想是在支持的语言之间有一个相对统一的 API。这意味着如果你习惯于在 MySQL Connector/Python 中使用 X DevAPI,很容易实现另一个项目,例如使用 MySQL Connector/node . js .1

为了开始使用 Python 的 X DevAPI,您必须导入mysqlx模块:

import mysqlx

就这样。下一步是创建一个会话,但是让我们首先看一下图 6-1 ,它显示了mysqlx模块是如何根据将在 X DevAPI 讨论的剩余部分中使用的类来组织的。

img/463459_1_En_6_Fig1_HTML.jpg

图 6-1。

mysqlx模块的组织

类别显示为较小的黄色(浅灰色)框。类周围较大的方框是类所在的子模块。例如,Session类位于mysqlx.connection中。

图 6-1 也显示了代码执行的一般流程。您从一个会话开始,可以从会话对象获得一个模式对象,用于 CRUD 语句或 SQL 语句。SQL 语句的流程很简单,因为这会产生一个可能返回行的 SQL 结果。

CRUD 模式对象包括集合、表和视图。集合用于文档存储,而表和视图用于 SQL 表。CRUD 对象可用于创建 CRUD 语句。这些是statement子模块中的语句;总共将讨论八个 CRUD 语句类。(“…”表示图中未包括的类别。)

对于没有结果集的查询,CRUD 语句以“普通”结果结束。返回数据的语句以集合的文档结果或 SQL 表和视图的行结果结束。文档结果返回数据作为DbDoc对象,行结果返回Row对象。

当您完成所有部分的工作时,请随时返回本概述。在继续创建会话之前,让我们讨论一下命令是如何执行的以及对链接它们的支持。

实现一系列命令有两种不同的方式:每个方法一行代码和execute()(已经定义并准备执行的语句所必需的),或者链接方法调用。考虑一个 find 语句,其中您想要定义从文档中提取的字段,设置一个过滤器,并执行查询。用省略号代替实际的参数,可以像下面这样创建、优化和执行这个查询

statement = collection.find()
statement.fields(...)
statement.where(...)
result = statement.execute()

或者,相同的查询可以写成一个链,如下所示:

result = collection.find().fields(...).where(...).execute()

也可以使用混合语句,语句的一部分使用链,而另一部分不使用,或者可以使用几个较短的链。如果您需要执行几个具有公共库的查询,但随后更改了过滤器或值,这将非常有用。

一种方法并不比另一种更正确。您应该根据代码的风格、需求以及语句的使用方式来决定使用哪一个。接下来的两章包括了将命令流组合在一起的各种方法的例子。

MySQL Shell

MySQL Shell 是一个新的命令行客户端,与传统的mysql命令行客户端相比,它提供了几个额外的特性。它包含的特性之一是支持在 Python 中使用 X DevAPI。虽然 MySQL Shell 不使用 MySQL Connector/Python,因此mysqlx模块并不是 100%相同,但是您可以使用 MySQL Shell 来交互式地测试 X DevAPI。

一个使用 MySQL Shell 创建会话并执行读取请求的示例是

MySQL  Py > connect_args = {
         ...   'host'        : '127.0.0.1',
         ...   'port'        : 33060,
         ...   'user'        : 'pyuser',
         ...   'password'    : 'Py@pp4Demo',
         ... };
         ...
 MySQL  Py > db = mysqlx.get_session(**connect_args)
 MySQL  Py > schema = db.get_schema('world_x')
 MySQL  Py > countries = schema.get_collection('countryinfo')
 MySQL  Py > country = countries.get_one('AUS')
 MySQL  Py >
 MySQL  Py > fmt = "{0:13s} {1}"
 MySQL  Py > print(fmt.format(
         ...   "Name ........:",
         ...   country["Name"]
         ... ))
         ...
Name ........: Australia

 MySQL  Py > print(fmt.format(
         ...   "Continent ...:",
         ...   country["geography"]["Continent"]
         ... ))
         ...
Continent ...: Oceania

如果代码还没有意义,不要担心;这就是这一章和接下来两章的内容。到最后,一切都应该清楚了。第十章还将探讨在开发过程中使用 MySQL Shell 作为工具。

请注意,该示例使用的是world_x示例数据库,它是从前面章节中使用的world示例数据库派生而来的。如果想玩world_x数据库,可以从 https://dev.mysql.com/doc/index-other.html 下载。

创建会话

在 X DevAPI 中,一个会话对应于传统 MySQL 连接器/Python mysql.connector模块中的连接对象。无论您是希望创建一个会话来使用文档存储、使用 SQL 表的 CRUD 方法,还是执行传统的 SQL 查询,都没有关系。这是 X DevAPI 的优势之一:它结合了 NoSQL 和 SQL 世界。

小费

X DevAPI 的主要用途是 NoSQL CRUD 方法。如果您需要的不仅仅是基本的 SQL 特性,建议使用本书前面介绍的mysql.connector模块。也可以结合使用mysqlxmysql.connector模块来获得两个世界的最佳效果。

使用get_session()方法创建一个会话。传递给函数的参数用于配置会话。该函数的定义是

mysqlx.get_session(*args, **kwargs)

可以用两种方式之一指定连接参数:

  • 以与在mysql.connector模块中创建连接时相同的方式传递选项

  • 创建 URI

让我们看看如何使用每种方式,从明确指定选项开始。在讨论了配置会话之后,将会有使用get_session()函数创建会话的例子。

警告

不要将密码硬编码到应用中。这既不安全也不实际。通常,最好将连接选项放在应用之外。这也确保了可以在不更新应用的情况下更新连接选项。

传递单个选项

如果您习惯于使用 MySQL Connector/Python 进行编码,指定选项的最简单方法是将它们分别作为直接参数或字典进行传递。

与前几章中创建的连接相比,一个重要的区别是不可能指定 MySQL 配置文件。因此,如果您使用 X DevAPI,建议将选项存储在自定义配置文件中。支持选项的完整列表见表 6-1 。这些选项按字母顺序排列。

表 6-1。

用于创建会话的 X DevAPI 选项

|

名字

|

缺省值

|

描述

|
| --- | --- | --- |
| aut h | 使用 SSL、Unix 套接字和 Windows 命名管道:PLAIN否则,尝试MYSQL41最后,试试SHA256_MEMORY | auth取三个值之一:MYSQL41SHA256_MEMORYPLAIN. MYSQL41应该与mysql_native_password认证插件一起使用。SHA256_MEMORY可用于在没有 SSL 连接的情况下使用sha2_caching_password身份验证插件连接帐户,前提是自 MySQL 服务器上次重启以来,在使用 SSL 之前至少建立了一次连接。PLAIN用于大多数其他情况,但是需要安全连接,使用 SSL、Unix 套接字或 Windows 命名管道。很少需要设置此选项。 |
| hos t | localhost | 要连接的主机名;默认设置是连接到本地主机。 |
| passwor d |   | 用于身份验证的密码;对于测试用户来说,就是Py@pp4Demo。 |
| por t | 33060 | MySQL 正在监听 X DevAPI 连接的端口。端口 33060 是标准的 MySQL X DevAPI 端口。 |
| router s |   | 带有hostport键的字典列表定义了可能要连接的 MySQL 实例。可选地,也可以设置priority;该值越高,使用该实例的可能性越大。如果为一个实例设置了优先级,则必须为所有实例设置优先级。如果没有给定优先级,实例将按照它们在列表中出现的顺序使用。如果除了routers之外还指定了host选项,则hostport选项用于在routers列表的末尾创建一个实例。 |
| schem a |   | 用于会话的默认模式(数据库)。不要求模式已经存在。默认模式只适用于mysqlx.crud.Session.get_default_schema()方法。 |
| socke t |   | Unix 套接字或 Windows 命名管道。 |
| ssl-``c |   | 包含 SSL 证书颁发机构(CA)的文件的路径。 |
| ssl-``cr |   | 包含 SSL 证书吊销列表的文件的路径。 |
| ssl-``cer |   | 包含公共 SSL 证书的文件的路径。 |
| ssl-``ke |   | 包含私有 SSL 密钥的文件的路径。 |
| ssl-mode | REQUIRE D | 使用哪种 SSL 模式。这与 MySQL 服务器附带的客户端相同。它可以取一系列值:DISABLEDPREFERREDREQUIREDVERIFY_CAVERIFY_IDENTITY。值VERIFY_IDENTITY相当于旧的ssl_verify_cert选项。亦见 https://dev.mysql.com/doc/refman/en/encrypted-connection-options.html#option_general_ssl-mode 。 |
| use_pure | Fals e | 是使用纯 Python 实现(当use_pure = True)还是 C 扩展。 |
| use r |   | 应用用户的用户名。不要包括@和后面的主机名(也就是说,对于测试用户,只需指定pyuser)。 |

正如您从选项列表中看到的,选项远没有mysql.connector模块多。最值得注意的是没有字符集选项。X DevAPI 总是使用utf8mb4

小费

如果您的数据使用不同于utf8utf8mb3utf8mb4的字符集存储,请在查询中转换它或使用mysql.connector模块。

支持的 SSL 选项列表(除了ssl-mode)存储在mysqlx模块的_SSL_OPTS常量中,完整的选项列表可以在_SESS_OPTS常量中找到。这使得获取选项列表变得很容易,例如,通过使用以下代码:

import mysqlx

print("SSL options ...: {0}".format(
  mysqlx._SSL_OPTS
))
print("All options ...: {0}".format(
  mysqlx._SESS_OPTS
))

这段代码举例说明了在 MySQL Connector/Python 和 MySQL Shell 中使用 X DevAPI 的区别。在 MySQL Shell 中,mysqlx模块没有这两个属性,所以这个例子会引发一个异常。使用 MySQL 连接器/Python 8.0.11 的输出是

SSL options ...: ['ssl-cert', 'ssl-ca', 'ssl-key', 'ssl-crl']
All options ...: ['ssl-cert', 'ssl-ca', 'ssl-key', 'ssl-crl', 'user', 'password', 'schema', 'host', 'port', 'routers', 'socket', 'ssl-mode', 'auth', 'use_pure']

MySQL Connector/Python 还支持使用连接选项指定 URI。让我们看看这是如何做到的。

路过一个 URI

连接到数据库的一种常见方法是创建一个包含所有连接选项的 URI(统一资源标识符)。URI 也用于访问网站(所有 URL(统一资源定位符)也是 URIs)。从 MySQL Connector/J (Java)中也可以了解到使用 URI 进行数据库连接。

X DevAPI 的 URI 的基本形式是

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

scheme 始终是mysqlx,可以省略(MySQL Connector/Python 会在缺少的情况下添加)。在撰写本文时,MySQL Connector/Python 不支持 URI 中的字符转义(这在其他方面是正常的),因此目前还不支持某些值。

注意

由于 https://bugs.mysql.com/89614 中描述的 bug,参数中目前不支持某些字符。最值得注意的是,密码中不支持@字符。如果您想使用 URI 进行测试,您必须将密码更改为不包含@字符,直到错误被修复。

目标是套接字选项、主机选项或者用冒号分隔的主机和端口选项。属性是除userpasswordhostportsocketschema之外的任何支持选项。

例如,考虑使用以下参数(按照它们在 URI 中出现的顺序)创建一个连接:

  • user : Pyuser

  • password : PyApp4Demo

  • host : 127.0.0.1

  • port : 33060

  • schema : py_test_db

  • ssl-mode : REQUIRED

  • auth : PLAIN

由此产生的 URI 是

mysqlx://pyuser:PyApp4Demo@127.0.0.1:33060/py_test_db?ssl-mode=REQUIRED&auth=PLAIN

关于 X DevAPI 的其余讨论将逐个传递连接选项。

连接示例

是时候创建一个使用 X DevAPI 连接到 MySQL 的实际会话了。为了避免将连接选项编码到示例中,本章剩余部分和后面两章中的示例所共有的配置将存储在config模块中。

本书源代码中包含的config模块的内容见下文config.py:

connect_args = {
  'host': '127.0.0.1',
  'port': 33060,
  'user': 'pyuser',
  'password': 'Py@pp4Demo',
};

使用config模块,可创建如下会话:

import mysqlx
from config import connect_args

db = mysqlx.get_session(**connect_args)

使用完会话后,建议将其关闭,以确保连接完全终止。这是通过close()方法实现的:

import mysqlx
from config import connect_args

db = mysqlx.get_session(**connect_args)

...

db.close()

仅此而已。所以,让我们继续,开始使用 X DevAPI 会话。

使用会话

session 对象包含一些自己的有用方法。请记住,会话相当于mysql.connector连接对象,因此是在会话级别控制事务。本节将介绍这些功能。此外,会话还包含用于操作模式的方法,如创建和删除模式。下一节将介绍如何使用模式。

处理

X DevAPI 支持除操作模式对象之外的所有操作的事务。这与使用mysql.connector模块或任何其他方式与 MySQL 服务器交互时是一样的。X DevAPI 实际上比mysql.connector模块具有更好的事务支持,因为除了事务本身之外,它还支持保存点。

注意

mysqlxmysql.connector的一个重要区别是autocommit. mysqlx从 MySQL 服务器中的全局缺省设置中继承值,除了使用 SQL 语句之外,没有其他方法可以改变它的值。因此,建议总是显式使用事务。

有六种方法可以控制事务和保存点。它们在表 6-2 中进行了总结。这些方法的顺序取决于它们在事务中的使用时间。

表 6-2。

控制事务的会话方法

|

方法

|

争吵

|

描述

|
| --- | --- | --- |
| start_``transactio |   | 开始交易。 |
| set_``savepoin | name | 设置保存点。如果未指定名称,将使用uuid.uuid1()函数生成一个名称。将返回保存点的名称。 |
| release_``savepoin | name | 释放具有指定名称的保存点(相当于提交,但不保存更改)。 |
| rollback_``t | name | 回滚自指定保存点以来所做的更改。 |
| commi t |   | 提交(保存)自start_transaction()调用以来所做的所有更改。 |
| rollbac k |   | 取消自start_transaction()调用以来所做的所有更改。保存点的使用不会改变回滚的结果。 |

在 X DevAPI 讨论的剩余部分将会有使用事务的例子。在此之前,有几个会话实用程序方法需要讨论。

其他会话方法

让我们讨论几种对各种目的有用的方法。它们主要围绕检索对象展开,比如获取底层连接对象或启动 SQL 语句。表 6-3 按字母顺序总结了实用方法。所有返回的对象都相对于mysqlx模块。

表 6-3。

会话实用程序方法

|

方法

|

争吵

|

返回对象

|

描述

|
| --- | --- | --- | --- |
| get_``connectio |   | connection.Connection | 检索基础连接。 |
| is_``ope |   |   | 根据连接是否打开,返回TrueFalse。 |
| sq l | sql | statement.SqlStatement | 用于执行 SQL 查询。另请参见第章第八部分中的“SQL 语句”部分。 |

这些方法中的一些将在后面的例子中使用。这个方法列表中缺少的一点是,如果你想使用 NoSQL CRUD 方法,该如何进行。这是通过获取一个模式对象来实现的,这是下一步要讨论的内容,还有其他模式方法。

计划

模式(schemata)是可以包含表或文档集合的容器。它们本身并不直接使用,在某种程度上可以看作是一种名称空间。在 X DevAPI 中,只有在使用 CRUD 方法时才需要模式对象。

使用模式的方法分为两类。创建、删除和检索模式的方法在mysqlx. Session类中,而使用模式或获取模式信息的方法在mysqlx.crud.Schema类中。本节研究这些方法,但与操作集合和表相关的方法除外,这是下两章的主题。在本节的最后,有一个例子将讨论的方法和属性放在一起。

模式操作

当应用需要一个模式时,要做的第一件事就是创建一个新的模式或者检索一个现有的模式。或者,如果不再需要该模式,可以在最后将其删除。

执行这些任务的方法的共同点是它们都存在于mysqlx. Session类中。表 6-4 总结了本节将要讨论的方法。

表 6-4。

会话模式方法

|

方法

|

争吵

|

返回对象

|

描述

|
| --- | --- | --- | --- |
| create_``schem | name | crud.Schema | 使用指定的名称作为参数创建架构。 |
| drop_``schem | name |   | 删除由 name 参数指定的架构。 |
| get_default_``schem |   | crud.Schema | 返回创建会话时指定的架构的架构对象。如果不存在默认模式,就会出现ProgrammingError异常。 |
| get_``schem | name | crud.Schema | 返回请求的架构对象。如果不存在具有指定名称的模式,仍然会返回一个Schema对象。 |
| get_schemas |   |   | 返回用户有权访问的架构名称列表。8.0.12 版中引入了此方法。 |

图 6-2 显示了模式在会话和对象类之间的工作流中的位置。红色(深灰色)框是可用于从一个类(大框)转到另一个类的方法示例。

img/463459_1_En_6_Fig2_HTML.jpg

图 6-2。

模式对象周围的工作流示例

图 6-2 开始使用mysqlx. get_session()方法创建一个会话,如本章前面所讨论的。然后使用get_schema()方法获得一个模式对象。另一个选择是对新模式使用create_schema()。此时,可以选择使用哪种对象,从那时起,语句就被定义了。这是下两章的主题。

您将首先使用create_schema()方法创建一个模式,然后您将使用get_default_schema()get_schema()方法为一个现有的模式获取一个mysqlx.Schema类的对象,最后您将使用drop_schema()方法删除一个模式。

创建模式

模式操作通常不在应用代码中进行,因为模式是长期存在的数据库对象。然而,能够不时地创建和删除模式仍然是有用的,例如在实用程序脚本中。

创建模式的方法是create_schema(),它是会话对象的一部分。它只需要一个参数:要创建的模式的名称。创建py_test_db模式的一个例子是

import mysqlx
from config import connect_args

db = mysqlx.get_session(**connect_args)

schema = db.create_schema("py_test_db")

db.close()

create_schema()方法为模式返回一个mysqlx.crud.Schema类的对象,该对象可用于操作模式对象,例如创建或查找集合,这将在下一章中讨论。

如果您将它与一个CREATE SCHEMA SQL 语句进行比较,会发现两个明显的差异。首先,没有为模式设置默认字符集的选项。用create_schema()创建的模式的默认字符集总是用character_set_server MySQL 服务器选项指定的。同样,collation_server MySQL 服务器选项的值用于指定默认的模式排序规则。

另一个区别是没有与CREATE SCHEMAIF NOT EXISTS子句等价的子句。同样,drop_schema()也没有对应的IF EXISTS。相反,即使模式已经存在,create_schema()也会成功。事实上,上述示例执行的底层 SQL 语句是

CREATE DATABASE IF NOT EXISTS `py_test_db`;

因此,IF NOT EXISTS子句将始终被包含在内。这并不意味着建议只使用create_schema()来获取现有的模式。CREATE DATABSE IF NOT EXISTS导致了不必要的开销,并且使得代码的意图不那么清晰。相反,使用两种专用方法之一来获取现有模式的模式对象。

正在检索默认架构

实际应用中最常见的情况是模式已经存在,应用需要获得一个模式对象来操作模式,或者更深入地获得一个集合或一个表来使用。有两种方法可以在 X DevAPI 中获得模式对象:要么请求创建会话时指定的默认模式,要么通过模式名请求它。

创建会话时,可以使用schema选项指定默认模式。要获得默认模式的模式对象,可以使用get_default_schema() session 方法。例如,考虑一个使用默认的py_test_db模式创建的会话:

import mysqlx
from config import connect_args

db = mysqlx.get_session(
  schema="py_test_db",
  **connect_args
)

print("Retrieving default schema")

schema = db.get_default_schema()

print("Schema name: {0}"
  .format(schema.name)
)

db.close()

创建连接时添加了schema='py_test_db'选项,然后使用get_default_schema()方法检索模式对象。最后,使用模式的name属性打印模式名,并关闭会话。输出是

Retrieving default schema
Schema name: py_test_db

get_default_schema()方法是检索默认模式的好方法,例如,如果在编写应用时不知道模式名。但是,在其他情况下,无论默认模式如何,都有必要检索特定的模式。让我们看看这是如何做到的。

按名称检索模式

按名称检索特定模式类似于创建新模式。不同的是使用了get_schema()方法,而不是create_schema()。需要一个参数:name,它是一个带有模式名称的字符串。考虑下面的例子来获取py_test_db模式的模式对象:

import mysqlx
from config import connect_args

db = mysqlx.get_session(**connect_args)

print("Retrieving non-default schema")

schema = db.get_schema("py_test_db")

print("Schema name: {0}"
  .format(schema.name)
)

db.close()

创建会话后,将检索模式并打印名称。这个例子与前面使用get_default_schema()的例子非常相似,除了模式的名称是在检索时指定的,而不是在创建会话时指定的。该示例的输出与前面的类似:

Retrieving non-default schema
Schema name: py_test_db

操作模式的最后一步是删除模式。

删除模式

删除模式的方式与创建模式的方式非常相似。该会话包括drop_schema()方法,该方法接受要删除的模式的名称。删除本节中使用的py_test_db模式的一个例子是

import mysqlx
from config import connect_args

db = mysqlx.get_session(**connect_args)

db.drop_schema("py_test_db")

db.close()

与本节前面的create_schema()示例相比,这里唯一的区别是使用了drop_schema()方法。与创建模式类似,如果模式不存在,也不会出现错误。也就是说,底层 SQL 语句是

DROP DATABASE IF EXISTS `py_test_db`;

与会话一样,mysqlx.crud.Schema类也有一些实用方法和两个属性。它们是模式对象最后要考虑的事情。

其他模式方法和属性

mysqlx.Schema类包括几个方法和三个属性,对于检查模式对象的状态很有用。它们包括关于模式名、模式是否存在等信息。此外,还有一些方法可以获取底层连接和会话。所有这些都将在这里讨论。

表 6-5 总结了讨论中涉及的模式方法。返回的对象都是相对于mysqlx模块的。这些方法都不带任何参数。

表 6-5。

模式实用程序方法

|

方法

|

返回对象

|

描述

|
| --- | --- | --- |
| exists_in_``databas |   | 根据模式是否存在,返回TrueFalse。 |
| get_``connectio | connection.Connection | 获取会话的基础连接。 |
| get_``nam |   | 返回架构的名称。这与使用name属性是一样的。 |
| get_``schem | crud.Schema | 这将返回模式本身。这与使用schema属性是一样的。它在应用中并不常用。 |
| get_``sessio | connection.Session | 返回架构的会话对象。当模式作为参数传递给函数时,这很有用。在 8.0.12 和更高版本中,也可以从会话属性中检索会话。 |

最常用的方法是获取名称(get_name())或检查模式是否实际存在(exists_in_database())。该名称也可以使用name属性获得。类似地,可以使用get_session()方法或者在 MySQL Connector/Python 8.0.12 和后来的 session 属性中检索会话。此外,模式本身存储在schema属性中;但是,在应用中通常不使用这种方法。

在继续讨论 CRUD 参数之前,有必要看一个结合了本节中讨论的特性的示例。

模式示例

到目前为止,这些都有点抽象,只有很少的代码示例。因此,让我们来看一个扩展的例子,它将讨论的几件事情放在一起,并展示了方法和属性是如何关联的。清单 6-1 使用默认模式,然后使用实用程序和模式操作方法进行调查并采取行动。

import mysqlx
from config import connect_args

# Create the session
db = mysqlx.get_session(
  schema="py_test_db",
  **connect_args
)

# Retrieve the default schema
# (py_test_db)

py_schema = db.get_default_schema()

print("Schema name: {0} - Exists? {1}"
  .format(
    py_schema.name,
    py_schema.exists_in_database()
  )
)

# If py_test_db does not exist,
# create it
if (not py_schema.exists_in_database()):
  db.create_schema(py_schema.name)

print("Schema name: {0} - Exists? {1}"
  .format(
    py_schema.name,
    py_schema.exists_in_database()
  )
)

# Get the world schema

w_schema = db.get_schema("world")

print("Schema name: {0} - Exists? {1}"
  .format(
    w_schema.name,
    w_schema.exists_in_database()
  )
)

# Get the session object of the world
# schema and see if it is the same as
# the db object.

w_session = w_schema.get_session()

print("db == w_session? {0}".format(
  db == w_session))

# Drop the py_test_db schema.

db.drop_schema(py_schema.name)

print("Schema name: {0} - Exists? {1}"
  .format(
    py_schema.name,
    py_schema.exists_in_database()
  )
)

db.close()

Listing 6-1.Manipulating and Checking Schemas

该示例首先创建一个会话,其中默认模式被设置为py_test_db,并且使用get_default_schema() session 方法来获取py_test_db模式的模式对象。schema 对象的name属性和exists_in_database()方法用于打印名称和它是否存在。每当 schema 对象发生变化时,就会在整个示例中重复这一过程。

如果py_test_db模式不存在,则创建它。在调用create_schema()方法之前,实际上并不需要检查模式是否不存在,但是这使得意图更加清晰。

然后检索第二个模式对象,这次使用get_schema()方法为world模式获取一个对象。使用get_session()方法检索新创建的w_schema对象的会话,并确认dbw_session对象是相同的(因为它们的身份或内存地址是相同的)。

最后,使用drop_schema()方法再次删除py_test_db模式,使数据库保持与示例之前相同的状态(假设py_test_db模式在开始时不存在)。假设py_test_db不存在,而前面章节中的world数据库存在,运行该示例的输出是

Schema name: py_test_db - Exists? False
Schema name: py_test_db - Exists? True
Schema name: world - Exists? True
db == w_session? True
Schema name: py_test_db - Exists? False

输出显示开始时py_test_db并不存在,但是在create_schema()调用之后它存在了。由于您在第一章中手动加载了world模式,所以它没有被创建就存在了。当比较会话的两个副本时,您可以看到它们是相同的。最后,在删除了py_test_db模式之后,exists_in_database()方法再次返回False

为了完成这一章,你将会看到下两章中讨论的方法的三个共同点:首先,CRUD 方法使用的参数。

混乱的争论

将在第七章和第八章中讨论的 CRUD 方法都使用一组有限的参数。与其一遍又一遍地解释这些论点是什么意思,不如让我们现在就来讨论一下。

CRUD 方法使用以下四种参数类型:

  • 文档

  • 文档 id

  • 情况

  • 田地(复数);场;域;字段

文档和文档 id 是文档存储所独有的,而字段和条件是两者共享的。下面的讨论将会看到这四种类型的每一种,并探究它们的用法。

文档

文档是存储在文档存储中的数据的容器。在 MySQL 中,文档存储为 JavaScript 对象符号(JSON)文档。在 Python 中,JSON 可以表示为一个字典,其中 JSON 对象是带有值的字典键。JSON 数组在 Python 中被创建为一个列表。

小费

如果您想了解更多关于 JSON 文档的信息,一些参考资料有 https://json.org/https://en.wikipedia.org/wiki/JSONhttps://dev.mysql.com/doc/refman/en/json.html

作为一个例子,考虑一个名为 John Doe 的雇员,他目前是团队领导。他有一个经理,之前担任过开发人员(2010-2014 年)和高级开发人员(2014-2017 年)。代表这一点的文件是

document = {
  "_id"        : "10001",
  "Name"       : "John Doe",
  "Manager_id" : "10000",
  "Title"      : "Team Lead",
  "Previous_roles": [
    {
      "Title"      : "Developer",
      "Start_year" : "2010",
      "End_year"   : "2014"
    },
    {
      "Title"      : "Senior Developer",
      "Start_year" : "2014",
      "End_year"   : "2017"
    },
  ]
}

文档从字典开始,顶层是关于雇员的所有标量数据,如姓名、经理等。该员工在公司的历史表现为一个列表,其中包含该员工以前担任的每个角色的字典。

数组中的数据不需要有结构。例如,如果存储了一个人最喜欢的水果,它们可以表示为一个简单的列表,每个水果指定为一个字符串。

文档中的一个特殊字段是_id元素。这是文档 ID,是文档的唯一标识符(主键)。让我们来看看文档 ID。

文档标识

文档 ID 是 MySQL 用来唯一标识文档的。它也是底层存储中用作主键的内容。所有文档都必须有文档 ID。如果在连接到 MySQL Server 8.0 时没有明确提供,它将自动生成。

文档 ID 是一个(最多)32 字节长的二进制字符串。如果没有提供 ID,MySQL Server 8.0 将使用三个部分创建一个 ID:

  • 一个前缀:这是一个十六进制编码的无符号整数。它是 0,除非它是由数据库管理员设置的,或者 MySQL 服务器是 InnoDB 集群组的一部分。前缀存储在mysqlx_document_id_unique_prefix MySQL 服务器选项中。

  • 一个时间戳:MySQL 服务器实例最后一次以十六进制编码启动的时间。

  • 一个自动递增计数器:这是一个十六进制编码的无符号整数。初始值是 MySQL 服务器选项auto_increment_offset的值,然后随着生成的每个 ID 增加auto_increment_increment

mysqlx_document_id_unique_prefix = 5678生成的 ID 的一个例子是162e5ae987780000000000000003。前缀是前四个十六进制数字(162e),后面是时间戳;末尾的3显示它是 MySQL 服务器上次重启后生成的第三个 ID。

以这种方式生成的 id 是经过专门设计的,因此它们可以是全局唯一的,同时还针对 InnoDB 存储引擎进行了优化,并且生成成本低廉。当使用单调递增的主键时,InnoDB 的性能最佳。自动生成的文档 id 为此进行了优化。

从表面上看,自然选择可能是 UUID,但它们不是单调递增的。即使您交换了 UUID 的高位和低位时间部分,如果有来自多个连接主机的应用实例,您也会得到交错的值。这就是开发三部分 ID 的原因。

下一个要讨论的参数类型是条件。

情况

条件用于过滤哪些文档应该受到操作的影响,并用于文档存储和 SQL 表的 CRUD 方法。对于读取操作,只返回与过滤器匹配的文档;对于更新和删除操作,条件指定应该更改哪些文档。条件的 SQL 等价物是一个WHERE子句。除了 8.0.12 和更高版本中集合的modify()之外,接受条件作为参数的方法还在其语句对象中提供了一个可以替代使用的where()方法。

该条件编写起来相当简单,并且使用了与 MySQL SQL WHERE子句相同的语法。例如,要包含所有Office字段设置为 Sydney 的文档,可以使用以下条件:

Office = 'Sydney'

SQL 表和文档之间的一个区别是,在过滤字符串数据类型时,MySQL 默认将WHERE子句视为不区分大小写,而文档存储总是区分大小写。对此的解释是,JSON 文档被存储为二进制对象,因为否则就不可能在文档中存储任意值。

注意

MySQL WHERE子句和 SQL 表默认执行不区分大小写的匹配,但是文档存储总是区分大小写的。

最后一个参数类型是 fields,它与 CRUD 方法一起用于指定要返回或修改的内容。

菲尔茨

例如,fields 参数指定在 select 语句中返回哪些字段,或者在 insert 语句中为哪些字段设置值。fields 参数类型也用在一些语句方法中,如FindStatement.fields()方法。在 SQL 语言中,字段等同于列。

每个字段都表示为一个字符串,带有要包含或设置其值的字段的名称。对于 JSON 文档,该字段是指向该字段在 JSON 文档中的位置的 JSON 路径。文档本身由$表示。然后通过指定由句点(.)分隔的元素来构建路径。除了在集合上创建索引时,前导的$.是可选的。

可以使用*作为通配符。指定.*意味着对象的所有成员都匹配。或者,您可以使用语法[prefix]**{suffix},其中前缀是可选的,后缀是强制的。这将匹配以前缀开始并以后缀结束的所有内容。

对于数组,方括号可用于指定要包含哪些元素。[N]返回数组的第 N 个元素(从 0 开始)。使用[*]等同于根本不指定索引(即返回整个数组)。

小费

在 MySQL 中指定 JSON 路径的规则记录在 https://dev.mysql.com/doc/refman/en/json-path-syntax.html 中。

这些字段可以指定为单个参数、元组或列表。select 语句的以下三个初始化做同样的事情:

stmt = city.select("Name", "District")

fields = ("Name", "District")
stmt = city.select(fields)

fields = ["Name", "District"]
stmt = city.select(fields)

关于 CRUD 方法使用的参数的讨论到此结束。接下来,您将看到查询的中间部分:语句对象。

声明

下两章将要讨论的大多数方法都涉及到一个语句对象。这用于细化查询并执行它。表面上不涉及语句对象的方法,比如count()方法,仍然在幕后使用语句。

语句对象是完成大部分查询工作的地方。例如,您可以使用 statement 对象为一个find()查询设置过滤条件,或者限制结果中的文档数量。表 6-6 总结了接下来两章将遇到的语句类。

表 6-6。

语句类

|

班级

|

范围

|

create, read, update, and delete

|

方法

|
| --- | --- | --- | --- |
| AddStatement | 募捐 | 创造 | add() |
| FindStatement | 募捐 | 阅读 | find() |
| ModifyStatement | 募捐 | 更新 | modify() |
| RemoveStatement | 募捐 | 删除 | remove() |
| InsertStatement | 桌子 | 创造 | insert() |
| SelectStatement | 桌子 | 阅读 | select() |
| UpdateStatement | 桌子 | 更新 | update() |
| DeleteStatement | 桌子 | 删除 | delete() |
| SqlStatement | 结构化查询语言 |   | sql() |

该类是为该方法返回的mysqlx.statement模块中的类。范围指定它是用于基于集合、基于表还是基于 SQL 的查询。CRUD 列显示了相应的 CRUD 操作。最后,“方法”列列出了用于创建语句的方法。

具体的报表方法将在讨论各自的方法时介绍。但是,有些特性是所有或几个语句共有的。表 6-7 中列出了这些常用方法。

表 6-7。

方法来获取有关语句的信息

|

方法

|

声明

|

返回类型

|

描述

|
| --- | --- | --- | --- |
| get_binding_``ma | 阅读更新删除 | 词典 | 返回带有绑定映射的字典。 |
| get_``binding | 阅读更新删除 | 目录 | 返回绑定列表。每个绑定由一个带有名称和值的字典表示。 |
| get_``groupin | 阅读更新删除 | 目录 | 返回用于对结果进行分组的表达式。 |
| get_``havin | 阅读更新删除 | protobuf.Message对象 | 返回 having 筛选器的对象。 |
| get_limit_``offse | 阅读更新删除 | Integer | 返回限制的偏移量。 |
| get_limit_row_``coun | 阅读更新删除 | Integer | 返回查询返回的最大文档数。 |
| get_projection_``exp | 阅读更新删除 | List | 返回包含字段投影映射的列表。 |
| get_sort_``exp | 阅读更新删除 | List | 返回一个列表,其中包含用于对结果进行排序的表达式。 |
| get_``sq | 挑选 | String | 根据当前语句定义返回 SQL 语句。 |
| get_update_``op | 更新 | UpdateSpec对象 | 返回包含更新操作的列表。 |
| get_``value | 创造 | List | 返回该语句将创建或已经创建的值的列表。 |
| get_where_``exp | 阅读更新删除 | protobuf.Message object | 返回 where 筛选器的对象。 |
| is_doc_``base | 全部 | Boolean | 对于基于集合的语句,总是返回True,对于基于表和基于 SQL 的语句,总是返回False。 |
| is_lock_``exclusiv | 阅读 | Boolean | 如果请求了独占锁,则返回True。 |
| is_lock_``share | 阅读 | Boolean | 如果请求了共享锁,则返回True。 |
| is_``upser | 创造 | Boolean | 如果语句执行 upsert(即,如果记录已经存在则替换,否则添加新文档)操作,则返回True。对于 insert 语句,它总是返回False。第七章包括一个 upsert 的例子。 |

Statements列显示了该方法适用于哪些语句类。在大多数情况下,语句类将由具有该方法的 CRUD 操作指定;例如,“读取”意味着两个读取方法(Collection.find()Table.select())在返回的语句对象中有它。两个特殊的值是“All”,这意味着它适用于所有的语句类型,以及“Select”,这意味着它只适用于SelectStatement对象(来自Table.select())。

get_having()get_where_expr()方法返回一个mysqlx.protobuf.Message类的对象。get_update_ops()方法返回一个mysqlx.statement.UpdateSpec类的对象。这本书不会深入讨论如何使用这个类的任何细节。

一旦执行了一个语句,就会返回一个结果对象。返回的结果对象将取决于语句类。

结果

执行查询时,会返回一个结果对象。result 对象的确切性质取决于查询以及它是使用集合、SQL 表还是 SQL 语句。本节将讨论与 X DevAPI 一起使用的各种结果对象。

对于文档存储 CRUD 语句,对于不返回任何数据的查询,例如添加文档时,返回一个result.Result类的对象。对于返回数据的查询,返回一个result.DocResult类的对象。唯一的例外是count()方法,它直接以整数形式返回集合中的文档总数。

对于 SQL 表,模式是类似的,除了 select 语句的结果对象属于result.RowResult类。SQL 语句总是以一个result.SqlResult对象结束。

表 6-8 总结了哪个方法返回哪个结果对象。如何得到结果和使用结果的例子将在接下来的两章中给出。

表 6-8。

语句类型到结果对象的映射

|

语句类型

|

收集

|

桌子

|

结构化查询语言

|
| --- | --- | --- | --- |
| CRUD–创建 | Result | Result |   |
| CRUD–读取 | DocResult | RowResult |   |
| CRUD–更新 | Result | Result |   |
| CRUD–删除 | Result | Result |   |
| 结构化查询语言 |   |   | SqlResult |

这四个结果类值得仔细研究一下。下面的讨论将从较高的层次来看结果类。在 CRUD 和 SQL 方法的讨论中,将给出使用这些结果的示例。

结果。结果

result.Result类用于没有结果集的 CRUD 语句,并提供关于查询的元数据。例如,在将文档插入到集合中之后,它将包含关于插入的文档数量的信息。它既用于基于集合的语句,也用于基于表的语句。

表 6-9 概述了result.Result类的一些最重要的方法,包括它们返回的内容。这些方法都不带任何参数。

表 6-9。

result.Result类的重要方法

|

方法

|

返回

数据类型

|

描述

|
| --- | --- | --- |
| get_affected_items_``coun | 整数 | 返回受查询影响的文档数或行数,例如插入或更新了多少文档。 |
| get_autoincrement_``valu | 整数 | 返回为表插入语句生成的最后一个自动递增 ID。这在插入单行时非常有用。它仅适用于表格对象。 |
| get_generated_``id | 字符串列表 | 返回由查询插入到集合中的所有文档 id。它仅适用于集合对象。 |
| get_``warning | 元组列表 | 返回查询生成的警告。 |
| get_warnings_``coun | 整数 | 返回查询发生的警告数。 |

这些方法与mysql.connector模块中光标可用的方法相似或提供相似的数据。

结果。文档结果和结果。RowResult

result.DocResultresult.RowResult类分别用于Collection.find()Table.select()方法。这些类的工作方式类似于在mysql.connector模块中使用光标来处理查询结果。

表 6-10 总结了result.DocResultresult.RowResult类最重要的方法。这些方法都不带任何参数。

表 6-10。

result.DocResultresult.RowResult类的重要方法

|

方法

|

返回

数据类型

|

描述

|
| --- | --- | --- |
| fetch_``al | 文件清单 | 返回结果集中所有剩余的文档。列表中的每个元素都是表后描述的mysqlx.dbdoc.DbDocmysql.result.Row类的实例。 |
| fetch_``on | 目标 | 返回结果集中的下一个文档或行,如果已检索到所有文档/行,则返回 None。返回的对象类型在表后讨论。 |
| get_columns | 对象列表 | 从 column 属性返回列信息。它只存在于result.RowResult类。在 8.0.12 版本中添加。 |
| get_``warning | 元组列表 | 返回查询生成的警告。 |
| get_warnings_``coun | 整数 | 返回查询发生的警告数。 |

除了列出的方法之外,还有count属性,它被设置为使用fetch_all()方法检索的文档总数。result.RowResult类还包括 columns 属性,该属性包括与第 3 和 4 章中讨论的列信息类似的信息;在版本 8.0.12 和更高版本中,也可以使用get_columns()方法检索列。由fetch_one()返回的对象类型以及组成由fetch_all()返回的列表取决于语句类型:

  • Collection.find():该对象属于mysqlx.dbdoc.DbDoc

  • Table.select():mysqlx.result.Row类的对象

在这两种情况下,对象的行为都像一个字典,因此在使用返回的文档时不需要特别考虑。

使用的最后一个结果类是SqlResult,用于所有 SQL 语句。

结果。SqlResult

X DevAPI 的 MySQL 连接器/Python 实现在确定为 SQL 语句返回哪种类型的结果对象时,不区分SELECT类型查询和其他查询。返回的总是result.SqlResult类的对象。

表 6-11 总结了SqlResult类最重要的方法。

表 6-11。

result.SqlResult类的重要方法

|

方法

|

争吵

|

返回

数据类型

|

描述

|
| --- | --- | --- | --- |
| fetch_``al |   | 行列表 | 返回结果集中剩余的所有行。每一行都是mysql.result.Row类的一个实例。 |
| fetch_``on |   | result.Row | 返回结果集中的下一行,如果检索了所有行,则返回 None。 |
| get_autoincrement_``valu |   | 整数 | 如果查询是 insert 语句,则返回最后生成的自动递增 ID。这在插入单行时非常有用。 |
| get_columns |   | 对象列表 | 从 column 属性返回列信息。在 8.0.12 版本中添加。 |
| get_``warning |   | 元组列表 | 返回查询生成的警告。 |
| get_warnings_``coun |   | 整数 | 返回查询发生的警告数。 |
| has_data |   | 布尔代数学体系的 | 返回查询是否有结果集。对于不返回任何行的选择查询,该值为 False。在 8.0.12 版本中添加。 |
| index_``o | col_name | 整数 | 返回具有指定名称的列的数字索引。 |
| next_``resul |   | 布尔代数学体系的 | 当查询生成多个结果集时,重新初始化 result 对象以处理下一个结果集。如果有另一个结果要处理,则返回True;否则False。 |

此外,SqlResult类有两个有用的属性:

  • :结果的列列表

  • 计数:用fetch_all()方法检索到的项目总数

关于 X DevAPI 使用的结果类的讨论到此结束。在接下来的两章中,当你看如何执行语句时,它们会再次出现。

摘要

本章介绍了 MySQL X DevAPI。它首先简要概述了 X 插件、X 协议、X DevAPI 和mysqlx模块。为了让 MySQL Connector/Python 能够使用 X DevAPI 执行查询,需要在 MySQL 服务器上安装 X 插件。在 MySQL Server 8.0.11 和更高版本中,这是默认情况。

本章的其余部分讨论了与 X DevAPI 的哪一部分无关的通用特性。程序从创建一个可用于创建、获取和删除模式的会话开始。模式对象是使用 CRUD 方法的下一步。工作流的最后一部分是获取结果对象,这样就可以检查查询的结果或使用结果集。

在创建会话并可能获得模式对象之后,本章没有讨论中间的大部分。这就是有趣的部分所在(定义和执行语句),也是接下来两章的内容。下一章着眼于如何在 MySQL 文档存储中使用 X DevAPI。

**

七、MySQL 文档存储

顾名思义,MySQL 传统上就是使用 SQL 语句来执行查询。这不仅反映在用于描述查询应该做什么的语言中,还反映在数据的基本构造方式中。MySQL 文档存储颠倒了这一点,不仅提供 NoSQL 数据库,还使用 JSON 文档,就像其他用于存储数据的文档存储一样。

文档存储不会替换 SQL 数据库。这两者应该共存,所以您可以使用最适合您的应用和数据的那一个。您甚至可以将两者混合使用,因此一些数据存储在传统的 SQL 表中,而另一些数据存储为文档。

本章将探讨如何使用 MySQL 连接器/Python 和 X DevAPI 来处理 MySQL 文档存储。

MySQL 文档存储

MySQL 文档存储是 X 插件的一部分,该插件在 MySQL Server 5.7.12 中作为测试版特性引入,在 MySQL Server 8.0 中成为 GA。文档存储将数据存储为 JSON 文档,但使用 InnoDB 存储引擎来提供事务支持等特性。

注意

文档存储本身就是一个很大的主题,给出全面的介绍超出了本书的范围。如果您打算使用文档存储,建议您阅读更多关于它的内容。两个优秀的参考是介绍查尔斯·贝尔的 MySQL 8 文档库(https://www.apress.com/gp/book/9781484227244)和 MySQL 参考手册 ( https://dev.mysql.com/doc/refman/en/document-store.html )。

虽然 X 协议和文档存储的细节将留给读者作为练习,但在继续之前,有几个特征值得考虑。顾名思义,数据存储在文档中。这意味着与普通关系模式不同,所有“列”都存储在同一个数据对象中。

在文档存储中,据说文档存储在一个集合中。文档使用 JSON 格式。如果您想到一个常规 MySQL 表中的一行,列名是 JSON 文档中每个对象的名称,列值是对象的值。与 SQL 表不同,不要求每个文档(“行”)都有相同的字段或包含相同类型的数据。这些文档被称为集合的一部分(SQL 术语中的“表”)。

警告

与其他文档存储一样,MySQL 文档存储是无模式的。从开发的角度来看,这似乎是一个非常吸引人的特性;事实上,这使得向应用添加新类型的数据变得更加容易。但是,它也消除了数据库层验证数据和检查约束的机会。因此,如果您选择无模式数据模型,那么确保数据一致性完全取决于开发人员。

所有文档都必须有一个惟一的键,这个键总是 JSON 文档中带有键_id的对象。通常通过在保存集合的表上创建虚拟列来支持索引。X DevAPI 支持为集合创建索引,这是操作索引的首选方式。如果您需要为集合创建或删除索引,MySQL Shell 非常有用。

现在让我们看看在文档存储中使用集合时的一般工作流。

小费

本章中有许多示例程序。列表中出现的所有示例程序都可以下载。有关使用示例程序的更多信息,参见第一章中对示例程序的讨论。一般来说,更改会被回滚,以允许重新执行示例并得到相同的结果。清单 7-3 中加载样本数据时例外。

工作流程

工作流程在前一章中已经暗示过了,但还是值得更详细地看一下。让我们集中讨论如何使用集合。通过架构获取集合,并从集合中创建语句。最后,语句返回结果。当然,还有更多的内容,这一节将详细介绍。这不是一个详尽的讨论;相反,它的意思是作为本章其余部分的概述。

从方案开始到结果结束的工作流程如图 7-1 所示。主要关注获取集合和创建语句的步骤。省略了细化和执行语句的细节,但将在本章稍后针对每种查询方法进行讨论。红色(深灰色)框是被调用的方法。

img/463459_1_En_7_Fig1_HTML.jpg

图 7-1

使用集合时的工作流

正如您将在本章前半部分看到的,集合是使用get_collection()get_collections()create_collection()从模式中获得的。该架构也可用于删除集合。在集合对象中,一种可能是创建和删除索引。

另一种可能性是执行查询,这将在本章的后半部分讨论。有许多方法可以执行查询。主要有add()find()modify()remove()。它们返回一条语句,一旦该语句被执行,该语句又返回一个结果。但是,也有三个补充方法将所有步骤结合起来,直接返回一个结果。最后,还有count()方法,图中没有包括。

工作流就绪后,您就可以开始研究集合的工作方式了。

收集

在前一章中,您已经看到了如何从会话中创建模式。然而,模式只是真正重要的东西的容器:集合和表。在关系数据库中,我们谈论数据存储在表中。X DevAPI 也可以处理表(它们将在下一章讨论),但是目前我们将坚持使用文档存储。文档存储中的表的等价物是集合。

这一节将介绍如何创建、维护和删除不再需要的集合。该流程将类似于用于模式的流程。实际的集合操作是使用模式对象上的方法完成的,就像模式操作是使用会话方法完成的一样。集合对象本身也以与架构相同的方式包含方法和属性。数据的实际操作将推迟到下面的部分。第一步是创建、获取和删除集合。

集合操作

创建和删除集合不是应用最常见的任务,但它们可能是部署脚本或实用程序的一部分,应用可能暂时需要一个集合,或者您可能使用 MySQL Shell 来优化集合的性能。在所有情况下,应用都需要获得一个集合对象,以便能够对它执行查询。创建、获取、删除和创建集合索引是本次讨论的主题。

集合的操作方式与模式非常相似。例如,要创建一个模式,可以使用 session create_schema()方法。同样,您可以使用 schema create_collection()方法创建一个集合。这使得学习如何操作集合变得很容易。将要讨论的模式收集方法在表 7-1 中列出。返回的对象是相对于mysqlx模块的。

表 7-1

架构收集方法

|

方法

|

争论

|

描述

|
| --- | --- | --- |
| create_collection | name | 创建一个名称指定为参数的集合。集合作为一个crud.Collection对象返回。如果集合存在,就会出现一个ProgrammingError异常,除非reuseTrue,在这种情况下,将返回现有的集合。 |
| reuse=False |
| drop_collection | name | 删除具有指定名称的集合。不要求集合存在。 |
| get_collection | name | 将具有指定名称的集合作为crud.Collection对象返回。如果check_existence = True,如果集合不存在,则发生ProgrammingError异常。 |
| check_existence=False |
| get_collections |   | 返回架构中集合的列表。列表中的每个元素都是mysqlx.crud.Collection类的一个实例。 |

该表显示,与模式相比,只有一些差异:主要是在创建集合时,可能会影响集合是否必须不存在,以及在获取集合时,集合是否必须存在。此外,还将讨论表 7-2 中创建和删除索引的两种收集方法。这两种方法都不返回值。

表 7-2

创建和删除索引的集合方法

|

方法

|

争论

|

描述

|
| --- | --- | --- |
| create_ index | index_name | 创建一个索引,该索引根据index_name的值命名,并由fields_desc中指定的字典定义(“desc”代表描述)。集合的索引名称必须是唯一的。注意:调用create_index()只定义索引,返回一个statement.CreateCollectionIndexStatement对象。要创建索引,对返回的对象调用execute()方法。 |
| fields_desc |
| drop_ index | index_name | 删除参数中指定名称的索引。 |

用于定义新索引的字典具有以下结构:

{
  "type"   : INDEX|SPATIAL,
  "unique" : False,
  "fields" : [...]
}

普通索引的类型可以是INDEX(这是默认设置),几何索引的类型可以是SPATIAL。目前不支持全文索引。unique元素定义了它是否是唯一的索引。默认值是False,这也是当前唯一支持的值(即当前不支持唯一索引)。fields元素包含了要包含在索引中的字段列表。每个字段是一个字典,包含表 7-3 中的元素。

表 7-3

定义索引中的字段的字典

|

元素

|

缺省值

|

类型

|

描述

|
| --- | --- | --- | --- |
| Field |   | 线 | 如何在文档中查找值的定义。例如,要获得名为Name的顶级对象,使用$.Name。 |
| type |   | 线 | 类似于表列的数据类型。此表后面给出了支持类型的示例。 |
| required | False | 布尔代数学体系的 | 该值是否必须存在于文档中(“NOT NULL”)。对于 GeoJSON 类型,这必须设置为True。 |
| collation |   | 线 | 类型为TEXT(…)时的排序规则。仅当类型为TEXT类型时才能设置。从 MySQL Server 8.0.11 开始,索引不支持自定义排序规则,因此该值被忽略。 |
| options | 1 | 整数 | 创建空间索引时,ST_GeomFromGeoJSON()函数的options参数。它只允许用于空间索引。支持的值为 1、2、3 和 4。它定义了文档包含大于 2 的维值时的行为。 |
| sri d | 4326 | 整数 | 创建空间索引时,ST_GeomFromGeoJSON()函数的srid(空间参考系统标识符)参数。它只允许用于空间索引。这必须是一个无符号的 32 位整数。information_schema中的ST_SPATIAL_REFERENCE_SYSTEMS表包括 MySQL 支持的所有空间参考系统。SRIDs 的两个参考是 https://epsg.io/http://spatialreference.org/ 。 |

字段定义中总是需要fieldtype元素,而其余元素取决于创建的索引。一些常用的type值有

  • INT:指定一个有符号整数。变奏包括TINYINTSMALLINTMEDIUMINTBIGINT。要获得无符号值,请添加UNSIGNED,例如INT UNSIGNED

  • FLOAT:四字节浮点数。

  • DOUBLE:八字节浮点数。

  • DECIMAL:定点(精确)数。可以选择指定精度。默认精度是(10,0)

  • DATE:一个日期。

  • TIME:一个时间规格。

  • TIMESTAMP:由日期和时间组成的时间戳。范围是从 1970 年 1 月 1 日午夜后一秒到 2038 年 1 月 19 日 03:14:07 UTC。为秒添加的小数位数可以在括号中指定,例如TIMESTAMP(3)具有毫秒精度。

  • DATETIME:类似于时间戳,但支持从 1000 年 1 月 1 日午夜到 9999 年 12 月 31 日白天结束的范围。为秒添加的小数位数可以在括号中指定,例如DATETIME(3)具有毫秒精度。

  • YEAR:四位数的年份,例如 2018。

  • BIT:指定一个位值。

  • BLOB:用于存储二进制对象(即没有字符集的字节)。必须指定索引值的最大大小,例如BLOB(50)索引值的前 50 个字节。不支持TINYBLOBSMALLBLOBMEDIUMBLOBLONGBLOB的变化。

  • TEXT:用于存储字符串。这需要指定排序规则。必须指定要索引的值的最大大小,例如TEXT(50)来索引值的前 50 个字符。不支持TINYTEXTSMALLTEXTMEDIUMTEXTLONGTEXT的变化。

  • GEOJSON:使用 GeoJSON 格式的空间值。使用时,required必须是True,并且可以设置optionssrid元素。GeoJSON 格式仅支持空间索引。使用ST_GeomFromGeoJSON()函数( https://dev.mysql.com/doc/refman/en/spatial-geojson-functions.html#function_st-geomfromgeojson )从文档中提取 GeoJSON 值。optionssrid值是该功能支持的值。

值得注意的是,类型本身不能是 JSON 仅支持标量类型。

小费

如果您想深入了解索引规范的需求,请参见 MySQL Connector/Python 中的mysqlx/ lib/mysqlx/statement.py文件中的CreateCollectionIndexStatement类和 MySQL 服务器源代码中的plugin/x/src/admin_cmd_index.cc文件中的Admin_command_index::Index_field::create()方法。

本节后面将有一个创建索引的示例。下面的讨论将依次讨论这四种方法,从创建集合的任务开始。

创建收藏

使用create_collection()方法创建一个集合,该方法将集合的名称作为第一个参数或作为name关键字参数。还有第二个可选参数reuse,默认为False。如果reuseFalse,则集合不存在或者发生ProgrammingError异常。如果reuseTrue,则不存在则创建集合;否则,将返回现有集合。在大多数情况下,最好让reuse保持默认值,这样就可以知道是否存在同名的现有集合。否则,很容易出现细微的错误。

以下示例显示了如何在py_test_db模式中创建集合my_docs:

import mysqlx
from config import connect_args

db = mysqlx.get_session(**connect_args)

# Reset the py_test_db schema
db.drop_schema("py_test_db")
schema = db.create_schema("py_test_db")

docs = schema.create_collection("my_docs")

db.close()

该示例首先删除了py_test_db模式,以确保您从一个空模式开始。create_collection()方法返回一个集合对象,所以可以直接工作。

mysql命令行客户端中使用SHOW CREATE TABLE语句显示表定义可能会很有趣:

mysql> SHOW CREATE TABLE py_test_db.my_docs\G
*************************** 1\. row ***************************
       Table: my_docs
Create Table: CREATE TABLE `my_docs` (
  `doc` json DEFAULT NULL,
  `_id` varbinary(32) GENERATED ALWAYS AS (json_unquote(json_extract(`doc`,_utf8mb4'$._id'))) STORED NOT NULL,
  PRIMARY KEY (`_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
1 row in set (0.00 sec)

在创建任何辅助索引之前,这是文档存储中集合的标准表定义。doc列是文档(使用 JSON 数据类型)。_id列是一个生成的列,它从doc列中提取了_id对象。_ id列被用作主键。

这还显示了如何对文档中的值创建索引。对于每个索引,都会创建一个生成的列并用于索引。虽然主键必须是存储生成的列,但对于辅助索引,可以使用虚拟列。虚拟列的优点是它不需要任何存储,而是在需要时进行计算。但是,索引仍然使用与存储列时相同的空间。您将很快看到如何为集合创建额外的索引。

小费

要阅读有关生成的列的更多信息,请参见 MySQL 服务器参考手册中的 https://dev.mysql.com/doc/refman/en/create-table-generated-columns.htmlhttps://dev.mysql.com/doc/refman/en/create-table-secondary-indexes.html

create_collection()方法总是将字符集设置为 utf8mb4 但是,请注意,JSON 数据类型是作为 BLOB 存储的(即,作为没有字符集的二进制数据)。主键始终是一个(最多)32 个字符长的二进制字符串,存储在文档内的_id对象中,并使用生成的列进行检索,以允许 MySQL 对其进行索引。

对于现有的集合,最好使用get_collection()get_collections()。让我们看看它们是如何工作的。

检索单个集合

检索集合的最简单的情况是根据名称获取一个集合,这是使用get_collection()方法完成的。这使得控制获取哪些集合变得容易,并且为它们分配有意义的变量名也变得容易。

例如,考虑检索在前面的例子中创建的my_docs集合。这可以使用以下代码来完成:

import mysqlx
from config import connect_args

db = mysqlx.get_session(**connect_args)

schema = db.create_schema("py_test_db")

docs = schema.get_collection("my_docs")

print("Name of collection: {0}".format(
  docs.name))

db.close()

这个例子中唯一的新东西是将使用get_collection()方法检索的集合分配给docs变量的那一行。使用集合对象的name属性打印集合的名称:

Name of collection: my_docs

如果您需要检索许多集合,那么在一次调用中获取所有集合会很有用。这可以使用get_collections()来完成,如下所示。

检索架构中的所有集合

在某些情况下,应用可能会使用几个集合。一种解决方案是使用get_collection()逐个检索集合。不过,还有另一种方法。

另一种方法是使用get_collections()方法,它将所有集合作为一个列表返回。这是集合的name属性变得非常有用的情况之一,因为否则就不可能知道哪个集合对象包含哪个文档。

下面的代码显示了一个在具有两个集合的架构中检索集合的示例。这个例子类似于前两个例子,但是从删除模式开始,从头开始。示例代码是

import mysqlx
from config import connect_args

db = mysqlx.get_session(**connect_args)

# Reinitialize the py_test_db schema
db.drop_schema("py_test_db")
schema = db.create_schema("py_test_db")

# Create two collections
schema.create_collection("employees")
schema.create_collection("customers")

# Get all collections and print their
# names

collections = schema.get_collections()

for collection in collections:
  print("Collection name: {0}".format(
    collection.name))

# Create an index using the collection
# name as the key and the collection
# as the value

coll_dict = {

  collection.name: collection
    for collection in collections

}

db.close()

在用两个集合employeescustomers建立了模式之后,使用get_collections()方法检索这两个集合。然后,代码遍历这些集合,并打印出每个集合的名称。此外,使用集合的名称作为键来创建字典。这使得以后检索特定集合更加容易。例如employees集合可以参照coll_dict["employees"]使用。该程序的输出是

Collection name: customers
Collection name: employees

下一个集合操作方法是drop_collection()方法,用于删除不再需要的集合。

删除收藏

当应用不再需要一个集合时,可以使用 schema 对象上的drop_collection()方法删除它。方法接受要删除的集合的名称。如果收藏不存在,drop_collection()会默默忽略。

删除本章中使用的所有三个集合的示例是

import mysqlx
from config import connect_args

db = mysqlx.get_session(**connect_args)

# Reinitialize the py_test_db schema
schema = db.get_schema("py_test_db")

# Drop all three collections that has
# been used in this section

schema.drop_collection("my_docs")

schema.drop_collection("employees")

schema.drop_collection("customers")

# For good measure also drop the schema
db.drop_schema("py_test_db")

db.close()

由于drop_schema()将删除模式中的所有集合(和表),drop_collection()调用是多余的,但是它们展示了如何一个接一个地删除集合。

与 SQL 查询一样,通过添加索引可以极大地提高文档集合的查询性能。这将在下一节中介绍。

创建索引

索引提供了值的搜索树,这些值通过引用包含这些值的实际文档来索引。这使得查找特定值比扫描所有文档并逐个检查它们是否满足要求要快得多。

注意

还有其他类型的索引,如全文索引。在撰写本文时,集合只支持 B 树(“普通”)和 R 树(空间)索引。

与常规的 SQL 表相比,为集合创建索引稍微复杂一些,因为除了索引定义本身之外,还需要定义如何从文档中检索值以及这些值代表什么。这是使用无模式数据存储的代价。

可以使用集合对象的create_index()方法定义索引。该方法的完整定义已在前面讨论过,不再重复。相反,我们来看一个例子。清单 7-1 展示了一个创建集合并添加三个索引的例子。

import mysqlx
from config import connect_args

db = mysqlx.get_session(**connect_args)

# Reinitialize employees collection in
# the py_test_db schema
schema = db.create_schema("py_test_db")
schema.drop_collection("employees")
employees = schema.create_collection(
  "employees")

# Define the three fields that will be
# used in the indexes.

field_name = {

  "field"     : "$.Name",
  "type"      : "TEXT(60)",
  "required"  : True,
  "collation" : "utf8mb4_0900_ai_ci",

}

field_office_location = {

  "field"     : "$.Office.Location",
  "type"      : "GEOJSON",
  "required"  : True,
  "options"   : 1,
  "srid"      : 4326,

}

field_birthday = {

  "field"     : "$.Birthday",
  "type"      : "DATE",
  "required"  : False,

}

# Create a normal index on the
# employee's name

index_name = "employee_name"

index_def =   {

  "fields" : [
    field_name
  ],
  "type"   : "INDEX",

}

index = employees.create_index(

  index_name, index_def)

index.execute()

print(
  "Index created: {0}".format(index_name)
)

# Create a spatial index for the
# location the employee work at.

index_name = "employee_office_location"

index_def =   {

  "fields" : [
    field_office_location
  ],
  "type"   : "SPATIAL",

}

employees.create_index(

  index_name, index_def

).execute()

print(
  "Index created: {0}".format(index_name)
)

# Create a normal index on the
# employee's birthday and name

index_name = "employee_birthday_name"

index_def =   {

  "fields" : [
    field_birthday,
    field_name
  ],
  "type"   : "INDEX",

}

index = employees.create_index(

  index_name, index_def)

index.execute()

print(

  "Index created: {0}".format(index_name)

)

db.close()

Listing 7-1Creating Indexes for a Collection

该示例从重新初始化集合开始。这确保了即使多次执行该示例,添加索引的起点也是相同的。然后定义将在索引中使用的三个字段(SQL 语言中的列)。这允许您根据需要重用它们。在这种情况下,您需要设置雇员的姓名和办公地点,但是生日是可选的。

创建的第一个索引位于Name字段。这是一个普通的索引,您指定应该使用utf8mb4_0900_ai_ci排序规则。索引宽度设置为 60 个字符。不用担心;仍然支持超过 60 个字符的名称。只是索引只考虑前 60 个字符。由create_index()创建的对象存储在index变量中,索引的实际创建由index.execute()执行。

第二个索引是雇员工作的办公室位置的空间索引。使用optionssrid的默认值。在这种情况下,索引执行与定义索引相结合。

第三个索引结合了雇员的生日和姓名。这允许你搜索生日和名字。当应用多个条件时,将多个字段组合成一个索引是提高查询性能的有效方法。

对于三个索引中的每一个,在创建索引后都会打印一条确认消息:

Index created: employee_name
Index created: employee_office_location
Index created: employee_birthday_name

小费

如果您将最后一个索引创建为(NameBirthday),这将使仅基于名称的索引变得多余,因为使用组合索引也可以解决仅基于名称的搜索。这是因为 MySQL 允许您在索引的左前缀中搜索 B 树索引。组合多个字段时的另一个注意事项是,当最具选择性的值位于第一位时,可以获得最佳性能。

检查执行该示例产生的表定义可能会很有趣。稍微重新格式化的输出是

mysql> SHOW CREATE TABLE py_test_db.employees\G
*************************** 1\. row ***************************
       Table: employees
Create Table: CREATE TABLE `employees` (
  `doc` json DEFAULT NULL,
  `_id` varbinary(32) GENERATED ALWAYS AS
        (json_unquote(json_extract(`doc`,_utf8mb4'$._id')))
        STORED NOT NULL,
  `$ix_t60_r_4CB1E32CCBE4FE2585D3C8F059CB3A909FC536B7` text
        GENERATED ALWAYS AS
        (json_unquote(json_extract(`doc`,_utf8mb4'$.Name')))
        VIRTUAL NOT NULL,
  `$ix_gj_r_E933A4A981E8AB89AF33A3DB0B1D45F8E76A6E38` geometry
        GENERATED ALWAYS AS
        (st_geomfromgeojson(
           json_extract(`doc`,_utf8mb4'$.Office.Location'),1,4326)
        ) STORED NOT NULL /*!80003 SRID 4326 */,
  `$ix_d_CAA21771B5BB2089412F3D426AF25DEE3EDD1B76` date
        GENERATED ALWAYS AS
        (json_unquote(json_extract(`doc`,_utf8mb4'$.Birthday')))
        VIRTUAL,
  PRIMARY KEY (`_id`),
  SPATIAL KEY `employee_office_location`
        (`$ix_gj_r_E933A4A981E8AB89AF33A3DB0B1D45F8E76A6E38`),
  KEY `employee_name`
        (`$ix_t60_r_4CB1E32CCBE4FE2585D3C8F059CB3A909FC536B7`(60)),
  KEY `employee_birthday_name`
        (`$ix_d_CAA21771B5BB2089412F3D426AF25DEE3EDD1B76`,
         `$ix_t60_r_4CB1E32CCBE4FE2585D3C8F059CB3A909FC536B7`(60))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
1 row in set (0.00 sec)

当然,首先突出的是生成的列的难以阅读的名称。它们是自动生成的,是唯一的,但是 X DevAPI 可以很容易地使用它们。这些名称取决于字段定义,因此对于相同的提取字段,它们将保持不变。这样,当同一个字段在多个索引中使用时,就不会被多次添加。在这个例子中,雇员姓名用于两个索引中,但是只定义了一次如何提取它。

另一件事是,对于这些新的二级索引,生成的列有VIRTUAL子句。这意味着这些值实际上并不存储在表中,而是根据需要提取。这可以节省磁盘空间。

create_index()的恭维语是drop_index(),接下来讨论。

删除索引

删除索引比创建索引简单得多。所需要做的就是用要删除的索引名调用集合的drop_index()方法。MySQL 将负责删除索引,如果索引中生成的列不再为其他索引所需要,它们也将被删除。

清单 7-2 展示了一个删除employee_name索引的例子,它是上一个例子中创建的索引之一。

import mysqlx
from config import connect_args

db = mysqlx.get_session(**connect_args)

# Reinitialize employees collection in
# the py_test_db schema
schema = db.get_schema("py_test_db")
employees = schema.get_collection(
  "employees")

# Drop the index on the Name field.

employees.drop_index("employee_name")

print("Index employee_name has been dropped")

db.close()

Listing 7-2Dropping an Index

在这种情况下,只需调用drop_index()方法就可以删除索引。该脚本的输出是

Index employee_name has been dropped

读者可以在删除索引之前和之后比较SHOW CREATE TABLE py_test_db.employees\G的输出,以确认Name字段仍然被提取。第二个练习是修改示例,删除employee_birthday_name索引,并看到NameBirthday生成的列被删除。

这就完成了可用于操作集合的方法的演练(不考虑存储在集合中的文档)。在继续执行查询之前,还有一些额外的方法需要考虑。

其他收集方法和属性

mysqlx.Collection类包括一些方法和三个属性,在处理集合对象时会很有用。它们与已经讨论过的模式非常相似,所以这里只包括一个简单的介绍。

表 7-4 中列出了可以返回集合信息的方法。返回的对象是相对于mysqlx模块的。这些方法都不带任何参数。

表 7-4

收集实用程序方法

|

方法

|

返回对象

|

描述

|
| --- | --- | --- |
| exists_in_``databas |   | 根据集合是否存在,返回TrueFalse。 |
| get_``connectio | connection.Connection | 返回基础连接对象。 |
| get_``nam |   | 返回集合的名称。这与name属性相同。 |
| get_``schem | crud.Schema | 返回集合所在架构的对象。这与schema属性相同。 |
| get_ session | connection.Session | 返回会话的对象。这与会话属性相同。在 8.0.12 版本中添加。 |

从返回名称和模式的方法的描述中可以看出,还有三个属性。name属性是集合的名称,schema属性保存模式对象,会话属性(在 8.0.12 和更高版本中可用)保存会话对象。

由于收集实用程序的方法和属性本质上与模式相同,所以让我们继续执行一些查询。

查询–CRUD

处理数据的有趣部分是执行查询。现在您已经知道如何使用会话、模式和集合,您可以开始使用会话来使用 API 的 NoSQL 部分。实现是围绕 CRUD(创建、读取、更新、删除)原则构建的,接下来的四个部分将依次介绍这四个操作。

在深入研究之前,有必要对 CRUD 方法进行一个高层次的了解。它们被收集在表 7-5 中。返回的对象是相对于myqslx模块的。

表 7-5

集合对象的 CRUD 方法

|

方法

|

争论

|

返回对象

|

描述

|
| --- | --- | --- | --- |
| ad d | 文件清单 | Statement.AddStatement | 准备将文档添加到词典列表中。每本词典都是一个文档。 |
| add_or_replace_``on | doc_id | Result.Result | 向上插入文档,因此如果文档 ID 存在,它将替换现有文档;否则,它会将其添加为新文档。 |
| doc |
| coun t |   |   | 以整数形式返回集合中的文档数。 |
| fin d | condition | Statement.FindStatement | 准备 find 语句,返回符合条件的文档。 |
| modif y | condition | Statement.ModifyStatement | 准备一条修改语句,更新符合条件的文档。在 8.0.12 及更高版本中,该条件是强制性的。 |
| remov e | condition | Statement.RemoveStatement | 准备删除符合条件的文档的 remove 语句。 |
| remove_``on | doc_id | result.Result | 移除具有指定文档 ID 的文档。 |
| replace_``on | doc_id | result.Result | 用新文档更新具有指定文档 ID 的文档。 |
| doc |

从表中可以看出,CRUD 方法使用了一些常见的参数:

  • 文档:这是一个描述 JSON 文档的字典。

  • 单据 ID :该单据的唯一键。

  • 条件:这相当于 SQL 中的WHERE子句,它定义了搜索文档时使用的过滤器。

注意

当然,这不仅仅是三种参数类型的列表所暗示的。例如,可以对读取请求的结果进行排序。你很快就会知道如何去做。

对于某些方法,直接返回结果对象。它将包含有关已执行操作的信息。除了count()之外,其余的方法都返回statement模块中的一个类的对象。它们要求你在执行动作之前调用返回的语句对象的execute()方法,然后execute()返回一个结果对象。

是时候停止讨论,开始创建一些文档了。这意味着您需要查看 CRUD 的创建部分。

CRUD:创建

使用存储在数据库中的数据的第一步是创建数据。在数据存在于集合中之前,没有什么可查询或修改的。因此,您的第一个任务是用一些数据填充一个集合。本节中插入的数据是其余文档存储 CRUD 讨论的基础。

有两种方法可以向集合中添加数据:add()add_or_replace_one()。本节将讨论add()方法,而add_or_replace_one()将被推迟到“CRUD: Update”一节,因为它既可以添加新数据,也可以更新现有数据。

add()方法将零个或多个文档作为参数。每个文档都是使用 Python 字典定义的,这自然形成了一个 JSON 文档。当在同一个add()调用中插入多个文档时,它们可以作为列表或元组提供,或者通过指定多个参数来提供。

add()方法返回一个mysqlx.statement.AddStatement对象。两个最重要的方法是add()execute()。这些方法总结在表 7-6 中。

表 7-6

方法来使用 Add 语句

|

方法

|

争论

|

描述

|
| --- | --- | --- |
| add | *values | 将值中指定的文档添加到 add 语句中。 |
| execut e |   | 通过向 MySQL 服务器提交值来执行 add 语句。 |

可以使用AddStatement. add()方法将更多的文档添加到语句中,AddStatement.execute()将文档发送到文档存储中。所有添加的文档都作为一份声明发送。

将要插入的数据可以在本书源代码中的cities.py文件中找到。该文件包括cities字典中 15 个澳大利亚城市的数据。文件的开头是

cities = {
  "Sydney": {
    "Name"           : "Sydney",
    "Country_capital": False,
    "State_capital"  : True,
    "Geography": {
      "Country" : "Australia",
      "State"   : "New South Wales",
      "Area"    : 12367.7,
      "Location": "{'Type': 'Point', 'Coordinates': [151.2094, -33.8650]}",
      "Climate" : {
        "Classification"      : "humid subtropical",
        "Mean_max_temperature": 22.5,
        "Mean_min_temperature": 14.5,
        "Annual_rainfaill"    : 1222.7
      },
    },
    "Demographics": {
      "Population": 5029768,
      "Median Age": 36
    },
    "Suburbs": [
      "Central Business District",
      "Parramatta",
      "Bankstown",
      "Sutherland",
      "Chatswood"
    ]
  },
...

清单 7-3 展示了一个例子,其中使用了三次add()方法将城市插入到一个新创建的集合中。使用add()的第一个例子插入一个城市,第二个例子在一个add()调用中插入两个城市,第三个例子使用多个add()调用插入几个城市,创建一个大的语句。

import mysqlx
from config import connect_args

from cities import cities

db = mysqlx.get_session(**connect_args)
schema = db.create_schema("py_test_db")

# Reinitalize the city collection
schema.drop_collection("city")
city_col = schema.create_collection("city")

# Insert a single city
sydney = cities.pop("Sydney")

db.start_transaction()

result = city_col.add(sydney).execute()

db.commit()

items = result.get_affected_items_count()
print("1: Number of docs added: {0}"
  .format(items))
ids = result.get_generated_ids()
print("1: Doc IDs added: {0}".format(ids))
print("")

# Insert two cities in one call
melbourne = cities.pop("Melbourne")
brisbane  = cities.pop("Brisbane")
data = (melbourne, brisbane)

db.start_transaction()

result = city_col.add(data).execute()

db.commit()

items = result.get_affected_items_count()
print("2: Number of docs added: {0}"
  .format(items))
ids = result.get_generated_ids()
print("2: Doc IDs added: {0}".format(ids))
print("")

# Insert the rest of the cities by
# adding them to the statement object
# one by one.
db.start_transaction()

statement = city_col.add()

for city_name in cities:

  statement.add(cities[city_name])

result = statement.execute()

db.commit()

items = result.get_affected_items_count()
print("3: Number of docs added: {0}"
  .format(items))
print("")

db.close()

Listing 7-3Adding Data to a Collection

该示例首先确保py_test_db模式中的city集合不存在,然后创建它。然后添加从cities.py文件导入的城市。

第一个插入的城市是悉尼。这是通过插入城市本身来完成的。该命令被链接起来,在一行代码中执行所有的工作,结果是一个result.Result对象。注意调用是如何被db.start_transaction()db.commit()调用包装的。因为autocommit的值是从 MySQL 服务器全局设置继承的,所以显式添加事务是最安全的。

注意

和往常一样,您应该在提交数据之前检查是否出现了任何警告或错误。为了让示例更容易阅读,我们省略了对警告和错误的处理。第九章将会更详细地介绍如何检查警告和错误。

接下来,插入墨尔本和布里斯班。这是通过用文档创建一个元组来完成的,这个元组被传递给add()方法。文档也可以作为两个参数添加,例如:

result = city_col.add(
  melbourne, brisbane
).execute()

最后,添加剩余的城市。这是通过首先创建AddStatement对象,然后遍历剩余的城市,并逐个添加它们来完成的。最后,调用AddStatement. execute()方法在一个数据库调用中插入所有的城市。

执行该示例的输出类似于以下示例:

1: Number of docs added: 1
1: Doc IDs added: ['00005af3e4f7000000000000008f']

2: Number of docs added: 2
2: Doc IDs added: ['00005af3e4f70000000000000090', '00005af3e4f70000000000000091']

3: Number of docs added: 12

实际的文档 id 将会不同,因为它们是自动生成的。city集合现在有 15 个澳大利亚城市要查询,所以是时候看看读操作了。

读取

对于大多数数据库,大多数查询都是读取查询(不修改任何数据的查询)。这些查询是本节的主题。

有两种方法可以在不改变文档存储中任何数据的情况下读取集合:count()find()count()方法是最简单的,因为它只是将集合中文档的数量作为一个整数返回。find()方法更复杂,因为它支持条件(SQL 语言中的WHERE子句)、排序、返回文档等等。

find()方法返回一个mysqlx.statement.FindStatement对象。这是可以通过调用FindStatement方法来修改查询的主要构件。这些修饰符方法都返回FindStatement对象本身,因此可以进一步修改。最后有一个定义查询的调用链,可以使用execute()方法提交查询。表 7-7 显示了可用于修改语句的FindStatement方法。这些方法按照它们通常被调用的顺序进行排序。

表 7-7

方法来修改 Find 语句

|

方法

|

争论

|

描述

|
| --- | --- | --- |
| field s | *fields | 定义要包含在结果中的字段。每个字段都可以是使用与 SQL 相同语言的表达式。 |
| group_``b | *fields | 描述对于涉及聚合函数的查询,要对哪些字段进行分组。 |
| havin g | condition | 描述在查询以其他方式得到解决后,根据什么来筛选结果(排序除外)。这对于根据聚合函数的值进行过滤非常有用。 |
| sor t | *sort_clauses | 描述结果的排序依据。 |
| limi t | row_count``offset=0 | 第一个参数设置要返回的最大文档数。第二个可选参数定义偏移量。默认偏移量为 0。注意:offset 参数在 8.0.12 版本中已被否决,并将在以后的版本中被删除。相反,引入了offset()方法来设置偏移。 |
| offset | offset | 设置要返回的行的偏移量。在 8.0.12 版本中添加。 |
| lock_``exclusiv |   | 使语句获得独占锁。一次只能有一个语句拥有独占锁。如果稍后将在同一事务中更新文档,则使用。 |
| lock_``share |   | 使语句获取共享锁。这可以防止其他语句修改匹配的文档,但是它们可以读取这些文档。 |
| bin d | *args | 第一个参数提供要替换的参数的名称。第二个参数提供值。每个参数调用bind()一次。 |
| execut e |   | 执行 find 语句。 |

查询修改方法的调用顺序并不重要,但建议在整个程序中坚持相同的顺序,以便一眼就能确定查询是如何修改的。

清单 7-4 展示了一个例子,其中首先确定了city集合中城市的总数。然后显示集合中城市最多的州。结果按州内城市的数量排序,最多限于三个州。

import mysqlx
from config import connect_args

db = mysqlx.get_session(**connect_args)
schema = db.create_schema("py_test_db")
city_col = schema.get_collection("city")

# Use the count() method to get the
# total number of cities.

num_cities = city_col.count()

print("Total number of cities: {0}"
  .format(num_cities))
print("")

statement = city_col.find(
  "Geography.Country = :country") \

  .fields("Geography.State AS State",
          "COUNT(*) AS NumCities") \
  .group_by("Geography.State") \
  .having("COUNT(*) > 1") \
  .sort("COUNT(*) DESC") \
  .limit(3)

result = statement.bind(

  "country", "Australia"

).execute()

states = result.fetch_all()
print("Num states in result: {0}"
  .format(result.count))
print("")

print("{0:15s}   {1:8s}"
  .format("State", "# Cities"))
print("-"*26)
for state in states:
  print("{State:15s}       {NumCities:1d}"
    .format(**state))

  db.close()

Listing 7-4Querying the city Collection

除了新的语法,这个例子很简单。集合的count()方法用于获取总行数。然后定义查询。您要求查询由Geography.Country元素过滤,该元素的值被占位符(:country)占用,该占位符后来在bind()方法中被设置为澳大利亚的值。查询将返回要返回并重命名为StateGeography.State元素,每个州的城市数返回为NumCities

由于字段包括聚合函数(COUNT())和非聚合字段(Geography.State),因此还需要定义分组依据。这是通过调用group_by()方法来完成的。此外,在这种情况下,您选择进行筛选,以便只包含拥有一个以上城市的州;这是使用having()方法完成的。

最后,您告诉文档存储库,您希望按照州内城市的数量以降序对结果进行排序,并且您最多希望返回三个州(城市最多的三个州)。此时,语句保存在statement变量中。在这种情况下,这并不重要,但是通过在执行时指定绑定参数,可以重用查询。

小费

如果语句对象存储在一个变量中,就有可能多次执行同一个查询。通过将bind()调用保存到执行时间,可以重用具有不同值的相同查询模板。下面是一个在修改文档时重用查找查询的例子。

一旦执行了查询,就使用fetch_all()获取行。这也将结果的count属性设置为结果中的文档数。该程序的输出是

Total number of cities: 15

Num states in result: 3

State             # Cities
--------------------------
Queensland            5
New South Wales       3
Victoria              2

已知city集合中所有城市的位置。作为第二个例子,让我们看看地理数据是如何使用的。清单 7-5 显示了一个查找所有城市、计算到悉尼的距离并从最近的城市开始按距离排序的例子。悉尼本身将被跳过,因为它不提供任何信息。

import mysqlx
from config import connect_args

db = mysqlx.get_session(**connect_args)
schema = db.get_schema("py_test_db")
city_col = schema.get_collection("city")

# Get the location of Sydney
statement = city_col.find("Name = :city")
statement.fields(
  "Geography.Location AS Location")
statement.bind("city", "Sydney")
result = statement.execute()
sydney = result.fetch_one()

# Define the formula for converting
# the location in GeoJSON format to
# the binary format used in MySQL

to_geom = "ST_GeomFromGeoJSON({0})"

sydney_geom = to_geom.format(
  sydney["Location"])
other_geom = to_geom.format(
  "Geography.Location")

distance = "ST_Distance({0}, {1})".format(

  sydney_geom, other_geom)

statement = city_col.find("Name != 'Sydney'")
statement.fields(
    "Name",
    "Geography.State AS State",
    distance + " AS Distance"
)
statement.sort(distance)
result = statement.execute()

cities = result.fetch_all()
print("{0:14s}   {1:28s}   {2:8s}"
  .format("City", "State", "Distance"))
print("-"*56)
for city in cities:
  # Convert the distance to kilometers
  distance = city["Distance"]/1000
  print("{Name:14s}   {State:28s}"
    .format(**city)
    + "     {0:4d}"
    .format(int(distance))
  )

db.close()

Listing 7-5Querying Geographical Data

这个例子的基本原理与上一个例子相同。有趣的部分是两个 MySQL 几何函数的使用。ST_GeomFromGeoJSON()函数用于将 GeoJSON 坐标转换为 MySQL 中使用的二进制几何格式。然后,ST_Distance()函数计算两点之间的距离,将空间参考系统考虑在内;这里使用默认值,即地球。程序的输出如清单 7-6 所示。

City             State                          Distance
--------------------------------------------------------
Wollongong       New South Wales                    69
Newcastle        New South Wales                   116
Canberra         Australian Capital Territory      249
Gold Coast       Queensland                        681
Melbourne        Victoria                          714
Brisbane         Queensland                        730
Geelong          Victoria                          779
Sunshine Coast   Queensland                        819
Hobart           Tasmania                         1056
Adelaide         South Australia                  1164
Townsville       Queensland                       1676
Cairns           Queensland                       1960
Darwin           Northern Territory               3145
Perth            Western Australia                3297

Listing 7-6The Output of the Example Program in Listing 7-5

添加和检索数据涵盖了很多数据库用例,但是如果数据不再是最新的,该怎么办呢?让我们看看如何更新数据库中的数据。

CRUD:更新

在 CRUD 中,更新部分是修改现有的数据,因此一个或多个字段的值被改变或者字段被添加或删除。例如,当一个城市的人口因为人口迁移、出生和死亡而发生变化时,更新该城市的人口。这一节将研究可以做到这一点的方法。

在 MySQL Connector/Python X DevAPI 中为文档存储修改数据有三种方法:

  • add_or_replace_one():用于追加插入一张单据。例如,如果有一个文档具有相同的文档 ID,它将被替换;否则,将添加一个新文档。

  • modify():更新现有文档。查找文档的方式与读取查询过滤文档的方式相同。

  • replace_one():用给定的文档 ID 替换文档。

由于add_or_replace_one()replace_one()方法非常相似,所以将一起讨论它们,然后讨论modify()方法。

替换文档

add_or_replace_one()replace_one()的区别在于如果没有指定 ID 的文档会发生什么。add_or_replace_one()方法添加了一个新文档,而replace_one()方法最终什么也没做。

清单 7-7 显示了一个例子,其中 Geelong 的文档被替换为人口被更改为 193000 的文档。之后,它会尝试用不存在的文档 ID 替换文档。

import mysqlx
from config import connect_args

db = mysqlx.get_session(**connect_args)
schema = db.get_schema("py_test_db")
city_col = schema.get_collection("city")

# Get the current document for Geelong
db.start_transaction()
result = city_col.find("Name = :city_name") \
  .bind("city_name", "Geelong") \
  .lock_exclusive() \
  .execute()
geelong = result.fetch_one()

# Get the Geelong document ID and
# update the population
geeling_id = geelong["_id"]
geelong["Demographics"]["Population"] = 193000

# Upsert the document

result = city_col.add_or_replace_one(

  geeling_id, geelong)

print("Number of affected docs: {0}"
  .format(result.get_affected_items_count()))

# Attempt to use the same document
# to change a non-existing ID

result = city_col.replace_one(

  "No such ID", geelong)

print("Number of affected docs: {0}"
  .format(result.get_affected_items_count()))

# Leave the data in the same state as
# before the changes
db.rollback()
db.close()

Listing 7-7Upserting a Document and Attempting to Replace One

该示例从检索 Geelong 的现有文档开始。它用于获取文档 ID,并作为新文档的基础。这也是一个使用独占锁的例子,所以在事务完成之前,其他人不能访问文档。使用add_or_replace_one()方法将新文档向上插入到文档存储中。之后,相同的文档被用于replace_one()方法,但是文档 ID 不存在。对于这两种用途中的每一种,都会打印受影响文档的数量。输出是

Number of affected docs: 2
Number of affected docs: 0

add_or_replace_one()调用的结果显示两个文档受到了影响。怎么会?这是因为 upserts 的实现方式。首先,尝试插入新文档。如果有重复的文档,则更新现有的文档(SQL 中的一个INSERT ... ON DUPLICATE KEY UPDATE ...语句)。如果文档不存在,只有一个文档会受到影响。另一方面,replace_one()不影响任何文件;它既不插入新文档,也不更新现有文档。

注意

不要认为replace_one()是 SQL REPLACE语句。与replace_one()对应的 SQL 语句是UPDATE,在主键上有一个WHERE子句。

如果您只想更新已经更改的字段,而不是整个文档,那么您需要使用modify()方法。

修改文档

您在前面的讨论中看到的两种替换方法用于替换整个文档。对于示例集合中相对较小的文档来说,这不是一个大问题,但是对于较大的文档来说,这很快就会变得低效。此外,这两种替换方法一次只能修改一个文档,这使得更新一个国家中的所有城市或集合中的所有文档变得很麻烦。

这就是modify()方法发挥作用的地方。它支持指定一个类似于 read 查询的条件来指定要更改的文档,并且更改可以基于现有的值。modify()方法返回一个mysqlx.statement.ModifyStatement对象,它和find()返回的语句对象一样,可以用来定义整个修改语句。表 7-8 包含了最重要的方法。

表 7-8

修改修改查询的方法

|

方法

|

争论

|

描述

|
| --- | --- | --- |
| sor t | *sort_clauses | 描述文档的排序依据。 |
| limi t | row_count | 该参数设置要修改的最大文档数。 |
| array_``appen | doc_path | 在文档中由doc_path指定的点将值追加到现有的数组元素。 |
| value |
| array_``inser | field | 将值插入到由field指定的数组中。 |
| value |
| se t | doc_path | 如果文档路径已经存在,则更新该值;否则,该字段将添加给定值。 |
| value |
| patc h | Doc | 添加或替换文档中包含的字段。它还支持删除现有字段。 |
| unse t | *doc_paths | 从文档中删除匹配的文档路径。 |
| bin d | *args | 第一个参数提供要替换的参数的名称。第二个参数提供值。每个参数调用bind()一次。 |
| execut e |   | 执行 modify 语句。 |

有几个方法与find()相同,这并不奇怪,因为 modify 语句的第一个任务是定位要更新的文档。与 find 语句不同,它要求定义一个条件。否则,8.0.11 中会出现ProgrammingErrror异常:

mysqlx.errors.ProgrammingError: No condition was found for modify

在 MySQL Connector/Python 8.0.12 及更高版本中,必须在创建修改语句时指定条件。否则会发生 TypeError 异常:

TypeError: modify() missing 1 required positional argument: 'condition'

这是一项安全功能,可防止因编码错误而意外更新所有文档。如果您确实需要修改所有文档,请添加一个始终计算为True的条件,例如:

collectoin.modify("True")

注意

modify 语句必须有一个指定要更新哪些文档的条件。如果真的要更新所有文档,用True作为条件;这也使你的意图变得清晰,这在你以后回到代码的时候很有帮助。

还有几种方法可以改变匹配文档的内容。让我们看一些使用这些方法的例子。

set()和 unset()

修改文档最简单的方法是set()unset()方法。set()unset()的方法非常相似。set()方法将改变指定字段的值。如果该字段不存在,它将被添加。新值可以是包含旧值的计算结果。另一方面,unset()方法删除一个存在的字段。

假设已经进行了人口普查,维多利亚州的数据显示人口增长了 10%。清单 7-8 展示了如何使用modify()方法进行更改。

import mysqlx
from config import connect_args

db = mysqlx.get_session(**connect_args)
schema = db.get_schema("py_test_db")
city_col = schema.get_collection("city")

db.start_transaction()

# Get the current population
statement = city_col.find(   "Geography.State = :state")
statement.fields("Name AS CityName",
  "Demographics.Population AS Population")
statement.sort("Name")
statement.bind("state", "Victoria")

before = statement.execute()

# Update the population for cities
# in the state of Victoria to increase
# the population with 10%

result = city_col.modify(
  "Geography.State = :state") \

  .set("Demographics.Population",
    "FLOOR(Demographics.Population * 1.10)") \
  .bind("state", "Victoria") \
  .execute()

print("Number of affected docs: {0}"
  .format(result.get_affected_items_count()))
print("")

after = statement.execute()

before_cities = before.fetch_all()
after_cities  = after.fetch_all()

print("{0:10s}   {1:¹⁷s}"
  .format("City", "Population"))
print("{0:10s}   {1:7s}   {2:7s}"
  .format("", "Before", "After"))
print("-"*30)

for before_city, after_city \
  in zip(before_cities, after_cities):
  print("{0:10s}   {1:7d}   {2:7d}"
    .format(
      before_city["CityName"],
      int(before_city["Population"]),
      int(after_city["Population"])
    )
  )

# Leave the data in the same state as
# before the changes
db.rollback()
db.close()

Listing 7-8Modifying Several Documents at a Time with set()

在修改维多利亚州的城市之前,定义了一个 find 语句来获取城市名称及其人口。这用于获取 modify 语句前后的填充,因此有可能确认结果是预期的。

该示例最有趣的部分是调用modify()和后续的方法调用来定义更新。set()方法用于定义更新本身,新群体是基于旧群体计算的。注意FLOOR()函数是如何围绕决定新人口的计算的。这是为了避免分数人。即使您允许分数,也有必要在新值两边加上括号,以表示应该使用Demographics.Population的值,而不是字符串“Demographics”。人口”。

执行 modify 语句后,将再次读取人口,并打印城市的前后人口。最后,事务被回滚,因此可以多次执行程序并获得相同的输出,如下所示:

Number of affected docs: 2

City            Population
             Before    After
------------------------------
Geelong       192393    211632
Melbourne    4725316   5197847

接下来要讨论的两种修改方法是array_append ()array_insert()方法。

array_append()和 array_insert()

顾名思义,array_append()array_insert()方法处理文档中的数组。虽然它们的用途相对有限,但它们非常擅长自己的工作。它们的用法是相似的,所以将它们放在一起讨论。

这两种方法采用相同的两个参数,尽管它们的名称不同。这两个论点是

  • 要修改的数组元素的路径或插入值的位置

  • 价值

主要的区别是在指定的元素上做了什么。array_append()方法用一个数组替换现有值,该数组由现有值后跟新值组成。另一方面,array_insert()将新元素插入数组中的那个位置。

解释它们如何工作的最简单的方法是举一个例子。清单 7-9 使用这两种方法来修改悉尼的郊区。使用array_append()方法将中央商务区郊区变成一个由自身作为第一元素,郊区地点作为第二元素组成的数组。array_insert()方法用于将利物浦添加到郊区列表中。

import mysqlx
from config import connect_args
import pprint

printer = pprint.PrettyPrinter(indent=1)

db = mysqlx.get_session(**connect_args)
schema = db.get_schema("py_test_db")
city_col = schema.get_collection("city")

# Run inside a transaction, so the
# changes can be rolled back at the end.
db.start_transaction()

# Get the current suburbs, the document
# id, and the index of Central Business
# District in the Suburbs array.
statement = city_col.find("Name = :city_name")
statement.fields(
  "_id",
  "Suburbs",
  "JSON_SEARCH("
     + "Suburbs,"
     + " 'one',"
     + " 'Central Business District')"
  + " AS Index")
statement.bind("city_name", "Sydney")

before = statement.execute().fetch_one()
print("Suburbs before the changes:")
print("-"*27)
printer.pprint(before["Suburbs"])
print("")

docid = before["_id"]
# The returned index includes $ to
# signify the start of the path, but
# that is relative to Suburbs, so
# remove for this use.

index = before["Index"][1:]

print("Index = '{0}'\n"
  .format(before["Index"]))

# Use array_append() to change the
# Central Busines District suburb into
# an array of itself plus an array of
# some places within the suburb.
modify = city_col.modify("_id = :id")

modify.array_append(

  "Suburbs{0}".format(index),
  ["Circular Quay", "Town Hall"])

modify.bind("id", docid)

modify.execute()

after1 = statement.execute().fetch_one()
print("Suburbs after the array_append:")
print("-"*31)
printer.pprint(after1["Suburbs"])
print("")

# Reset the data
db.rollback()

# Use array_insert to add the suburb
# Liverpool
db.start_transaction()
num_suburbs = len(before["Suburbs"])
modify = city_col.modify("_id = :id")

modify.array_insert(

  "Suburbs[{0}]".format(num_suburbs),
  "Liverpool")
modify.bind("id", docid)
modify.execute()

after2 = statement.execute().fetch_one()
print("Suburbs after the array_insert:")
print("-"*31)
printer.pprint(after2["Suburbs"])

# Reset the data
db.rollback()
db.close()

Listing 7-9Updating a Document with array_append() and array_insert()

示例的开始只是像往常一样设置环境。然后读取悉尼现有的郊区以及郊区数组中的文档 ID 和中央商务区的索引。

文档 ID 允许您在 modify 语句的过滤条件中使用它。通过文档 ID 查找文档总是最有效的定位方法。

中央商务区元素的索引作为$[0](数组中的第一个元素)返回。美元符号表示文档的头部,但是因为您使用了JSON_SEARCH()函数来搜索Suburbs数组,所以它是相对于Suburbs数组的,而不是文档的根。所以,为了使用指数,有必要去掉美元符号。

小费

MySQL 中有几个 JSON 函数可以用来搜索或处理 JSON 文档。一般来说,它们都是围绕将 MySQL 作为文档存储时所需的功能展开的,但也有一些功能与 SQL 方面更相关。关于 MySQL 中 JSON 函数的完整概述,请参见 https://dev.mysql.com/doc/refman/en/json-functions.html

现在,您可以使用array_append()添加郊区内的位置,并使用array_insert()添加利物浦郊区。对于插入,路径被设置为Suburbs[{0}],其中{0}被替换为更改前的郊区数,以在末尾添加新的郊区。在每个 modify 语句之后,事务回滚以重置数据。输出是

Suburbs before the changes:
---------------------------
['Central Business District',
 'Parramatta',
 'Bankstown',
 'Sutherland',
 'Chatswood']

Index = '$[0]'

Suburbs after the array_append:
-------------------------------
[['Central Business District', ['Circular Quay', 'Town Hall']],
 'Parramatta',
 'Bankstown',
 'Sutherland',
 'Chatswood']

Suburbs after the array_insert:
-------------------------------
['Central Business District',
 'Parramatta',
 'Bankstown',
 'Sutherland',
 'Chatswood',
 'Liverpool']

输出清楚地表明了所做的更改(参见输出中的粗体部分)。array_append()把字符串'Central Business District'改成了数组['Central Business District', ['Circular Quay', 'Town Hall']]array_insert()方法在现有数组的末尾插入了'Liverpool'

现在,修改语句只剩下patch()方法了。

补丁()

修改文档的最后一种方法是patch()方法。这是最强大的方法,但是一旦你掌握了它,它会出奇的简单。在某种程度上,它的工作方式类似于用于对源代码进行修改的patch命令,因此得名;但是,语法不相关。

对于作为参数提供的文档所匹配的每个 JSON 字段,patch()方法有三种可能的结果:

  • 元素被删除:当字段名匹配而补丁文档中没有值时会发生这种情况。

  • 元素被修改:当字段匹配并且在补丁文档中有值时,就会发生这种情况。

  • 元素被插入:当字段不存在并且它在补丁文档中有一个值时会发生这种情况。

指定字段缺少值的方式(删除或不添加)取决于如何指定文档。如果文档被指定为一个内部写有 JSON 文档的字符串,那么缺少的值通过写null来指定。当文档被指定为字典时,使用None表示没有值。

因此,要修补文档,您需要提供一个具有所需新值的新文档。然后 MySQL 解决剩下的问题。这就是该方法的简单之处。

以阿德莱德市为例。数据已经过时,所以现在应该确定文档中存储的字段的最新值。城市面积和人口已经更改,但是无法找到Median weekly individual income的更新数量,所以您决定删除该字段。描述补丁的文档就变成了

doc = {
  "Geography": {
    "Area": 3400
  },
  "Demographics": {
    "Population": 1500000,
    "Median weekly individual income": None
  }
}

这相当简单,而且很容易看出发生了什么变化。清单 7-10 展示了进行更改的完整示例。

import mysqlx
from config import connect_args
import pprint

printer = pprint.PrettyPrinter(indent=1)

db = mysqlx.get_session(**connect_args)
schema = db.get_schema("py_test_db")
city_col = schema.get_collection("city")

# Run inside a transaction, so the
# changes can be rolled back at the end.
db.start_transaction()

# Get the current suburbs, the document
# id, and the index of Central Business
# District in the Suburbs array.
statement = city_col.find(
  "Name = :city_name")
statement.bind("city_name", "Adelaide")
before = statement.execute().fetch_one()
print("Adelaide before patching:")
print("-"*25)
printer.pprint(dict(before))
print("")

docid = before["_id"]

# Make the following changes:
#  * Increase the area to 3400
#  * Increase the population to 1500000
#  * Remove the median weekly individual
#    income.

doc = {

  "Geography": {
    "Area": 3400
  },
  "Demographics": {
    "Population": 1500000,
    "Median weekly individual income": None
  }

}

modify = city_col.modify("_id = :id")

modify.patch(doc)

modify.bind("id", docid)

modify.execute()

after = statement.execute().fetch_one()
print("Adelaide after patching:")
print("-"*24)
printer.pprint(dict(after))

# Reset the data
db.rollback()
db.close()

Listing 7-10Using patch() to Modify a Document

本例中唯一的新部分是定义了用来修补现有文档的文档,并使用了patch()方法。清单 7-11 显示了阿德莱德之前和之后的文件。

Adelaide before patching:
-------------------------
{'Country_capital': False,
 'Demographics': {'Median weekly individual income': 447,
                  'Population': 1324279},
 'Geography': {'Area': 3257.7,
               'Climate': {'Annual rainfaill': 543.9,
                           'Classification': 'Mediterranean',
                           'Mean max temperature': 22.4,
                           'Mean min temperature': 12.3},
               'Country': 'Australia',
               'Location': {'Coordinates': [138.601, -34.929], 'Type': 'Point'},
               'State': 'South Australia'},
 'Name': 'Adelaide',
 'State_capital': True,
 'Suburbs': ['Adelaide',
             'Elizabeth',
             'Wingfield',
             'Henley Beach',
             'Oaklands Park'],
 '_id': '00005af3e4f70000000000000093'}

Adelaide after patching:
------------------------
{'Country_capital': False,
 'Demographics': {'Population': 1500000},
 'Geography': {'Area': 3400,
               'Climate': {'Annual rainfaill': 543.9,
                           'Classification': 'Mediterranean',
                           'Mean max temperature': 22.4,
                           'Mean min temperature': 12.3},
               'Country': 'Australia',
               'Location': {'Coordinates': [138.601, -34.929], 'Type': 'Point'},
               'State': 'South Australia'},
 'Name': 'Adelaide',
 'State_capital': True,
 'Suburbs': ['Adelaide',
             'Elizabeth',
             'Wingfield',
             'Henley Beach',
             'Oaklands Park'],
 '_id': '00005af3e4f70000000000000093'}

Listing 7-11The Before and After Documents for Adelaide After the Patching Process

请注意这里的Median weekly individual income是如何被移除的,而PopulationArea是如何被更新的。_id的值将不同于该输出中的值,但是_id在修补前后是相同的。

小费

如果您想了解更多关于修补文档的信息,可以从提供底层功能的JSON_MERGE_PATCH() SQL 函数的文档开始。https://dev.mysql.com/doc/refman/en/json-modification-functions.html#function_json-merge-patch见。

关于修改语句的讨论到此结束。CRUD 的最后一部分是删除文档。

CRUD:删除

在大多数应用中,有些文档应该在某个时候被删除。删除文档减少了数据的大小,这不仅减少了磁盘的使用,而且使查询更有效,因为它们需要处理更少的数据。文档存储有两种从集合中删除文档的方法:

  • remove():根据条件任意删除文件。

  • remove_one():用于根据文档 ID 删除单个文档。

这些方法是最简单的 CRUD 方法,因为所需要的只是指定要删除哪些文档。remove()方法提供了一个最多删除多少文档的选项。

remove_one()方法是两种方法中最简单的一种,因为它只需要一个文档 ID 并直接返回一个Result对象。remove()方法接受一个条件,比如find()modify(),并返回一个RemoveStatement类的对象。可以使用表 7-9 中列出的方法进一步细化该陈述。

表 7-9

修改移除查询的方法

|

方法

|

争论

|

描述

|
| --- | --- | --- |
| sor t | *sort_clauses | 描述文档的排序依据。 |
| limi t | row_count | 设置要删除的最大文件数。 |
| bin d | *args | 第一个参数提供要替换的参数的名称。第二个参数提供值。每个参数调用bind()一次。 |
| execute |   | 执行删除语句。 |

这些修饰符方法现在应该都很熟悉了。与modify()方法一样,必须指定一个条件;否则,8.0.11 中会出现ProgrammingError异常:

mysqlx.errors.ProgrammingError: No condition was found for remove

在 MySQL Connector/Python 8.0.12 及更高版本中,调用remove()创建 remove 语句时必须指定条件。如果没有给定条件,将发生 TypeError 异常:

TypeError: remove() missing 1 required positional argument: 'condition'

如果需要删除所有文档,可以使用True作为条件,或者删除/重新创建集合。

清单 7-12 展示了如何首先使用文档 ID 删除一个城市,然后通过国家过滤删除几个城市。

import mysqlx
from config import connect_args

db = mysqlx.get_session(**connect_args)
schema = db.get_schema("py_test_db")
city_col = schema.get_collection("city")

# For printing information along the way
fmt = "{0:36s}: {1:2d}"

# Run inside a transaction, so the
# changes can be rolled back at the end.
db.start_transaction()

# Get the document ID for Canberra.
statement = city_col.find("Name = :city_name")
statement.fields("_id")
statement.bind("city_name", "Canberra")

result = statement.execute()
canberra_id = result.fetch_one()["_id"]

# Number of rows in the collection
# before removing any documents
print(fmt.format(
  "Initial number of documents",
  city_col.count()
))
print("")

result = city_col.remove_one(

  canberra_id)

items = result.get_affected_items_count()
print(fmt.format(
  "Number of rows deleted by remove_one",
  result.get_affected_items_count()
))
print(fmt.format(
  "Number of documents after remove_one",
  city_col.count()
))
print("")

statement = city_col.remove(   "Geography.Country = :country")

statement.bind("country", "Australia")

result = statement.execute()

print(fmt.format(
  "Number of rows deleted by remove",
  result.get_affected_items_count()
))
print(fmt.format(
  "Number of documents after remove",
  city_col.count()
))

# Reset the data
db.rollback()
db.close()

Listing 7-12Deleting Documents in a Collection

在这个例子中,堪培拉的文档 ID 首先是使用find()方法找到的。然后,在使用remove()方法删除所有剩余的澳大利亚城市之前,使用remove_one()删除 Canberra 文档 ID 的文档。在此过程中,将打印受删除操作影响的城市数和行数。输出是

Initial number of documents         : 15

Number of rows deleted by remove_one:  1
Number of documents after remove_one: 14

Number of rows deleted by remove    : 14
Number of documents after remove    :  0

集合中有 15 个城市。不出所料,remove_one()删除了一个城市,剩下 14 个城市。由于集合中只有澳大利亚的城市,使用过滤器Geography.Country = 'Australia'删除会删除剩余的 14 个文档。

文档存储世界的漫长旅程到此结束。MySQL X DevAPI 不仅仅是关于文档存储;它还能以 NoSQL 的方式处理 SQL 表并执行 SQL 查询,所以下一章会有更多的内容要深入探讨。

摘要

本章详细介绍了 MySQL 文档库。具体来说,您了解了集合以及如何使用它们。集合是相关文档的容器;例如,您可以使用一个名为city的容器来存储关于城市的信息,比如城市所在的国家、人口等等。

文档本身是以无模式方式存储数据的 JSON 文档。这允许开发人员快速向数据库添加新类型的数据,但也将保持数据一致性的任务推给了开发人员。

您已经开始学习如何从创建集合到删除集合进行操作。在这两者之间,可以创建和删除索引,还可以检索一个集合以供 create-read-update-delete (CRUD)方法使用。本章的其余部分详细介绍了每个 CRUD 方法。

MySQL X DevAPI 不仅仅用于文档存储。下一章将介绍如何对 SQL 表使用 CRUD 方法,以及如何执行任意 SQL 语句。

八、SQL 表

到目前为止,X DevAPI 的主要焦点是在 MySQL 文档存储中使用它。然而,正如第六章所解释的,它也支持“传统的”SQL 表。事实上,有两个接口可以处理 SQL 表。第七章的焦点中的创建-读取-更新-删除(CRUD)动作也存在于 SQL 表中,并提供了一个 NoSQL API。此外,X DevAPI 可用于执行任意 SQL 语句。

本章将首先概述使用 SQL 表时的工作流程,然后介绍 X DevAPI 如何通过 NoSQL API 使用 SQL 表。本章的第二部分将介绍执行 SQL 语句的接口。

工作流程

在 NoSQL API 中使用 SQL 表的工作流程与您在上一章中看到的集合非常相似。然而,当使用 SQL 语句时,工作流有些不同。在研究细节之前,有必要先了解一下一般的工作流程。

图 8-1 显示了从模式开始到结果结束的工作流程。主要关注的是获得语句对象之前的步骤。本章将详细介绍如何使用这些语句。红色(深灰色)框是被调用的方法。

img/463459_1_En_8_Fig1_HTML.jpg

图 8-1

使用 SQL 表时的工作流概述

该图从模式对象开始。有四种不同的方式继续;然而,实际上只有两条不同的路径。get_collection_as_table()用存储集合的 SQL 表返回一个 table 对象,由get_view()返回的 view 对象是一个 table 的子类,唯一的区别是用于检查对象是否存在于数据库中的查询。因此,在大多数情况下,集合(如表、视图和表)可以被视为同一事物。

一旦有了 table 对象,就可以用它来执行 insert、select、update 或 delete 语句。除了count()方法(没有包含在图中),没有像您看到的用于集合的其他方法。一旦执行了该语句,select 语句将返回一个行结果,其他方法将返回一个没有行的结果。

工作流中最大的不同是 SQL 语句,它们是直接从 schema 对象创建的。不管查询类型如何,SQL 语句总是返回 SQL 结果。

工作流就绪后,是时候看看用于 SQL 表的 NoSQL API 了。

小费

本章中有许多示例程序。列表中出现的所有示例程序都可以下载。有关使用示例程序的更多信息,参见第一章中对示例程序的讨论。一般来说,更改会被回滚,以允许重新执行示例并得到相同的结果。

SQL 表的 NoSQL API

在前一章中,您使用了文档存储,这意味着纯粹使用 NoSQL API 来管理。然而,也可以继续使用 SQL 表,但避免编写 SQL 语句。NoSQL 表 API 类似于文档存储 API,本节的讨论假设您熟悉第六章中描述的 X DevAPI。

表格 API 是本节的主题。首先,您将获得一个表对象,并获得关于该对象的信息。然后,表对象将用于执行查询。

注意

对表 API 的讨论与用于处理集合的 API 有相当大的重叠。建议在本章之前阅读第六章,以便从这个表 API 讨论中获得最大收益。

表和视图对象

表对象相当于集合对象;不同之处在于,table 对象被设计用于一般的 SQL 表,而集合专用于带有存储文档的 JSON 列的表。事实上,如果您愿意,您可以请求以表的形式检索集合。视图对象与表对象非常相似,但它是针对视图(虚拟表)而不是基表的。除非明确指出,否则表和视图的行为是相同的。

获取表格对象有三种不同的方法。它们在表 8-1 中列出。返回的对象是相对于mysqlx模块的。

表 8-1

模式表方法

|

方法

|

争论

|

描述

|
| --- | --- | --- |
| get_collection_as_table | Name | 将具有指定名称的集合作为crud.Table对象返回。如果check_existence = True,如果集合不存在,则发生ProgrammingError异常。 |
| check_existence=False |
| get_table | name | 以一个crud.Table对象的形式返回指定名称的表格。get_table()方法也可以用来打开一个作为crud.Table对象的视图。如果check_existence = True,如果表格不存在,则发生ProgrammingError异常。 |
| check_existence=False |
| get_ tables |   | 返回架构中表和视图的列表。列表中的每个元素都是crud.Table类的一个实例。 |
| get_view | name | 以一个crud.View对象的形式返回具有指定名称的视图。如果check_existence = True,如果视图不存在,将出现ProgrammingError异常。 |
| check_existence=False |

get_table()get_tables()方法相当于前一章中的get_collection()get_collections()方法。没有创建表、更改表定义或添加索引的方法。这必须使用 SQL 语句来完成。

有许多方法可以获得关于表对象的信息。这些方法总结在表 8-2 中。返回的对象是相对于mysqlx模块的。这些方法都不需要参数。

表 8-2

表和视图实用程序方法

|

方法

|

返回对象

|

描述

|
| --- | --- | --- |
| exists_in_ database |   | 根据表格是否存在,返回TrueFalse。 |
| get_ connection | connection.Connection | 返回基础连接对象。 |
| get_ name |   | 返回表的名称。这与name属性相同。 |
| get_ schema | crud.Schema | 返回表所在架构的对象。这与schema属性相同。 |
| get_ session | connection.Session | 返回会话的对象。这与会话属性相同。在 8.0.12 版本中添加。 |
| is_ view |   | 根据表格对象是否为视图,返回TrueFalse。这是基于数据库中的实际定义,而不是使用了get_table()还是get_view()来获取对象。 |

模式对象、会话对象(8.0.12 和更高版本)和表名也可以分别在schemasessionname属性中找到。到目前为止,除了表不具备集合的所有特性之外,集合之间的差别很小。对于查询,有一些区别,我们来看看。

表查询

使用 table 对象和 NoSQL 方法查询表在某种程度上是使用 SQL 语句和文档存储创建、读取、更新和删除(CRUD)方法的混合。还不支持连接表或更高级的 SQL 特性,如通用表表达式(cte)和窗口函数。可用的方法也属于 CRUD 功能之一。到目前为止,这听起来又像是集合,但是方法名反映了用于操作的 SQL 语句。

注意

使用表对象执行的查询主要是针对使用 UTF-8 字符集的表。还有一些更复杂的结构;例如,转换列的字符集,这是不支持的。在这些情况下,您必须使用直接 SQL 语句,如本章后面所述。

表格 CRUD 方法可在表格 8-3 中找到。除了count()方法之外,所有的方法都返回对应于查询类型的语句对象。语句对象类都在mysqlx.statement模块中。

表 8-3

表和视图对象的 CRUD 方法

|

方法

|

争论

|

返回对象

|

描述

|
| --- | --- | --- | --- |
| count |   |   | 以整数形式返回表中的行数。 |
| delete |   | DeleteStatement | 创建 delete 语句。 |
| insert | *字段 | InsertStatement | 创建 insert 语句,在该语句中为指定的字段设置值。 |
| select | *字段 | SelectStatement | 创建将检索指定字段的 select 语句。 |
| update |   | UpdateStatement | 创建更新语句。 |

虽然对于那些习惯于编写 SQL 语句的人来说,方法名听起来非常熟悉,但是它们的用法需要更多的解释。本节的剩余部分将逐一介绍 CRUD 方法。count()方法将作为insert()select()update()delete()方法的补充。

CRUD:创建

CRUD 的第一部分是创建数据。对于 SQL 表,这是将数据插入到表中,因此创建数据的 table 对象方法就是insert()方法。这是下面讨论的话题。

insert()方法的参数是您将为其提供数据的字段。该方法返回一个mysqlx.statement.InsertStatement类的对象。该对象可用于添加值和执行 insert 语句。表 8-4 中列出了InsertStatement类的重要方法。

表 8-4

insert 语句方法

|

方法

|

争论

|

描述

|
| --- | --- | --- |
| execute |   | 执行 insert 语句,并将结果作为一个result.Result对象返回。 |
| values | *values | 要添加到 insert 语句中的一行的值。values()可以被多次调用来插入几行。每行的值必须与字段的顺序相同。 |

InsertStatement对象有两个属性:

  • 模式:插入语句的模式对象

  • 目标:插入语句的表对象

这些属性的主要用途是当 insert 语句被传递给其他函数或方法时,返回到父对象。

要查看插入数据的工作流程,请参见清单 8-1 。这是一个在world.city表中插入两个城市的例子。返回结果后,将打印插入的行数和第一个生成的自动递增 ID。在 insert 语句前后,表中的行数是用Table. count()方法确定的。

import mysqlx
from config import connect_args

fmt = "{0:28s}: {1:4d}"

db = mysqlx.get_session(**connect_args)

# Get the world.city table
schema = db.get_schema("world")

city = schema.get_table("city")

db.start_transaction()

print(fmt.format(
  "Number of rows before insert",
  city.count()

))

# Define the insert statement

insert = city.insert(

  "Name",
  "CountryCode",
  "District",
  "Population"

)

# Add row using a list
darwin = [
  "Darwin",
  "AUS",
  "Northern Territory",
  145916
]

insert.values(darwin)

# Add row by arguments

insert.values(

  "Sunshine Coast",
  "AUS",
  "Queensland",
  302122

)

# Execute the insert

result = insert.execute()

# Get the auto-increment ID generated
# for the inserted row
print(fmt.format(
  "Number of rows inserted",
  result.get_affected_items_count()))
print(fmt.format(
  "First ID generated",
  result.get_autoincrement_value()))

print(fmt.format(
  "Number of rows after insert",
  city.count()))

# Reset the data
db.rollback()
db.close()

Listing 8-1Inserting Rows into a Table Using the 
insert()

Method

首先,使用get_table()模式方法获得表对象。insert 语句是使用insert()方法创建的,您为其指定值的四个字段作为参数给出。如果在代码流中更好地工作,字段也可以作为列表或元组给出。city表还有一个ID字段,默认情况下,它被赋予一个自动递增的值。在这个例子中,您为ID字段使用默认行为。

使用values()方法将这两行添加到 insert 语句中。Darwin 是通过首先用值创建一个元组来添加的,然后这个元组被传递给values()。阳光海岸是通过将每个字段值作为单独的参数传递给values()来添加的。

最后,使用execute()方法插入两行。返回的result.Result对象可用于检查插入的行数和生成的第一个要插入的行的自动增量 ID(Darwin)。不可能为后面的行获取自动递增 ID。

运行该示例的输出示例如下

Number of rows before insert: 4079
Number of rows inserted     :    2
First ID generated          : 4111
Number of rows after insert : 4081

生成的第一个 ID 取决于之前如何使用city表,因此通常与示例输出不同。数据插入到表中后,让我们看看如何再次检索它。

读取

read 语句是大多数数据库的主力。对于表和视图对象,select()方法用于从底层数据库对象获取数据。本节将展示如何做到这一点。

select()方法获取应该从表中检索的字段。如果没有指定字段,将包含所有字段;这相当于SELECT * FROM。该方法返回一个可用于细化查询的mysqlx.statement.SelectStatement对象。

可用于进一步定义查询的SelectStatement方法在表 8-5 中列出。它们可用于指定要包含的行必须满足的条件、要返回多少行、分组等。

表 8-5

修改 Select 语句的方法

|

方法

|

争论

|

描述

|
| --- | --- | --- |
| where | condition | 这是过滤查询结果的条件。 |
| group_ by | *fields | 描述对于涉及聚合函数的查询,要对哪些字段进行分组。 |
| having | condition | 描述在查询以其他方式得到解决后,根据什么来筛选结果(排序除外)。这对于按聚合函数的值进行过滤非常有用。 |
| order_ by | *clauses | 描述结果的排序依据。 |
| limit``offset | row_count``offset=0 | 第一个参数设置要返回的最大行数。第二个可选参数定义偏移量。默认偏移量为 0。注意:这在版本 8.0.12 中有所改变,其中 offset 已被弃用。请改用offset()方法。设置要返回的行的偏移量。在 8.0.12 版本中添加。 |
| offset |
| lock_ exclusive |   | 使语句获得独占锁。一次只能有一个语句拥有独占锁。如果稍后将在同一事务中更新行,请使用此选项。 |
| lock_ shared |   | 使语句获取共享锁。这可以防止其他语句修改匹配的行,但是它们可以读取这些行。 |
| bind | *args | 第一个参数提供要替换的参数的名称。第二个参数提供值。每个参数调用bind()一次。 |
| execute |   | 执行 select 语句。 |

一旦语句被完全定义,就可以使用execute()方法来执行。这将返回一个RowResult对象,该对象可用于获取关于结果的信息并获取行。

例如,考虑一个 select 语句,其中找到美国人口超过 1,000,000 的城市,并按州进行分组(District字段)。找到了每个州的城市数量和最大城市的人口。根据城市数量,然后根据人口最多的城市,对结果进行降序排序。这个例子可以在清单 8-2 中看到。

import mysqlx
from config import connect_args

db = mysqlx.get_session(**connect_args)

# Get the world.city table
schema = db.get_schema("world")
city = schema.get_table("city")

db.start_transaction()

statement = city.select(

  "District",
  "COUNT(*) AS NumCities",
  "MAX(Population) AS LargestCityPop")

statement.where(

  "CountryCode = :country"
  + " AND Population > :min_pop")

statement.group_by("District")

statement.order_by(

  "NumCities DESC",
  "LargestCityPop DESC")

statement.bind("country", "USA")

statement.bind("min_pop", 1000000)

print("SQL statement:\n{0}"
  .format(statement.get_sql()))

result = statement.execute()
print("Number of rows in result: {0}\n"
  .format(result.count))

fmt = "{0:12s}   {1:6d}   {2:12d}"
print("{0:12s}   {1:6s}   {2:12s}"
  .format(
    "State",
    "Cities",
    "Largest City"
))
print("-"*37)

for row in result.fetch_all():

  print(fmt.format(
    row["District"],
    row["NumCities"],
    row["LargestCityPop"]
  ))

print("")
print("Number of rows in result: {0}\n"
  .format(result.count))

db.commit()
db.close()

Listing 8-2Example of Using a Select Statement

检索到表对象后,创建语句并定义结果中应包含的字段。然后使用where()方法设置一个过滤器,并设置分组和排序。传递给where()方法的值使用两个命名参数countrymin_pop,以允许稍后使用bind()方法绑定这些值。这确保了报价对于所提供的类型是正确的(但并不确保它是正确的类型!)并允许您在需要再次执行相同的查询(但两个参数的值不同)时重用语句的其余部分。

在执行语句之前,打印生成的 SQL。get_sql()方法是SelectStatement类独有的。该方法采用SelectStatement并构建 SQL 语句,该语句将作为已定义查询的结果来执行。如果您希望通过 MySQL Shell 手动执行查询,或者需要将生成的 SQL 与查询所基于的 SQL 语句进行比较,这将非常有用。

获得结果后,打印结果中的行数(在检索结果后再次打印)。使用fetch_all()方法获得结果,该方法返回一个result.Row对象。在打印结果时,Row对象可以用作字典。输出是

SQL statement:
SELECT District,COUNT(*) AS NumCities,MAX(Population) AS LargestCityPop FROM world.city WHERE CountryCode = :country AND Population > :min_pop GROUP BY District ORDER BY NumCities DESC,LargestCityPop DESC

Number of rows in result: 0

State          Cities   Largest City
------------------------------------
Texas               3        1953631
California          2        3694820
New York            1        8008278
Illinois            1        2896016
Pennsylvania        1        1517550
Arizona             1        1321045

Number of rows in result: 6

输出中最值得注意的是,结果中的行数在检索结果之前报告为 0,而在检索结果之后报告为 6。这在第六章中针对RowResult对象进行了讨论:count 属性是在使用fetch_all()方法读取行时设置的。

现在让我们更新表格中的数据。

CRUD:更新

对表的更新用新值替换一个或多个字段的值。与文档不同,无法添加新字段或删除现有字段;这些操作需要更改表定义。下面的讨论将详细介绍表更新在 MySQL Connector/Python 的 CRUD 世界中是如何工作的。

update()方法本身不接受任何参数。调用它的唯一目的是创建一个可以用来定义更新的UpdateStatement对象。做这件事的方法可以在表 8-6 中看到。

表 8-6

方法来定义更新语句

|

方法

|

争论

|

描述

|
| --- | --- | --- |
| set | field | 为给定字段设置新值。该值必须是标量。 |
| value |
| where | condition | 这是筛选应该更新哪些行的条件。 |
| order_by | *clauses | 描述行的更新顺序。在 8.0.12 版本中添加。 |
| sort | *sort_clauses | 描述行的更新顺序。注意:这在 8.0.12 版本中已被弃用;用order_by()代替。 |
| limit | row_count | 该参数设置要更新的最大行数。 |
| bind | *args | 第一个参数提供要替换的参数的名称。第二个参数提供值。每个参数调用bind()一次。 |

需要注意的一点是,绑定参数不能与set()方法一起使用。如果新值是基于用户输入的,请确保像往常一样验证输入。该语句在执行时返回一个result.Result对象。

警告

在语句中使用用户输入之前,请始终验证用户输入。这不仅适用于 update 语句,甚至适用于 X DevAPI。这应该是处理用户输入的标准部分。

执行该语句时,必须定义一个where条件。否则,将会出现 ProgrammingError 异常:

mysqlx.errors.ProgrammingError: No condition was found for update

这是一种安全预防措施,旨在避免意外更新表中的所有行。如果您真的想更新所有的行,将where()条件设置为True或类似的评估为True的条件:

update.where(True)

该要求不仅意味着您不会因为缺少条件而错误地更新所有行,设置为True的条件还有助于证明您确实想要更新所有行。

清单 8-3 展示了一个城市人口被更新的例子。在更新之前和之后打印填充,以验证 update 语句的效果。

import mysqlx
from config import connect_args

db = mysqlx.get_session(**connect_args)

# Get the world.city table
schema = db.get_schema("world")
city = schema.get_table("city")

db.start_transaction()

# Check the population before the update
select = city.select()
select.where(
  "Name = :city"
  + " AND CountryCode = :country"
)
select.bind("city", "Sydney")
select.bind("country", "AUS")
result = select.execute()
sydney = result.fetch_one()
print("Old population: {0}".format(
  sydney["Population"]))

# Define the update

update = city.update()

update.set("Population", 5000000)

update.where(

  "Name = :city"
  + " AND CountryCode = :country")

update.bind("city", "Sydney")

update.bind("country", "AUS")

result = update.execute()

print("Number of rows updated: {0}"
  .format(
    result.get_affected_items_count())
)

# Check the affect
result = select.execute()
sydney = result.fetch_one()
print("New population: {0}".format(
  sydney["Population"]))

# Reset the data
db.rollback()
db.close()

Listing 8-3Using an Update Statement

定义 update 语句的步骤遵循 X DevAPI CRUD 方法的通常模式。该程序的输出是

Old population: 3276207
Number of rows updated: 1
New population: 5000000

这就剩下了最后一个 CRUD 方法:如何删除行。

CRUD:删除

关于 CRUD 方法,最后要讨论的是如何删除表中的行。这是通过使用带有可选过滤条件、排序和限制的delete()表格方法完成的。

delete()方法采用一个可选参数来定义过滤行的条件,该方法返回一个DeleteStatement对象。可以通过DeleteStatement对象的方法对删除语句进行进一步的细化。这些方法在表 8-7 中列出。

表 8-7

方法来定义 Delete 语句

|

方法

|

争论

|

描述

|
| --- | --- | --- |
| where | condition | 这是筛选应该删除哪些行的条件。 |
| order_by | *clauses | 描述删除行的顺序。在 8.0.12 版本中添加。 |
| sort | *sort_clauses | 描述删除行的顺序。注意:这在 8.0.12 版本中已被弃用;use order_by()相反。 |
| limit | row_count | 该参数设置要删除的最大行数。 |
| bind | *args | 第一个参数提供要替换的参数的名称。第二个参数提供值。每个参数调用bind()一次。 |
| execute |   | 执行 delete 语句。 |

如果您很好地掌握了其他 CRUD 方法,delete 语句的使用就很简单了。与update()方法一样,必须设置一个条件;否则出现ProgrammingError:

mysqlx.errors.ProgrammingError: No condition was found for delete

如果需要删除表中的所有行,将条件设置为True。或者,您可以使用TRUNCATE TABLE SQL 语句重新创建表。

清单 8-4 展示了一个删除所有人口少于 1000 的城市的例子。

import mysqlx
from config import connect_args

fmt = "{0:22s}: {1:4d}"

db = mysqlx.get_session(**connect_args)

# Get the world.city table
schema = db.get_schema("world")
city = schema.get_table("city")

db.start_transaction()

# Check the number of rows before
# deleting rows.
print(fmt.format(
  "Number of rows before",
  city.count()
))

# Define the update

delete = city.delete()

delete.where("Population < :min_pop")

delete.bind("min_pop", 1000)

result = delete.execute()

print(fmt.format(
  "Number of rows deleted",
  result.get_affected_items_count()
))

# Check the affect
print(fmt.format(
  "Number of rows after",
  city.count()
))

# Reset the data
db.rollback()
db.close()

Listing 8-4Deleting Rows from a Table

使用where()方法指定过滤条件。它也可以在第一次创建 delete 语句时指定。输出是

Number of rows before : 4079
Number of rows deleted:   11
Number of rows after  : 4068

关于 SQL 表的 CRUD 方法的讨论到此结束。如上所述,这些方法目前有一些局限性。如果您需要生成不受支持的查询,仍然可以使用 SQL 语句,这将在本章的剩余部分讨论。

SQL 语句

到目前为止,关于 X DevAPI 的讨论都是关于 CRUD 方法,无论是文档集合还是 SQL 表。优秀的旧 SQL 语句哪里去了?他们仍然在这里,这就是本节的内容。

在某些方面,我把最简单的留到了最后,但它也是当前执行最多不同查询的一个。这两件事是相关的,因为 SQL 语句并没有对您在特定 MySQL 服务器版本限制之外的用途施加任何限制。这也意味着不可能在相同的程度上知道每个语句是关于什么的,因此也不可能具体地知道它是如何工作的。这使得它比 CRUD 方法更简单。当然,代价是,它在更大程度上是由开发商来照顾的事情。

小费

X DevAPI 的 SQL 语句功能远不如使用mysql.connector模块时完整。如果您需要的不仅仅是简单的查询,建议使用第 2 到第五章中描述的方法。这包括所有需要参数的情况。

本节将介绍如何使用 X DevAPI 执行 SQL 语句。

执行 SQL 语句

执行 SQL 语句非常简单。使用sql()方法直接从会话中创建 SQL 语句,该方法执行 SQL 语句。返回SqlStatement对象。

SqlStatement类很简单,只有两个方法,总结在表 8-8 中。这两种方法都不带任何参数。

表 8-8

方法来处理 SQL 语句

|

方法

|

返回

|

描述

|
| --- | --- | --- |
| execute | result.SqlResult对象 | 将查询发送到 MySQL 服务器。返回一个mysqlx.result.SqlResult类的对象。 |
| is_doc_ based | Boolean | 该语句是否用于集合。它总是为一个SqlStatement返回False,当一个方法或函数可以处理几种不同类型的语句时最有用。 |

结果对象总是属于SqlResult类,它结合了对修改数据或模式的查询和获取数据的查询有用的信息。

清单 8-5 显示了一个查询德国各州的例子,其中至少有一个城市的人口超过 500,000。

import mysqlx
from config import connect_args

db = mysqlx.get_session(**connect_args)

sql = db.sql("""

SELECT CONVERT(District USING utf8mb4)

         AS District,
       COUNT(*) AS NumCities
  FROM world.city
 WHERE CountryCode = 'DEU'
       AND Population > 500000
 GROUP BY District
 ORDER BY NumCities DESC, District""")

result = sql.execute()

fmt = "{0:19s}   {1:6d}"
print("{0:19s}   {1:6s}".format(
  "State", "Cities"))
print("-"*28)

row = result.fetch_one()

while row:
  print(fmt.format(
    row["District"],
    row["NumCities"]
  ))
  row = result.fetch_one()

db.close()

Listing 8-5Querying Data with a SELECT SQL Statement

首先要注意的是,在查询中,District列被显式转换为utf8mb4。如上所述,这样做的原因是 X DevAPI 只希望返回 UTF-8 数据。如果未完成此转换,将返回一个错误:

UnicodeDecodeError: 'utf-8' codec can't decode byte 0xfc in position 7: invalid start byte

这是另一个更好使用mysql.connector模块中的遗留 API 的例子。如示例所示,解决方法是转换查询中的数据。

结果的处理类似于select() CRUD 方法的结果。可以使用fetch_one()方法一次获取一行,或者使用fetch_all()方法获取所有行。在这种情况下使用前者。输出是

State                 Cities
----------------------------
Nordrhein-Westfalen        5
Baden-Württemberg          1
Baijeri                    1
Berliini                   1
Bremen                     1
Hamburg                    1
Hessen                     1
Niedersachsen              1

SqlStatment类还有一个值得讨论的特性:如何处理返回多个结果集的查询。

具有多个结果集的查询

在第四章中,您看到了在mysql.connector模块中使用遗留 API 时处理来自一个查询的多个结果集。mysqlx模块中的 X DevAPI 也可以处理多个结果集,但是没有额外的功能。

第一个结果集的处理方式与上一个示例相同。不同之处在于,一旦处理了第一个结果,就可以使用SqlResult.next_result()方法重新初始化该结果。这允许您处理下一个结果。根据是否有更多的结果要处理,next_result()方法返回TrueFalse

为了了解这在实践中是如何工作的,考虑清单 8-6 中的world.top_cities存储过程。这类似于第四章中使用的存储过程。

DROP PROCEDURE IF EXISTS world.top_cities;
DELIMITER $$
CREATE PROCEDURE world.top_cities(
    IN in_country char(3)
)
SQL SECURITY INVOKER
BEGIN
  SELECT Name, District, Population
    FROM world.city
   WHERE CountryCode = in_country
         AND Population
   ORDER BY Population ASC
   LIMIT 3;

  SELECT Name, District, Population
    FROM world.city
   WHERE CountryCode = in_country
         AND Population
   ORDER BY Population DESC
   LIMIT 3;
END$$
DELIMITER ;

Listing 8-6The world.top_cities Stored Procedure

该过程返回两个结果集:首先,找到该国人口最少的三个城市,然后找到人口最多的三个城市。清单 8-7 展示了一个处理两个结果集的例子。

import mysqlx
from config import connect_args

db = mysqlx.get_session(**connect_args)

sql = db.sql(

  "CALL world.top_cities('USA')")
result = sql.execute()

fmt = "{0:11s}   {1:14s}   {2:10d}"
print("{0:11s}   {1:14s}   {2:10s}"
  .format(
    "City", "State", "Population"
  )
)

more = True

while more:

  print("-"*41)
  row = result.fetch_one()
  while row:
    print(fmt.format(
      row["Name"],
      row["District"],
      row["Population"]
    ))
    row = result.fetch_one()
  more = result.next_result()

db.close()

Listing 8-7Handling Multiple Result Sets in an X DevAPI SQL Statement

查询照常执行。第一个结果集被正常处理。它被包裹在一个while循环中。处理完第一个结果后,通过调用next_result()来搜索更多的结果。这也将重置结果对象以处理下一个结果。一旦处理完所有结果集,next_result()返回False,循环终止。输出是

City          State            Population
-----------------------------------------
Charleston    South Carolina        89063
Carson        California            89089
Odessa        Texas                 89293
-----------------------------------------
New York      New York            8008278
Los Angeles   California          3694820
Chicago       Illinois            2896016

MySQL 连接器/Python X DevAPI 中关于 SQL 语句的讨论到此结束。

摘要

本章介绍了如何在 X DevAPI 中使用 SQL 表。有两种选择:使用 NoSQL CRUD 方法或执行 SQL 语句。

用于 SQL 表的 NoSQL CRUD 接口非常相似,但比前一章中用于 MySQL 文档存储的接口更简单。CRUD 方法是根据执行该方法底层操作的 SQL 语句来命名的。例如,为了读取数据,使用了select()方法。不支持使用 NoSQL API 更改 SQL 表的模式。

支持使用mysqlx.Session.sql()方法执行任意 SQL 语句。它对于简单的查询很有用;然而,对于更复杂的任务以及向查询中添加用户输入时,建议使用mysql.connector模块的方法。

本章从 MySQL 连接器/Python 的角度完成了 X DevAPI 的演练。还剩下两项非常重要的任务:处理错误和对 MySQL Connector/Python 程序进行故障排除。接下来将讨论这些主题。

九、错误处理

前八章集中于特定的用例:安装、执行查询、处理结果等。在一些地方,提到了错误处理的重要性,但是没有提供太多的细节。这种情况即将改变,因为这一章专门讨论错误处理。

错误处理是所有编程中最重要的主题之一,不仅仅是在使用 MySQL Connector/Python 时。你可以说它和测试应该是你学习的两个首要主题。这种说法有很大程度的真实性;然而,我决定将错误处理放在本书的倒数第二章。不是因为它不重要(短语“最后但并非最不重要”当然适用于本章和下一章关于故障排除的内容),而是因为两个原因:首先,这不是一本像使用 MySQL Connector/Python 那样关于编程的书,所以假设您已经很好地掌握了编程最佳实践。第二,它允许我给例子更多的上下文。

注意

不要将错误处理和测试视为次要任务。确保它们的优先级至少和实现实际功能的优先级一样高。这并不是 MySQL Connector/Python 独有的。

本章将从 MySQL 服务器中的警告、错误和严格模式开始。然后,您将继续讨论 MySQL Connector/Python 本身,在这里,讨论的第一部分将是一般的警告和错误处理、警告配置以及如何获取警告。第二部分将讨论 MySQL 错误号和 SQL 状态。最后,第三部分将概述mysql.connectormysqlx模块的错误类别。

小费

本章中有许多示例程序。列表中出现的所有示例程序都可以下载。有关使用示例程序的更多信息,参见第一章中对示例程序的讨论。

MySQL 服务器中的警告、错误和严格模式

在讨论 MySQL Connector/Python 中的错误处理时,在连接的 MySQL 服务器端需要考虑一些事情。本节将查看配置设置,以指定注释级消息是否应被视为警告,严格模式如何工作,以及应用如何将消息记录在 MySQL 错误日志中。

将注释级别的消息视为警告

执行语句时发生的事件有三个严重级别。最严重的是一个总是阻止语句完成的错误。下一个级别是警告,允许语句完成,但向用户或应用返回警告,以便决定要做什么。严重性最低的是音符级别,这是本次讨论的主题。

默认情况下,语句的注释级消息(例如,如果数据库存在,并且您试图使用CREATE DATABLASE IF NOT EXISTS创建它)会导致出现警告。例如:

mysql> CREATE SCHEMA IF NOT EXISTS py_test_db;
Query OK, 1 row affected, 1 warning (0.28 sec)

mysql> SHOW WARNINGS\G
*************************** 1\. row ***************************
  Level: Note
   Code: 1007
Message: Can't create database 'py_test_db'; database exists
1 row in set (0.00 sec)

请注意,在SHOW WARNINGS输出中,电平为Note。可以避免产生警告的注释级消息。这是通过改变sql_notes选项的值来实现的。当sql_notes的值为ON(默认值)时,会产生一个警告。如果是OFF,则不产生警告。该选项可以针对会话进行更改,因此,如果您通常希望发出警告,但对于给定的语句,您希望禁用它,则可以禁止显示该消息。要暂时隐藏注释级别的消息,您可以使用以下工作流程:

mysql> SET SESSION sql_notes = OFF;
Query OK, 0 rows affected (0.00 sec)

-- Execute statements

mysql> SET SESSION sql_notes = ON;
Query OK, 0 rows affected (0.00 sec)

因此,如果您知道一个查询将导致一个注释级消息,那么您可以在执行该语句时更改会话的值sql_notes。然而,一般来说,最好是通过改变陈述来避免该消息。

小费

建议启用sql_notes,仅对您知道不需要警告的特定语句禁用它。

严格模式

服务器端配置的第二部分是严格模式。当启用严格模式时,它会告诉 MySQL 将无效数据视为错误而不是警告。在 MySQL 的旧版本中,默认情况下会容忍不适合表的数据,并尽最大努力使其适合。这使得开发应用变得更加容易,但是这样做的一个主要缺点是,它可能会导致数据库以与预期不同的数据结束。

强制数据适应的操作示例有将字符串转换为整数或截断数据。考虑名为table_1的表,其定义如下:

mysql> CREATE SCHEMA db1;
Query OK, 1 row affected (0.41 sec)

mysql> CREATE TABLE db1.table_1 (
          id int unsigned NOT NULL PRIMARY KEY,
          val varchar(5)
       ) ENGINE=InnoDB;
Query OK, 0 rows affected (0.24 sec)

在未启用严格模式的情况下,尝试插入六个字符的值将会导致警告,但仍会插入值被截断为五个字符的行:

mysql> INSERT INTO db1.table_1 VALUES (1, 'abcdef');
Query OK, 1 row affected, 1 warning (0.15 sec)

mysql> SHOW WARNINGS\G
*************************** 1\. row ***************************
  Level: Warning
   Code: 1265
Message: Data truncated for column 'val' at row 1
1 row in set (0.00 sec)

mysql> SELECT * FROM table_1;
+----+-------+
| id | val   |
+----+-------+
|  1 | abcde |
+----+-------+
1 row in set (0.00 sec)

在 MySQL 5.7 和更高版本中,默认情况下为事务表启用严格模式(InnoDBNDBCluster存储引擎)。在这种情况下,如果数据不合适,就会出现错误。防止插入的严格模式的一个例子是

mysql> INSERT INTO db1.table_1 VALUES (2, 'ghijkl');
ERROR 1406 (22001): Data too long for column 'val' at row 1

事务表的严格模式是通过在 SQL 模式列表中包含STRICT_TRANS_TABLES来实现的。有关 SQL 模式的更多信息,请参见 https://dev.mysql.com/doc/refman/en/sql-mode.html 。从 MySQL 连接器/Python 内部,可以使用sql_mode连接选项设置 SQL 模式。

STRICT_TRANS_TABLES SQL 模式相关的是innodb_strict_mode选项。此选项仅适用于 InnoDB 表格。SQL 模式控制数据修改语言(DML)查询,而innodb_strict_mode选项控制数据定义语言(DDL)查询,如CREATE TABLEALTER TABLECREATE INDEX。由innodb_strict_mode选项触发错误的一个最常见的原因是,当创建一个表时,其定义将导致该表的最大可能行大小超过 InnoDB 的限制。

强烈建议同时启用STRICT_TRANS_TABLES SQL 模式和innodb_strict_mode选项。在开始开发应用之前启用它,这样您就可以尽早得到关于不兼容问题的警告。

小费

与完成应用之后相比,在初始开发阶段修复违反严格模式的情况要容易得多。因此,在开始编码之前,启用严格模式;从长远来看,这会节省你的工作。

MySQL 错误日志

在 MySQL 服务器端需要注意的最后一点是,应用执行(或不执行)的操作可能会触发 MySQL 错误日志中的消息。例如,如果应用试图使用无效凭据进行连接,或者它没有正确关闭其连接,则可能会出现类似于以下示例的消息:

2018-03-03T04:10:19.943401Z 52 [Note] [MY-010926] Access denied for user 'pyuser'@'localhost' (using password: YES)

2018-03-03T04:10:28.330173Z 53 [Note] [MY-010914] Aborted connection 53 to db: 'unconnected' user: 'pyuser' host: 'localhost' (Got an error reading communication packets).

第一条消息说来自localhostpyuser用户试图使用密码进行连接,但是密码是错误的(或者该用户不存在)。第二条消息指出,在试图从其中一个连接中读取数据时出现了错误。在这种情况下,是因为连接消失了。

这些消息只有在log_error_verbosity MySQL 服务器选项设置为 3 时才会显示。建议在开发过程中确保这一点,并定期检查错误日志以捕获应用触发的所有消息。这可以通过在 MySQL 配置文件中设置选项来实现。在 MySQL 8.0 中,也可以使用如下的SET PERSIST语句来实现:

mysql> SET PERSIST log_error_verbosity = 3;
Query OK, 0 rows affected (0.04 sec)

这段代码设置当前值,并在 MySQL 重启时保存该值。

关于 MySQL 服务器已经说得够多了。下一个主题是 MySQL 连接器/Python 本身的警告和错误处理。

警告和错误处理

当您在程序中使用 MySQL Connector/Python 时,您会遇到内置 Python 异常和您使用的 MySQL Connector/Python 模块的自定义异常的混合。此外,还有一个子模块将 MySQL 错误代码作为常量。本节将介绍与警告相关的配置以及如何获取警告。MySQL 错误号、SQL 状态和异常类将在接下来的两节中讨论。

配置

当您使用mysql.connector模块中的方法时,可以配置 MySQL 连接器/Python 应该如何处理警告。有两个选项:是否自动获取查询的所有警告,以及是否将警告提升为异常。

MySQL 处理三种不同严重级别的错误消息:

  • 注意:这只是一个关于所发生事情的通知。这通常不是问题的征兆。例如,当您使用CREATE DATABASE IF NOT EXISTS创建一个数据库(模式)并且该数据库确实存在时,就会出现一个注释。在某些情况下,如果注意到了,通常这可能是潜在问题或不良做法的迹象。因此,您不应该自动忽略音符级别的信息。默认情况下,注释级消息被视为警告;这由sql_notes MySQL 服务器选项控制。

  • 警告:这是不妨碍 MySQL 继续下去的事情,但是行为可能不是你所期待的。例如,如果您提供的值不符合列定义,MySQL 会截断或转换所提供的值,就会发生这种情况。如果 MySQL 服务器启用了严格模式,一些警告(如示例中的警告)可能会升级为错误。

  • 错误:这是针对阻止 MySQL 执行查询的情况。他们总是会在 MySQL Connector/Python 中抛出一个异常。一个例子是出现了重复键错误。

一般来说,建议认真对待所有警告和错误。警告通常是事情不正常的迹象,在发展的第一阶段处理警告可以避免以后的重大灾难。

小费

如果您从应用开发的一开始就处理所有警告,您就不会被意外的数据转换或其他问题所困扰。如果警告被忽视,那么一个字符的输入错误在被发现之前可能会导致多年的问题。

当使用mysql.connector模块连接到 MySQL 时,有两个选项控制 MySQL 连接器/Python 如何处理警告。它们在表 9-1 中列出。

表 9-1

警告相关选项

|

名字

|

缺省值

|

描述

|
| --- | --- | --- |
| get_warnings | False | 当设置为True时,每次查询后自动提取警告。这使得无需手动执行SHOW WARNINGS即可获取警告。 |
| raise_on_warnings | False | 当设置为True时,警告会引发异常。设置raise_on_warnings总是将get_warnings设置为相同的值。注意:在获取警告之前,不会引发异常。对于有结果的查询,这意味着何时提取行。 |

这两个选项仅在使用游标时适用。在撰写本文时,没有选项可以改变在mysqlx模块中使用 X DevAPI 时的警告行为。

也可以在连接完成后改变get_warningsraise_on_warnings的值。例如,这对于临时启用或禁用设置非常有用,如以下代码片段所示:

import mysql.connector

db = mysql.connector.connect(
  get_warnings=True,
  raise_on_warnings=True,
  option_files="my.ini",
)

cursor = db.cursor()

db.get_warnings = False

db.raise_on_warnings = False

cursor.execute(
  "CREATE SCHEMA IF NOT EXISTS py_test_db")

db.get_warnings = True

db.raise_on_warnings = True

db.close()

建议至少在开发期间启用raise_on_warningsget_warnings。在生产中,也建议至少检查警告。与手动获取警告相比,get_warnings选项没有任何开销,因为只有当查询返回警告时才会执行SHOW WARNINGS语句。当get_warnings启用时,可以使用fetchwarnings()方法检索警告。谈到fetchwarnings(),我们来看看警告是如何获取的。

在 cmd_query()之后提取警告

mysql.connector模块中,获取警告的方式取决于是通过连接对象还是游标执行查询。在这两种情况下,都使用SHOW WARNINGS语句来检索警告,但是游标允许您让它为您处理这个问题。

当您直接通过连接对象执行查询时,您必须自己获取警告。此外,您必须注意在获取警告之前获取所有的行,因为否则您将得到一个错误,即您有未读的行。如果您启用了consume_results,则应格外小心,因为在这种情况下获取警告将导致原始结果被取消。

警告

如果您已经启用了consume_results,那么执行SHOW WARNINGS来获得查询的警告将会消除任何未完成的行。

根据使用 C 扩展还是纯 Python 实现,在使用cmd_query()时处理警告的方式有所不同。因此,这两种情况都值得一看。

清单 9-1 展示了一个使用 C 扩展实现的例子,警告是在一个CREATE TABLE语句和一个SELECT语句之后获取的。

import mysql.connector

def print_warnings(warnings):

  if mysql.connector.__version_info__[0:3] > (8, 0, 11):
    (warnings, eof) = warnings

  for warning in warnings:
    print("Level  : {0}".format(
      warning[0]))
    print("Errno  : {0}".format(
      warning[1]))
    print("Message: {0}".format(
      warning[2]))

db = mysql.connector.connect(
  option_files="my.ini", use_pure=False)

# This example only works with the C
# Extension installed. Exit if that is
# not the case.
is_cext = isinstance(
  db,
  mysql.connector.connection_cext.CMySQLConnection
)
if not is_cext:
  print("The example requires the C "
    + "Extension implementation to be "
    + "installed")
  exit()

print("Using the C Extension implementation\n")

# Ensure the DDL statement will cause
# a warnings by executing the same
# CREATE SCHEMA IF NOT EXISTS statement
# twice.
db.cmd_query(
  "CREATE SCHEMA IF NOT EXISTS py_test_db")

# For a DDL statement
result = db.cmd_query(
  "CREATE SCHEMA IF NOT EXISTS py_test_db")

print("Warnings for CREATE SCHEMA:")
print("---------------------------")

print("DDL: Number of warnings: {0}"

  .format(result["warning_count"]))

# Get the warnings

db.cmd_query("SHOW WARNINGS")

warnings = db.get_rows()

print_warnings(warnings)
db.free_result()
print("")

# Try a SELECT statement
result = db.cmd_query("SELECT 1/0")
rows = db.get_rows()
db.free_result()

print("Warnings for SELECT:")
print("--------------------")

print("SELECT: Number of warnings: {0}"

  .format(db.warning_count))

# Get the warnings
db.cmd_query("SHOW WARNINGS")
warnings = db.get_rows()
print_warnings(warnings)

db.close()

Listing 9-1Checking Warnings with the C Extension Implementation and cmd_query()

警告打印在print_warnings()功能中。在 8.0.12 版和更高版本中使用 C 扩展时,由于有一个变化,所以eof包也包括在内,因此有必要拥有版本相关的代码。__version_info__房产用于此。

对于CREATE TABLE语句,cmd_query()返回的结果直接将警告数作为warning_count元素。对于SELECT的说法,稍微复杂一点。首先需要使用结果,然后可以在连接对象的warning_count属性中找到警告的数量。

警告本身是使用SHOW WARNINGS语句获取的,这些语句像任何其他语句一样执行。输出是

Using the C Extension implementation

Warnings for CREATE SCHEMA:
---------------------------
DDL: Number of warnings: 1
Level  : Note
Errno  : 1007
Message: Can't create database 'py_test_db'; database exists

Warnings for SELECT:
--------------------
SELECT: Number of warnings: 1
Level  : Warning
Errno  : 1365
Message: Division by 0

每个 warning()有三个元素:严重性(注意、警告或错误)、错误号(将在本章后面讨论)和描述错误的错误消息。如果 C 扩展名不可用,程序将退出并显示错误:

The example requires the C Extension implementation to be installed

如果使用纯 Python 实现,会有一些不同。首先,SELECT语句的警告计数可以在所有版本的get_row()get_rows()返回的eof部分中找到。另一件事是SHOW WARNINGS语句()的结果在 MySQL Connector/Python 8.0.11 中是以字节数组的形式返回的,所以必须解码。清单 9-2 展示了 8.0.12 及更高版本的示例的纯 Python 等价物。代码示例包括文件 Chapter _ 09/listing _ 9 _ 2 _ version _ 8 _ 0 _ 11 . py 中的 8.0.11 和更早版本。

import mysql.connector

def print_warnings(warnings):
  for warning in warnings:
    print("Level  : {0}".format(
      warning[0]))
    print("Errno  : {0}".format(
      warning[1]))
    print("Message: {0}".format(
      warning[2]))

db = mysql.connector.connect(
  option_files="my.ini", use_pure=True)

print("Using the pure Python implementation\n")

# Ensure the DDL statement will cause
# a warnings by executing the same
# CREATE SCHEMA IF NOT EXISTS statement
# twice.
db.cmd_query(
  "CREATE SCHEMA IF NOT EXISTS py_test_db")

# For a DDL statement
result = db.cmd_query(
  "CREATE SCHEMA IF NOT EXISTS py_test_db")

print("Warnings for CREATE SCHEMA:")
print("---------------------------")
print("DDL: Number of warnings: {0}"
  .format(result["warning_count"]))

# Get the warnings

db.cmd_query("SHOW WARNINGS")

(warnings, eof) = db.get_rows()

print_warnings(warnings)
print("")

# Try a SELECT statement
result = db.cmd_query("SELECT 1/0")
(rows, eof) = db.get_rows()

print("Warnings for SELECT:")
print("--------------------")
print("SELECT: Number of warnings: {0}"
  .format(eof["warning_count"]))

# Get the warnings

db.cmd_query("SHOW WARNINGS")

(warnings, eof) = db.get_rows()

print_warnings(warnings)

db.close()

Listing 9-2Checking Warnings with the Pure Python Implementation and cmd_query()

该示例经历了与前面相同的步骤,但是这一次在获取行时从eof部分检索到了SELECT语句的警告数。和以前一样,警告计数只有在获取了所有行()后才可用。除了头之外,该示例的输出与之前相同:

Using the pure Python implementation

Warnings for CREATE SCHEMA:
---------------------------
DDL: Number of warnings: 1
Level  : Note
Errno  : 1007
Message: Can't create database 'py_test_db'; database exists

Warnings for SELECT:
--------------------
SELECT: Number of warnings: 1
Level  : Warning
Errno  : 1365
Message: Division by 0

如果使用游标,事情通常会简单一些。让我们看看光标和警告是如何工作的。

用游标提取警告

用光标获取警告时所做的工作原则上与使用cmd_query()方法获取警告时相同。然而,许多工作是由光标在后台处理的,这总体上使它更容易使用。

清单 9-3 显示了清单 9-1 和清单 9-2 中检查的等价示例,只是这次使用了一个光标,并启用了get_warnings

import mysql.connector

def print_warnings(warnings):
  for warning in warnings:
    print("Level  : {0}".format(
      warning[0]))
    print("Errno  : {0}".format(
      warning[1]))
    print("Message: {0}".format(
      warning[2]))

print("Using cursors\n")

db = mysql.connector.connect(
  option_files="my.ini")

cursor = db.cursor()

# Ensure the DDL statement will cause
# a warnings by executing the same
# CREATE SCHEMA IF NOT EXISTS statement
# twice.
cursor.execute(
  "CREATE SCHEMA IF NOT EXISTS py_test_db")

# Enable retriaval of warnings

db.get_warnings = True

# For a DDL statement
cursor.execute(
  "CREATE SCHEMA IF NOT EXISTS py_test_db")

# Get the warnings

warnings = cursor.fetchwarnings()

print("Warnings for CREATE SCHEMA:")
print("---------------------------")
print("DDL: Number of warnings: {0}"
  .format(len(warnings)))
print_warnings(warnings)
print("")

# Try a SELECT statement
cursor.execute("SELECT 1/0")

rows = cursor.fetchall()

# Get the warnings
warnings = cursor.fetchwarnings()

print("Warnings for SELECT:")
print("--------------------")
print("SELECT: Number of warnings: {0}"
  .format(len(warnings)))
print_warnings(warnings)

db.close()

Listing 9-3Fetching Warnings Using a Cursor with get_warnings Enabled

在执行任何查询之前,get_warnings选项被启用。这也可以在选项文件中完成,或者作为mysql.connector.connect()函数的一个单独的参数。

启用get_warnings后,DDL 和SELECT语句获得警告的工作流是相同的。这是这种方法的主要优点。使用光标的fetchwarnings()方法获取警告。这将以与上一示例相同的方式返回警告列表。发现警告的数量是列表的长度。对于SELECT语句,您必须在获取警告之前检索结果集中的所有行。输出与清单 9-1 和清单 9-2 相同:

Using cursors

Warnings for CREATE SCHEMA:
---------------------------
DDL: Number of warnings: 1
Level  : Note
Errno  : 1007
Message: Can't create database 'py_test_db'; database exists

Warnings for SELECT:
--------------------
SELECT: Number of warnings: 1
Level  : Warning
Errno  : 1365
Message: Division by 0

用 X DevAPI 获取警告

使用 X DevAPI 时对警告的处理类似于它对游标的工作方式。最大的区别是警告是结果对象的一部分。这确保了不管使用 X DevAPI 的哪一部分和执行的查询类型如何,都有一个统一的处理警告的方法。

无论返回哪种结果对象,警告的处理都使用相同的两种方法。这两种方法是

  • get_warnings():返回查询生成的警告元组列表

  • get_warnings_count():返回一个带有警告数的整数

不需要在查询前启用警告。警告总是可用的。

作为一个例子,让我们重复一下用于cmd_query()和光标的例子,看看在使用 X DevAPI 的程序中如何处理警告。结果代码可以在清单 9-4 中看到。

import mysqlx
from config import connect_args

db = mysqlx.get_session(**connect_args)

# Ensure the DDL statement will cause
# a warnings by executing the same
# CREATE SCHEMA IF NOT EXISTS statement
# twice.
sql = db.sql(
  "CREATE SCHEMA IF NOT EXISTS py_test_db")
sql.execute()

# For a DDL statement
sql = db.sql(
  "CREATE SCHEMA IF NOT EXISTS py_test_db")
result = sql.execute()

# Get the warnings
print("Warnings for CREATE SCHEMA:")
print("---------------------------")
print("DDL: Number of warnings: {0}"
  .format(result.get_warnings_count()))

print(result.get_warnings())

print("")

# Try a SELECT statement
sql = db.sql("SELECT 1/0")
result = sql.execute()

row = result.fetch_all()

# Get the warnings
print("Warnings for SELECT:")
print("--------------------")
print("SELECT: Number of warnings: {0}"
  .format(result.get_warnings_count()))
print(result.get_warnings())

db.close()

Listing 9-4Handling Warnings with the X DevAPI

该示例类似于游标示例,只是可以使用get_warnings_count()方法而不是使用警告列表的长度来找到警告计数。对于作为结果的一部分返回行或文档的查询,在检索警告之前必须获取结果。输出是

Warnings for CREATE SCHEMA:
---------------------------
DDL: Number of warnings: 1
[{'level': 1, 'code': 1007, 'msg': "Can't create database 'py_test_db'; database exists"}]

Warnings for SELECT:
--------------------
SELECT: Number of warnings: 1
[{'level': 2, 'code': 1365, 'msg': 'Division by 0'}]

输出显示警告作为字典列表返回。与其他示例相比,输出中有一个主要区别:严重性级别是一个整数,而不是一个字符串。可以返回的级别是 1 和 2,其含义如下:

  • 1 :这是一个音符级别的信息。

  • 2 :这是一个警告级别的消息。

警告字典的代码元素是 MySQL 错误号,但是 1007 是什么意思呢?我们来看看。

MySQL 错误号和 SQL 状态

MySQL 使用错误号来指定发生了哪个注释、警告或错误事件。您在示例中看到了警告是如何包含错误号的。对于下一节讨论的异常,错误号也起着重要作用。所以在继续之前,让我们暂停一下,更详细地考虑一下错误号。

错误号是四到五位数,用于唯一标识遇到的警告或错误。这些数字是 MySQL 特有的,因此不能与其他数据库系统中的错误进行比较。如果错误号不再相关,可以不再使用,但不会重复使用。这意味着检查是否遇到了给定的错误号并据此采取行动是安全的。

除了错误号,还有 SQL 状态,这意味着可以跨 SQL 数据库移植。这种可移植性的代价是无法对错误进行详细说明。然而,SQL 状态很适合用来表示一组错误。SQL 状态仅作为错误异常的一部分返回。

本节的其余部分将查看错误号和 SQL 状态。

MySQL 错误号

每个已知错误的错误号和 SQL 状态可以在 https://dev.mysql.com/doc/refman/en/error-handling.html 中找到。这些错误分为服务器端错误和客户端错误。客户端错误的数字都在 2000 到 2999 之间。服务器端错误使用范围 1000-1999 和 3000 以上。正如这些范围所表明的,有成千上万的错误号,并且这个数字随着每个 MySQL 版本的增加而增加。

幸运的是,MySQL Connector/Python 有一个映射到常量的错误号列表。如果您需要检查是否遇到了给定的错误,这允许您使用应用中的常量。当您在编写代码几年后再阅读代码时,使用常量可以更容易地发现错误所在。

对于mysql.connectormysqlx模块,错误代码常量在errorcode子模块中定义,用法相同。清单 9-5 显示了一个检查当试图创建一个数据库时返回的警告是否是该数据库已经存在的例子;在这种情况下,忽略该警告是安全的,因为您已经知道该数据库可能存在。

import mysqlx

from mysqlx.errorcode import *

from config import connect_args

db = mysqlx.get_session(**connect_args)

# Ensure the DDL statement will cause
# a warnings by executing the same
# CREATE SCHEMA IF NOT EXISTS statement
# twice.
sql = db.sql(
  "CREATE SCHEMA IF NOT EXISTS py_test_db")
sql.execute()

# For a DDL statement
sql = db.sql(
  "CREATE SCHEMA IF NOT EXISTS py_test_db")
result = sql.execute()

# Get the warnings
for warning in result.get_warnings():
  if warning["code"] == ER_DB_CREATE_EXISTS:
    print("Ignoring the warning")
  else:
    raise mysqlx.errors.DatabaseError(
      warning["msg"], warning["code"])

db.close()

Listing 9-5Comparing an Error Code Against a Constant from the errorcode Module

这个例子从mysqlx.errorcode模块导入所有的错误代码。这允许您检查警告的错误号是否为ER_DB_CREATE_EXISTS (1007)。如果是,警告将被忽略;否则,该警告将用于引发异常。

MySQL 连接器/Python 错误异常还包括 SQL 状态,所以在讨论异常类之前,让我们先来看看 SQL 状态。

SQL 状态

与 MySQL 错误号不同,SQL 状态在所有 SQL 数据库之间共享。如果您编写的代码支持不同的底层数据库存储,那么最好尽可能多地使用 SQL 状态,因为这样更有可能保持应用的可移植性。SQL 状态也可以用来确定错误的类别。在 MySQL Connector/Python 中,SQL 状态仅用于错误异常。

SQL 状态由五个字母和数字组成。前两个字符定义类别,其余三个字符提供详细信息。在表 9-2 中可以看到一些常见的错误类别。Exception列是用于该 SQL 状态类的异常类。

表 9-2

常见 SQL 状态类

|

班级

|

描述

|

例外

|

评论

|
| --- | --- | --- | --- |
| 00 | 成功 |   | 查询已成功执行。这绝不会导致异常。 |
| 01 | 警告 |   | 该查询会导致警告。只有启用了raise_on_warnings选项,才会导致异常。 |
| 02 | 尚无数据 | DataError | 当查询不再有数据时,这可能源于存储的程序。 |
| 08 | 连接异常 | OperationalError | 这涵盖了创建连接时的各种问题,如连接过多,MySQL Connector/Python 不支持服务器请求的认证协议等。对于此 SQL 状态类,错误必须发生在服务器端。 |
| Twenty-one | 数错了 | DataError | 例如,在给定的值数目与指定的字段数目不匹配的插入中会发生这种情况。 |
| Twenty-two | 数据不符合 | DataError | 例如,如果字符串对于列来说太长,或者数值超出范围,就会出现这种情况。 |
| Twenty-three | 违反约束 | IntegrityError | 当唯一键约束失败、违反外键或试图将NULL指定为NOT NULL列的值时发生。 |
| Twenty-five | 无效的事务状态 | ProgrammingError | 如果您试图执行当前事务状态不允许的操作,则会发生这种情况。例如,如果您试图在只读事务中插入数据。 |
| Twenty-eight | 未授权 | ProgrammingError | 由于使用了错误的凭据,连接失败。 |
| 三维(three dimension 的缩写) | 没有模式 | ProgrammingError | 在没有默认数据库(架构)的情况下执行查询,并且查询中没有显式设置数据库时发生。 |
| Forty | 交易错误 | InternalError | 例如,这可能是由于死锁而发生的。另一个原因是使用 MySQL 组复制,事务在提交期间回滚,因为它不能应用于所有节点。 |
| forty-two | 语法错误或无法访问 | ProgrammingError | 当 SQL 语句无效或您无权访问所请求的数据时,会出现此错误。 |
| 【男子名】亨利 | 其他错误 | DatabaseError | 对于没有定义更具体的 SQL 状态的错误。例如,这包括 InnoDB 的锁等待超时。MySQL 错误号 1210 和 1243 是使用DatabaseError异常类的例外;这两个错误反而引发了一个ProgrammingError异常。 |
| 辅助放大器(auxiliary amplifier 的缩写) | XA 交易 | IntegrityError | 用于与 XA 事务相关的所有错误。 |

还有更多 SQL 状态类,但是表 9-2 中列出的是 MySQL Connector/Python 中最常见的。每个 SQL 状态类都映射到一个异常类。所以,让我们看看异常类是如何工作的。

警告

语法错误(SQL 状态类 42)可能是 SQL 注入尝试的迹象。确保给予这些错误高优先级。

异常类

MySQL Connector/Python 使用异常来报告在连接器内部处理命令时遇到的错误,或者在执行查询时出现错误。例外可以是三个类别之一,如本节所述。

可能的异常类别包括从标准 Python 异常到开发人员创建的自定义异常。这三个类别是

  • 标准 Python 异常:用于与 MySQL 无关的错误,不再赘述。

  • MySQL Connector/Python 内置异常:遇到 MySQL 相关错误时会遇到的异常,除非该异常已被自定义异常覆盖。

  • 自定义异常:可以定义自己的异常,并为给定的 MySQL 错误号注册它。

本节的其余部分将讨论内置的 MySQL 连接器/Python 异常和自定义异常。

内置类

根据错误的类型,在 MySQL Connector/Python 中预定义了许多异常类。让我们来探索它们。

无论是使用mysql.connector还是mysqlx模块,预定义的类都是相同的,我们将一起讨论它们。这些职业都使用errors.Error职业作为他们的基础,除了Warning职业。所有基于errors.Error的类都有相同的可用属性。

表 9-3 总结了 MySQL 连接器/Python 中使用的异常类以及它们在哪个(哪些)模块中可用。所有的类都存在于errors子模块中(即mysql.connector.errorsmysqlx.errors,取决于所使用的模块)。

表 9-3

MySQL 连接器/Python 异常类

|

异常类

|

模块

|

描述

|
| --- | --- | --- |
| DatabaseError | mysql.connector``mysqlx | 对于一般数据库错误。除了以 HY 开头的 SQL 状态之外,这个类不常被直接使用。 |
| DataError | mysql.connector``mysqlx | 与非约束错误的数据相关的错误。示例包括数据类型错误或不适合字段,或者提供了错误数量的值。 |
| Error | mysql.connector``mysqlx | 这是基本异常类。不直接使用。 |
| IntegrityError | mysql.connector``mysqlx | 约束错误或 XA 事务错误。 |
| InterfaceError | mysql.connector``mysqlx | 用于与连接相关的错误。 |
| InternalError | mysql.connector``mysqlx | 内部数据库错误,如死锁和未处理的结果。 |
| NotSupportedError | mysql.connector``mysqlx | 当使用尚未实现的功能时发生。这通常与在错误的上下文中使用特性有关,例如在存储函数中返回结果集。当 4.1.1 (MySQL Server 版本)之前的身份验证协议不可用时,也可以使用该协议进行连接。 |
| OperationalError | mysql.connector``mysqlx | 与数据库操作相关的错误。这是进行连接时最常遇到的情况。 |
| PoolError | mysql.connector``mysqlx | 有关连接池的错误。 |
| ProgrammingError | mysql.connector``mysqlx | 广义上与应用相关的错误。包括语法错误和试图访问不存在或用户无权访问的数据库对象。 |
| Warning | mysql.connector | 用于重要警告。 |

下面的类都是DatabaseError类的子类:InternalErrorOperationalErrorProgrammingErrorIntegrityErrorDataErrorNotSupportedError

所有类的特征都是在基类Error中定义的,所以它们对于除了Warning类之外的所有异常类都是一样的。除了所有异常所具有的特性之外,Warning类没有什么特别的特性。为了帮助讨论错误异常的特征,考虑以下未捕获的异常:

mysql.connector.errors.ProgrammingError: 1046 (3D000): No database selected

错误类有三个公共属性,可以在处理异常时使用:

  • msg :这是描述错误的字符串。在本例中,它是“没有选择数据库”

  • errno:MySQL 错误号。在示例中,它是 1046。

  • SQL state:SQL 状态。在示例中,它是 3D000。

清单 9-6 显示了一个触发与刚才讨论的异常相同的异常的例子。异常被捕获,每个属性都被打印出来。最后,将错误号与来自errorcode子模块的常数进行比较。

import mysql.connector
from mysql.connector import errors
from mysql.connector.errorcode import *

db = mysql.connector.connect(
  option_files="my.ini")

cursor = db.cursor()
try:
  cursor.execute("SELECT * FROM city")

except errors.ProgrammingError as e:

  print("Msg .........: {0}"
    .format(e.msg))
  print("Errno .......: {0}"
    .format(e.errno))
  print("SQL State ...: {0}"
    .format(e.sqlstate))
  print("")
  if e.errno == ER_NO_DB_ERROR:
    print("Errno is ER_NO_DB_ERROR")

db.close()

Listing 9-6Example of Handling an Exception

在 Python 中,异常像往常一样被捕获,属性的用法很简单。正如您之前看到的,错误号可以与errorcode子模块中的常数进行比较,以便更容易地看到异常与哪个错误进行比较。该示例的输出是

Msg .........: No database selected
Errno .......: 1046
SQL State ...: 3D000

Errno is ER_NO_DB_ERROR

MySQL 连接器/Python 如何决定应该使用哪个类?这在上一节讨论 SQL 状态时已经部分回答了,但是让我们更详细地看一下这个主题。

将错误映射到异常类

当错误发生时,MySQL Connector/Python 使用错误号和 SQL 状态来确定使用哪个异常类。在大多数情况下,您不需要担心这一点,但是在某些情况下,您可能需要修改所使用的类(目前只有mysql.connector模块支持这一点),并且在所有情况下,理解底层过程都是有用的。

使用以下步骤确定异常类别:

  1. 如果已经为 MySQL 错误号定义了一个自定义异常,就使用它。自定义异常仅适用于mysql.connector模块,将在这些步骤后讨论。

  2. 如果 MySQL 错误号是在errors._ERROR_EXCEPTIONS列表中定义的,那么使用为该错误定义的类。

  3. 如果没有为错误定义 SQL 状态,则使用DatabaseError类。对于作为错误引发的警告,会发生这种情况。

  4. 根据 SQL 状态在errors._SQLSTATE_CLASS_EXCEPTION列表中找到类。

  5. 使用DatabaseError类。

如果您需要一个错误来触发不同的异常,当然可以修改_ERROR_EXCEPTIONS_SQLSTATE_CLASS_EXCEPTION列表。但是,不建议这样做,因为它们应该是私有的(因此名称开头有下划线)。在mysql.connector模块中,有一种更好的方法:自定义异常。

自定义例外

在某些情况下,使用自定义异常来处理特定错误会很有用。当错误发生时,您可能希望触发一个特殊的工作流,例如将一条消息记录到应用日志中。目前只有mysql.connector模块支持自定义异常。

使用errors. custom_error_exception()功能登记自定义异常。您需要提供将使用异常的 MySQL 错误号和异常本身。建议自定义异常类继承error .Error类来包含基本特性。

清单 9-7 显示了一个例子,其中MyError类被用于ER_NO_DB_ERROR错误。与普通类相比,唯一的区别是它向stderr输出一条包含错误信息的消息。如果你使用的是 Python 2.7,你需要添加"from __future__ import print_function"作为代码的第一行。

import mysql.connector
from mysql.connector import errors
from mysql.connector.errorcode \
  import ER_NO_DB_ERROR

# Define the custom exception class

class MyError(errors.Error):

  def __init__(
    self, msg=None, errno=None,
    values=None, sqlstate=None):

    import sys
    super(MyError, self).__init__(
      msg, errno, values, sqlstate)
    print("MyError: {0} ({1}): {2}"
      .format(self.errno,
              self.sqlstate,
              self.msg
      ), file=sys.stderr)

# Register the class

errors.custom_error_exception(

  ER_NO_DB_ERROR,
  MyError

)

# Now cause the exception to be raised
db = mysql.connector.connect(
  option_files="my.ini")

cursor = db.cursor()
try:
  cursor.execute("SELECT * FROM city")

except MyError as e:

  print("Msg .........: {0}"
    .format(e.msg))
  print("Errno .......: {0}"
    .format(e.errno))
  print("SQL State ...: {0}"
    .format(e.sqlstate))

db.close()

Listing 9-7Using a Custom Exception

首先,定义了MyError类。它调用自己的超类的__init__方法来设置所有的标准属性。然后错误信息被打印到stderr。这也可以使用日志服务或其他逻辑。其次,MyError类被注册为错误的异常类,MySQL 错误号被设置为ER_NO_DB_ERROR

程序的其余部分与之前相同,只是您现在捕获的是MyError异常,而不是ProgrammingError异常。执行程序时的输出是

MyError: 1046 (3D000): No database selected
Msg .........: No database selected
Errno .......: 1046
SQL State ...: 3D000

这里假设stderrstdout都被打印到控制台。作为练习,先将stderr重定向,然后将stdout重定向到其他地方,看看这会如何改变输出。

还有一些问题不一定会返回警告或错误。一组可能返回错误也可能不返回错误的问题是锁定问题。因为锁定问题是关于使用数据库的,所以您应该检查它们。

锁定问题

当两个或多个事务(可以是单个查询)试图以不兼容的方式访问或更新相同的数据时,就会发生锁定问题。数据库中的锁这个主题既庞大又复杂,但也很有趣。关于锁定的细节已经超出了本书的范围,但是本节将提供一个简要的概述。

注意

简化了锁的讨论;例如,只提到了行(记录)锁。其他一些锁是间隙锁、表锁、元数据锁和全局读锁。还有不同的锁类型,如意向锁。事务隔离级别也对锁起作用。MySQL 参考手册有好几页是关于 InnoDB 锁的。起点是 https://dev.mysql.com/doc/refman/en/innodb-locking-transaction-model.html

锁定的原因是允许对数据的并发访问,同时仍然确保一致的结果。如果一个事务更新给定的行,然后另一个事务尝试更新同一行,则第二个事务必须等待第一个事务完成(提交或回滚)才能访问该行。如果不是这样,最终结果将是不确定的。

MySQL 中的两个事务存储引擎 InnoDB 和 NDBCluster 都使用行级锁。这意味着只有查询读取或更改的行被锁定。因为直到访问行时才知道是否需要它,所以查询乐观地执行,假设有可能获得所需的锁。

乐观方法在大多数情况下都很有效,但这也意味着有时查询必须等待锁。甚至可能是因为等待时间太长而导致超时。

另一种可能性是,当两个事务都在等待对方的锁时,就会发生冲突。这种情况永远不会自行解决,被称为死锁。死锁这个名字听起来很吓人,但它只是一种情况的名称,在这种情况下,数据库必须进行干预以解决锁问题。InnoDB 选择完成工作最少的事务并回滚它。在这种情况下会返回一个死锁错误,这样应用就知道了事务失败的原因。清单 9-8 显示了一个简单的例子,其中两个连接以死锁结束。

Connection 1> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

Connection 1> UPDATE world.city
                 SET Population = Population + 100
               WHERE Name = 'San Francisco' AND CountryCode = 'USA';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

Connection 2> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

Connection 2> UPDATE world.city
                 SET Population = Population + 200
               WHERE Name = 'Sydney' AND CountryCode = 'AUS';
Query OK, 1 row affected (0.04 sec)
Rows matched: 1  Changed: 1  Warnings: 0

Connection 1> UPDATE world.city
                 SET Population = Population + 100
               WHERE Name = 'Sydney' AND CountryCode = 'AUS';
-- Connection 1 blocks until the deadlock occurs for Connection 2.

Connection 2> UPDATE world.city
                 SET Population = Population + 200
               WHERE Name = 'San Francisco' AND CountryCode = 'USA';

ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting

transaction

Listing 9-8Example of Two Transactions Causing a Deadlock

这两项交易都试图增加悉尼和旧金山的人口;连接 1 有 100 人的那个,连接 2 有 200 人的那个。但是,它们以相反的顺序更新这两个城市,并且是交错的。因此,最后,连接 1 等待悉尼的锁被释放,连接 2 等待旧金山的锁被释放。这是永远不会发生的,所以这是一个僵局。

使用细粒度锁时,锁等待和死锁是不可避免的。记住这一点并确保您的应用能够处理锁问题是很重要的。如果锁等待超时或死锁很少发生,重试事务通常就足够了。如果问题发生得如此频繁,以至于影响到性能,那么您需要努力减少锁争用。下一章将简要讨论锁的故障排除。

小费

确保您的应用能够处理锁等待和死锁。第一种方法是重试事务,可能会有一点延迟,以便给另一个事务一个完成的机会。应该进一步调查频繁出现的锁定问题。

最后要讨论的是当出现警告、错误或其他问题时该怎么办。

出问题时该怎么办

到目前为止,关于警告和错误的讨论一直集中在警告和错误如何与 MySQL Connector/Python 一起工作。然而,一个相当重要的问题仍然存在:当您遇到警告、错误和其他问题时,您会怎么做?

这个问题的简短答案是“视情况而定”不仅要看问题,还要看情况。以下是一些需要考虑的事项:

  • 严重性:问题有多严重?

  • 影响:问题影响了多少人和谁?

  • 频率:问题出现的频率?

  • 可重试:是否值得重试导致错误的操作?

  • 努力:要避免这个问题需要做多少工作?

另一个考虑是如何报告失败。如果它涉及到生产环境,相对简短的消息是最好的。如果可能,提供如何避免问题和/或如何获得帮助的信息。

包含完整的堆栈跟踪和确切的异常似乎是个好主意;但是,最终用户无法使用这些信息。事实上,在某些情况下,向前端返回如此多的细节看起来是不专业的,甚至可能会泄露终端用户不应该知道的应用的细节。

错误的全部细节,包括跟踪,当然是开发人员非常感兴趣的。具体如何记录取决于应用,但是可以将其写入应用日志或单独的错误日志。开发环境中的另一个选项是支持“调试”模式,该模式将完整的细节输出到前端,以便在测试期间更容易获得信息。

最后,您希望如何呈现错误取决于您的特定需求、目标用户等等。在某些情况下,甚至有可能避免最终用户受到所遇到问题的影响。这使我们回到了本节开头列出的五个项目;它们将在下面的小节中讨论。

严重

问题的严重性在于它对应用的其余部分和用户有多重要。如果一个错误导致应用根本无法工作,那么处理这个错误显然比导致响应稍慢的错误更重要。

严重程度高的问题需要快速处理。如果是安全问题,或者网站不可用,延迟的解决方案会给公司造成损失。另一方面,如果一百万个请求中有一个比正常情况下多花了 5%的时间处理,这可能仍然是一件令人烦恼的事情,但并不值得你放弃正在做的一切。除了严重性之外,影响是决定问题紧迫性的另一个主要因素。

影响

无论是在面向客户的生产环境、内部非关键应用还是开发系统中遇到问题,都有很大的不同。受影响的用户数量越多,公司对应用的依赖程度越高,解决问题就越紧迫。

在给定的环境中,也可能存在差异。考虑一个开发环境。如果您是唯一受影响的人,并且该问题不影响您当前正在进行的工作,您可以推迟解决方案的工作。

然而,如果 100 个其他开发人员无所事事,或者他们的工作受到影响,那么解决这个问题就变得更加紧迫。问题出现的频率显然也会对影响产生影响。

频率

一个问题的频繁程度会影响需要付出多少努力。如果您很少遇到死锁或锁等待超时的情况,只需重试查询就可以了(参见下一条)。如果相同的锁定问题在一分钟内发生多次,就有必要研究如何避免该问题。

问题发生频率的限制取决于问题的性质,这将频率与严重性和影响联系起来。如果客户遇到应用崩溃,这很快就会成为必须立即处理的问题。同样,一个小时后失败的报告作业必须从头重新启动。

另一方面,如果同样一个小时的报告任务在每次出现问题时都被延迟了几秒钟,那么它就不太可能是优先任务。

可重试

您在 MySQL Connector/Python 中遇到的错误可以分为两类:一类是无论您尝试多少次都会失败的错误,另一类是如果您重试就可能成功的错误。后者更值得关注,因为您可以添加对自动处理它们的支持。

可重试错误通常是由锁争用或资源耗尽引起的。我已经讨论过锁,所以让我们从 MySQL 的角度来看一下是什么导致了资源耗尽。

在连接的生命周期中,有几个地方需要资源。当第一次创建连接时,MySQL 服务器中必须有更多可用的连接,并且操作系统必须允许创建新的线程(默认情况下,MySQL 为每个连接创建一个操作系统线程)。当执行一个查询时,它将需要内存来执行不同的部分,例如对结果进行排序。如果您插入或更改数据,也可能导致表格增长,这需要额外的磁盘空间。如果这些资源耗尽,查询将会失败。

如果您重试查询,并非所有这些错误都有可能消失。例如,如果磁盘已满,可能需要数据库管理员和/或系统管理员进行干预,然后才能再次插入数据。另一方面,如果您有一个锁定问题,并且您的事务都是短期的,那么重试失败的事务可能会成功。

表 9-4 显示了一些可以选择重试的典型错误号。

表 9-4

可能失效的 mysql 错误号

|

错误号

|

常数

|

SQL 状态

|

描述

|
| --- | --- | --- | --- |
| 1028 | ER_FILSORT_ABORT | HY000 | 排序操作已中止。 |
| 1038 | ER_OUT_OF_SORTMEMORY | HY001 | 由于内存不足,排序操作中止。可能需要增加 MySQL 会话变量sort_buffer_size。 |
| 1040 | ER_CON_COUNT_ERROR | 08004 | MySQL 连接器/Python 无法连接到 MySQL 服务器,因为所有分配的连接(max_connections)都已被使用。 |
| 1041 | ER_OUT_OF_RESOURCES | HY000 | MySQL 服务器内存不足,但也许可以继续。 |
| 1043 | ER_HANDSHAKE_ERROR | 08S01 | 如果存在网络问题,在创建连接时会发生这种情况。 |
| 1114 | ER_RECORD_FILE_FULL | HY000 | 桌子满了。 |
| 1135 | ER_CANT_CREATE_THREAD | HY000 | 不可能为新连接创建线程。这可能是由于内存、文件描述符或允许的进程数耗尽。 |
| 1180 | ER_ERROR_DURING_COMMIT | HY000 | 提交事务时出错。 |
| 1181 | ER_ERROR_DURING_ROLLBACK | HY000 | 回滚事务时出错。 |
| 1203 | ER_TOO_MANY_USER_CONNECTIONS | 42000 | 用户有太多连接。 |
| 1205 | ER_LOCK_WAIT_TIMEOUT | HY000 | 事务等待锁的时间超过了超时时间(InnoDB 默认为 50 秒)。 |
| 1206 | ER_LOCK_TABLE_FULL | HY000 | 如果与 InnoDB 缓冲池的大小相比锁太多,InnoDB 就会发生这种情况。只有当不是事务本身导致大量锁时,才值得重试。 |
| 1213 | ER_LOCK_DEADLOCK | 40001 | 出现死锁,该事务被选作牺牲品。 |
| 1226 | ER_USER_LIMIT_REACHED | 42000 | 超过了用户资源限制。 |
| 1613 | ER_XA_RBTIMEOUT | XA106 | XA 事务已回滚,因为花费的时间太长。 |
| 1614 | ER_XA_RBDEADLOCK | XA102 | 由于死锁,XA 事务已回滚。 |
| 1615 | ER_NEED_REPREPARE | HY000 | 准备好的语句需要重新准备。 |

错误列表并不是详尽无遗的,重试成功的机会各不相同。

请注意,您可能能够针对某些错误编写解决方案。例如,如果超过了max_sp_recursion_depth变量允许的递归深度,就会出现错误号 1456 ( ER_SP_RECURSION_LIMIT,SQL 状态 HY000)。如果您已将此选项设置为相对较低的值,但在某些情况下接受增加该值,则可以增加会话的值并重试。显然,对于这种特定情况,如果在第一次尝试之前增加该值会更好,但可能会有一些特殊的考虑因素阻止这样做。

如果决定重试一个事务,还需要决定重试最近的语句是否足够,或者是否必须重试整个事务。通常是后者,并且在所有情况下,它是最安全的。

警告

可能很想重试事务中的最后一条语句,但是要小心,因为前面的语句可能已经回滚了。

自动重试由于连接丢失而失败的查询是很诱人的。但是,在这种情况下要小心,要确保查询依赖的所有内容,比如同一事务中的早期查询,也会被重新执行。

努力

最后要注意的是解决问题的努力。在一个理想的世界里,所有的 bug 都会被修复,但是在现实中,资源是有限的,所以经常需要进行优先级排序。这个想法把所有先前的考虑联系在一起。

软件项目越大,确定哪些问题应该按照什么顺序修复就变得越复杂。可能存在一些利益冲突,例如两个客户受到不同问题的影响。可能还需要新功能的开发保持在正轨上。在这种情况下,有必要让多方参与讨论,以确定所需工作的优先顺序。

MySQL Connector/Python 中警告和错误处理的讨论到此结束。一个相关的主题是如何解决你遇到的错误,这将在下一章讨论。

摘要

本章探讨了警告和错误在 MySQL Server 和 MySQL Connector/Python 中是如何工作的。警告和错误有三种严重级别:注释、警告和错误。

您首先看到了如何使用 MySQL Server 中的sql_notes选项来改变注释级消息是否被视为警告。还可以配置 MySQL 服务器是否应该在严格模式下运行。最后,您看到了应该监视 MySQL 错误日志,以检查应用是否记录了任何消息。

在 MySQL Connector/Python 中,您应该检查警告,并验证它们是否是更严重问题的迹象。至少在开发过程中,让 MySQL Connector/Python 抛出警告作为异常是有用的;然而,这仅在对mysql.connector模块使用游标时才可用,并且仍然需要您获取警告。

错误消息由错误号、SQL 状态和文本消息组成。在mysql.connectormysqlxerrorcode子模块中,错误号也是常量。当您回到代码的这一部分,并且不再记得例如错误号 1046 的含义时,使用常量可以更容易地理解正在使用的错误。

SQL 状态可用于确定错误的总体类别。它们还与错误号一起用于决定使用哪个异常类。非 MySQL 错误通常使用一个标准的 Python 异常类,而 MySQL 错误使用几个特定于 MySQL Connector/Python 的类中的一个。当您使用mysql.connector模块时,也可以为给定的 MySQL 错误号注册您自己的定制异常类。

本章的最后一部分研究了什么是锁定问题,以及遇到问题时应该怎么做。最后,错误可能需要排除,这是下一章的主题。

十、解决冲突

你写了一个大型复杂的应用,离截止日期只有很短的时间了。您开始了最后的测试,但是有些事情没有像预期的那样工作。一旦应用投入生产,客户或支持工程师可能会抱怨错误。如何尽可能快速有效地解决这些问题?继续读!

小费

本章有几个示例程序。列表中出现的所有示例程序都可以下载。有关使用示例程序的更多信息,参见第一章中对示例程序的讨论。

故障排除步骤

当您刚接触编程语言、库、数据库等时,可能很难解决问题。为了帮助您排除 MySQL Connector/Python 中的故障,本节将讨论一些特定于 MySQL Connector/Python 的通用故障排除技术。还建议熟悉通用软件、Python 和 MySQL 故障排除。

小费

对一般 MySQL 故障排除可能有用的一本书是斯维塔·斯米尔诺娃( http://shop.oreilly.com/product/0636920021964.do )的 MySQL 故障排除。它已经有几年的历史了,所以没有涵盖所有最新的特性,但是如果您不熟悉 MySQL 故障诊断,它仍然是一个很好的起点。

在您开始钻研故障排除技巧之前,请记住,最容易解决的问题是那些在编写代码时立即发现的问题。所以,确保你有一个好的测试框架,并且你有好的测试覆盖率。

小费

拥有一个好的测试套件是避免问题的第一步,这些问题稍后将需要掌握故障排除技能来调试。

故障排除讨论将从 MySQL 连接器/Python 问题故障排除的五种一般方法开始:检查警告;确定 SQL 语句;处理原始数据;MySQL 连接器/Python 源代码;并更改 MySQL 连接器/Python 实现。此外,下一小节将描述 MySQL 服务器日志,这也很有用。

检查警告

警告的检查在前一章已经讨论过了。所以,这只是为了重申检查警告是很重要的,因为它们可能是一些错误的早期指标,一些可能会在以后导致更严重的错误。错误可能发生在同一程序的执行过程中,或者在以后的某个时间出现,甚至是几年以后。

因此,最理想的情况是,您的程序不应该导致任何警告,除非您完全知道为什么会创建警告,并且可以显式地处理警告。预期出现警告的一个例子是EXPLAIN语句,其中重新格式化的查询和其他信息可以通过警告返回。

建议检查所有警告,如果可能的话,启用raise_on_warnings在出现警告时引发异常。完全避免警告可能是不可能的,但是如果您通过捕获异常或临时禁用raise_on_warnings来显式地处理它们,至少可以确保您意识到确实发生的警告,并且您可以调查它们的原因。调查警告原因的一种方法是查看执行的确切 SQL 语句;这是下一个话题。

确定 SQL 语句

在某些情况下,执行哪条 SQL 语句是非常清楚的,例如,无论何时执行显式编写的 SQL 语句。然而,在其他情况下,就不那么清楚了。您可以使用参数,通过 X DevAPI 执行查询,或者使用其他框架为您生成实际的 SQL 语句。

一旦有了语句,就可以尝试手动执行它,例如,通过 MySQL Shell。使用命令行客户端是调试 SQL 语句的好方法,MySQL Shell 支持直接执行 SQL 语句和使用 Python 代码。

让我们看看如何找出实际执行了哪些 SQL 语句。提取查询有多种方法。以下示例展示了如何获取 cursor 语句、X DevAPI 中的 select 语句以及使用 MySQL 性能模式的一般情况。

游标.语句

使用游标时,可以在游标的statement属性中检索最后执行的查询。这甚至适用于参数,因为返回的查询带有参数替换。以下示例显示了如何找到 SQL 语句:

import mysql.connector

db = mysql.connector.connect(
  option_files="my.ini",
)

cursor = db.cursor()
cursor.execute(
  "SELECT * FROM world.city WHERE ID = %(id)s",
  params={"id": 130}
)
print("Statement: {0};"
  .format(cursor.statement))

db.close()

这将打印以下输出:

Statement: SELECT * FROM world.city WHERE ID = 130;

mysqlx SelectStatement.get_sql()

对于 X DevAPI 中的 select 语句,get_sql()方法返回基于查询定义生成的语句。这类似于 cursor 语句属性,只是不包括参数替换。使用get_sql()检索语句的一个例子是

import mysqlx
from config import connect_args

db = mysqlx.get_session(**connect_args)
world = db.get_schema("world")
city = world.get_table("city")
stmt = city.select()
stmt.where("ID = :city_id")
stmt.bind("city_id", 130)

print("Statement: {0}"
  .format(stmt.get_sql()))

db.close()

这将打印以下输出:

Statement: SELECT * FROM world.city WHERE ID = :city_id

令人惊讶的是,这里使用了占位符名称(:city_id),而不是实际的 ID。X DevAPI 直到执行时才应用绑定,所以当使用get_sql()生成 SQL 语句时,只有占位符的名称可用。

使用性能模式

除了使用准备好的语句之外,所有情况下都可以使用的一种方法是查询 MySQL 性能模式。这在测试实例上最容易做到,因为您可以确保没有其他查询被执行。在繁忙的服务器上也可以使用类似的步骤,但是这需要更多的关注和过滤来找到来自应用的查询。

为了使用性能模式,您需要准备配置,以便即使在应用关闭连接时也能捕获和保留查询。一种方法是启用events_statements_history_long消费者,并禁用对将检索查询的数据库连接的监控:

mysql> UPDATE performance_schema.setup_consumers
          SET ENABLED = 'YES'
        WHERE NAME = 'events_statements_history_long';
Query OK, 1 row affected (0.09 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> UPDATE performance_schema.threads
          SET INSTRUMENTED = 'NO'
        WHERE PROCESSLIST_ID = CONNECTION_ID();
Query OK, 1 row affected (0.11 sec)
Rows matched: 1  Changed: 1  Warnings: 0

此时,可以执行您需要从中获取查询的应用部分。考虑清单 10-1 中的示例程序,该程序使用 mysql.connector 模块和 X DevAPI 对 sql 表的 CRUD 访问对world模式中的city表执行一条 select 语句(总共两次查询)。

import mysql.connector
import mysqlx
from config import connect_args

# Execute a query using the traditional
# API
db_trad = mysql.connector.connect(
  option_files="my.ini")
cursor = db_trad.cursor()

sql = """SELECT *

           FROM world.city
          WHERE ID = %(id)s"""

params = {'id': 130}

cursor.execute(sql, params=params)

for row in cursor.fetchall():
  print(row)

db_trad.close()

# Execute a query using the X DevAPI
dbx = mysqlx.get_session(**connect_args)
world = dbx.get_schema("world")

city = world.get_table("city")

city_stmt = city.select()

city_stmt.where("ID = :city_id")

city_stmt.bind("city_id", 131)

res = city_stmt.execute()

for row in res.fetch_all():

  print("({0}, '{1}', '{2}', '{3}', {4})"
    .format(
       row[0], row[1],
       row[2],row[3],row[4]
  ))

dbx.close()

Listing 10-1Executing Two Simple Queries Using mysql.connector and mysqlx

一旦执行完成,您可以使用清单 10-2 中的语句让程序执行查询。添加了一个LIMIT 8,因为通常输出中最多可以有 10,000 行。由于其他连接执行的查询也将被记录,因此示例程序返回的可能不是第一行,在这种情况下,可能需要增加输出中包含的行数。

mysql> SELECT THREAD_ID, EVENT_ID, EVENT_NAME, SQL_TEXT
         FROM performance_schema.events_statements_history_long
        ORDER BY THREAD_ID DESC, EVENT_ID
        LIMIT 8\G
*************************** 1\. row ***************************
 THREAD_ID: 182
  EVENT_ID: 1
EVENT_NAME: statement/sql/set_option
  SQL_TEXT: SET NAMES 'utf8' COLLATE 'utf8_general_ci'
*************************** 2\. row ***************************
 THREAD_ID: 182
  EVENT_ID: 2
EVENT_NAME: statement/sql/set_option
  SQL_TEXT: SET NAMES utf8
*************************** 3\. row ***************************
 THREAD_ID: 182
  EVENT_ID: 3
EVENT_NAME: statement/sql/set_option
  SQL_TEXT: set autocommit=0
*************************** 4\. row ***************************
 THREAD_ID: 182
  EVENT_ID: 4
EVENT_NAME: statement/com/Ping
  SQL_TEXT: NULL
*************************** 5\. row ***************************
 THREAD_ID: 182
  EVENT_ID: 5

EVENT_NAME: statement/sql/select

  SQL_TEXT: SELECT *
           FROM world.city
          WHERE ID = 130
*************************** 6\. row ***************************
 THREAD_ID: 182
  EVENT_ID: 7
EVENT_NAME: statement/com/Quit
  SQL_TEXT: NULL
*************************** 7\. row ***************************
 THREAD_ID: 179
  EVENT_ID: 1
EVENT_NAME: statement/sql/select
  SQL_TEXT: /* xplugin authentication */ SELECT @@require_secure_transport, `authentication_string`, `plugin`,(`account_locked`='Y') as is_account_locked, (`password_expired`!
ord`, @@offline_mode and (`Super_priv`='N') as `is_offline_mode_and_not_super_user`,`ssl_type`, `ssl_cipher`, `x509_issuer`, `x509_subject` FROM mysql.user WHERE 'pyuser' = `u
*************************** 8\. row ***************************
 THREAD_ID: 179
  EVENT_ID: 3

EVENT_NAME: statement/sql/select

  SQL_TEXT: SELECT * FROM `world`.`city` WHERE (`ID` = 131)
8 rows in set (0.00 sec)

Listing 10-2Obtaining the Statements from the Performance Schema

THREAD_ID = 182的行是使用mysql.connector模块时的查询,带THREAD_ID = 179的行是针对mysqlx模块的。实际的线程和事件 id 是不同的,从这个例子中可以看出,线程 id 不是单调递增的(这个例子创建了与THREAD_ID = 182的连接,然后创建了与THREAD_ID = 179的连接)。给定线程 ID 的语句按照EVENT_ID的顺序执行。

如您所见,通过 MySQL Connector/Python 执行查询还包括执行其他查询和命令。突出显示的两个查询是您要求执行的查询。

performance_schema. events_statements_history_long table不包括作为服务器端预准备语句执行的查询。它们可以在performance_schema.prepared_statements_instances中找到,但是只有在应用连接的情况下,才根据准备好的语句进行聚合。

还有一种方法可以获得执行的 SQL 语句:通用查询日志。这将在本章后面与其他 MySQL 服务器日志一起讨论。

一旦您确认了实际的查询存在,如果您的问题没有得到解决,您可能需要查看返回的原始数据。

检索原始数据

如果查询看起来是正确的,并且当您手动执行它时可以工作,那么问题可能出在对返回数据的处理上。在使用游标时,调查这是否是个问题的一种方法是要求数据原始返回。

注意

原始结果数据仅与普通游标和缓冲游标一起受支持。

当你有了原始数据,你可以看看它是否像预期的那样,然后从那里着手解决问题。可能有必要看看 MySQL 连接器/Python 源代码,这是下一个主题。

阅读 MySQL 连接器/Python 源代码

使用像 MySQL Connector/Python 这样的库的原因之一是,您不想自己实现一个连接器,您想把它作为一个黑盒来使用。这也是目的,但是编写 Python 程序有一个优势:很容易了解这些库是如何实现的,因为它们大部分是用 Python 本身编写的,并且您可以直接打开库文件,而不是有一个单独的源代码树。

如果您想查看源代码,有三个选项:

如果您使用 C 扩展,那么只有有限数量的源代码是用 Python 编写的。但是,您可以根据需要在纯 Python 实现和 C 扩展之间切换,这是本节的最后一个主题。

更改实现

最后一个选项是改变您是使用纯 Python 还是 MySQL Connector/Python 的 C 扩展实现。一般来说,使用哪种实现并不重要,但是在某些情况下可能会有所不同。

通过更改use_pure连接选项的值,可以在两种实现之间进行切换。默认情况下,这是禁用的。如果有超出本书描述或手册中记录的行为差异,可能是 MySQL Connector/Python 中的 bug 可以在 https://bugs.mysql.com/ 记录一个 bug。

小费

意外行为可能是由于 MySQL Connector/Python 中的一个错误造成的。版本越老,越有可能出现这种情况。建议使用发布系列的最新补丁版本,以确保您有尽可能多的错误修复。发布说明可以在 https://dev.mysql.com/doc/relnotes/connector-python/en/ 找到。

将考虑的故障排除信息的最终来源是 MySQL 服务器日志。

MySQL 服务器日志

MySQL Server 包括几个日志,对于调查正在发生的事情和出现的问题非常有用。在研究 MySQL Connector/Python 程序中的问题时,有必要看一看它们以及如何使用它们。

MySQL 服务器包含的日志有

  • 错误日志:这是 MySQL 服务器实例在服务器端出现问题或发生启动和停止实例等重要变化时记录消息的地方。

  • 通用查询日志:可以记录所有执行的查询。

  • 慢速查询日志:这可以用来记录所有超过一定时间的查询或者不使用索引的查询。

  • 二进制日志:记录对模式或数据的所有更改,但不记录选择数据的查询。

  • ****审计日志:这可以用来记录所有查询或者查询的子集。它类似于一般的查询日志,但是更灵活,有更多的特性,并且可以选择更低的开销。这仅在企业版中可用,不再进一步讨论。有兴趣的话,看 https://www.mysql.com/products/enterprise/audit.html

**每种日志都有自己的优势,因此不是只启用其中一种的问题。为了更好地理解每个日志(除了审计日志),让我们更详细地了解它们。

错误日志

错误日志是查找 MySQL 服务器端问题的主要地方。但是,它也可能包含有关中止的连接、失败的身份验证以及与客户端相关的其他问题的信息。

使用log_error选项指定错误日志位置。默认值取决于您的平台和如何启动 MySQL。建议总是显式地设置它,以便在配置文件中指定路径,并避免在主机名(在 Linux 和 Unix 上)更新时更改文件名。这也确保了错误日志总是处于启用状态。

使用log_error_verbosity选项控制详细程度。它可以设置为 1、2 或 3。MySQL 8.0 中的默认值是 2。较高的值意味着包含不太重要的消息。

  • 1 :错误信息

  • 2 :错误和警告信息

  • 3 :错误、警告和注释级别信息

在 MySQL 8.0 中,有一个额外的消息类别:系统消息。无论log_error_verbosity的值如何,它们总是被包括在内。

从开发的角度来看,这些是最重要的设置。还有其他一些设置,如记录到 syslog 工具和高级过滤选项。这些设置超出了本书的范围,但是你可以在 https://dev.mysql.com/doc/refman/en/error-log.html 了解更多。

常规查询日志

常规查询日志在执行查询之前记录所有查询。这使得它成为调试问题时的一个很好的资源,因为您不需要显式地手动编写和执行每个查询。不利的一面是,一般查询日志有很大的性能开销,所以不建议在生产环境中启用它,除非是在很短的时间内。

警告

常规查询日志的开销很大。如果您在生产系统上启用它,请非常小心。然而,它是开发系统调试的一个很好的工具。

使用general_log选项启用通用查询日志,并使用general_log_file设置文件的位置。默认位置是一个使用主机名作为基本名称和的文件。日志为扩展名。例如,您可以使用以下 SQL 命令启用常规查询日志并检查当前文件位置:

mysql> SET GLOBAL general_log = ON;
Query OK, 0 rows affected (0.07 sec)

mysql> SELECT @@global.general_log_file;
+----------------------------------+
| @@global.general_log_file        |
+----------------------------------+
| D:\MySQL\Data_8.0.11\general.log |
+----------------------------------+
1 row in set (0.00 sec)

常规日志文件的内容包括连接和使用时间戳执行的查询。一个例子是

D:\MySQL\mysql-8.0.11-winx64\bin\mysqld.exe, Version: 8.0.11 (MySQL Community Server - GPL). started with:
TCP Port: 3306, Named Pipe: MySQL
Time                          Id Command  Argument
2018-05-13T04:53:35.717319Z  164 Connect  pyuser@localhost on using SSL/TLS
2018-05-13T04:53:35.717850Z  164 Query    SET NAMES 'utf8' COLLATE 'utf8_general_ci'
2018-05-13T04:53:35.718079Z  164 Query    SET NAMES utf8
2018-05-13T04:53:35.718304Z  164 Query    set autocommit=0

2018-05-13T04:53:35.718674Z  164 Query    SELECT *

           FROM world.city
          WHERE ID = 130
2018-05-13T04:53:35.719042Z  164 Quit
2018-05-13T04:53:36.167636Z  165 Connect
2018-05-13T04:53:36.167890Z  165 Query    /* xplugin authentication */ SELECT @@require_secure_transport, `authentication_string`, `plugin`,(`account_locked`='Y') as is_account_locked, (`password_expired`!='N') as `is_password_expired`, @@disconnect_on_expired_password as `disconnect_on_expired_password`, @@offline_mode and (`Super_priv`='N') as `is_offline_mode_and_not_super_user`,`ssl_type`, `ssl_cipher`, `x509_issuer`, `x509_subject` FROM mysql.user WHERE 'pyuser' = `user` AND 'localhost' = `host`

2018-05-13T04:53:36.169665Z  165 Query    SELECT * FROM `world`.`city` WHERE (`ID` = 131)

2018-05-13T04:53:36.170498Z  165 Quit

这与使用性能模式确定查询的示例相同。Id列是连接 ID,因此它不能与性能模式中的THREAD_ID进行比较。有关通用查询日志的更多信息,请参见 https://dev.mysql.com/doc/refman/en/query-log.html

慢速查询日志

慢速查询日志是研究 MySQL 中慢速查询的传统工具。现在,性能模式提供了很多慢速查询日志的功能,但是仍然有一些情况下,您可能希望将日志记录到文件中,比如在 MySQL 重新启动时保存日志。

使用slow_query_log选项启用慢速查询日志。默认情况下,耗时超过long_query_time秒的查询会被记录到由slow_query_log_file指定的文件中。一个例外是管理查询(ALTER TABLEOPTIMIZE TABLE等)。),这需要在记录前启用log_slow_admin_statements选项。

此外,还有log_queries_not_using_indexes选项,它会记录所有不使用索引的查询,而不管执行查询需要多长时间。启用该选项时,将long_query_time增加到一个较大的值(如 10000)会很有用,以便只关注不使用索引的查询。min_examined_row_limit可用于避免只检查少量行的日志查询;例如,如果一个表只有 10 行,那么可以进行全表扫描。

有关慢速查询日志的更多信息,请参见 https://dev.mysql.com/doc/refman/en/slow-query-log.html

二进制日志

二进制日志与其他日志有些不同,因为它的主要目的不是记录不寻常的事情,也不是作为审计日志。它用于允许复制和时间点恢复。但是,由于二进制日志记录了对模式和数据的所有更改,因此它对于确定何时进行更改也很有用。

在 MySQL 8.0.3 和更高版本中,默认情况下启用二进制日志,并由log_bin选项控制。该选项既可用于启用二进制日志,也可用于设置二进制日志文件的路径和文件名前缀。要禁用二进制日志记录,请使用选项skip_log_bin。在给定的连接(会话)中,可以通过将sql_log_bin选项设置为OFF来禁用二进制日志记录,并通过将其设置为ON来重新启用它:

mysql> SET SESSION sql_log_bin = OFF;
Query OK, 0 rows affected (0.04 sec)

mysql> -- Some queries not to be logged

mysql> SET SESSION sql_log_bin = ON;
Query OK, 0 rows affected (0.00 sec)

改变sql_log_bin需要SYSTEM_VARIABLES_ADMINSUPER权限,只有在你有充分理由的情况下你才应该这样做。如果二进制日志中缺少模式或数据更改,复制从属服务器可能会变得不同步;也就是说,它没有与复制主服务器相同的数据,这意味着需要重建从服务器。

使用 MySQL 服务器安装中包含的mysqlbinlog实用程序读取二进制日志。关于二进制日志的更多信息,参见 https://dev.mysql.com/doc/refman/en/binary-log.html

到目前为止,本章介绍了一种非常手动的故障诊断方法。通过工具可以使这变得更容易。下一节将讨论两个工具:MySQL Shell 和 PyCharm。

调试工具

对于简单的程序和简单的问题,通过读取源代码、添加打印语句等手动调试来解决问题可能会更快。然而,通常这不是最有效的方法。本节将介绍两个可以帮助调试 MySQL 连接器/Python 程序的工具。首先你会看一下 MySQL Shell,在第六章中简单提到过,然后你会简单看一下使用 PyCharm 进行调试。

MySQL Shell

命令行客户端 MySQL Shell 于 2017 年 4 月首次发布为 GA。它将成为下一代工具,不仅可以执行 SQL 查询,还可以执行管理任务以及 Python 和 JavaScript 代码。

MySQL Shell 不是这样一个调试工具。但是,因为它是交互式的,并且支持 Python 和直接 SQL 语句,所以它是试验和研究代码和查询如何工作的一种方便的方法。它还包括对 X DevAPI 的支持,因此您可以交互式地调试使用mysqlx模块的代码。

注意

MySQL Shell 中包含的不是 MySQL Connector/Python。因此,无论您是使用 MySQL Connector/Python 程序中的 X DevAPI,还是使用 MySQL Shell,都会有一些不同。但是,API 本身是一样的。

可以从 MySQL Connector/Python 和 MySQL Server 相同的位置下载 MySQL Shell。社区下载的链接是 https://dev.mysql.com/downloads/shell/ 。如果您在 Microsoft Windows 上使用 MySQL 安装程序,也可以通过这种方式安装 MySQL Shell。一旦安装了 MySQL Shell(安装将作为读者的练习),您可以通过几种方式启动它:

  • 从 shell 中执行mysqlsh二进制文件。这是 Linux 和 Unix 上最常见的方式,但它也适用于 Microsoft Windows。

  • 在 Microsoft Windows 上,您也可以从“开始”菜单执行它。

从 shell 调用mysqlsh的一个优点是,您可以在命令行上指定选项;比如--py在 Python 模式下启动 MySQL Shell。一旦启动,你会得到如图 10-1 所示的提示。

img/463459_1_En_10_Fig1_HTML.jpg

图 10-1

MySQL Shell 欢迎消息

屏幕截图中的颜色已经更改,以便在打印时更好地工作。默认配色方案针对黑色背景进行了优化。提示符显示了 shell 所处的模式。表 10-1 总结了采油树模式。

表 10-1

MySQL 外壳模式

|

方式

|

提示

|

命令行

|

命令

|
| --- | --- | --- | --- |
| Java Script 语言 | 射流研究… | --js | \js |
| 计算机编程语言 | 巴拉圭 | --py | \py |
| 结构化查询语言 | 结构化查询语言 | --sql | \sql |

Prompt栏显示了用于提示文本中模式的缩写。Command-Line列包含从命令行启动 MySQL Shell 时启用模式的选项。Command列显示了在 shell 中用来改变模式的命令。

在 MySQL Shell 中执行 Python 代码时,可以使用六个全局对象:

  • mysqlx:模块mysqlx;然而,这与 MySQL Connector/Python 中的mysqlx模块并不相同(但非常相似)。

  • session:保持与 MySQL 服务器连接的会话对象。

  • db:一个模式对象,如果在创建连接时已经在 URI 中定义了一个模式对象的话。

  • 这个对象包含了管理 MySQL InnoDB 集群的方法。

  • shell:具有各种通用方法的对象,例如用于配置 MySQL Shell。

  • util:该对象包含各种实用方法。

清单 10-3 展示了一个使用 MySQL Shell 测试代码的例子,该代码创建了一个集合并添加了两个文档。

JS> \py
Switching to Python mode...

Py> \c pyuser@localhost
Creating a session to 'pyuser@localhost'
Enter password: **********
Fetching schema names for autocompletion... Press ^C to stop.
Your MySQL connection id is 179 (X protocol)
Server version: 8.0.11 MySQL Community Server - GPL
No default schema selected; type \use <schema> to set one.

Py> session

<Session:pyuser@localhost>

Py> session.drop_schema('py_test_db')
Py> db = session.create_schema('py_test_db')
Py> people = db.create_collection('people')
Py> add_stmt = people.add(
...   {
...     "FirstName": "John",
...     "LastName": "Doe"
...   }
... )
...

Py> add_stmt.add(

...   {

...     "FirstName": "Jane",

...     "LastName": "Doe"

...   }

... )

...

Query OK, 2 items affected (0.1715 sec)

Py> find_stmt = people.find("FirstName = 'Jane'")
Py> find_stmt.fields("FirstName", "LastName")
[
    {
        "FirstName": "Jane",
        "LastName": "Doe"
    }
]
1 document in set (0.0007 sec)

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

SQL> SELECT _id,
 ...        doc->>'$.FirstName' AS FirstName,
 ...        doc->>'$.LastName' AS LastName
 ...   FROM py_test_db.people;
+------------------------------+-----------+----------+
| _id                          | FirstName | LastName |
+------------------------------+-----------+----------+
| 00005af3e4f700000000000000a2 | John      | Doe      |
| 00005af3e4f700000000000000a3 | Jane      | Doe      |
+------------------------------+-----------+----------+
2 rows in set (0.0006 sec)

SQL> \q
Bye!

Listing 10-3Using the MySQL Shell

示例中的提示已经过修改,只显示语言模式。该示例由几个步骤组成:

  1. 切换到使用 Python。

  2. 连接到 MySQL。

  3. 如果存在的话,删除py_test_db模式。

  4. 创建py_test_db模式。

  5. 创建people集合。

  6. 将两个人添加到people集合中。

  7. 使用 CRUD read 语句查询 people 集合。

  8. 切换到 SQL 模式。

  9. 使用 SQL 查询people集合(现在认为是一个表)。

大多数步骤现在应该看起来很熟悉了;但是,有几件事需要注意。首先,一旦创建了连接,就会自动设置session变量。下一件重要的事情是,当 Jane Doe 被添加到add_stmt时,即使没有对execute()的调用,语句也会被执行。find 语句也会发生类似的情况。这是 MySQL Shell 的一个特性。当您使用 CRUD 方法并且没有将结果赋给变量时,会有一个对execute()的隐式调用。

在示例的最后,使用 SQL 语句检索 people 集合中的记录。这使用了->>操作符,它是提取字段和取消引用字段的组合。MySQL 有两种从 JSON 文档中提取值的简写符号。->运算符相当于JSON_EXTRACT()函数,->>JSON_UNQUOTE(JSON_EXTRACT())相同。

使用 MySQL Shell 可以做更多的事情。成为 it 大师的最好方法就是开始使用它。虽然它起初看起来像是一个复杂的工具,但如果您遇到困难,有几个内置资源可以提供帮助。第一个是--help命令行参数。这将提供 MySQL Shell 的高级描述,包括命令行参数列表。此外,所有与 X DevAPI 相关的对象都有内置的帮助,您可以通过执行help()方法获得这些帮助。这不仅适用于前面列出的全局变量,也适用于其他对象,如集合。对于前面清单中的people集合,帮助文本可以在清单 10-4 中看到。

Py> people = db.get_collection('people')
Py> people.help()

A Document is a set of key and value pairs, as represented by a JSON object.

A Document is represented internally using the MySQL binary JSON object,
through the JSON MySQL datatype.

The values of fields can contain other documents, arrays, and lists of
documents.

The following properties are currently supported.

 - name    The name of this database object.
 - session The Session object of this database object.
 - schema  The Schema object of this database object.

The following functions are currently supported.

 - add                 Inserts one or more documents into a collection.
 - add_or_replace_one  Replaces or adds a document in a collection.
 - create_index        Creates an index on a collection.
 - drop_index          Drops an index from a collection.
 - exists_in_database  Verifies if this object exists in the database.
 - find                Retrieves documents from a collection, matching a specified criteria.
 - get_name            Returns the name of this database object.
 - get_one             Fetches the document with the given _id from the collection.
 - get_schema          Returns the Schema object of this database object.
 - get_session         Returns the Session object of this database object.
 - help                Provides help about this class and it's members
 - modify              Creates a collection update handler.
 - remove              Creates a document deletion handler.
 - remove_one          Removes document with the given _id value.
 - replace_one         Replaces an existing document with a new document.

Listing 10-4The Output of the help() Method for a Collection

小费

使用mysqlsh --help获得 MySQL Shell 的高级帮助,使用 X DevAPI 对象的help()方法获得更具体的帮助。有关在线帮助,请参见 https://dev.mysql.com/doc/refman/en/mysql-shell.htmlhttps://dev.mysql.com/doc/refman/en/mysql-shell-tutorial-python.html

除了 MySQL Shell,还有其他调试工具可供选择。对于专用的 Python IDE,我们来看看 PyCharm。

PyCharm

有许多编辑器和 ide 可用于源代码编辑和调试。PyCharm 就是这样一个 IDE,它是专门为 Python 编写的。PyCharm 的全部潜力超出了本书的范围,这个简短的例子更多地是为了展示使用 IDE 进行开发和故障排除的想法,而不是说明如何使用 PyCharm。

注意

本例中使用了 PyCharm,但是其他 ide 也有类似的功能。使用您熟悉的、符合您的要求的、并且在您的公司中可用的 IDE。

PyCharm 可以从 https://www.jetbrains.com/pycharm/ 的产品主页下载。IDE 可用于 Microsoft Windows、macOS 和 Linux。安装很简单,假设您已经安装了 PyCharm。

要开始使用 PyCharm,您需要创建一个新项目。这可以在欢迎屏幕上完成,如图 10-2 所示。

img/463459_1_En_10_Fig2_HTML.jpg

图 10-2

PyCharm 欢迎屏幕

点击新建项目后,选择项目名称,如图 10-3 所示。在这种情况下,名称是 MySQL 连接器-Python 揭示了

img/463459_1_En_10_Fig3_HTML.jpg

图 10-3

创建新项目

一旦创建了项目,就可以进入 IDE 环境本身。在这里,您可以创建新的源文件,执行源文件,调试它们,等等。第一步是确保 MySQL 连接器/Python 可用。PyCharm 将项目所需的所有文件与项目隔离,所以需要先为项目安装 MySQL Connector/Python。这可以从设置页面完成,在顶部菜单中选择文件,然后选择设置,如图 10-4 所示。

img/463459_1_En_10_Fig4_HTML.jpg

图 10-4

导航到设置

在设置中,有一部分是可以配置项目本身的。这包括从 PyPi 安装软件包。因为 MySQL Connector/Python 可以从 PyPi 获得,所以这就是你要做的。项目解释器设置屏幕如图 10-5 所示。

img/463459_1_En_10_Fig5_HTML.jpg

图 10-5

带有项目解释器设置的屏幕

您可以使用已安装软件包列表区域右上角的绿色+图标添加软件包。这将把你带到图 10-6 所示的屏幕,在这里你可以搜索或浏览要安装的软件包。

img/463459_1_En_10_Fig6_HTML.jpg

图 10-6

搜索和浏览要安装的 PyPi 包

找到 MySQL Connector/Python 最简单的方法就是搜索包名: mysql-connector-python 。一旦选择了包,您就可以看到它的详细信息。确保软件包的作者是甲骨文和/或其附属公司,并且版本是 8.0.11 或更新版本。您可以选择不同于最新版本的版本,但通常建议使用为您选择的版本。点击安装包安装包。

安装需要一点时间,因为必须下载并安装软件包及其依赖项。一旦您返回到 settings 屏幕,您可以看到一些其他的包作为依赖项被拉进来,包括 protobuf 包。点击 OK 返回主 IDE 窗口,在这里你可以创建你的第一个 Python 程序。

要创建一个新的源文件,点击左侧菜单中的项目文件夹,然后在顶部菜单中选择文件(与进入设置相同),然后选择新建。选择创建一个新文件,命名为test.py。以同样的方式创建第二个文件,名为my.ini(您可以选择格式为文本)。

my.ini文件中,您可以输入连接 MySQL 的常用 MySQL 连接器/Python 配置:

[connector_python]
user     = pyuser
host     = 127.0.0.1
port     = 3306
password = Py@pp4Demo

您可以使用 CTRL + s 保存文件。然后转到test.py文件,输入并保存您的程序:

import mysql.connector

db = mysql.connector.connect(
    option_files="my.ini", use_pure=True)

cursor = db.cursor()
cursor.execute("SELECT * FROM city")
db.close()

图 10-7 显示了输入代码的编辑器。

img/463459_1_En_10_Fig7_HTML.jpg

图 10-7

代码编辑器

这是 IDE 发挥作用的时间点,因为您现在可以执行或调试代码了。这些操作可从运行菜单中获得。请随意继续并尝试运行代码。该示例包含一个 bug 查询应该是SELECT * FROM world.city。该错误意味着抛出了一个MySQL ProgrammingError异常,因为尚未选择用于查询的数据库:

mysql.connector.errors.ProgrammingError: 1046 (3D000): No database selected

Process finished with exit code 1

如果你选择调试,而不是“运行”程序,那么 IDE 会检测到异常。除了输出与从控制台运行程序时相同的回溯信息之外,您还可以获得调试器输出,并访问发生异常的源代码;这种情况下,在 MySQL 连接器/Python 的connection.py文件中触发。图 10-8 显示了部分信息以及connection.py文件在异常发生时是如何打开的。

img/463459_1_En_10_Fig8_HTML.jpg

图 10-8

调试异常

由于该异常是在mysql.connector代码中触发的,它暗示了以下问题之一:

  • 该查询无效。

  • 该程序以无效的方式使用 MySQL 连接器/Python。

  • MySQL 连接器/Python 中遇到了一个 bug。

在这种情况下,很容易解决 bug 它只需要用SELECT * FROM world.city替换查询。在这种情况下,即使是异常中的错误消息也足够了。然而,在更大的程序和更复杂的工作流中,要确定错误在哪里以及是什么可能要困难得多。如本例所示,使用 IDE 可以更容易、更快速地解决问题。

小费

即使有 IDE 的强大功能,也不要低估异常返回的错误信息的有用性。通常会显示足够的信息,因此您可以确定问题是什么。

这仅仅是使用 ide 的皮毛。还有许多其他功能,如单步执行代码、使用断点等。

故障排除的一般讨论到此结束。在我结束之前,将讨论一些错误和问题的具体例子。

故障排除示例

本章的最后一部分是几个使用 MySQL Connector/Python 时可能会遇到的问题的例子。这绝不是详尽无遗的,但希望能为你可能遇到的问题以及如何处理它们提供一个思路。

注意

其中一些例子可能看起来有些牵强。然而,我从事 MySQL 支持工作已经很多年了,我见过这种错误——也曾被它们绊倒过——好几次。它们确实发生在现实生活中。

找到未读结果

使用mysql.connector模块时,可能会出现“发现未读结果”的错误。例外是使用了InternalError类。这是由于在您完全使用完上一个查询的结果集之前试图执行一个查询造成的。

导致错误的一个基本示例是

import mysql.connector

db = mysql.connector.connect(
  option_files="my.ini",
  database="world"
)

cursor = db.cursor()
cursor.execute("SELECT * FROM city")
cursor.execute("SELECT * FROM country")

db.close()

如果您执行这个例子,您将会以一个InternalError异常结束。确切的回溯取决于 MySQL Connector/Python 的安装位置和版本,但它看起来类似于以下输出:

Traceback (most recent call last):
  File "test.py", line 10, in <module>
    cursor.execute("SELECT * FROM country")
  File "C:\Users\jesper\AppData\Local\Programs\Python\Python36\lib\site-packages\mysql\connector\cursor_cext.py", line 232, in execute
    self._cnx.handle_unread_result()
  File "C:\Users\jesper\AppData\Local\Programs\Python\Python36\lib\site-packages\mysql\connector\connection_cext.py", line 599, in handle_unread_result
    raise errors.InternalError("Unread result found")

mysql.connector.errors.InternalError: Unread result found

如果您使用第一个查询的一些结果,但满足触发第二个查询的某些条件,这种情况很可能发生。还要记住,整个连接只能有一个未完成的结果,因此如果您需要并排执行多个查询,您需要使用两个连接,或者确保前一个结果已被完全使用,例如,通过使用缓冲游标。

在执行第二个查询之前,mysql.connector模块要求您指出想要对第一个查询的剩余结果做什么。有几种方法可以处理它:

  • 显式提取所有剩余的行。

  • 当使用 C 扩展时,使用 connection 对象上的free_result()方法显式释放结果集。在 8.0.11 和更低版本中,当使用 C 扩展时,即使所有行都已提取,这也是必需的。

  • 如果您需要并排执行查询,请对除最后一个查询之外的所有查询使用缓冲游标,如第四章中的“缓冲结果”一节所述。

  • 启用consume_results选项。这告诉 MySQL Connector/Python 在执行新的查询时自动丢弃剩余的行。

通常,首选前两种方法,因为它们明确地解释了意图,并避免了在应用的内存中保存整个结果的开销。最不可取的方法是自动使用结果,因为这很容易隐藏编程错误。

mysqlx模块自动丢弃未完成的结果,就像consume_resultsmysql.connector模块启用一样。

数据太长或超出范围值

如果您使用的是较旧版本的 MySQL Server(5.6 版和更早版本),并且升级到了 MySQL 5.7 和更高版本,您可能会发现以前工作的应用开始抛出有关数据过长或超出范围的错误。新的应用和旧版本的 MySQL Server 也会出现这种情况,但新版本的 MySQL Server 更有可能出现这种情况。

可能遇到的错误的两个例子是

mysql.connector.errors.DataError: 1406 (22001): Data too long for column 'Code' at row 1

mysql.connector.errors.DataError: 1264 (22003): Out of range value for column 'IndepYear' at row 1

如前一章所述,原因是在 MySQL Server 5.7 及更高版本中默认启用了严格模式。这意味着,如果在INSERTUPDATE语句中提供的数据不符合列的定义,查询将被拒绝,而不是试图将数据做成某种形状。触发列错误的数据过长的INSERT语句的一个例子是

import mysql.connector

db = mysql.connector.connect(
  option_files="my.ini",
  database="world"
)
cursor = db.cursor()

db.start_transaction()

cursor.execute("""
INSERT INTO country (Code, Name)
VALUES ('Foobar', 'Foobar Country')""")

db.rollback()
db.close()

追溯和异常的一个例子是

Traceback (most recent call last):
  File "C:\Users\jesper\AppData\Local\Programs\Python\Python36\lib\site-packages\mysql\connector\connection_cext.py", line 377, in cmd_query
    raw_as_string=raw_as_string)
_mysql_connector.MySQLInterfaceError: Data too long for column 'Code' at row 1

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "test.py", line 13, in <module>
    VALUES ('Foobar', 'Foobar Country')""")
  File "C:\Users\jesper\AppData\Local\Programs\Python\Python36\lib\site-packages\mysql\connector\cursor_cext.py", line 264, in execute
    raw_as_string=self._raw_as_string)
  File "C:\Users\jesper\AppData\Local\Programs\Python\Python36\lib\site-packages\mysql\connector\connection_cext.py", line 380, in cmd_query
    sqlstate=exc.sqlstate)

mysql.connector.errors.DataError: 1406 (22001): Data too long for column 'Code' at row 1

Code列被定义为char(3),因此六个字符的代码不适合该列。有几种可能的方法来处理这些类型的错误:

  • 更改列定义以便能够存储数据。

  • 更改应用中的数据以满足列定义。例如,如果可以截断一个字符串或舍入一个数字,那么就显式地这样做。

  • 如果查询中的数据不是直接来自应用,而是将数据从一个表插入到另一个表中,则如果可以接受,请在查询中操作数据。例如,LEFT()函数可用于截断超过给定字符数的字符串。

  • 通过从sql_mode变量中移除STRICT_TRANS_TABLES来禁用严格模式。这是迄今为止最不可取的解决方案,因为您最终可能会在数据库中得到与预期不同的数据。

数据一致性是一个重要的特性。严格模式的存在是为了帮助您实现它,所以只有在没有其他选择的情况下才禁用它。

警告

禁用严格模式将允许静默数据损坏。除非别无选择,否则不要禁用它。

数据更改丢失

如果您发现您在应用中所做的更改不持久,或者从进行更改的同一个连接中可见,但从其他连接中不可见,那么您可能没有提交更改。如果禁用了autocommit(mysql.connector模块的默认设置),这种情况很可能发生。

有时发现这一点的另一种方式是,在复制设置中,更改在复制源上看起来是可见的(假设您从做出更改的同一连接中查询数据),但似乎更改没有复制。

这是一个可以让你的头发在很短的时间内变白的问题;然后,当你发现原因时,你就准备拔掉你所有的头发。问题是默认情况下 MySQL 服务器启用了autocommit,所以当你使用一个禁用它的连接器时,你很容易被发现。在清单 10-5 中可以看到一个简单的例子。

import mysql.connector

db1 = mysql.connector.connect(
  option_files="my.ini",
  database="world",
  autocommit=False
)
db2 = mysql.connector.connect(
  option_files="my.ini",
  database="world",
)
cursor1 = db1.cursor()
cursor2 = db2.cursor()

sql = """
SELECT Population
  FROM city
 WHERE ID = 130"""

cursor1.execute("""

UPDATE city

   SET Population = 5000000
 WHERE ID = 130""")

cursor1.execute(sql)
row1 = cursor1.fetchone()
print("Connection 1: {0}"
  .format(row1[0]))

cursor2.execute(sql)
row2 = cursor2.fetchone()
print("Connection 2: {0}"
  .format(row2[0]))

db1.close()
db2.close()

Listing 10-5Apparently Losing Data

对于db1连接,自动提交是禁用的,这意味着 MySQL 会自动启动一个事务,并在执行查询时保持它打开。由于没有提交事务,事务隔离防止其他连接看到数据。程序的输出显示了更新发生后每个连接发现的群体:

Connection 1: 5000000
Connection 2: 3276207

即使提交了更改,这个问题也可能以一种更微妙的方式出现。如果另一个连接在提交之前以默认的事务隔离级别(可重复读取)打开了一个事务,那么第二个连接将一直看到旧数据,直到它关闭它的事务。

此问题的可能解决方案包括以下内容:

  • 务必确保在完成后提交或回滚事务。这也适用于SELECT语句。

  • 启用autocommit选项。这是 MySQL 服务器和 X DevAPI 的默认设置(它继承了 MySQL 服务器的设置)。

对于一种解决方案,没有强烈的偏好。启用自动提交功能不太可能导致意外,尤其是如果您已经习惯了这种行为。此外,当autocommit选项被启用时,如果很明显语句不能更改数据并且没有启动显式事务,InnoDB 存储引擎会将事务更改为只读模式。

此 MySQL 版本不允许使用 Used 命令

这个错误会引起很多混乱。您的应用运行良好。然后你升级了 MySQL 服务器。突然你被告知这个 MySQL 版本不允许使用某个命令。那是什么意思?

完整错误消息的一个示例是

mysql.connector.errors.ProgrammingError: 1148 (42000): The used command is not allowed with this MySQL version

这个错误有点误导。它与 MySQL 版本没有任何关系,除了对默认值的潜在更改。当您尝试使用LOAD DATA LOCAL INFILE语句,并且在 MySQL Server 或 MySQL Connector/Python 中禁用了对加载本地文件的支持时,就会出现该错误。

允许加载本地文件的 MySQL 服务器选项是local_infile。在 MySQL Server 5.7 和更早版本中,默认情况下它是启用的,但在 MySQL Server 8.0 中是禁用的。MySQL 连接器/Python 选项是allow_local_infile,在所有最新版本中默认启用。

如果需要使用LOAD DATA LOCAL INFILE,解决方案是同时启用 MySQL 服务器local_infile选项和 MySQL 连接器/Python allow_local_infile选项。

警告

在允许LOAD DATA LOCAL INFILE之前,请阅读 https://dev.mysql.com/doc/refman/en/load-data-local.html 了解有关安全影响的信息。

批量更改会导致损坏或错误

数据正变得越来越标准化,如果不是一个标准,而是几个标准。现在世界上使用 UTF-8 作为字符集,JSON 和 XML 格式通常用于存储或传输需要灵活的自描述模式的数据,等等。然而,仍然有大量的机会被赶上。

如果您发现自己正在将数据加载到数据库中或执行一批 SQL 语句,但是结果数据似乎被破坏了,或者工作因为错误而失败了,那么您很可能是混合字符集或行尾的受害者。这种混淆的结果差别很大。

例如,如果您以 Latin1 的形式加载一个包含 UTF 8 数据的文件,那么将不会出现任何错误,因为 Latin1 可以处理任何字节序列。如果反过来,可能会导致 UTF-8 无效字节序列的错误。如果您试图加载以 Latin1 编码的字符串“Wür ”,就好像它是 UTF-8 一样,您将得到以下错误信息:

mysql.connector.errors.DatabaseError: 1300 (HY000): Invalid utf8mb4 character string: 'W'

行尾更改更有可能导致多行被插入到一个字段中,或者行尾的一部分没有被消耗。

当你使用LOAD DATA [LOCAL] INFILE时,建议总是明确你在文件中期望的字符集和文件结尾。这确保了对 MySQL 服务器默认值的更改、操作系统的更改或类似的更改不会导致文件被错误地解释。这些问题的一个常见来源是在 Microsoft Windows 上准备和测试作业,但生产系统在 Linux 上,反之亦然。

创建连接时不支持的参数

使用mysql.connector模块时,有几种方法可以创建连接。灵活性可能是一件好事,但它也为混淆各种方法留下了潜在的错误空间。创建连接时很容易出现的一个问题是,您会得到一个由不支持的参数引起的带有AttributeError的错误。

由不支持的参数引起的错误的一个例子是use_pure参数,但它也可能是由其他参数引起的:

AttributeError: Unsupported argument 'use_pure'

具体到use_pure,当你使用mysql.connector模块时,只有当你使用mysql.connector.connect()函数创建连接时,才允许使用该参数。这是一个包装器函数,确保您获得一个纯 Python 或 C 扩展实现的对象。因此,如果您没有明确选择底层类来创建自己的实例,那么包含use_pure参数才有意义。

这个问题也可能是由于在 MySQL 配置文件中添加了一个只有mysql.connector.connect()函数才能理解的参数,例如use_puremysql.connector.connect()函数本身并不查看配置文件中的配置,而只是将它传递给底层的连接类。因此,连接对象本身必须理解配置文件中的选项。

如果您遇到类似这样的错误,请查看选项的定义位置。如果它是在配置文件中定义的,将它移到对mysql.connector.connect()的调用中,看看是否有帮助。显然,您还应该检查拼写错误。

MySQL 服务器错误日志中中止的连接

检查 MySQL 错误日志时的一个常见问题是,有许多关于中止连接的注释。在 MySQL Server 5.7 中尤其如此,默认情况下,错误日志的详细程度高于其他 MySQL Server 版本。

中止的连接可能由多种情况触发,例如网络问题。然而,关于编写 MySQL Connector/Python 程序,更有趣的原因是应用没有正确关闭连接。这可能是因为应用崩溃或者它没有显式关闭其数据库连接。

以下示例可用于触发关于中止连接的消息:

import mysql.connector

db = mysql.connector.connect(
  option_files="my.ini", use_pure=True)

# Do some database work, but do not
# close the connection (db.close()).

exit()

MySQL 错误日志中的结果消息类似于以下示例:

2018-03-04T07:28:22.753264Z 148 [Note] [MY-010914] Aborted connection 148 to db: 'unconnected' user: 'pyuser' host: 'localhost' (Got an error reading communication packets).

该消息仅在 MySQL 服务器的log_error_verbosity选项设置为 3 时显示,具体消息取决于 MySQL 服务器版本。您还可以通过检查 MySQL 服务器中的Aborted_clients状态变量来监控中止连接的数量。

该问题的解决方案是确保在终止应用之前正确关闭所有连接:

db.close()

不幸的是,有一个案例并不容易解决。当您使用连接池时,没有正式的方法来关闭连接。在连接上使用close()方法不会关闭底层连接,而是将它返回到池中。这意味着当应用关闭时,池中的每个连接都将有一个中止的连接。目前,选项包括忽略消息、减少错误日志的冗长性或设置错误日志过滤器(仅在 MySQL Server 8.0 中受支持)。在这两种情况下,它还会妨碍发现网络或应用的真正问题。

锁定问题

众所周知,锁定问题很难调试。它们通常需要特定的工作负载才能出现锁争用,因此很难按需重现该问题。下面的讨论将给出在调查锁定问题期间可用的一些工具的概述。

如果您能在锁定问题发生时捕捉到它,那么sys.innodb_lock_waits视图是开始调查的绝佳地方。它将显示持有锁的连接和等待锁的连接的信息。在清单 10-6 中可以看到一个例子。确切的输出将取决于 MySQL 服务器版本;该示例来自版本 8.0。

mysql> SELECT * FROM sys.innodb_lock_waits\G
*************************** 1\. row ***************************
                wait_started: 2018-03-06 21:28:45
                    wait_age: 00:00:19
               wait_age_secs: 19
                locked_table: `world`.`city`
         locked_table_schema: world
           locked_table_name: city
      locked_table_partition: NULL
   locked_table_subpartition: NULL
                locked_index: PRIMARY
                 locked_type: RECORD
              waiting_trx_id: 29071
         waiting_trx_started: 2018-03-06 21:28:45
             waiting_trx_age: 00:00:19
     waiting_trx_rows_locked: 1
   waiting_trx_rows_modified: 0
                 waiting_pid: 154
               waiting_query: UPDATE world.city SET Population = Population + 1 WHERE ID = 130
             waiting_lock_id: 29071:2:7:41
           waiting_lock_mode: X
             blocking_trx_id: 29069
                blocking_pid: 151
              blocking_query: NULL
            blocking_lock_id: 29069:2:7:41
          blocking_lock_mode: X
        blocking_trx_started: 2018-03-06 21:26:20
            blocking_trx_age: 00:02:44
    blocking_trx_rows_locked: 1
  blocking_trx_rows_modified: 1
     sql_kill_blocking_query: KILL QUERY 151
sql_kill_blocking_connection: KILL 151
1 row in set (0.00 sec)

Listing 10-6InnoDB Lock Wait Information

该信息包括等待开始的时间和等待的时间,以小时:分钟:秒表示法和秒表示。接下来的几列包含关于被锁定的表和索引以及锁类型的信息。然后是等待连接的信息,包括当前正在执行的查询。阻塞连接也包含相同的信息。最后,有两个 SQL 语句可以用来终止阻塞的查询或连接。

在本例中,阻塞查询是NULL。这意味着该连接当前没有执行任何查询。那它是如何持有锁的呢?答案是有一个活跃的交易。当在事务内部执行查询时,可能需要锁,直到事务完成。空闲但活跃的事务是锁定问题的常见原因之一。

调查 InnoDB 和元数据锁定问题的一些其他资源包括

  • sys.schema_table_lock_waits:这个视图类似于sys.innodb_lock_waits视图,但是包括关于元数据锁等待的信息。要使这个视图工作,必须启用wait/lock/metadata/sql/mdl性能模式工具。该工具在 MySQL Server 8.0 中默认启用,但在 5.7 版中不启用。

  • performance_schema.data_locks:这个表是 MySQL Server 8.0 中新增的,包含了 InnoDB 持有的锁的信息。被sys.innodb_lock_waits使用。

  • performance_schema.data_lock_waits:该表是 MySQL Server 8.0 中新增的,包含了关于 InnoDB 锁等待的信息。被sys.innodb_lock_waits使用。

  • information_schema.INNODB_LOCKS:相当于performance_schema.data_locks的 MySQL Server 5.7。它只包含另一个事务正在等待的锁的信息。这是sys.innodb_lock_waits用的。

  • information_schema.INNODB_LOCK_WAITS:相当于performance_schema.data_lock_waits的 MySQL Server 5.7。这是sys.innodb_lock_waits用的。

  • information_schema.INNODB_TRX:关于正在进行的 InnoDB 交易的信息。这是sys.innodb_lock_waits用的。

  • performance_schema.metadata_locks:关于元数据锁的信息。这是由sys.table_lock_waits视图使用的。

  • SHOW ENGINE INNODB STATUS:生成 InnoDB 监视器输出的语句。当全局变量innodb_status_output_locks被启用时,事务列表也将包括关于锁的信息。输出还将包括有关上次发生的死锁的详细信息。

  • innodb_print_all_deadlocks:当这个全局变量被启用时,所有 InnoDB 死锁的信息将被打印到 MySQL 错误日志中。注意,输出非常详细,所以如果有很多死锁,很难注意到日志中的其他注释、警告和错误。

虽然在研究锁时有几个来源可以参考,但是很难理解这些数据。提供这方面的指导超出了本书的范围。然而,这绝对是一个熟能生巧的例子。花点时间检查一些锁问题的输出是值得的。例如,自己创建一个锁等待或死锁情况。这样,您就知道是什么导致了锁冲突,从而更容易理解各种来源中提供的信息。

摘要

本章讨论了使用 MySQL Connector/Python 开发应用时出现的故障排除问题。故障排除需要多年的实践才能掌握,但希望这篇介绍能让您更容易入门。

本章首先展示了获取问题信息的一些步骤:检查警告;确定发出时执行的 SQL 语句;检索结果作为原始数据,并研究 MySQL 连接器/Python 源代码;并在纯 Python 和 C 扩展实现之间切换。MySQL 服务器日志在故障排除的情况下也非常有用。

MySQL Shell 和第三方 Python IDEs 是调试 MySQL 连接器/Python 程序的有用工具。MySQL Shell 允许您交互式地尝试代码,并在 Python 和 SQL 模式之间切换。当使用内置于 MySQL Shell 中的 X DevAPI 时,它特别有用。ide 提供了更复杂的调试工具,例如检测异常和显示发生异常的源代码,即使异常发生在外部模块内部。ide 还支持断点和变量检查等功能,这些功能在调试程序时非常有用。

本章的最后一部分列举了几个可能遇到的问题。这些例子包括数据问题、编码问题和锁定问题。

这本书的最后一章到此结束。希望您已经发现 MySQL Connector/Python 世界的旅程很有趣,并且您已经准备好在您的工作中使用它。快乐编码。**

第一部分:做好准备

第二部分:遗留 API

第三部分:X DevAPI

第四部分:错误处理和故障排除

posted @ 2024-08-09 17:40  绝不原创的飞龙  阅读(2)  评论(0编辑  收藏  举报