MySQL8-文档存储入门指南-全-
MySQL8 文档存储入门指南(全)
一、MySQL 8 简介——一个新的开始
这是对 Oracle MySQL 工程师(以及 Oracle 本身)奉献精神的证明,MySQL 将继续通过新功能进行改进。MySQL 工程部门的动力是继续为互联网开发颠覆性的数据库技术。甲骨文不仅培养了这种进取精神,而且继续兑现其投资和扩展 MySQL 业务的承诺。最新版本 MySQL 8 最终证明了 Oracle 已经兑现了承诺,确保 MySQL 仍然是世界上最受欢迎的开源数据库系统。
自 MySQL 5.0 1 以来,MySQL 的早期版本增加了一些新的有趣的功能,使 MySQL 成为一个更好的产品。尽管这些特性已经被广泛接受并被用来解决很多问题,但这些变化在很大程度上是渐进的改进,而不是革命性的变化。
这种趋势不是 MySQL 独有的,在一个稳定、成熟的产品中也不罕见。这并不意味着进化发展是不好的——事实并非如此。然而,考虑到一些有竞争力的技术已经出现,MySQL 的工程师们意识到,如果他们想继续统治这个行业,他们必须达到更高和更远的目标。
因此,MySQL 的这个新版本打破了以前版本的许多模式,增加了新的、革命性的功能,改变了一些人使用 MySQL 的方式。事实上,仅版本号一项就从 5.x 跃升至 8.0 2 ,这标志着技术复杂性的飞跃,也标志着持续了 13 年的 5.x 代码库开发的中断。
MySQL 8.0 的变化包括对现有功能的更改以及一些改变游戏规则的新功能。这本书研究了最重要和最新的特性之一:MySQL 文档存储。然而,还有其他同样重要的特性,如组复制和 InnoDB 集群。虽然我主要关注文档存储,但我也将看到如何利用这些其他特性来将您的 MySQL 安装带入未来。
MySQL—What Does it Mean?
MySQL 这个名字是一个专有名称和一个缩写的组合。SQL 是结构化查询语言。“我的部分”不是所有格形式——它是一个名称。在这种情况下,My 是创始人女儿的名字。至于发音,MySQL 专家发音为“My-S-Q-L”——而不是“我的续集”。
在这一章中,我将研究 MySQL 8 的一些新特性,包括对以前版本中的一些新兴技术的简短介绍,MySQL 8 独有的新特性,以及那些使 MySQL 8 成为迄今为止最伟大的 MySQL 版本的革命性特性。
Note
这本书基于 MySQL 8.0.11 版本,重点是文档存储。除了本章列出的功能之外,还有许多新功能。请务必查阅最新的 MySQL online MySQL 参考手册( https://dev.mysql.com/doc/refman/8.0/en/
)以获得新增、更新和删除特性的完整列表。 3
这些新功能非常复杂。正如您将看到的,一些功能设计为协同工作,而其他功能设计为附加功能。下面几节不是解释每一个细节或列出特性和优点,而是介绍 MySQL 8 中目前可用的各种特性的基础,这样您就可以对可用特性有所了解。您还将看到新版本已经超越了传统的 MySQL 存储和检索机制。
让我们先来看看一些特性,这些特性是早期版本的一部分,但是现在已经过改进,并且更加全面地集成到了服务器中。
旧功能再创新
第一类特性包括 MySQL 5.7 中正在开发的特性,或者作为一个单独的实验性开发项目;一个插件;或者作为以后稳定版本的计划功能。因此,这些特性已经以某种有限的形式发布了。大多数被认为是“开发版本”,并附有免责声明,强烈建议不要在生产环境中使用。有些已经包含在服务器的最新发布候选(RC)版本中。
更确切地说,Oracle 将这些特性作为早期版本发布,以便系统和数据库管理员、信息技术架构师和其他规划者可以试用这些特性并提供反馈以帮助特性成熟。它还允许客户在开发环境的早期调整技术,以防功能需要对基础设施或应用进行更改。
What is a Plugin?
插件是向服务器添加功能的手段,而不必编译和重建服务器本身来合并新功能。插件技术已经存在很长时间了。事实上,MySQL 最初支持可插拔存储引擎,允许您动态添加和删除存储引擎选项。从那时起,MySQL 插件技术不断发展,但概念是相同的。只要插件与服务器版本兼容,就可以从 Oracle 下载 MySQL 插件并安装到您的服务器上立即使用。
插件也是 Oracle 将新特性发布到现有稳定版本中的一种便捷方式。例如,组复制等新功能已经作为插件引入(但包含在最新版本中)。即使一个插件是作为一个开发版本发布的(想想早期的测试版),你仍然可以在服务器的兼容 GA 版本中使用它。这使得 Oracle 能够比必须将它们与主要的服务器版本捆绑在一起更快地产生特性。在组复制和类似技术的情况下,通过在接近创纪录的时间内向用户提供这些功能,节省了 Oracle 多年的开发工作。
MySQL 5.7 代码库中发展了几个特性。以下是我在本书中探索的一些关键特征。其中包括 JSON 数据类型和 MySQL Shell。
JSON 数据类型
从 MySQL version 5.7.8 开始,MySQL 支持一种原生 JSON 数据类型,这种数据类型支持高效地访问表行中 JSON 文档中的数据。因此,您的表中可以有 JSON 数据类型的列。JSON 代表 JavaScript 对象符号。 4 新的 JSON 数据类型是使用 MySQL 作为文档存储的关键组件。简而言之,JSON 是一种用于交换数据的标记语言。它不仅可读,还可以直接在应用中使用,在其他应用、服务器甚至 MySQL 之间存储和检索数据。
Note
在这一节中,我将简要概述 JSON 数据类型和 JSON 文档。我在第三章中对 JSON 进行了深入的分析。
事实上,程序员对 JSON 很熟悉,因为它类似于其他标记方案。JSON 也非常简单,因为它只支持两种类型的结构:1)包含对(名称、值)的集合,以及 2)有序列表(或数组)。当然,您也可以混合和匹配一个对象中的结构。当我们创建一个 JSON 对象时,我们称之为 JSON 文档。
与 MySQL 中的普通数据类型不同,JSON 数据类型允许您将 JSON 格式的对象(文档)存储在一行的一列中。在一个表中可以有多个 JSON 列(字段)。虽然您可以用文本或 BLOB 字段来实现这一点(很多人都这样做),但是 MySQL 中没有内置与文本和 BLOB 字段中的数据进行交互的功能。因此,数据的操作在很大程度上取决于应用。此外,数据的结构通常是每行都有相同的列“格式”。在文本和 BLOB 字段中存储数据并不新鲜,已经存在多年了。
这可以通过使用单个字符串甚至数据的二进制表示并将其存储在文本或 BLOB 字段中来实现。如果数据足够小,可以将它存储在 VARCHAR 和类似的 string 列中。为了以这种方式存储和检索数据,您必须编码然后解码数据——这可能很繁琐——尤其是如果您试图从其他人那里获取数据。
使用 JSON 数据类型,您不必编写专门的代码来存储和检索数据。这是因为 JSON 文档很好理解,并且许多编程环境和脚本语言本身就支持它。可以把 JSON 看作是 XML 文档的派生或扩展。也就是说,它们提供了一种灵活的方式来存储可能因应用而异的数据。JSON 允许你存储当时的数据。与典型的数据库表不同,您不必担心默认值(它们是不允许的),也不必担心您是否有足够的列,甚至是主/从关系来将所有数据规范化并存储在一个漂亮、整洁、结构化的包中。
让我们来看一个可以存储在 MySQL 中的简单 JSON 文档。假设我们有一个联系人列表,其中每个联系人可能有也可能没有存档的地址,可能有也可能没有电子邮件、多个电话号码等等。如果您要创建一个典型的数据库表来存储这些信息,那么您可能会为只有一个姓名和一个电话号码的条目存储大量的空列。
事实上,我们可以随时添加新的数据项,而不必改变底层的表结构。例如,如果您发现以后需要在某些记录中添加一个 Skype Id,您可以在代码中添加您想要的条目的密钥,而不必返回并更改任何现有数据。唯一的问题是,读取数据的代码必须在访问它之前进行修改,以测试这个键是否存在。我在第 8 章和第 9 章中给出了一个例子。
让我们考虑一个示例联系人列表,它包含住在我的区域内为我提供服务的几个人。我需要存储的只是他们的姓名和电话号码。有时候,我只知道(或关心存储)他们的名字。我不需要他们的地址,因为我从来没有给他们寄过任何东西,毕竟他们就在这条街上。清单 1-1 展示了一些条目可能的样子。我选择通过使用 SQL INSERT
语句来演示 JSON 是什么样子,这样您就可以看到将非结构化数据插入数据库的一种方式。
INSERT INTO rolodex.contacts (contact_info) VALUES ('
{
"name": "Allen",
"phones": [
{
"work": "212-555-1212"
}
]
}
');
INSERT INTO rolodex.contacts (contact_info) VALUES ('
{ "name": {
"first": "Joe",
"last": "Wheelerton"
},
"phones": [
{
"work": "212-555-1213"
},
{
"home": "212-555-1253"
}
],
"address": {
"street": "123 main",
"city": "oxnard",
"state": "ca",
"zip": "90125"
},
"notes": "Excellent car detailer. Referrals get $20 off next detail!"
}
');
Listing 1-1Example of JSON Documents
请注意,我使用了一些带有换行符和空格的格式来使 JSON 更容易阅读。然而,这不是必须的。事实上,如果我们用 JSON 数据查询一个表,如清单 1-1 中的行,我们会看到数据显示会有一点不同。清单 1-2 显示了典型选择查询的输出。
mysql> SELECT * FROM rolodex.contacts \G
*************************** 1\. row ***************************
id: 1
contact_info: {"name": "Allen", "phones": [{"work": "212-555-1212"}]}
*************************** 2\. row ***************************
id: 2
contact_info: {"name": {"last": "Wheelerton", "first": "Joe"}, "notes": "Excellent car detailer. Referrals get $20 off next detail!", "phones": [{"work": "212-555-1213"}, {"home": "212-555-1253"}], "address": {"zip": "90125", "city": "oxnard", "state": "ca", "street": "123 main"}}
2 rows in set (0.00 sec)
Listing 1-2SELECT with JSON Columns
这不是很容易读懂,是吗?不要担心,因为您的应用可以很容易地接收这些数据(那些支持 JSON 的语言),所以没什么大不了的。
如果您想试验这个例子,您将需要创建结构和数据。在这种情况下,您将需要一个模式(想想数据库)和集合(想想表)。以下是创建模式和集合所需的 SQL 语句。但是,您通常不会对文档存储使用 SQL 语句,但是您可以使用,因为 MySQL 中集合的底层存储是一个特殊形式的表,如下所示。
CREATE DATABASE `rolodex`;
CREATE TABLE `rolodex`.`contacts` (
`id` INT NOT NULL AUTO_INCREMENT,
`contact_info` json DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
JSON 数据类型使您能够通过 MySQL 内置的对 JSON 文档的支持,以及通过 MySQL Shell、X 插件和 X 协议实现与 JSON 交互的附加工具,为您的数据存储增加灵活性。让我们看看 MySQL Shell。
MySQL Shell
MySQL Shell 是 MySQL 5.7 期间添加的另一个特性。在这种情况下,它是一种新的、独立的产品。MySQL Shell 是 MySQL 的下一代命令行客户端。您不仅可以执行传统的 SQL 命令,还可以使用包括 Python 和 JavaScript 在内的几种编程语言之一与服务器进行交互。此外,如果您还安装了 X 插件,那么您可以使用 MySQL Shell 来处理传统的关系数据以及 JSON 文档。这有多酷?
Tip
可以从 http://dev.mysql.com/downloads/shell/
下载 MySQL Shell。
如果你在想,“是时候了!”Oracle 开发了新的 MySQL 客户端,你并不孤单。MySQL Shell 代表了一种大胆的与 MySQL 交互的新方式。有许多选项,甚至有不同的方式来配置和使用 shell。虽然我们将在第 4 章中看到更多关于 shell 的内容,但是让我们看看如何使用 shell 来执行前面显示的查询。图 1-1 显示了新 MySQL Shell 的快照。请注意,它提供了一个非常熟悉的界面,尽管更现代、更强大。
图 1-1
The MySQL Shell
清单 1-3 展示了如何启动 shell 并执行显示结果的SELECT
语句。请注意用于启动 shell 的命令。在这种情况下,我们指定希望以类似于 SQL 模式下旧客户端的方式使用 shell(--sql
)。
$ mysqlsh -uroot --sql
Creating a session to 'root@localhost'
Enter password:
Your MySQL connection id is 281 (X protocol)
Server version: 8.0.11 MySQL Community Server (GPL)
No default schema selected; type \use <schema> to set one.
MySQL Shell 8.0.11
Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type '\help' or '\?' for help; '\quit' to exit.
MySQL localhost:33060+ ssl SQL > SELECT * FROM rolodex.contacts \G
*************************** 1\. row ***************************
doc: {"_id": "9801A79DE093991311E7FFCB243C3451", "name": {"first": "Allen"}, "phones": [{"work": "212-555-1212"}]}
_id: 9801A79DE093991311E7FFCB243C3451
*************************** 2\. row ***************************
doc: {"_id": "9801A79DE0939E0411E7FFCB243DCDE3", "name": {"last": "Wheelerton", "first": "Joe"}, "notes": "Excellent car detailer. Referrals get $20 off next detail!", "phones": [{"work": "212-555-1213"}, {"home": "212-555-1253"}], "address": {"zip": "90125", "city": "oxnard", "state": "ca", "street": "123 main"}}
_id: 9801A79DE0939E0411E7FFCB243DCDE3
2 rows in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > \exit
Bye!
Listing 1-3Querying JSON data in the MySQL Shell
Note
这些例子是在安装并启用了 X 插件的服务器上执行的。第 2 章演示了如何做到这一点。
虽然这确实很好,但它与旧客户端并没有太大的不同。让 shell 真正强大的是,您可以使用脚本语言来处理数据。清单 1-4 展示了如何以 Python 模式(--python
)启动 shell 并执行 Python 代码来检索相同的结果集。我还演示了一个很好的选项,它允许我们改进 JSON 输出格式(--json=pretty).
啊哈,所以现在我们看到了在结果中查看 JSON 的一种更好的方式!这个选项确实有点冗长。为了清楚起见,我取消了一些更详细的输出。
$ mysqlsh -uroot --python --json=pretty
...
MySQL localhost:33060+ ssl Py > \use rolodex
MySQL localhost:33060+ ssl rolodex Py > contacts = db.get_collection("contacts")
MySQL localhost:33060+ ssl rolodex Py > contacts.find()
{
"documents": [
{
"_id": "9801A79DE093991311E7FFCB243C3451",
"name": {
"first": "Allen"
},
"phones": [
{
"work": "212-555-1212"
}
]
},
{
"_id": "9801A79DE0939E0411E7FFCB243DCDE3",
"address": {
"city": "oxnard",
"state": "ca",
"street": "123 main",
"zip": "90125"
},
"name": {
"first": "Joe",
"last": "Wheelerton"
},
"notes": "Excellent car detailer. Referrals get $20 off next detail!",
"phones": [
{
"work": "212-555-1213"
},
{
"home": "212-555-1253"
}
]
}
],
"executionTime": "0.00 sec",
"warningCount": 0,
"warnings": []
}
MySQL localhost:33060+ ssl rolodex Py > \exit
Bye!
Listing 1-4Using the MySQL Shell with Python
好了,现在我们开始看到 shell 在多大程度上改变了我们的 MySQL 体验。请注意,输出被格式化,以便更好地阅读,我们使用的命令与之前的 SQL 命令有很大不同。如果您认为这看起来像应用代码,那么您的思路是正确的!我们将在第 4 章中看到更多关于 MySQL Shell 的内容。现在让我们通过研究新的 X 插件和 X 协议来发现是什么让 shell 变得强大。
X 插件、X 协议和 X DevAPI
MySQL 引入了新的协议和 API 来处理 JSON 文档。除了支持 JSON 数据类型,我们还有三种以简单名称“X”为前缀的技术:X 插件、X 协议和 X DevAPI。X 插件是一个启用 X 协议的插件。X 协议被设计成使用 X DevAPI 与服务器通信。X DevAPI 是一个应用编程接口,它允许你为 MySQL 开发 NoSQL 解决方案,并把 MySQL 作为一个文档库。
I Know SQL, But What Is NoSQL?
如果您曾经使用过关系数据库系统,那么您无疑非常熟悉 SQL(结构化查询语言),在 SQL 中,我们使用特殊的语句(命令)来与数据进行交互。事实上,大多数数据库系统都有自己的 SQL 版本,其中包括操作数据的命令(DML 数据操作语言)以及定义存储数据的对象(DDL 数据定义语言)甚至管理服务器的管理命令。 5
也就是说,您得到的结果集必须使用命令来搜索数据,然后将结果转换为内部编程结构,使数据看起来像是一个辅助组件,而不是解决方案的一个组成部分。NoSQL 接口打破了这种模式,它允许您使用 API(应用编程接口)来处理数据。更具体地说,你使用编程接口而不是基于命令的接口。
不幸的是,根据你的观点,NoSQL 可能意味着很多事情,包括“非 SQL”、“不仅仅是 SQL”或者“非关系”但是它们都是指这样一个事实,即你所使用的机制不是基于命令的接口,这个术语的大多数用法都表明你在使用编程接口。对于 MySQL 8,可以通过 SQL 或 NoSQL 使用 X 协议访问 JSON 文档,通过 X 插件访问 X DevAPI。
X 插件是 Oracle 如何利用插件技术来实现新特性的一个很好的例子。在这种情况下,X 插件是服务器内部的一个网关,允许使用 X 协议进行通信。MySQL X 插件是服务器自带的,默认情况下是启用的。如果您有一个旧版本的 MySQL Server,您可以使用 MySQL Shell 通过以下命令来启用插件。
$ mysqlsh -u root -h localhost --mysql --dba enableXProtocol
Creating a Classic session to 'root@localhost'
Enter password:
Your MySQL connection id is 527
Server version: 8.0.11 MySQL Community Server (GPL)
No default schema selected; type \use <schema> to set one.
enableXProtocol: X Protocol plugin is already enabled and listening for connections on port 33060
任何支持 X 协议的客户机(不仅仅是 MySQL Shell)都可以使用相关的 X DevAPI 将 MySQL 用作文档存储。事实上,X 协议旨在将 MySQL 的 ACID(原子性、一致性、隔离性和持久性)兼容存储能力作为文档存储来公开,使您能够对 JSON 文档执行创建、读取、更新和删除(CRUD)操作。X 协议还支持 MySQL 的普通 SQL 接口,因此您可以构建使用 SQL 和 NoSQL 接口的应用!
您可能想知道 shell 和插件是如何与服务器交互的。图 1-2 展示了组件是如何“堆叠”的
图 1-2
X Protocol stack
注意,shell 允许使用 X DevAPI,它通过 X 插件与服务器进行通信。因此,X Plugin 是一种由 X Protocol 和 X DevAPI 组成的真正的使能技术。
现在我们已经了解了将 MySQL 用作文档存储的技术,让我们看看 InnoDB 存储引擎在最近的版本中有什么变化。
InnoDB 改进
自 MySQL 5.6 以来,InnoDB 一直是 MySQL 的旗舰存储引擎(也是默认引擎)。Oracle 已经慢慢脱离了多存储引擎模型,该模型专注于现代数据库服务器应该做的事情—支持事务性存储机制。InnoDB 是满足这一需求的答案。
What is a Storage Engine?
存储引擎是一种以各种方式存储数据的机制。例如,有一个存储引擎允许您与逗号分隔值(文本)文件(CSV)进行交互,另一个为写日志文件(归档)进行了优化,一个只在内存中存储数据(内存),甚至还有一个根本不存储任何东西(黑洞)。您可以通过使用ENGINE=
表格选项将它们用于您的表格。除了 InnoDB,MySQL 服务器还附带了 Archive、Blackhole、CSV、Memory 和 MyISAM 存储引擎。InnoDB 存储引擎是唯一支持事务的引擎。有关其他存储引擎的更多信息,包括每个引擎的功能以及如何使用它们,请参见在线 MySQL 参考手册中的“备选存储引擎”一节。
在早期,InnoDB 是一家独立的公司,因此是一款独立的产品,既不是 MySQL 的一部分,也不属于 MySQL AB(MySQL 的最初所有者,现在完全归 Oracle 所有)。最终,Oracle 同时拥有了 InnoDB 和 MySQL,因此将两者结合起来是有意义的,因为它们具有相互包容的目标。尽管仍然有一个独立的 InnoDB 工程团队,但他们已经与核心服务器开发团队完全集成。
这种紧密集成带来了 InnoDB 的许多改进,包括大量性能增强。这在 InnoDB 如何随着这些改进而继续发展的过程中显而易见。
自 5.6 版本以来,改进的列表不断增加,尽管大多数改进相当细微,从某种意义上说,您不会注意到它们(除了通过更好的性能和可靠性,这是不可轻视的),但大多数都显示出致力于使 InnoDB 成为最佳的事务存储机制,并通过扩展 MySQL 成为强大的事务数据库系统。下面列出了 MySQL 8 中对 InnoDB 的一些更有趣的改进。其中一些可能看起来非常深奥,但是那些已经优化或调整了 InnoDB 安装的人在计划迁移到 MySQL 8 时可能需要注意这些。这里没有列出的是可靠性和性能方面的几十个小改进。
- 崩溃恢复:如果索引树损坏,InnoDB 会将损坏标志写入重做日志。这使得损坏标志崩溃安全(它不会在强制重启时丢失)。同样,InnoDB 还会在每个检查点上写一个内存损坏标志。当启动崩溃恢复时,InnoDB 可以读取这些标志,并使用它们来调整恢复操作。
- InnoDB memcached 插件:通过允许在单个 memcached 查询中提取多个(键,值)对得到了改进。
- 死锁检测:有几个新的选项,但是最有前途的包括一个动态配置死锁检测的选项(
innodb_deadlock_detect
)。这可以为高使用率系统提供额外的调优控制,在这些系统中,死锁检测会降低性能。 - 新的 INFORMATION_SCHEMA 视图:InnoDB 有新的视图,包括:
INNODB_CACHED_INDEXES
用于发现每个索引在 InnoDB 缓冲池中缓存的索引页数。INNODB_TABLESPACES_BRIEF
用于查看表空间的空间、名称、路径、标志和空间类型。
AUTO_INCREMENT
:自动递增字段有几处小的改进,包括以下内容:- 当前最大自动增量值现在在服务器重新启动后保持不变。
- 重启不再取消
AUTO_INCREMENT =
N
工作台选项的效果。 - 紧随
ROLLBACK
操作之后的服务器重启不再导致分配给回滚事务的自动增量值的重用。 - 将一个
AUTO_INCREMENT
列值设置为大于当前最大值的值是持久的,以后的新值(比如重启后)以新的更大的值开始。
- 临时表:所有临时表现在都在名为
ibtmp1
的共享临时表空间中创建。
虽然这个列表似乎只关注一些小的改进,但是其中一些对于寻求帮助来调整和规划数据库服务器安装的系统管理员来说非常重要。如果您想了解更多关于这些改进的信息,或者查看所有最新变化的列表,请参阅在线 MySQL 参考手册。 6
我还应该注意到,随着 MySQL 8 的成熟和新特性的增加,这个列表很可能会增加。事实上,InnoDB 集群就是我们在“InnoDB 集群”一节中讨论的一个新特性
下一节将描述 MySQL 8 新增的和独有的特性。
新功能
除了那些在 5.7 服务器版本中开发的特性之外,还有一些 MySQL 8 独有的特性。也就是说,它们目前还没有(甚至没有可能被合并到)旧版本中。这部分是因为服务器代码库为了适应新特性而做了大量的修改。MySQL 8.0 中的新特性包括新的数据字典和新的账户管理系统。
Note
有些功能可以作为插件单独下载,您可以安装,并可能以不同于服务器的评级单独发布。有些,比如组复制,也可以和 MySQL 5.7 一起使用。
数据字典
如果你曾经使用 MySQL 试图获得数据库中包含的对象的信息;无论是发现哪些对象在那里,搜索具有特定名称前缀的对象,还是试图发现存在哪些索引,您都有可能不得不访问mysql
数据库中的大量表,或者不得不浏览INFORMATION_SCHEMA
中的视图。
尽管这是多年来的默认设置,但这种机制存在许多问题。最值得注意的是,找东西没有简单的方法(你必须“学习”东西在哪里,然后如何搜索它们)。更重要的是,因为数据是在非事务性的表(和元数据文件)中,所以这些机制不是事务性的,并且,通过扩展,不是崩溃安全的。
事实上,许多 MySQL 数据库管理员通过恢复mysql
数据库中的数据、修复损坏或丢失的.frm
文件,以及许多其他可以访问大型 MySQL 安装的小问题来获得薪水。令人高兴的是,随着数据字典的加入,那些日子一去不复返了!
What’s An Frm File?
如果您检查 MySQL 版和更低版本安装的数据目录,您将看到一个名为 data 的文件夹,其中包含为每个创建的数据库命名的子文件夹。在这些文件夹中,您将看到以表名和文件扩展名.frm
命名的文件。许多 MySQL 开发者称这些文件为“FRM 文件”该文件是一个特殊格式的二进制文件,描述了表的格式(定义)。因此,database1
中一个名为table1
的表有一个名为/data/database1/table1.frm
的 FRM 文件。
遗憾的是,因为 FRM 文件是二进制文件,它们无法通过正常方式读取。事实上,格式多年来一直是个谜(它使用了一种叫做 Unireg 的布局)。因为 FRM 文件包含表的元数据,所以所有列定义和表选项(包括索引定义)都存储在该文件中。这意味着应该可以从 FRM 文件中提取重建 CREATE TABLE 语句所需的数据。不幸的是,考虑到 Unireg 的接口和唯一性,要解析这些文件中的信息并不容易。
幸运的是,您可以通过 MySQL Utilities 产品中的 Python 工具解密 FRM 文件。如果您需要读取一个 FRM 文件来恢复一个表,请参阅在线 MySQL 实用程序文档了解更多详细信息: http://dev.mysql.com/doc/mysql-utilities/1.6/en/utils-task-get-structure.html
.
您可能会觉得奇怪甚至有点奇怪的是,数据字典的实现是隐藏的,而且非常隐蔽。也就是说,数据字典表是不可见的,不能直接访问。您不会很容易找到数据字典表(尽管如果您足够努力的话是有可能的)。这样做主要是为了使数据字典崩溃安全,并且您不必进行管理。幸运的是,您可以通过INFORMATION_SCHEMA
数据库甚至是SHOW
命令来访问存储在数据字典中的信息。mysql
数据库仍然存在,但它主要包含额外的信息,如时区、帮助和类似的非重要信息。
Tip
当您计划从 MySQL 的旧版本升级时,数据字典是您必须了解的关键因素之一。我在第 10 章中研究了这些问题。
有关数据字典的更多信息,请参见在线 MySQL 参考手册中的“MySQL 数据字典”一节。
添加数据字典最终使得许多人一段时间以来一直想实现的许多特性成为可能。最新的变化之一是账户管理的变化。
账户管理
如果您曾经管理过一个 MySQL 数据库服务器(或许多服务器),那么您可能会遇到这样的情况:您需要为一组用户分配相同的权限。例如,您的服务器可能支持多个应用或数据库,这些应用或数据库具有对数据库对象具有特定权限的用户组。在大多数情况下,精明的数据库管理员(DBA)会复制一份用户权限(通常以GRANT
语句的形式),以便在需要创建另一个具有相同权限的用户时可以重用它们。
虽然 MySQL Utilities 产品有一个 Python 实用程序来帮助管理这种单调乏味的工作(参见 http://dev.mysql.com/doc/mysql-utilities/1.6/en/
中的mysqluserclone
),但是必须创建几十个不同“类型”的用户可能是一个相当大的挑战。真正需要的是一种创建角色并为角色定制权限,然后将角色授予用户的方法。幸运的是,随着数据字典的出现,MySQL 中的支持角色在 MySQL 8 中已经成为现实!
可以创建、删除角色,授予或撤销权限。我们还可以向用户授予角色或从用户处撤销角色。角色最终使得管理 MySQL 上的用户账户变得更加简单。有关角色的更多信息,请参见在线 MySQL 参考手册中的使用角色。
服务器中的 SSL(安全套接字层)支持也发生了变化。
删除了选项、变量和功能
关于 MySQL 8,您可能注意到的第一件事是启动选项、变量等的许多小变化。幸运的是,其中大部分都与支持最新功能和删除旧的和过时的设置有关。此外,许多在 MySQL 5.7(及更早版本)中被标记为不推荐使用的选项、变量和特性在 MySQL 8 中被正式删除。MySQL 8 中移除的一些更熟悉的项目包括。
--bootstrap
:用于控制服务器如何启动,通常用于创建 MySQL 权限表,而无需启动完整的 MySQL 服务器。--innodb_file_format_*
:用于配置 InnoDB 存储引擎的文件格式。--partition
和-skip partition
:用于控制 MySQL 服务器中的用户自定义分区支持。
新数据字典的结果之一是不再需要.frm
文件(FRM)。因为数据字典包含关于所有数据库中每个对象的所有信息,这些数据库以可靠的、可恢复的存储机制托管,所以不再需要将这样的信息存储在单独的文件中。对于我们这些经常与 FRM 文件丢失或损坏的服务器进行斗争或试图修复该服务器时遇到独特挫折的人来说,删除 FRM 文件是一个早就应该做的事情,也是最受欢迎的遗漏。
对于使用 SSL 的人来说,一个可能需要关注的地方是删除了一些 SSL 选项,并引入了一个新的身份验证插件(caching_sha2_password
)来提高安全连接。8.0.4 版中引入了新的身份验证插件。如果需要的话,大多数安装包会让您选择旧的身份验证方法,但是强烈建议您使用新的身份验证插件。
错误代码是您将看到一些变化的另一个方面。在最新版本中,许多错误代码都进行了更改,包括删除了几十个鲜为人知的错误代码。如果您的应用使用 MySQL 服务器错误代码,您应该检查文档以确保错误代码没有被更改或删除。
还有许多小项目被删除,包括 mysql_plugin 实用程序、 7 嵌入式服务器(libmysqld
)、通用分区引擎(InnoDB 现在有原生分区)、脚本mysql_install_db
(这已被替换为--initialize
选项)等等。
正如我在前面几节中提到的,随着更多特性的成熟和添加,MySQL 8 中删除的特性列表可能会增加。如果您已经定义了调优过程、存储过程、DevOps、 8 或其他使用选项和变量或与之交互的机制,您应该仔细检查 MySQL 8 文档中的条目,以确保您可以修改您的工具。
Tip
参见 http://dev.mysql.com/doc/refman/8.0/en/added-removed-variables-options.html
获取 MySQL 8 中要删除的特性的完整列表。
范式转变特征
当 MySQL 工程师和产品管理团队决定开发突破性的高可用性功能和存储非结构化数据的新方法时,他们知道他们正在做的事情将极大地改变 MySQL 世界。
在这一节中,我们将看到两个高可用性特性,它们将以一种全新的、引人注目的方式改变 MySQL 的高可用性。我们还将看到新的结构化存储机制将如何改变您可以存储的内容,以及您如何与 MySQL 进行交互,以便为数据可能发生变化的应用存储数据,从而使您的应用无需重新构建存储层即可适应变化。
让我们从高可用性解决方案开始。
组复制
如果您使用过 MySQL 复制,那么您无疑非常熟悉如何在构建高可用性解决方案时利用它。事实上,您很可能已经发现了许多使用 MySQL 复制来提高应用可用性的方法。
What Is Replication? And How Does it Work?
MySQL 复制是一个易于使用的特性,也是 MySQL 服务器的一个复杂和主要的组件。本节提供了复制的鸟瞰图,目的是解释它是如何工作的以及如何设置一个简单的复制拓扑。有关复制及其众多特性和命令的更多信息,请参见在线 MySQL 参考手册( http://dev.mysql.com/doc/refman/8.0/en/replication.htm
l )。
复制需要两台或更多服务器。必须将一台服务器指定为源服务器或主服务器。主角色意味着对数据的所有数据更改(写入)都发送到主服务器,并且只发送到主服务器。拓扑中的所有其他服务器维护主数据的副本,并且根据设计和要求是只读服务器。因此,当您的传感器发送数据进行存储时,它们会将数据发送给主设备。您编写的使用传感器数据的应用可以从从属服务器读取这些数据。
复制机制使用一种称为二进制日志的技术,该技术以一种特殊的格式存储更改,从而保留所有更改的记录。这些变化然后被运送到从设备并在那里执行。因此,一旦从机执行了更改(称为事件),从机就拥有了数据的精确副本。
主服务器维护更改的二进制日志,从服务器维护该二进制日志的副本,称为中继日志。当从设备向主设备请求数据更改时,它从主设备读取事件并将它们写入其中继日志;然后,从属线程中的另一个线程执行中继日志中的那些事件。可以想象,从主服务器上发生更改到从服务器上发生更改会有一点延迟。幸运的是,除了在高流量(大量变化)的拓扑中,这种延迟几乎是不明显的。
此外,显而易见的是,您的高可用性需求和解决方案扩展得越多(复杂性越大),您就越需要采用更好的方法来管理节点丢失、数据完整性和集群的一般维护(复制数据的服务器组—有时称为副本集)。事实上,大多数高可用性解决方案已经超越了基本的主服务器和从服务器拓扑,发展成为由服务器集群组成的层。有些公司复制了一部分数据,以提高吞吐量和实现分区存储。所有这些导致许多人发现他们需要更多的 MySQL 复制。Oracle 通过组复制满足了这些需求以及更多需求。
组复制于 2016 年 12 月作为 GA 发布,以插件的形式与服务器捆绑在一起。虽然它是一个 GA 版本,但我在这里将它列为一个范式转换特性,因为它提供了允许 MySQL 高可用性远远超出原始 MySQL 复制特性范围的承诺,从而使 MySQL 8 成为高可用性数据库解决方案中的一个重要组件。
Note
我只涉及组复制的最基本的内容,以便让您了解它的复杂性和好处。对使用组复制及其实现的深入探究超出了本书的范围。
组复制使拓扑最终同步复制(在属于同一组的节点之间)成为现实,而现有的 MySQL 复制特性是异步的(或至多是半同步的)。因此,可以提供更好的高可用性保证,因为事务以相同的顺序交付给所有成员(尽管在被接受后在每个成员中以自己的速度应用)。
组复制是通过分布式状态机来实现的,在分配给组的服务器之间有很强的协调性。这种通信允许服务器在组内自动协调复制。更具体地说,组维护成员关系,以便服务器之间的数据复制在任何时间点都是一致的。即使从组中删除了服务器,当添加它们时,一致性也会自动启动。此外,对于离线或变得不可达的服务器,还有一个故障检测机制。图 1-3 显示了您将如何在我们的应用中使用组复制来实现高可用性。
图 1-3
Using Group Replication with applications for high availability (Courtesy of Oracle)
请注意,组复制可以与 MySQL 路由一起使用,以允许您的应用拥有一个与集群隔离的层。当我们研究 InnoDB 集群时,我们将会对路由有所了解。
组复制和标准复制的另一个重要区别是,组中的所有服务器都可以参与更新数据,并自动解决冲突。是的,您不再需要精心设计您的应用来发送写入(更新)到特定的服务器!但是,您可以将组复制配置为只允许一台服务器(称为主服务器)进行更新,其他服务器充当辅助服务器或备份服务器(用于故障转移)。
所有这些功能以及更多功能都是通过组复制中内置的三种特定技术实现的:组成员资格、故障检测和容错。 9
- 组成员资格:这管理服务器是否是活动的(在线的)以及是否加入到组中。此外,确保组中的每台服务器都有一致的成员集视图。也就是说,每个服务器都知道组中服务器的完整列表。当服务器添加到组中时,组成员资格服务会自动重新配置成员资格。
- 故障检测:一种机制,能够发现并报告哪些服务器离线(不可达)并被认为是死的。故障检测器是一种分布式服务,它允许组中的所有服务器测试假定失效服务器的状况,这样,组就可以确定服务器是否不可达(失效)。这允许该组通过协调排除故障服务器的过程来自动重新配置。
- 容错:该服务使用 Paxos 10 分布式算法的实现来提供服务器之间的分布式协调。简而言之,该算法允许在组内自动提升角色,以确保组保持一致(数据一致且可用),即使一个(或几个)服务器出现故障或离开组。与类似的容错机制一样,失败(失败的服务器)的数量是有限的。目前,组复制容错被定义为 n = 2f + 1,其中 n 是容忍 f 个故障所需的服务器数量。例如,如果您希望容忍多达 5 台服务器出现故障,则该组中至少需要 11 台服务器。
虽然组复制是一个插件,但它现在与 MySQL 5.7(从 5.7.17 版本开始)和 MySQL 8 的服务器安装捆绑在一起。 11
Tip
要了解有关内部机制、设计、实现以及如何设置和使用组复制的更多信息,请参见位于 http://mysqlhighavailability.com/mysqlha/gr/doc/index.html
的开发者文档。
与其单独演示组复制,不如在下一节研究另一个名为 InnoDB Cluster 的新特性时,让我们看看这个特性有多强大。正如您将在 InnoDB Cluster 的演示中看到的,组复制易于使用,并且当作为 InnoDB Cluster 的一部分时,这两种技术以最显著的方式改变了我们使用 MySQL 复制的方式。
InnoDB 集群
另一个新出现的特性叫做 InnoDB 集群。它旨在使高可用性更易于设置、使用和维护。InnoDB Cluster 通过 MySQL Shell 和管理 API、组复制以及 MySQL 路由 12 与 X AdminAPI 协同工作,将高可用性和读取可扩展性提升到一个新的水平。也就是说,它将 InnoDB 中用于克隆数据的新功能与组复制、MySQL Shell 和 MySQL 路由相结合,提供了一种设置和管理高可用性的新方法。
Note
AdminAPI 是一个特殊的 API,可通过 MySQL Shell 进行配置并与 InnoDB 集群交互。因此,Admin API 具有旨在使使用 InnoDB Cluster 更容易的特性。
在此使用案例中,群集设置有一个主节点(在标准复制术语中称为主节点),它是所有写入(更新)的目标。多个辅助服务器(从属服务器)维护数据的副本,这些副本可以被读取,因此能够在不给主服务器增加负担的情况下读取数据,从而实现读取可伸缩性(但是所有服务器都参与协商和协调)。组复制的引入意味着集群是容错的,并且组成员是自动管理的。MySQL 路由缓存 InnoDB 集群的元数据,并执行到 MySQL 服务器实例的高可用性路由,从而更容易编写应用来与集群进行交互。
您可能想知道这与标准复制的读出可伸缩性设置有何不同。从高层次来看,这些解决方案似乎正在解决同一个用例。但是,使用 InnoDB Cluster,您可以从 MySQL Shell 创建、部署和配置集群中的服务器,从而提供一个易于管理的完整的高可用性解决方案。也就是说,您可以通过 shell 使用 InnoDB Cluster AdminAPI,使用 JavaScript 或 Python 以编程方式创建和管理 InnoDB 集群。
现在让我们来看看这些新技术的应用。接下来是部署三台服务器的演示,在新的 MySQL Shell 中使用 JavaScript 命令通过组复制将它们配置为一个集群。虽然这听起来很费力,但实际上并不困难,而且非常容易。
Note
以下命令是在安装了 MySQL 8.0.11、InnoDB Cluster 和 MySQL 路由的系统上使用 InnoDB Cluster 运行的。
让我们从启动 shell 并使用 AdminAPI 部署三台服务器开始。在这种情况下,我们将使用dba
对象中的deploySandboxInstance()
方法为每个服务器创建新的实例。所有这些都将在我们的本地主机上运行。清单 1-5 展示了如何部署三台服务器。我突出显示了用于帮助从消息中识别命令的命令。
$ mysqlsh
MySQL Shell 8.0.11
Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type '\help' or '\?' for help; '\quit' to exit.
MySQL JS > dba.deploySandboxInstance(3307)
A new MySQL sandbox instance will be created on this host in
/Users/cbell/mysql-sandboxes/3307
Please enter a MySQL root password for the new instance:
Deploying new MySQL instance...
Instance localhost:3307 successfully deployed and started.
Use shell.connect('root@localhost:3307'); to connect to the instance.
MySQL JS > dba.deploySandboxInstance(3308)
A new MySQL sandbox instance will be created on this host in
/Users/cbell/mysql-sandboxes/3308
Please enter a MySQL root password for the new instance:
Deploying new MySQL instance...
Instance localhost:3308 successfully deployed and started.
Use shell.connect('root@localhost:3308'); to connect to the instance.
MySQL JS > dba.deploySandboxInstance(3309)
A new MySQL sandbox instance will be created on this host in
/Users/cbell/mysql-sandboxes/3309
Please enter a MySQL root password for the new instance:
Deploying new MySQL instance...
Instance localhost:3309 successfully deployed and started.
Use shell.connect('root@localhost:3309'); to connect to the instance.
MySQL JS >
Listing 1-5Creating Local Server Instances
请注意,本文解释了我们正在使用沙箱,这个术语适用于在本地主机上的一个特殊目录中运行服务器:用户主目录中的 mysql-sandboxes 文件夹。特别是在这种情况下,我们使用/Users/cbell/mysql-sandboxes
。请注意,我们现在有三台服务器运行在端口 3307、3308 和 3309 上。还要注意,shell 将提示您输入新密码。
Tip
JavaScript 是区分大小写的,所以要确保对变量、对象和方法使用正确的拼写。也就是说,名为abc
的变量与名为Abc
的变量不同。
我们需要做的下一件事是设置一个新的集群。我们用dba
对象中的createCluster()
方法来实现这一点。但是首先,我们必须连接到我们希望作为主服务器的服务器。清单 1-6 展示了如何创建集群。请注意,这是我们的 shell 会话的延续,演示了如何创建新的集群。
MySQL JS > \connect root@localhost:3307
Creating a session to 'root@localhost:3307'
Enter password:
Your MySQL connection id is 12
Server version: 8.0.11 MySQL Community Server (GPL)
No default schema selected; type \use <schema> to set one.
MySQL localhost:3307 ssl JS > my_cluster = dba.createCluster('my_cluster')
A new InnoDB cluster will be created on instance 'root@localhost:3307'.
Creating InnoDB cluster 'my_cluster' on 'root@localhost:3307'...
Adding Seed Instance...
Cluster successfully created. Use Cluster.addInstance() to add MySQL instances.
At least 3 instances are needed for the cluster to be able to withstand up to
one server failure.
<Cluster:my_cluster>
Listing 1-6Creating a Cluster in InnoDB Cluster
注意,我们将集群命名为my_cluster
,并使用同名的变量来存储从createCluster()
方法返回的对象。请注意,我们连接的第一台服务器已经成为主服务器。
接下来,我们使用新的my_cluster
对象的addInstance()
添加另外两个服务器实例来完成集群。这些服务器自动成为组中的辅助服务器(从属服务器)。清单 1-7 展示了如何将实例添加到集群中。
MySQL localhost:3307 ssl JS > my_cluster = dba.getCluster('my_cluster')
<Cluster:my_cluster>
MySQL localhost:3307 ssl JS > my_cluster.addInstance('root@localhost:3308')
A new instance will be added to the InnoDB cluster. Depending on the amount of data on the cluster this might take from a few seconds to several hours.
Please provide the password for 'root@localhost:3308':
Adding instance to the cluster ...
The instance 'root@localhost:3308' was successfully added to the cluster.
MySQL localhost:3307 ssl JS > my_cluster.addInstance('root@localhost:3309')
A new instance will be added to the InnoDB cluster. Depending on the amount of data on the cluster this might take from a few seconds to several hours.
Please provide the password for 'root@localhost:3309':
Adding instance to the cluster ...
The instance 'root@localhost:3309' was successfully added to the cluster.
Listing 1-7Adding Instances to the Cluster
一旦创建了集群并添加了实例,我们就可以使用我们的my_cluster
对象的status()
方法来获取集群的状态,如清单 1-8 所示。
MySQL localhost:3307 ssl JS > my_cluster.status()
{
"clusterName": "my_cluster",
"defaultReplicaSet": {
"name": "default",
"primary": "localhost:3307",
"ssl": "REQUIRED",
"status": "OK",
"statusText": "Cluster is ONLINE and can tolerate up to ONE failure.",
"topology": {
"localhost:3307": {
"address": "localhost:3307",
"mode": "R/W",
"readReplicas": {},
"role": "HA",
"status": "ONLINE"
},
"localhost:3308": {
"address": "localhost:3308",
"mode": "R/O",
"readReplicas": {},
"role": "HA",
"status": "ONLINE"
},
"localhost:3309": {
"address": "localhost:3309",
"mode": "R/O",
"readReplicas": {},
"role": "HA",
"status": "ONLINE"
}
}
}
}
MySQL localhost:3307 ssl JS > \exit
Bye!
Listing 1-8Getting the Status of the Cluster
至此,我们已经了解了 InnoDB Cluster 如何设置服务器并将它们添加到组中。您在幕后看不到的是所有的组复制机制—您可以免费获得它们!这有多酷?
现在我们有了一个集群,我们还需要做一件事来使应用能够使用组复制的容错功能。也就是说,即使其中一个服务器出现故障,我们也需要能够连接到集群并与 MySQL 交互。请注意,因为我们只有三台服务器,所以只能容忍一个故障。例如,求解组复制容忍的故障数量中的f
,我们得到3 = 2
f
+ 1
或f
= 1
。
我们现在必须使用 MySQL 路由来管理应用的连接。虽然我们没有一个应用来演示,但是我们可以使用 shell 来看到这一点。现在让我们看看设置路由有多简单。清单 1-9 显示了如何在引导模式下启动路由。请注意,通过连接到集群,路由会自动获取该组的成员。回想一下上一节,这是通过成员服务进行组复制的原则之一。
& mysqlrouter --bootstrap localhost:3307 --user=cbell
Please enter MySQL password for root:
Bootstrapping system MySQL Router instance...
MySQL Router has now been configured for the InnoDB cluster 'my_cluster'.
The following connection information can be used to connect to the cluster.
Classic MySQL protocol connections to cluster 'my_cluster':
- Read/Write Connections: localhost:6446
- Read/Only Connections: localhost:6447
X protocol connections to cluster 'my_cluster':
- Read/Write Connections: localhost:64460
- Read/Only Connections: localhost:64470
& mysqlrouter &
Listing 1-9Setting Up the MySQL Router
好了,现在路由开始运行了。如果集群中的某个服务器发生问题,我们的应用可以使用路由的特性来自动重新路由我们的应用连接。
让我们来看一个这个特性的简短演示。在这种情况下,我们将使用 shell 通过端口 6446 上的路由连接到集群,如清单 1-9 所示。我们使用此端口是因为路由用于自动转发连接。也就是说,如果我们连接的服务器出现故障,例如端口 3307 上的服务器,我们不必重新启动应用来重新连接到另一个端口上的服务器。因此,路由为我们路由通信。让我们来看看实际情况。
清单 1-10 演示了如何通过路由连接到集群。我们在 shell 中切换到 SQL 模式,并使用 SQL 命令查看我们所连接的服务器端口。然后我们切换回 JavaScript 并使用 AdminAPI 来终止实例。然后,我们再次尝试发出 SQL 命令,现在注意到,一旦 shell 自动重新连接,我们现在就连接到了另一个服务器。酷!
$ mysqlsh --uri root@localhost:6446 --sql
Creating a session to 'root@localhost:6446'
Enter password:
Your MySQL connection id is 47
Server version: 8.0.11 MySQL Community Server (GPL)
No default schema selected; type \use <schema> to set one.
MySQL Shell 8.0.11
Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type '\help' or '\?' for help; '\quit' to exit.
MySQL localhost:6446 ssl SQL > SELECT @@port;
+--------+
| @@port |
+--------+
| 3307 |
+--------+
1 row in set (0.00 sec)
MySQL localhost:6446 ssl SQL > \js
Switching to JavaScript mode...
MySQL localhost:6446 ssl JS > dba.killSandboxInstance(3307)
The MySQL sandbox instance on this host in
/Users/cbell/mysql-sandboxes/3307 will be killed
Killing MySQL instance...
Instance localhost:3307 successfully killed.
MySQL localhost:6446 ssl JS > \sql
Switching to SQL mode... Commands end with ;
MySQL localhost:6446 ssl SQL > SELECT @@port;
ERROR: 2006 (HY000): MySQL server has gone away
The global session got disconnected.
Attempting to reconnect to 'root@localhost:6446'..
The global session was successfully reconnected.
MySQL localhost:6446 ssl SQL > SELECT @@port;
+--------+
| @@port |
+--------+
| 3308 |
+--------+
1 row in set (0.00 sec)
MySQL localhost:6446 ssl SQL > \quit
Bye!
Listing 1-10
Fault Tolerance Demonstration
请注意,尽管 shell 丢失了连接,但它会自动重新连接,以便我们可以重试该命令。非常好。
最后,让我们看看如何将失败的实例重新投入使用。在这种情况下,我们模拟恢复关闭的服务器,将其添加回集群,在集群中,组复制通过应用任何丢失的事务来确保新服务器变得一致。清单 1-11 显示了可以用来恢复服务器的命令。
$ mysqlsh --uri root@localhost:6446
MySQL localhost:6446 ssl JS > dba.startSandboxInstance(3307)
The MySQL sandbox instance on this host in
/Users/cbell/mysql-sandboxes/3307 will be started
Starting MySQL instance...
Instance localhost:3307 successfully started.
MySQL localhost:6446 ssl JS > my_cluster = dba.getCluster('my_cluster')
<Cluster:my_cluster>
MySQL localhost:6446 ssl JS > my_cluster.rejoinInstance('root@localhost:3307')
Rejoining the instance to the InnoDB cluster. Depending on the original
problem that made the instance unavailable, the rejoin operation might not be
successful and further manual steps will be needed to fix the underlying
problem.
Please monitor the output of the rejoin operation and take necessary action if the instance cannot rejoin.
Please provide the password for 'root@localhost:3307':
Rejoining instance to the cluster ...
The instance 'root@localhost:3307' was successfully rejoined on the cluster.
The instance 'localhost:3307' was successfully added to the MySQL Cluster.
MySQL localhost:6446 ssl JS > \q
Bye!
Listing 1-11Recovering a Lost Server
很明显,使用 shell 来设置和管理集群要比设置和管理标准的组复制设置容易得多。特别是,您不必手动配置复制!更好的是,如果服务器出现故障,您不必担心重新配置您的应用或拓扑来确保解决方案保持可行——InnoDB Cluster 会自动为您完成这一任务。
要了解有关 InnoDB 集群的更多信息,请参阅位于 https://dev.mysql.com/doc/mysql-innodb-cluster/en/
的在线文档。
摘要
自从开发者下载代码、修改代码并在他们快速开发的平台上投入使用以来,MySQL 已经走过了漫长的道路。作为一个目睹并参与其发展的人,我带着一些自豪回顾过去糟糕的日子,看看 MySQL 已经走了多远。
这段旅程并不容易。仅工程团队就经受住了连续两次收购(Sun Microsystems 和 Oracle ),以及一系列较小的团队开发和较小的人员变动。通过这一切,工程团队继续改进功能和添加新技术,致力于使 MySQL 成为最好的解决方案。
用户在使用 MySQL 的方式上也有所增长,从独立的单个数据库服务器安装到大规模的高可用性服务器群。通过所有这些,MySQL 产品已经为更好的东西做好了准备。现在,随着 MySQL 8.0 的发布,Oracle 已经展示了它的实力,它装载了顶级的技术。事实上,MySQL 世界已经准备好发现新的方法来利用 MySQL。我确信当你读到这本书时,你会对如何改进 MySQL 有自己的想法。
在本章中,我们探讨了新的 MySQL 服务器 8.0 版的一些亮点。我们发现了那些最初在早期版本中引入的功能,这些功能已经适应了 8.0 版的新模式,新功能,以及那些真正具有革命性的新功能,如文档存储、组复制和 InnoDB 集群。
在第 2 章中,我简短地介绍了安装和使用 MySQL 的入门知识。如果你以前没有使用过 MySQL 或任何形式的关系数据库系统,第 2 章将帮助你了解 MySQL 如何通过 SQL 命令以更传统的方式工作。如果您一直在使用 MySQL 的旧版本,您可能仍然希望浏览该章节,以了解如何安装和配置 MySQL 8 以用于文档存储。我会在第 4 章和第 10 章讨论更多关于 MySQL Shell 和升级到 MySQL 8 的内容。
Footnotes 1
MySQL 5.0 于 2003 年 12 月首次发布(alpha)。
有些人会说版本号的变化不仅受欢迎,而且早就应该出现了。
在线 MySQL 参考手册指的是 MySQL 服务器的参考手册。对其他此类手册的引用以产品名称开头。
如 Oracle 数据库中的: https://docs.oracle.com/cd/B14117_01/server.101/b10759/statements_1001.htm
downloads.mysql.com/docs/refman-8.0-en.pdf
我是这个实用程序的最初设计者和实现者。服务器中插件处理的改进使得这个工具变得没有必要。
https://en.wikipedia.org/wiki/DevOps
成功的高可用性解决方案需要故障检测和容错。
见 [https://en.wikipedia.org/wiki/Paxos_(computer_science
](https://en.wikipedia.org/wiki/Paxos_(computer_science) )
。
有关下载服务器的更多信息,请参见 http://www.mysql.com/downloads/
。有关组复制的更多信息,请参见在线 MySQL 参考手册 http://dev.mysql.com/doc
中的“组复制”部分或访问 http://dev.mysql.com/doc/refman/8.0/en/group-replication.html
。
http://dev.mysql.com/doc/mysql-router/en/
二、MySQL 入门
也许您以前从未使用过数据库系统,或者您作为用户使用过一个数据库系统,但从未需要从头开始建立一个。或者,您可能已经决定发现数据库系统有什么大惊小怪的。或者也许你只是作为一名开发者使用 MySQL,从未见过如何设置和配置服务器。
在这一章中,我将从一般的 SQL 接口角度(传统的 MySQL)对 MySQL 做一个简短的介绍。您不仅会看到 MySQL 8 是如何设置的,还会了解 SQL 接口的一些基础知识,这是完全管理 MySQL 服务器所必需的。也就是说,新的 shell、X 协议、X DevAPI,以及构建在其上的特性,但不提供管理服务器的完整机制;您将需要继续使用 SQL 命令来完成这些任务。
因此,尽管 MySQL 8 为应用和交互式会话提供了出色的 NoSQL 接口,但您仍然需要知道如何使用 SQL 接口。幸运的是,我在一篇关于如何使用 MySQL 的简短入门中介绍了基础知识。让我们先简单了解一下 MySQL 是什么,它能为我们做什么。
了解 MySQL
MySQL 是世界上最受欢迎的开源数据库系统,原因有很多。首先,它是开源的,这意味着任何人都可以免费使用它来完成各种各样的任务。最重要的是,MySQL 包含在许多平台库中,这使得它很容易获得和安装。如果您的平台在资源库中没有包含 MySQL(比如 aptitude),您可以从 MySQL 网站( http://dev.mysql.com
)下载。
甲骨文公司拥有 MySQL。Oracle 通过收购 Sun Microsystems 获得了 MySQL,Sun Microsystems 从其原始所有者 MySQL AB 获得了 MySQL。尽管担心会出现相反的情况,但 Oracle 通过继续投资于新功能的演进和开发以及忠实地维护其开源遗产,表现出了对 MySQL 的出色管理。尽管 Oracle 也提供 MySQL 的商业许可——就像它以前的所有者过去做的那样——MySQL 仍然是开源的,每个人都可以使用。
Is Open Source Really Free?
开源软件是从对公司财产心态的有意识抵制中成长起来的。理查德·斯托尔曼被认为是自由软件运动之父,他开创了一种许可机制来帮助保护软件的所有权,同时让所有人都可以免费使用该软件及其修订版。我们的目标是重建一个开发者社区,他们按照一个命令合作:保证自由而不是限制自由。
这最终导致了一些措辞巧妙的(具有法律约束力的)许可协议的发明,这些协议允许代码被无限制地复制和修改,声明衍生作品(修改后的副本)必须在与原始版本相同的许可下分发,而没有任何附加限制。一种这样的许可证(由 Stallman 创建)被称为 GNU 公共许可证(GPL)。这是 Oracle 用来许可 MySQL 的许可证,因此任何人都可以免费使用。
然而,GPL 和类似的许可证旨在保证使用、修改和发布的自由;大多数人从未想过“免费”意味着“没有成本”或“免费拥有一个好家庭”为了消除这种误解,开放源码倡议(OSI)成立了,后来采用并推广了“开放源码”一词来描述 GPL 许可证所保证的自由。更多关于开源软件和 GPL 的信息,请访问 www.opensource.org
。
MySQL 在您的系统上作为后台进程运行(或者作为前台进程,如果您从命令行启动它)。和大多数数据库系统一样,MySQL 支持结构化查询语言(SQL)。您可以使用 SQL 创建数据库和对象(使用数据定义语言;DDL)、写入或更改数据(使用数据操作语言;DML),并执行各种命令来管理服务器。
我如何连接到 MySQL?
我们已经简要了解了用于连接和使用 MySQL 服务器的新 MySQL Shell、用于配置 InnoDB 集群的 AdminAPI 以及用于访问数据的 X DevAPI。然而,MySQL 中还有一个已经存在了几十年的客户端。它是一个名为mysql
的应用,使您能够连接到服务器并在其上运行 SQL 命令。有趣的是,这个 MySQL 客户端最初被命名为 MySQL monitor,但长期以来一直被简单地称为“MySQL 客户端”、“终端监控器”,甚至是 MySQL 命令窗口。
New Default Authentication
在 MySQL 8 . 0 . 4 版之前,默认的身份验证机制使用一个名为 mysql_native_password 插件的身份验证插件,该插件使用 SHA1 算法。这种机制速度很快,不需要加密连接。然而,由于国家标准和技术研究所(NIST)建议他们应该停止使用 SHA1 算法;Oracle 已将 MySQL 8 . 0 . 4 版中的默认身份验证插件更改为 cachin_sha2_password 插件。
对于安装 MySQL 8.0.4 的任何组织来说,这种变化的后果应该不是问题,但对于那些升级到 8.0.4 或安装了较旧版本 MySQL 的组织来说,这可能是一个问题。最大的问题是较旧的客户端实用程序,如 5.7 版的 mysql 客户端,可能无法连接到 MySQL 8.0.4 或更高版本的新安装。
虽然您可以更改 MySQL 8.0.4 以使用旧的身份验证机制,但不建议这样做,您应该将所有客户端工具升级到 8.0.4 或更高版本,以便使用最新版本的 MySQL。
如果您想了解更多关于这些变化的信息,包括甲骨文做出这些变化的原因以及给用户带来的好处,请参见 https://mysqlserverteam.com/mysql-8-0-4-newdefault-authentication-plugin-caching_sha2_password
/ 。
要使用 MySQL 客户端(mysql
)连接到服务器,您必须指定一个用户帐户和您想要连接的服务器。如果连接到同一台计算机上的服务器,可以省略服务器信息(主机和端口),因为它们默认为本地主机的端口 3306。使用- user(或-u
)选项指定用户。您可以在命令中为用户指定密码,但更安全的做法是指定--password
(或-p
),客户端会提示您输入密码。如果您确实在命令行上指定了密码,您将会得到一个警告提示,鼓励您不要这样做。
在没有--host
(或-h
)和--port
选项的同一台机器上使用 mysql 客户端不使用网络连接。如果您想要使用网络连接进行连接,或者想要使用不同的端口进行连接,则必须使用环回地址。例如,要连接到同一台机器上端口 3307 上运行的服务器,使用命令mysql -uroot -p –h127.0.0.1 --port=3307
。清单 2-1 展示了几个使用 mysql 客户端的 SQL 命令的例子。
Tip
要查看客户端中可用命令的列表,请键入 help。并在提示符下按 Enter 键。
$ mysql -uroot -proot -h 127.0.0.1 --port=3307
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 14
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 of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> CREATE DATABASE greenhouse;
Query OK, 1 row affected (0.00 sec)
mysql> CREATE TABLE greenhouse.plants (plant_name char(50), sensor_value int, sensor_event timestamp);
Query OK, 0 rows affected (0.01 sec)
mysql> INSERT INTO greenhouse.plants VALUES ('living room', 23, NULL);
Query OK, 1 row affected (0.01 sec)
mysql> SELECT * FROM greenhouse.plants;
+-------------+--------------+--------------+
| plant_name | sensor_value | sensor_event |
+-------------+--------------+--------------+
| living room | 23 | NULL |
+-------------+--------------+--------------+
1 row in set (0.00 sec)
mysql> SET @@global.server_id = 106;
Query OK, 0 rows affected (0.00 sec)
mysql> quit
Bye
Listing 2-1Commands Using the mysql Client
在本例中,您将看到以 CREATE DATABASE 和 CREATE TABLE 语句形式出现的 DDL,以 INSERT 和 SELECT 语句形式出现的 DML,以及一个用于设置全局服务器变量的简单管理命令。接下来,您将看到创建一个数据库和一个表来存储数据,在表中添加一行,最后检索表中的数据。注意我是如何用大写字母表示 SQL 命令关键字的。这是一种常见的做法,有助于使 SQL 命令更容易阅读,更容易找到用户提供的选项或数据。
Tip
您可以通过键入命令 quit 退出 MySQL 客户端。在 Linux 和 Unix 系统上,您可以按 Ctrl+D 退出客户端。
MySQL 中有很多可用的命令。幸运的是,你只需要掌握几个比较常见的。以下是您最常使用的命令。<>中包含的部分表示用户提供的命令组件,而[…]表示需要额外的选项。
CREATE DATABASE <database_name>
:创建数据库USE <database>
:设置默认数据库(不是 SQL 命令)CREATE TABLE <table_name> [...]
:创建一个表格或结构来存储数据INSERT INTO <table_name> [...]
:向表格中添加数据UPDATE [...]
:更改特定行的一个或多个值DELETE FROM <table_name> [...]
:从表格中删除数据SELECT [...]
:从表格中检索数据(行)SHOW [...]
:显示对象列表
Note
您必须用分号(;)或者\G。
虽然这个列表只是一个简短的介绍,并不是一个完整的语法指南,但是有一个很好的在线 MySQL 参考手册,它更详细地解释了每个命令(以及更多)。当你对 MySQL 有任何疑问时,你应该参考在线的 MySQL 参考手册。你可以在 http://dev.mysql.com/doc/
找到它。
显示的一个更有趣的命令允许您查看对象列表。例如,您可以看到带有SHOW DATABASES
的数据库,带有SHOW TABLES
的表列表(一旦您更改为数据库),甚至带有SHOW GRANTS
的用户权限。我发现自己经常使用这些命令。
如果你认为 MySQL 不仅仅是几个简单的命令,那你就错了。尽管 MySQL 易于使用且启动时间快,但它是一个成熟的关系数据库管理系统(RDBMS)。比你在这里看到的要多得多。有关 MySQL 的更多信息,包括所有高级特性,请参阅在线 MySQL 参考手册。
如何获取和安装 MySQL
MySQL 服务器可用于多种平台,包括大多数 Linux 和 Unix 平台、Mac OS X 和 Windows。在撰写本文时,MySQL 8 还没有正式发布,因此只作为开发里程碑版本(DMR)提供。dmr 是您在正式发布之前尝试新版本和新特性的绝佳方式。一般来说,非 GA 版本被认为是开发版,或者是早期候选版本,如 MySQL 8.0.4,是候选版本。因此,您不应该在您的生产机器上安装和使用 DMR 版本。
要下载 MySQL 8 的 GA 版本,请访问 http://dev.mysql.com/downloads/
并点击社区,然后点击 MySQL 社区。您也可以点击下载页面底部附近名为 Community (GPL) Downloads 的链接,然后点击 MySQL Community Server。这是 MySQL 的 GPLv2 许可证。该页面将自动检测您的操作系统。如果您想为另一个平台下载,可以从下拉列表中选择。
下载页面将列出几个可供下载的文件。根据您的平台,您可能会看到几个选项,包括压缩文件、源代码和安装包。大多数人会选择在笔记本电脑或台式电脑上安装安装包。图 2-1 显示了 macOS 平台各种下载选项的示例。
图 2-1
Download page for macOS
最受欢迎的平台之一是微软视窗系统。Oracle 为 Windows 提供了一个名为 Windows Installer 的特殊安装包。这个包包含了社区许可下所有可用的 MySQL 产品,包括 MySQL 服务器、工作台、实用程序和所有可用的连接器(用于连接 MySQL 的程序库)。这使得在 Windows 上安装成为一站式、一次安装的事情。图 2-2 显示了 Windows installer 的下载页面。
图 2-2
Download page for Windows Installer
但是,您应该注意,Windows Installer 中可能不包括一些更高级的功能和一些处于开发者里程碑发布(DMR)状态的插件。因此,您应该考虑使用服务器软件包进行安装。我们在图 2-2 中的 Windows Installer 下载链接下面看到了这些。您可以选择 Windows Installer 32 位或 64 位安装。注意,这个包可能只不过是一个包含服务器代码的.zip
文件。在这种情况下,您可能需要从解压缩的文件夹中运行服务器,或者进行本地手动安装。
幸运的是,随着 MySQL 8 的成熟,更多的打包选项将变得可用,允许您使用半自动安装机制。让我们来看看其中的一个。在这个场景中,我们将在 macOS Sierra 机器上安装 MySQL 8。在这种情况下,我已经下载了文件mysql-8.0.11-macos10.13-x86_64.dmg
,这是一个压缩文件,包含一个名为mysql-8.0.11
- macos10.13-x86_64.pkg
的 macOS 软件包安装程序。一旦我启动安装程序,第一步是同意许可。图 2-3 显示安装对话框的许可协议面板。
图 2-3
License agreement
显示的许可证是社区版的 GPLv2 许可证。您可以阅读许可证,准备好后,点按“继续”。您将看到一个接受对话框打开,这将为您提供另一个阅读许可的机会。 1 当您准备好接受许可证时,单击接受。图 2-4 显示许可接受对话框。
图 2-4
Accept license
下一个面板显示设置或安装类型。像这个版本这样的早期版本可能不会显示任何可供选择的安装类型。如果运行 Windows Installer,您会看到几个选项。对于大多数平台,缺省安装类型是您开始所需的全部。图 2-5 为安装型面板。准备好后,点击Install
。
图 2-5
Installation type
安装程序可能会要求您授权安装,一旦授权完成,就会很快将 MySQL 安装到/usr/local/mysql
文件夹中(例如 Sierra)。
如果这是你第一次安装 MySQL 8,你会看到一个对话框,显示根帐户的默认密码。这是 MySQL 5.7 中的一个变化,它消除了匿名帐户,使服务器安装更加安全。您应该记下这个密码,因为它是一个您无法猜测的字符和符号的一般随机集合。图 2-6 显示了一个这样的示例对话框。
图 2-6
Root password notice
图 2-7 展示了如何从通知中心恢复 macOS 上的这个对话框,如果你和我一样,倾向于不完整阅读就关闭对话框。 2
图 2-7
Root password notice in macOS notification center
完成后,您将看到一个完成对话框,您可以放心地关闭它。最后,会询问您是要保留安装文件(.dmg
)还是删除它。如果你正在使用 MySQL 8 或者想把它安装在其他地方,不要删除这个文件。
Tip
如果还没有设置的话,将路径/usr/local/mysql/bin
添加到默认路径变量中可能是个好主意。这使得启动 MySQL 客户端工具变得更加容易。
正如您可能已经猜到的,您需要在安装后的第一个操作中更改 root 密码。这样做很容易。只需打开 mysql 客户端(MySQL)并发出以下 SQL 语句。因为我们在默认位置安装了服务器,所以我们可以像这样只使用用户和密码提示来启动客户机:mysql -uroot -p
。客户端将提示您输入密码。
SET PASSWORD="NEW_PASSWORD_GOES_HERE";
如果您收到无法连接到服务器的消息,这可能意味着服务器尚未启动。您可以使用以下命令在 macOS 上启动服务器。
sudo launchctl load -F /Library/LaunchDaemons/com.oracle.oss.mysql.mysqld.plist
Note
在 Windows 上安装 MySQL 8 时,确保在安装过程中选中标记为“Enable X Protocol/MySQL as a Document Store”的框,以确保启用 X 插件和 X 协议。
好了,现在我们已经安装了 MySQL 8 服务器,我们可以开始配置服务器了。您可以在此时安装 MySQL Shell,但是我们将在第 4 章更详细地探讨如何安装 MySQL Shell。
配置和管理对 MySQL 的访问
现在您已经知道了如何安装 MySQL,让我们简单地讨论一下如何配置 MySQL,如何授予其他人访问服务器(和数据库)的权限,以及如何设置 X 插件(启用文档存储的关键组件)。我们首先看一下用于定义 MySQL 中的行为和配置选项的配置文件。
配置文件
在 MySQL 中配置启动选项和变量的主要方法是使用一个名为my.cnf
(或 Windows 上的my.ini
)的文本文件。这个文件通常位于 Posix 系统的/etc
文件夹中。比如在 macOS 上,文件被命名为/etc/my.cnf
。清单 2-2 显示了典型 MySQL 配置文件的前几十行。
# Example MySQL config file for small systems.
#
# This is for a system with little memory (<= 64M) where MySQL is only used
# from time to time and it's important that the mysqld daemon
# doesn't use much resources.
#
# MySQL programs look for option files in a set of
# locations which depend on the deployment platform.
# You can copy this option file to one of those
# locations. For information about these locations, see:
# http://dev.mysql.com/doc/mysql/en/option-files.html
#
# In this file, you can use all long options that a program supports.
# If you want to know which options a program supports, run the program
# with the "--help" option.
# The following options will be passed to all MySQL clients
[client]
port = 3306
socket = /tmp/mysql.sock
# Here follows entries for some specific programs
# The MySQL server
[mysqld]
port = 3306
socket = /tmp/mysql.sock
skip-external-locking
key_buffer_size = 16K
max_allowed_packet = 1M
table_open_cache = 4
sort_buffer_size = 64K
read_buffer_size = 256K
read_rnd_buffer_size = 256K
net_buffer_length = 2K
thread_stack = 1024K
...
innodb_log_file_size = 5M
innodb_log_buffer_size = 8M
innodb_flush_log_at_trx_commit = 1
innodb_lock_wait_timeout = 50
innodb_log_files_in_group = 2
slow-query-log
general-log
...
Listing 2-2MySQL Configuration File Excerpt
请注意,我们使用方括号[]
定义了按部分分组的设置。例如,我们看到一个名为[client]
的部分,它用于为任何读取配置文件的 MySQL 客户端定义选项。同样,我们看到一个名为[mysqld]
的部分,它适用于服务器进程(因为可执行文件名为mysqld
)。请注意,我们还可以看到端口、套接字等基本选项的设置。但是,我们也可以使用配置文件来设置 InnoDB、复制等选项。
我建议您找到并浏览安装的配置文件,以便查看选项及其值。如果您遇到需要更改某个选项的情况——比方说测试效果或者进行实验——您可以使用SET
命令来更改值,作为全局设置(影响所有连接)或者会话设置(仅适用于当前连接)。
但是,如果您更改了配置文件中的全局设置,该值(状态)将仅保留到服务器重新启动为止。因此,如果您想要保留全局更改,您应该考虑将它们放在配置文件中。
另一方面,在会话级别设置一个值可能在有限的时间内是有益的,或者您可能只希望为特定的任务做一些事情。例如,以下代码关闭二进制日志,执行一个 SQL 命令,然后重新打开二进制日志。下面是一个简单而深刻的示例,说明如何在参与复制的服务器上执行操作,而不会影响其他服务器。 3
SET sql_log_bin=0;
CREATE USER 'hvac_user1'@'%' IDENTIFIED BY 'secret';
SET sql_log_bin=1;
有关配置文件以及如何使用它来配置 MySQL 8 的更多信息,包括使用多个选项文件以及这些文件在每个平台上的位置,请参见在线 MySQL 参考手册( http://dev.mysql.com/doc/refman/8.0/en/
)中的“使用选项文件”一节。
创建用户和授予访问权限
在使用 MySQL 之前,您需要了解另外两个管理操作:创建用户帐户和授予数据库访问权限。MySQL 可以用GRANT
语句来执行这两项操作,如果用户不存在,它会自动创建一个用户。但是更迂腐的方法是首先发出一个CREATE USER
命令,然后是一个或多个GRANT
命令。例如,下面显示了名为 hvac_user1 的用户的创建,并授予该用户对数据库room_temp
的访问权限:
CREATE USER 'hvac_user1'@'%' IDENTIFIED BY 'secret';
GRANT SELECT, INSERT, UPDATE ON room_temp.* TO 'hvac_user1'@'%';
第一个命令创建名为hvac_user1
的用户,但是该名称也有一个@后跟另一个字符串。第二个字符串是与用户相关联的机器的主机名。也就是说,MySQL 中的每个用户都有一个用户名和一个主机名,以user@host
的形式来惟一地标识他们。这意味着用户和主机hvac_user1@10.0.1.16
和用户和主机hvac_user1@10.0.1.17
是不同的。但是,%
符号可以用作通配符,将用户与任何主机关联起来。IDENTIFIED BY
子句为用户设置密码。
A Note About Security
为您的应用创建一个对 MySQL 系统没有完全访问权限的用户总是一个好主意。这是为了最大限度地减少任何意外更改,也是为了防止被利用。例如,建议您创建一个只能访问存储(或检索)数据的数据库的用户。
对于主机使用通配符%也要小心。虽然创建单个用户并让用户从任何主机访问数据库服务器变得更加容易,但这也使得恶意用户更容易访问您的服务器(一旦他们发现了密码)。
第二个命令允许访问数据库。您可以授予用户许多权限。该示例显示了您最有可能向传感器网络数据库用户提供的集合:读取(SELECT
)、添加数据(INSERT
)和更改数据(UPDATE
)。有关安全性和帐户访问权限的更多信息,请参见在线 MySQL 参考手册。
该命令还指定要授予特权的数据库和对象。因此,可以给用户一些表的读(SELECT
)权限,给另一些表的写(INSERT
、UPDATE
)权限。这个例子让用户可以访问room_temp
数据库中的所有对象(表、视图等等)。
如上所述,您可以将这两个命令合并成一个命令。你可能会在文献中更经常地看到这种形式。下面显示了组合语法。在这种情况下,您需要做的就是将IDENTIFIED BY
子句添加到GRANT
语句中。酷!
GRANT SELECT, INSERT, UPDATE ON room_temp.* TO 'hvac_user1'@'%' IDENTIFIED BY 'secret';
接下来,让我们看看如何配置服务器以用于文档存储;更具体地说,通过安装 X 插件。
配置文档存储
在探索 MySQL 文档库之前,您要做的最后一件事是确保安装了 X 插件。如果您在 Windows 上安装了 MySQL,并且选择启用 Enable X Protocol/MySQL 作为文档存储,则可以跳过这一步。但是,其他平台可能需要配置服务器以用于文档存储。
为了在旧的 MySQL 服务器上启用 X 协议,我们需要安装 X 插件。X 插件名为 MySQLX,可以通过下面的命令轻松安装。INSTALL PLUGIN
命令接受插件(mysqlx)的名称和共享库的名称。按照惯例,共享库被命名为与带有.so
后缀的插件相同(Windows 机器使用.dll
)。
INSTALL PLUGIN mysqlx SONAME 'mysqlx.so';
Note
MySQL 8 . 0 . 11 版及更高版本默认启用 X 插件。
您可以使用下面的命令检查哪些插件被启用。您将看到所有已安装的插件及其当前状态。请注意,我们看到列表中的 X 插件处于启用状态。
mysql> SHOW PLUGINS \G
*************************** 1\. row ***************************
Name: keyring_file
Status: ACTIVE
Type: KEYRING
Library: keyring_file.so
License: GPL
*************************** 2\. row ***************************
Name: binlog
Status: ACTIVE
Type: STORAGE ENGINE
Library: NULL
License: GPL
...
*************************** 43\. row ***************************
Name: mysqlx
Status: ACTIVE
Type: DAEMON
Library: mysqlx.so
License: GPL
43 rows in set (0.00 sec)
么事儿啦在那里。一旦启用,您的服务器将使用 X 协议与 MySQL Shell 或任何其他使用 X 协议的系统、服务或应用进行通信。
如果需要卸载 X 插件,可以使用以下命令:
UNINSTALL PLUGIN mysqlx;
在下一节中,我将对 MySQL 服务器进行更长时间的浏览,以展示如何使用基本的 SQL 命令。在后面的章节中会有更多关于文档存储的内容。
MySQL 第一
如果您从未使用过数据库系统,那么学习和掌握该系统需要培训、经验和极大的毅力。精通所需的主要知识是如何使用常见的 SQL 命令和概念。本节将介绍最常见的 MySQL 命令和概念,作为学习如何使用文档存储的基础,从而完成 MySQL 入门。
Note
本节介绍了更高层次的命令和概念,而不是重复在线 MySQL 参考手册。如果您决定使用任何命令或概念,请参考在线 MySQL 参考手册,了解更多详细信息、完整的命令语法和其他示例。
本节回顾了最常见的 SQL 和 MySQL 特有的命令,您需要了解这些命令才能充分利用 MySQL 服务器数据库。虽然您已经看到了其中一些工具的实际应用,但是本节提供了一些额外的信息来帮助您使用它们。
需要理解的一个重要规则是,用户提供的变量名区分大小写,并且服从主机平台的大小写区分。例如,解析last_name
和Last_Name
在不同平台上是不一致的。也就是说,Windows 上的区分大小写行为不同于 macOS。查看适用于您平台的在线 MySQL 参考手册,了解区分大小写如何影响用户提供的变量。
创建数据库和表
您需要学习和掌握的最基本的命令是CREATE DATABASE
和CREATE TABLE
命令。回想一下,MySQL 之类的数据库服务器允许您创建任意数量的数据库,您可以用逻辑方式添加表和存储数据。
要创建一个数据库,使用CREATE DATABASE
后跟一个数据库名称。如果您正在使用 MySQL 客户端,您必须使用USE
命令切换到特定的数据库。客户端焦点是在启动时(在命令行上)或通过USE
命令指定的最新数据库。
您可以通过首先引用数据库名称来覆盖它。例如,SELECT * FROM db1.table1
将执行,而不管默认的数据库设置。但是,省略数据库名称会导致 mysql 客户端使用默认数据库。下面显示了创建和更改数据库焦点的两个命令:
mysql> CREATE DATABASE greenhouse;
mysql> USE greenhouse;
Tip
如果您想查看服务器上的所有数据库,请使用 SHOW DATABASES 命令。
创建表格需要 yes,CREATE TABLE
命令。该命令有许多选项,不仅允许您指定列及其数据类型,还允许您指定附加选项,如索引、外键等。还可以使用CREATE INDEX
命令创建一个索引(参见下面的代码)。下面的代码显示了如何创建一个简单的表来存储植物传感器数据,例如用于监控个人温室的数据。 4
CREATE TABLE `greenhouse`.`plants` (
`plant_name` char(30) NOT NULL,
`sensor_value` float DEFAULT NULL,
`sensor_event` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`sensor_level` char(5) DEFAULT NULL,
PRIMARY KEY `plant_name` (`plant_name`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
注意这里我指定了表名(植物)和四列(plant_name
、sensor_value
、sensor_event
和sensor_level
)。我使用了几种数据类型。对于plant_name
,我使用了一个最多 30 个字符的字符字段,一个浮点数据类型用于sensor_value
,一个时间戳值用于sensor_event
,另一个字符字段用于 5 个字符的sensor_level
。
当您想要记录事件或动作的日期和时间时,TIMESTAMP
数据类型特别有用。例如,知道何时读取传感器值通常是有帮助的。通过向表中添加一个TIMESTAMP
列,您不需要计算、读取或格式化传感器甚至聚合节点上的日期和时间。
还要注意,我指定将plant_name
列定义为一个键,这将创建一个索引。在这种情况下,它也是主键。PRIMARY KEY
短语告诉服务器确保表中存在且只有一行匹配列的值。通过重复关键字,可以指定几个要在主键中使用的列。注意,所有主键列都不允许空值(NOT NULL
)。
如果您不能确定唯一标识一行的一组列(并且您想要这样的行为——有些人喜欢没有这种限制的表,但是一个好的 DBA 不会),那么您可以为 integer 字段使用一个称为AUTO INCREMENT
的人工数据类型选项。当用于列(这必须是第一列)时,服务器会为插入的每一行自动增加该值。这样,它就创建了一个默认主键。有关自动递增列的更多信息,请参见在线 MySQL 参考手册。
Tip
最佳实践表明,在某些情况下,在字符字段上使用主键并不是最佳选择,例如表中的每一列都有很大的值或者有许多唯一值。这可能会降低搜索和索引的速度。在这种情况下,您可以使用 auto increment 字段来人工添加一个更小的主键(但有点神秘)。
可用的数据类型比上一个示例中显示的多得多。您应该查看在线 MySQL 参考手册,以获得数据类型的完整列表。请参见“数据类型”一节如果你想知道一个表格的布局或“模式”,使用SHOW CREATE TABLE
命令。
Tip
像数据库一样,您也可以使用SHOW TABLES
命令获得数据库中所有表的列表。
搜索数据
您需要知道的最常用的基本命令是从表中返回数据的命令(也称为结果集或行)。为此,您可以使用SELECT
语句。这个 SQL 语句是数据库系统的核心。所有对数据的查询都将使用该命令执行。因此,我们将从列列表开始,多花一点时间看看可以使用的各种子句(部分)。
Note
虽然我们首先检查 SELECT 语句,但是如果您想在您的系统上尝试这些语句,请确保首先运行 INSERT 语句。
SELECT
语句允许您指定想要从数据中选择哪些列。该列表作为语句的第一部分出现。第二部分是FROM
子句,它指定了要从中检索行的表。
Note
FROM
子句可以用来用JOIN
操作符连接表。
指定列的顺序决定了结果集中的显示顺序。如果你想要所有的列,使用星号(*
)代替。清单 2-3 展示了生成相同结果集的三条语句。也就是说,在每个的输出中将显示相同的行。事实上,为了简单起见,我使用了一个只有四行的表。
mysql> SELECT plant_name, sensor_value, sensor_event, sensor_level FROM greenhouse.plants;
+------------------------+--------------+---------------------+--------------+
| plant_name | sensor_value | sensor_event | sensor_level |
+------------------------+--------------+---------------------+--------------+
| fern in den | 0.2319 | 2015-09-23 21:04:35 | NULL |
| fern on deck | 0.43 | 2015-09-23 21:11:45 | NULL |
| flowers in bedroom1 | 0.301 | 2015-09-23 21:11:45 | NULL |
| weird plant in kitchen | 0.677 | 2015-09-23 21:11:45 | NULL |
+------------------------+--------------+---------------------+--------------+
4 rows in set (0.00 sec)
mysql> SELECT * FROM greenhouse.plants;
+------------------------+--------------+---------------------+--------------+
| plant_name | sensor_value | sensor_event | sensor_level |
+------------------------+--------------+---------------------+--------------+
| fern in den | 0.2319 | 2015-09-23 21:04:35 | NULL |
| fern on deck | 0.43 | 2015-09-23 21:11:45 | NULL |
| flowers in bedroom1 | 0.301 | 2015-09-23 21:11:45 | NULL |
| weird plant in kitchen | 0.677 | 2015-09-23 21:11:45 | NULL |
+------------------------+--------------+---------------------+--------------+
4 rows in set (0.00 sec)
mysql> SELECT sensor_value, plant_name, sensor_level, sensor_event FROM greenhouse.plants;
+--------------+------------------------+--------------+---------------------+
| sensor_value | plant_name | sensor_level | sensor_event |
+--------------+------------------------+--------------+---------------------+
| 0.2319 | fern in den | NULL | 2015-09-23 21:04:35 |
| 0.43 | fern on deck | NULL | 2015-09-23 21:11:45 |
| 0.301 | flowers in bedroom1 | NULL | 2015-09-23 21:11:45 |
| 0.677 | weird plant in kitchen | NULL | 2015-09-23 21:11:45 |
+--------------+------------------------+--------------+---------------------+
4 rows in set (0.00 sec)
Listing 2-3Example SELECT Statements
注意,前两条语句以相同的顺序产生相同的行和相同的列。但是,第三条语句虽然生成相同的行,但以不同的顺序显示列。
您还可以使用列列表中的函数来执行计算和类似操作。一个特殊的例子是使用COUNT()
函数来确定结果集中的行数,如下所示。关于 MySQL 提供的函数的更多例子,请参阅在线 MySQL 参考手册。
SELECT COUNT(*) FROM greenhouse.plants;
SELECT
语句中的下一个子句是WHERE
子句。您可以在这里指定用于限制结果集中行数的条件。也就是说,只有那些符合条件的行。这些条件基于列,可能相当复杂。也就是说,您可以基于计算、连接结果等来指定条件。但是大多数条件将是一个或多个列上的简单等式或不等式来回答一个问题。例如,假设您想查看传感器读数小于0.40
的植物?在这种情况下,我们发出以下查询并接收结果。注意,我只指定了两列:工厂名称和从传感器读取的值。
mysql> SELECT plant_name, sensor_value FROM greenhouse.plants WHERE sensor_value < 0.40;
+---------------------+--------------+
| plant_name | sensor_value |
+---------------------+--------------+
| fern in den | 0.2319 |
| flowers in bedroom1 | 0.301 |
+---------------------+--------------+
2 rows in set (0.01 sec)
您还可以使用其他子句,包括用于对行进行分组以进行聚合或计数的GROUP BY
子句,以及用于对结果集进行排序的ORDER BY
子句。让我们从聚合开始,快速地看一下每一个。
假设您想要计算每个传感器在表中读取的传感器值的平均值。在这种情况下,我们有一个包含各种传感器随时间变化的传感器读数的表。尽管这个例子只包含四行(因此可能没有统计信息),但是这个例子非常清楚地展示了聚合的概念,如清单 2-4 所示。请注意,我们收到的只是四个传感器读数的平均值。
mysql> SELECT plant_name, sensor_value FROM greenhouse.plants WHERE plant_name = 'fern on deck';
+--------------+--------------+
| plant_name | sensor_value |
+--------------+--------------+
| fern on deck | 0.43 |
| fern on deck | 0.51 |
| fern on deck | 0.477 |
| fern on deck | 0.73 |
+--------------+--------------+
4 rows in set (0.00 sec)
mysql> SELECT plant_name, AVG(sensor_value) AS avg_value FROM greenhouse.plants WHERE plant_name = 'fern on deck' GROUP BY plant_name;
+--------------+-------------------+
| plant_name | avg_value |
+--------------+-------------------+
| fern on deck | 0.536750003695488 |
+--------------+-------------------+
1 row in set (0.00 sec)
Listing 2-4GROUP BY Example
注意,我在列列表中指定了 average 函数AVG()
,并传入了我想要平均的列的名称。MySQL 中有许多这样的函数可以用来执行一些强大的计算。显然,这是数据库服务器中存在多少功率的另一个示例,这将需要网络中典型的轻量级传感器或聚合器节点上的更多资源。
还要注意,我使用关键字AS
重命名了具有平均值的列。您可以使用它来重命名任何指定的列,这将更改结果集中的名称,如清单所示。
子句的另一个用途是计数。在本例中,我们用COUNT(
替换了AVG()
,得到了与WHERE
子句匹配的行数。更具体地说,我们想知道每个工厂存储了多少传感器值。
mysql> SELECT plant_name, COUNT(sensor_value) as num_values FROM greenhouse.plants GROUP BY plant_name;
+------------------------+------------+
| plant_name | num_values |
+------------------------+------------+
| fern in den | 1 |
| fern on deck | 4 |
| flowers in bedroom1 | 1 |
| weird plant in kitchen | 1 |
+------------------------+------------+
4 rows in set (0.00 sec)
现在,假设我们想要查看按传感器值排序的结果集的结果。我们使用为面板上的蕨类植物选择行的相同查询,但是我们使用ORDER BY
子句按照传感器值以升序和降序对行进行排序。清单 2-5 显示了每个选项的结果。
mysql> SELECT plant_name, sensor_value FROM greenhouse.plants WHERE plant_name = 'fern on deck' ORDER BY sensor_value ASC;
+--------------+--------------+
| plant_name | sensor_value |
+--------------+--------------+
| fern on deck | 0.43 |
| fern on deck | 0.477 |
| fern on deck | 0.51 |
| fern on deck | 0.73 |
+--------------+--------------+
4 rows in set (0.00 sec)
mysql> SELECT plant_name, sensor_value FROM greenhouse.plants WHERE plant_name = 'fern on deck' ORDER BY sensor_value DESC;
+--------------+--------------+
| plant_name | sensor_value |
+--------------+--------------+
| fern on deck | 0.73 |
| fern on deck | 0.51 |
| fern on deck | 0.477 |
| fern on deck | 0.43 |
+--------------+--------------+
4 rows in set (0.00 sec)
Listing 2-5ORDER BY Examples
正如我所提到的,SELECT 语句的内容比这里所展示的要多得多,但是我们所看到的将会让您受益匪浅,尤其是在处理大多数中小型数据库解决方案的典型数据时。
创建数据
现在您已经创建了一个数据库和表,您将希望向表中加载或插入数据。您可以使用INSERT INTO
语句来实现。这里我们指定表格和行的数据。下面是一个简单的例子:
INSERT INTO greenhouse.plants (plant_name, sensor_value) VALUES ('fern in den', 0.2319);
在这个例子中,我通过指定名称和值为我的一个工厂插入数据。你想知道其他的柱子呢?在这种情况下,其他列包括一个时间戳列,它将由数据库服务器填充。所有其他列(只有一列)将被设置为NULL
,这意味着没有值可用、值缺失、值不为零或值为空。
请注意,我在该行的数据之前指定了列。当您希望插入的列数少于表中包含的列数时,这是必要的。更具体地说,关闭列列表意味着您必须为表中的所有列提供数据(或NULL
)。此外,列出的列的顺序可以不同于它们在表中的定义顺序。关闭列列表将导致根据列数据在表中的显示方式对其进行排序。
您也可以使用逗号分隔的行值列表,使用相同的命令插入几行,如下所示:
INSERT INTO greenhouse.plants (plant_name, sensor_value) VALUES ('flowers in bedroom1', 0.301), ('weird plant in kitchen', 0.677), ('fern on deck', 0.430);
这里我用相同的命令插入了几行。请注意,这只是一种简化机制,除了自动提交之外,与发出单独的命令没有什么不同。
更新数据
有时,您需要更改或更新数据。您可能需要更改一列或多列的值,替换多行的值,或者更正数字数据的格式甚至比例。为了更新数据,我们使用UPDATE
命令。您可以更新特定的列、更新一组列、对一列或多列执行计算等等。
更有可能的是,您或您的用户想要重命名数据库中的对象。例如,假设我们确定甲板上的植物实际上不是蕨类植物,而是一种外来开花植物。在本例中,我们希望将所有植物名称为“fern on deck”的行改为“flowers on deck”以下命令执行更改:
UPDATE greenhouse.plants SET plant_name = 'flowers on deck' WHERE plant_name = 'fern on deck';
注意,这里的关键操作符是SET
操作符。这告诉数据库为指定的列分配一个新值。您可以在命令中列出多个 set 操作。
注意,我在这里使用了一个WHERE
子句将UPDATE
限制在一组特定的行中。这就是你在SELECT
语句中看到的同一个WHERE
子句,它做同样的事情;它允许您指定限制受影响的行的条件。如果不使用WHERE
子句,更新将应用于所有行。
Caution
别忘了WHERE
条款!发出不带WHERE
子句的UPDATE
命令将影响表中的所有行!
删除数据
有时,您最终会得到需要删除的表中的数据。也许您使用了测试数据,并想去掉虚假的行。也许您想要压缩或清除表,或者想要删除不再适用的行。要删除行,使用DELETE FROM
命令。
让我们看一个例子。假设您有一个正在开发的工厂监控解决方案,您发现您的一个传感器或传感器节点由于编码、布线或校准错误而读取的值过低。在这种情况下,我们希望删除传感器值小于 0.20 的所有行。以下命令可以做到这一点:
DELETE FROM plants WHERE sensor_value < 0.20;
Caution
别忘了WHERE
条款!发出不带WHERE
子句的DELETE FROM
命令将永久删除表中的所有行!
注意,我在这里使用了一个WHERE
子句。也就是说,一个条件语句来限制被操作的行数。您可以使用您想要的任何列或条件;只要确保你有正确的!我喜欢在SELECT
语句中首先使用相同的WHERE
子句。例如,我将首先发出下面的命令来检查我是否要删除我想要的行,并且只删除那些行。注意是同一个WHERE
子句。
SELECT * FROM plants WHERE sensor_value < 0.20;
使用索引
创建表时不使用任何排序;也就是说,它们是无序的。虽然 MySQL 每次都会以相同的顺序返回数据,但除非创建索引,否则没有隐含的(或可靠的)顺序。我这里所指的排序并不是你在排序时所想的那样(在SELECT
语句中的ORDER BY
子句是可能的)。
相反,索引是服务器在执行查询时用来读取数据的映射。例如,如果一个表上没有索引,并且希望选择某列中值大于某个值的所有行,则服务器必须读取所有行来查找所有匹配项。但是,如果我们在该列上添加了一个索引,服务器将只能读取那些符合标准的行。
我应该注意到有几种形式的索引。这里我指的是一个聚集索引,索引中列的值存储在索引中,允许服务器只读取索引,而不读取行来测试标准。
要创建索引,可以在 CREATE TABLE 语句中指定索引,或者发出一个CREATE INDEX
命令。下面是一个简单的例子:
CREATE INDEX plant_name ON plants (plant_name);
该命令在plant_name
列上添加一个索引。观察这对表格的影响。
CREATE TABLE `plants` (
`plant_name` char(30) NOT NULL,
`sensor_value` float DEFAULT NULL,
`sensor_event` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`sensor_level` char(5) DEFAULT NULL,
PRIMARY KEY (`plant_name`),
KEY `plant_name` (`plant_name`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
像这样创建的索引不会影响表中行的唯一性。换句话说,确保存在且只有一行可以被特定列的特定值访问。我所指的是主键(或主索引)的概念,这是在创建表时使用的一个特殊选项,如前所述。
视图
视图是一个或多个表的结果的逻辑映射。它们可以像查询中的表一样被引用,这使它们成为创建数据子集的强大工具。您用CREATE VIEW
创建一个视图,并给它起一个类似于表格的名字。下面显示了一个简单的例子,其中我们创建了一个测试视图来从表中读取值。在这种情况下,我们限制了视图的大小(行数),但是您可以为视图使用各种各样的条件,包括组合来自不同表的数据。
CREATE VIEW test_plants AS SELECT * FROM plants LIMIT 5;
在中小型数据库解决方案中通常不会遇到视图,但是我将它们包括在内是为了在您决定进行额外的分析并希望将数据组织成更小的组以便于阅读时让您了解它们。
扳机
另一个高级概念(以及相关的 SQL 命令)是使用事件驱动的机制,当数据发生变化时会“触发”该机制。也就是说,您可以创建一组简短的 SQL 命令(过程),这些命令将在插入或更改数据时执行。
触发器将在几种事件或条件下执行。您可以在更新、插入或删除操作之前或之后设置触发器。触发器与单个表相关联,其主体是一个特殊的构造,允许您对受影响的行进行操作。下面是一个简单的例子:
DELIMITER //
CREATE TRIGGER set_level BEFORE INSERT ON plants FOR EACH ROW
BEGIN
IF NEW.sensor_value < 0.40 THEN
SET NEW.sensor_level = 'LOW';
ELSEIF NEW.sensor_value < 0.70 THEN
SET NEW.sensor_level = 'OK';
ELSE
SET NEW.sensor_level = 'HIGH';
END IF;
END //
DELIMITER ;
该触发器将在每次插入表之前执行。在复合语句(BEGIN
)中可以看到。。。END
),我们根据 sensor_value 的值将名为sensor_level
的列设置为LOW
、OK
或HIGH
。要了解这一点,请考虑下面的命令。FOR EACH ROW
语法允许触发器作用于事务中的所有行。
INSERT INTO plants (plant_name, sensor_value) VALUES ('plant1', 0.5544);
因为我们提供的值小于中间值(0.70),所以我们期望触发器为我们填充sensor_level
列。下面显示了触发器触发时发生的情况:
+------------+--------------+---------------------+--------------+
| plant_name | sensor_value | sensor_event | sensor_level |
+------------+--------------+---------------------+--------------+
| plant1 | 0.5544 | 2015-09-23 20:00:15 | OK |
+------------+--------------+---------------------+--------------+
1 row in set (0.00 sec)
这展示了一种有趣而强大的方法,可以利用数据库服务器的能力创建派生列,并节省应用的处理能力和代码。我鼓励您考虑这个以及类似的强大概念,以利用数据库服务器的强大功能。
简单连接
数据库系统最强大的概念之一是在数据之间建立关系的能力(因此得名关系型)。也就是说,一个表中的数据可以引用另一个(或多个)表中的数据。最简单的形式称为主从关系,其中一个表中的一行引用或关联到另一个表中的一行或多行。
一个常见的(也是经典的)主从关系的例子来自订单跟踪系统,其中一个表包含订单的数据,另一个表包含订单的行项目。因此,我们只存储一次订单信息,如客户号和发货信息,并在检索订单时合并或“连接”这些表。
让我们看一个来自名为 world 的示例数据库的例子。你可以在 MySQL 网站上找到这个数据库( http://dev.mysql.com/doc/index-other.html
)。请随意下载它和任何其他示例数据库。它们都展示了数据库系统的各种设计。您还会发现练习查询数据很方便,因为它包含了许多简单的行。
Note
如果要运行以下示例,需要安装示例文档中描述的世界数据库( http://dev.mysql.com/doc/world-setup/en/world-setup-installation.html
)。
清单 2-6 展示了一个简单连接的例子。这里发生了很多事情,所以花点时间检查一下SELECT
语句的各个部分,尤其是我是如何指定JOIN
子句的。您可以忽略LIMIT
选项,因为它只是限制了结果集中的行数。
mysql> USE world;
mysql> SELECT Name, Continent, Language FROM Country JOIN CountryLanguage ON Country.Code = CountryLanguage.CountryCode LIMIT 10;
+-------------+---------------+------------+
| Name | Continent | Language |
+-------------+---------------+------------+
| Aruba | North America | Dutch |
| Aruba | North America | English |
| Aruba | North America | Papiamento |
| Aruba | North America | Spanish |
| Afghanistan | Asia | Balochi |
| Afghanistan | Asia | Dari |
| Afghanistan | Asia | Pashto |
| Afghanistan | Asia | Turkmenia |
| Afghanistan | Asia | Uzbek |
| Angola | Africa | Ambo |
+-------------+---------------+------------+
10 rows in set (0.00 sec)
Listing 2-6Simple JOIN Example
这里我使用了一个JOIN
子句,它接受两个指定的表,这样第一个表使用特定的列及其值连接到第二个表(ON
指定匹配)。数据库服务器所做的是从表中读取每一行,并只返回那些列中的值指定匹配的行。一个表中不在另一个表中的任何行都不会被返回。
Tip
您可以检索那些具有不同联接的行。请参阅在线 MySQL 参考手册中关于内部和外部连接的更多细节。
请注意,我只包括了几个专栏。在本例中,我从Country
表中指定了国家名称和大陆,从CountryLanguage
表中指定了language
列。如果列名不是惟一的(相同的列出现在每个表中),我就必须用表名来指定它们,比如Country.Name
。事实上,总是以这种方式限定列被认为是一种好的做法。
这个例子中有一个有趣的异常,我觉得有必要指出来。事实上,有些人会认为这是一个设计缺陷。注意在JOIN
子句中,我为每个表指定了表和列。这是正常且正确的,但是请注意,两个表中的列名并不匹配。虽然这真的没关系,并且只需要一点额外的输入,但是一些 DBA 会认为这是错误的,并且希望在两个表中使用相同的公共列名。
连接的另一个用途是检索公共数据、存档数据或查找数据。例如,假设您有一个表,其中存储了不变(或很少变)的事物的详细信息,如与邮政编码相关联的城市或与标识号相关联的名称(例如,SSN)。您可以将这些信息存储在一个单独的表中,并在需要时将数据连接到一个公共列(和值)上。在这种情况下,公共列可以用作外键,这是另一个高级概念。
外键用于维护数据完整性(即,如果一个表中的数据与另一个表相关,但这种关系需要保持一致)。例如,如果您想确保在删除主行时所有的细节行也被删除,您可以在主表中声明一个外键,指向细节表的一列(或多列)。有关外键的更多信息,请参见在线 MySQL 参考手册。
关于连接的讨论只涉及最基本的内容。事实上,连接可以说是数据库系统中最困难和最容易混淆的领域之一。如果您发现您想要使用联接来组合几个表或扩展数据,以便从几个表提供数据(外部联接),您应该花一些时间来深入研究数据库概念,如 Clare Churcher 的书《数据库设计入门》(Apress,2012)。
存储例程
MySQL 中还有更多可用的概念和命令,但有两个可能会引起人们的兴趣,那就是PROCEDURE
和FUNCTION
,它们有时被称为存储例程。我在这里介绍这些概念,以便如果您想探索它们,您可以理解它们是如何在高层次上使用的。
假设您需要运行几个命令来更改数据。也就是你需要在计算的基础上做一些复杂的改变。对于这些类型的操作,MySQL 提供了存储过程的概念。存储过程允许您在调用该过程时执行复合语句(一系列 SQL 命令)。存储过程有时被认为是一种主要用于定期维护的高级技术,但它们在更简单的情况下也很方便。
例如,假设您想要开发自己的使用 SQL 的数据库应用,但是因为您正在开发它,所以您需要定期重新开始,并且想要首先清除所有数据。如果只有一个表,存储过程不会有太大帮助,但是假设有几个表分布在几个数据库中(对于较大的数据库来说,这种情况并不少见)。在这种情况下,存储过程可能会有所帮助。
Tip
在 MySQL 客户端中输入带有复合语句的命令时,您需要临时更改分隔符(分号),以便行尾的分号不会终止命令条目。例如,在用复合语句编写命令之前使用DELIMITER //
,使用//
结束命令,用DELIMITER ;
将分隔符改回来。这仅在使用客户端时。
因为存储过程可能相当复杂,如果您决定使用它们,在尝试开发自己的存储过程之前,请阅读在线 MySQL 参考手册的“CREATE PROCEDURE
和CREATE FUNCTION
语法”一节。创建存储过程的内容远不止这一部分。
现在假设您想执行一个复合语句并返回一个结果—您想将它用作一个函数。您可以使用函数通过执行计算、数据转换或简单的翻译来填充数据。因此,函数可用于提供值来填充列值、提供聚合、提供日期操作等等。
您已经看到了几个函数(COUNT
、AVG
)。这些被认为是内置函数,在线 MySQL 参考手册中有一整节专门介绍它们。但是,您也可以创建自己的函数。例如,您可能希望创建一个函数来对您的数据执行数据规范化。更具体地说,假设您有一个传感器,它产生一个特定范围内的值,但是根据该值和来自不同传感器或查找表的另一个值,您想要对该值进行加、减、平均等操作来校正它。您可以编写一个函数来实现这一点,并调用它作为触发器来填充计算列的值。
Tip
对计算值使用新列,以便保留原始值。
What About Changing Objects?
当您需要修改表、过程、触发器等时,您可能想知道该怎么做。放心吧,你不必从头开始!MySQL 为每个对象提供了一个ALTER
命令。也就是说,有一个ALTER TABLE
、ALTER PROCEDURE
等等。有关每个ALTER
命令的更多信息,请参见在线 MySQL 参考手册“数据定义语句”一节。
摘要
MySQL 数据库服务器是一个强大的工具。鉴于 MySQL 作为互联网数据库服务器在市场上的独特地位,web 开发者(以及许多初创公司和类似的互联网公司)选择 MySQL 作为他们的解决方案也就不足为奇了。该服务器不仅功能强大且易于使用,还可以作为免费的社区许可证获得,您可以使用它来将您的初始投资控制在预算之内。
在这一章中,您发现了使用 MySQL 数据库服务器在传统角色中使用 SQL 接口的一些威力;如何发出创建数据库和存储数据的表的命令以及检索数据的命令。虽然这一章只介绍了 MySQL 的初级知识,但是您已经学会了如何开始安装 MySQL。
在第 3 章中,我们来看看 MySQL 的 NoSQL 接口。特别是,我们将 MySQL 用作文档存储。
Footnotes 1
你真的应该至少看一遍许可证。
是的,我知道。这是可耻的行为,我必须为此忏悔。承认吧。你也这么做,不是吗?
或者更糟,引入错误的事务。 https://dev.mysql.com/doc/mysql-utilities/1.6/en/utils-task-slavetrx.html
见。
我称之为温室,但它本质上是我们的阳光门廊。夏天只有几株植物,但到了冬天,它就变成了一个小温室。
三、JSON 文档
现在我们已经安装了 MySQL 服务器,我们可以开始学习更多关于什么是文档存储以及我们如何开始使用它。核心概念是 JavaScript 对象符号(JSON)文档。我们发现 MySQL 有两种方式处理 JSON 文档:一种纯粹的 NoSQL 文档存储机制,配有完整的开发者应用编程接口,以及一种非常酷的 JSON 与关系数据库的集成。
MySQL 文档存储库的起源在于几种技术,它们被结合在一起形成了文档存储库。特别是,Oracle 将键、值机制与新的数据类型、新的编程库和新的访问机制结合起来,创建了现在的文档存储。正如我们在第 1 章中了解到的,这不仅允许我们使用带有 NoSQL 接口的 MySQL,还允许我们构建混合解决方案,利用关系数据的稳定性和结构,同时增加 JSON 文档的灵活性。
在本章中,我们将了解 MySQL 如何支持 JSON 文档,包括如何添加、查找、更新和删除数据(通常分别称为创建、读取、更新和删除)。我们从你将在本书中遇到的概念和技术的更多信息开始。然后我们继续学习 MySQL 服务器中的 JSON 数据类型和 JSON 函数。尽管本章主要关注的是将 JSON 用于关系数据,但是要掌握 MySQL 文档存储 NoSQL 接口——X Developer API(X DevAPI ),需要有使用 JSON 的坚实基础。
让我们首先回顾一下在 MySQL 中使用文档存储和 JSON 时会遇到的概念和技术。
概念和技术:术语解释
正如我们在第 1 章中所了解到的,有几个新概念和新技术以及相关的术语可以用来学习如何使用 MySQL 中的文档存储。我们在第 1 章中遇到了一些这样的术语,但是我们在 MySQL 的上下文中更详细地探讨它们。也就是说,我们看到这些概念和技术如何解释 JSON 数据类型和文档存储接口的组成。让我们从 JSON 使用的最基本的概念开始:键、价值机制。
起源:关键,价值机制
正如世界上的大多数事物一样,没有什么东西是真正新的,因为它完全是原始的,没有之前存在的某种形式,并且通常是由以新方式应用的现有技术构建的。关键的价值机制是基础技术的典型例子。我使用术语“机制”,因为键的使用允许您访问值。
当我们说 key,value 时,我们的意思是存在一些标签(通常是一个字符串)构成了键,并且每个键都与一个值相关联。例如,"name":"George"
是一个示例,其中 key ( name
)具有一个值(George
)。虽然键值存储中的值通常是短字符串,但值可以是复杂的:数字;字母数字;列表;或者甚至是嵌套的键值集。
Key,value 机制最出名的是易于编程使用,同时仍然保持可读性。也就是说,通过大量使用空格,一个复杂的嵌套键,值数据结构可以被人类读取。下面显示了一个以开发者格式化代码的方式格式化的示例。如您所见,很容易看出这组键值存储了什么:姓名、地址和电话号码。
{ "name": {
"first":"George",
"last":"Folger"
},
"phones": [
{
"work":"555-1212"
},
{
"cell":"555-2121"
}
],
"address": {
"street":"123 Main Street",
"city":"melborne",
"state":"California",
"zip":"90125"
}
}
回想一下第 1 章,我们看到了这些结构的一些例子。现在我们知道它们是如何以及为什么被建造的。
键、值机制(或存储)的一个例子是可扩展标记语言(XML),它已经存在了一段时间。下面是一个使用上述数据的简单 XML 示例。它是 SQL SELECT
查询的结果,输出(行)以 XML 格式显示。 1 注意 XML 是如何使用 HTML 这样的标签(因为它是从 HTML 派生出来的)以及数据的键值存储的。这里,键是<row>
、<field>
,值是开始和结束标记符号之间的内容(<field> </field>
)。
<?xml version="1.0"?>
<resultset statement="select * from thermostat_model limit 1;" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<row>
<field name="model_id">acme123</field>
<field name="brand">Lennox</field>
</row>
</resultset>
有些系统是围绕键、值机制(称为键、值或关系存储)设计的,比如语义网。简言之,语义网试图利用数据的关联来描述事物、事件等等。有时,术语关系存储或三重存储被用来描述所采用的存储系统的类型。语义 Web 中使用了几种形式的关键值机制,包括资源描述框架(RDF)、Web 本体语言(OWL)和 XML。
还有其他一些关键值机制的例子,但是与文档存储最相关的是 JSON。
数据
我在第 1 章对 JSON 做了简单描述。回想一下,JSON 是一种人类和机器可读的数据交换格式。它也是独立于平台的,这意味着不存在禁止它在几乎任何编程语言中使用的格式概念。此外,JSON 是一种在互联网上广泛使用的流行格式。
JSON 允许您以任何想要的方式描述数据,而不强制任何结构。事实上,您可以按照自己的意愿设置数据的格式(布局)。唯一真正的限制是描述符(花括号、方括号、引号、逗号等)的正确使用。)必须对齐,并且在某些情况下正确配对。当编程语言支持时,开发者可以通过键访问数据来轻松读取数据。更好的是,开发者不需要知道键是什么(但这很有帮助!)因为它们可以使用语言支持机制来获取键并对它们进行迭代。这样,像 XML 一样,数据是自描述的。
现在让我们看看文档库的另一个关键组件——从编程库开始的 NoSQL 接口。
应用界面
应用编程接口(API),有时简称为库或编程库,是一组支持一个或多个功能的操作的类和方法。通过这些类和方法,这些功能允许程序员使用这些类和方法来执行各种任务。
例如,当我们在手机、平板电脑或电脑上使用任何带有图形用户界面的应用时,该应用是使用几种 API 之一构建的。图形用户界面本身是使用一个或多个 API 构建的,这些 API 封装了一组用于绘制窗口、创建按钮等的类和方法,所有这些都是图形用户界面设计为提供给开发者的。
在 MySQL 文档存储的情况下,我们使用 X DevAPI 通过一组类和方法来访问服务器,这些类和方法提供了到服务器的连接、概念的抽象(比如集合、表、SQL 操作)等等。正如我们之前了解到的,X DevAPI 也建立在其他一些技术之上,包括通过 X 插件实现的 X 协议。这些技术结合起来形成了 MySQL 服务器的 NoSQL 接口。
NoSQL 接口
有几个有时相互矛盾的 NoSQL 定义(如果不是例子的话)。对于本书和 MySQL 来说,NoSQL 接口是一个不需要使用 SQL 语句来访问数据的 API。API 本身提供了到服务器的连接,以及创建、检索、更新和删除数据的类和方法。
例如,如果要获取符合特定标准的所有数据,必须首先创建到服务器的连接,请求访问包含数据的对象,然后获取数据。每个步骤都需要创建对象实例,并调用这些对象实例的方法来操作 API。
相比之下,用于与 MySQL 交互的正常机制是通过 SQL 接口,在该接口中,您必须使用严格格式化的 SQL 命令来形成与对象和数据的所有交互。您发出命令并读取结果。如果您想编写一个使用 SQL 接口的应用,比如说获取数据,您必须使用命令来搜索数据,然后将结果转换为内部编程结构,使数据看起来像是一个辅助组件,而不是解决方案的一个组成部分。
NoSQL 接口打破了这种模式,它允许您使用 API 来处理数据。更具体地说,您使用编程接口,而不是基于命令的接口。
此时,您可能想知道 MySQL 如何处理将 JSON 文档与关系数据结合使用的混合选项。基本上,MySQL 被设计成允许在关系数据中存储和检索 JSON 文档(通过 SQL 接口)。也就是说,服务器已经被修改来处理 JSON 文档。还有一组函数允许您对 JSON 数据做各种各样的事情,使得通过 SQL 接口使用 JSON 变得很容易。
然而,您也可以通过 NoSQL X DevAPI 使用 JSON 文档,或者通过 SQL 命令,或者使用 X DevAPI 的特殊类和方法作为纯文档存储。我们将在第 5 章中学习更多关于 X DevAPI 的知识。
文档存储
文档存储(也称为面向文档的数据库)是一个用于管理半结构化数据(即文档)的存储和检索系统。现代文档存储系统支持 XML 和 JSON 中的键、值结构。因此,文档存储系统有时被认为是关键值存储系统的一个子类。
文档存储系统也通常由实现为编程接口(API)的 NoSQL 接口来访问,该 API 允许开发者将文档的存储和检索合并到他们的程序中,而不需要第三方访问机制(API 实现访问机制)。事实上,描述数据的元数据嵌入在数据本身中。粗略地说,这意味着键和键的布局(排列或嵌套)形成元数据,并且元数据对于存储机制变得不透明。更具体地说,数据如何排列(文档如何形成或描述数据)并不反映在存储机制中,也不由存储机制管理。对半结构化数据的访问需要使用 NoSQL 接口访问为处理文档本身而设计的机制。
这两个特性:半结构化数据和 NoSQL 接口将文档存储与关系数据分开。关系数据需要不灵活的结构,迫使所有数据符合特定的结构。数据也以相同的结构分组在一起,通常很少考虑内容可能不同的数据。因此,我们通常不会看到通过传统关系数据机制访问文档存储。也就是说,直到现在。
使用文档存储有趣的一点是,学习如何使用文档存储并不需要成为 JavaScript 或 Python 专家。事实上,你要做的大部分事情并不需要掌握任何编程语言。也就是说,有很多如何做事的例子,所以你不需要学习所有关于这门语言的知识来开始。事实上,你可以很快找到你需要的东西,然后随着你的需求的成熟,学习更多的语言知识。
现在,让我们深入了解什么是 JSON 文档,以及如何在 MySQL 中使用它们。
介绍 JSON 文档
在 MySQL 5.7.8 和更高版本中,我们可以使用 JSON 数据类型将 JSON 文档存储在表的列中。回想一下第 1 章,虽然可以在文本或 BLOB 字段中嵌入 JSON,但有几个很好的理由不这样做,但最令人信服的理由是因为您必须将数据解析添加到您的程序中,从而使它变得更加复杂,并可能容易出错。JSON 数据类型以两种方式克服了这个问题。
- 验证:JSON 数据类型提供文档验证。也就是说,只有有效的 JSON 才能存储在 JSON 列中。
- 高效访问:当 JSON 文档存储在表中时,存储引擎将数据打包成一种特殊的优化二进制格式,允许服务器快速访问数据元素,而不是每次访问数据时都解析数据。
这为以结构化形式(关系数据)存储非结构化数据开辟了一条全新的途径。然而,Oracle 并没有止步于简单地向 MySQL 添加 JSON 数据类型。Oracle 还增加了一个复杂的编程接口,以及将文档作为集合存储在数据库中的概念。我们将在本书后面看到更多关于这些方面的内容。现在,让我们看看如何将 JSON 用于关系数据。
JSON 格式规则
JSON 数据是由用某些符号括起来的字符串组成的。尽管我们已经讨论了与 JSON 相关的键、值机制,但 JSON 属性有两种类型:由逗号分隔的列表形成的数组和由一组键、值对形成的对象。您也可以嵌套 JSON 属性。例如,数组可以包含对象,对象键中的值可以包含数组或其他对象。JSON 数组和对象的组合称为 JSON 文档。
JSON 数组包含一个由逗号分隔并括在方括号([ ]
)中的值列表。例如,以下是有效的 JSON 数组。
["red", "green", "yellow", "blue"]
[1,2,3,4,5,6]
[true, false, false]
注意,我们用方括号开始和结束数组,并用逗号分隔值。虽然我没有使用空白,但是您可以使用空白,并且根据您的编程语言,您还可以使用换行符、制表符和回车符。例如,下面仍然是一个有效的 JSON 数组。
["red", 27, "yellow", 4.75, "blue", false]
JSON 对象是一组键/值对,其中每个键/值对都包含在左花括号和右花括号({ }
)中,并用逗号分隔。例如,以下是有效的 JSON 对象。注意,键地址有一个 JSON 对象作为它的值。
{"address": {
"street": "123 First Street",
"city": "Oxnard",
"state": "CA",
"zip": "90122"
}}
{"address": {
"street":"4 Main Street",
"city":"Melborne",
"state":"California",
"zip":"90125"
}}
{"address": {
"street":"173 Caroline Ave",
"city":"Montrose",
"state":"Georgia",
"zip":"31505"
}}
JSON 数组通常用于包含相关(嗯,有时)事物的列表,JSON 对象用于描述复杂的数据。JSON 数组和对象可以包含标量值,比如字符串或数字、null
文字(就像在关系数据中一样),或者布尔文字true
和false
。键必须总是字符串,并且通常用引号括起来。最后,JSON 值还可以包含时间信息(日期、时间或日期时间)。例如,下面显示了一个带有时间值的 JSON 数组。
["03:22:19.012000", "2016-02-03", "2016-02-03 03:22:19.012000"]
下一节描述了我们如何在 MySQL 中使用 JSON。在这种情况下,我们指的是关系数据,但是 JSON 文档的格式在文档存储中是相同的。
在 MySQL 中使用 JSON
在 MySQL 中使用时,JSON 文档被写成字符串。MySQL 解析 JSON 数据类型中使用的任何字符串来验证文档。如果文档无效——它不是一个格式正确的 JSON 文档——服务器将产生一个错误。您可以在任何合适的 SQL 语句中使用 JSON 文档。例如,你可以用在INSERT
和UPDATE
语句中,也可以用在像WHERE
这样的子句中。
对一些人来说,正确格式化 JSON 文档可能有点困难,尤其是那些不习惯用编程或脚本语言格式化数据结构的人。最需要记住的是平衡你的引号,正确使用逗号,平衡所有的花括号和方括号。很简单,对吧?只有一件事会阻碍一些人:报价!
将键和值指定为字符串时,必须使用双引号字符("),而不是单引号(')。因为 MySQL 期望 JSON 文档是字符串,所以可以在整个 JSON 文档中使用单引号,但不能在文档本身中使用单引号。幸运的是,MySQL 提供了许多特殊的函数,您可以在 JSON 文档中使用,其中一个是JSON_VALID()
函数,它允许您检查 JSON 文档的有效性。如果文档有效,则返回 1,否则返回 0。下面显示了使用单引号验证键和值的 JSON 文档与使用双引号验证格式正确的 JSON 文档的结果。
Tip
如果您想将 MySQL Shell 用于 SQL 命令,请确保以 SQL 模式(--sql
)启动,或者您可以在 Shell 启动后使用\sql
命令切换到 SQL 模式。
MySQL localhost:33060+ ssl JS > \sql
Switching to SQL mode... Commands end with ;
MySQL localhost:33060+ ssl SQL > SELECT JSON_VALID("{'address': {'street': '123 First Street','city': 'Oxnard','state': 'CA','zip': '90122'}}");
+-------------------------------------------------------------------------+
| JSON_VALID("{'address': {'street': '123 First Street','city': 'Oxnard','state': 'CA','zip': '90122'}}") |
+-------------------------------------------------------------------------+
| 0 |
+-------------------------------------------------------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_VALID('{"address": {"street": "123 First Street","city": "Oxnard","state": "CA","zip": "90122"}}');
+-------------------------------------------------------------------------+
| JSON_VALID('{"address": {"street": "123 First Street","city": "Oxnard","state": "CA","zip": "90122"}}') |
+-------------------------------------------------------------------------+
| 1 |
+-------------------------------------------------------------------------+
1 row in set (0.00 sec)
请注意,带双引号的字符串有效,但带单引号的字符串无效。这是大多数人在使用 JSON 时首先遇到的问题。
让我们看看如何在 SQL 语句中使用 JSON 文档。假设我们想将前面列出的地址存储在一个表中。对于这个例子,我们保持简单,将数据插入一个非常简单的表中。清单 3-1 显示了从创建一个测试表开始,然后插入前两个地址的练习的抄本。
Tip
您可以使用附加到 SQL 命令的\G
命令以垂直格式显示结果,以便于阅读。
MySQL localhost:33060+ ssl Py > \sql
Switching to SQL mode... Commands end with ;
MySQL localhost:33060+ ssl SQL > CREATE DATABASE `test`;
Query OK, 1 row affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > USE `test`;
Query OK, 0 rows affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > CREATE TABLE `test`.`addresses` (`id` int(11) NOT NULL AUTO_INCREMENT, `address` json DEFAULT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=latin1;
Query OK, 0 rows affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > INSERT INTO `test`.`addresses` VALUES (NULL, '{"address": {"street": "123 First Street","city": "Oxnard","state": "CA","zip": "90122"}}');
Query OK, 1 row affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > INSERT INTO `test`.`addresses` VALUES (NULL, '{"address": {"street":"4 Main Street","city":"Melborne","state":"California","zip":"90125"}}');
Query OK, 1 row affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT * FROM `test`.`addresses` \G
*************************** 1\. row ***************************
id: 1
address: {"address": {"zip": "90122", "city": "Oxnard", "state": "CA", "street": "123 First Street"}}
*************************** 2\. row ***************************
id: 2
address: {"address": {"zip": "90125", "city": "Melborne", "state": "California", "street": "4 Main Street"}}
2 rows in set (0.00 sec)
Listing 3-1Using JSON with SQL Statements
注意,在CREATE
语句中,我们使用了数据类型 JSON。这通知 MySQL 在存储引擎中分配特殊的存储机制来处理 JSON。与一些报告相反,JSON 数据类型不仅仅是字符串的直接存储。相反,它是在内部组织的,以优化元素的检索。因此,正确格式化 JSON 非常重要。一个表中可以有多个 JSON 列。然而,一个表行中 JSON 文档的总和被限制为变量max_allowed_packet
的值。
Note
JSON 列不能像表中的其他列(数据类型)一样有默认值。
现在,让我们看看如果在 SQL 语句中使用无效的 JSON 文档(字符串)会发生什么。下面显示了插入上一个示例中的最后一个地址的尝试,只是没有在关键字周围加上正确的引号。注意抛出的错误。
MySQL localhost:33060+ ssl SQL > INSERT INTO test.addresses VALUES (NULL, '{"address": {street:"173 Caroline Ave",city:"Monstrose",state:"Georgia",zip:31505}}');
ERROR: 3140: Invalid JSON text: "Missing a name for object member." at position 13 in value for column 'addresses.address'.
对于任何格式不正确的 JSON 文档,您都可能会看到这样或那样的错误。如果你想先测试你的 JSON,使用JSON_VALID()
函数。然而,在构建 JSON 文档时,还有另外两个函数可能会有所帮助:JSON_ARRAY()
和JSON_OBJECT()
。
JSON_ARRAY()
函数接受一个值列表,并返回一个有效的格式化 JSON 数组。下面显示了一个示例。注意,它返回了一个格式正确的 JSON 数组,带有正确的引号(双引号而不是单引号)和方括号。
MySQL localhost:33060+ ssl SQL > SELECT JSON_ARRAY(1, true, 'test', 2.4);
+----------------------------------+
| JSON_ARRAY(1, true, 'test', 2.4) |
+----------------------------------+
| [1, true, "test", 2.4] |
+----------------------------------+
1 row in set (0.00 sec)
JSON_OBJECT()
函数接受一个键、值对列表,并返回一个有效的 JSON 对象。下面显示了一个示例。注意,这里我在调用函数时使用了单引号。这只是一个令人困惑的例子。在这种情况下,函数的参数不是 JSON 文档;它们是普通的 SQL 字符串,可以使用单引号或双引号。
MySQL localhost:33060+ ssl SQL > SELECT JSON_OBJECT("street","4 Main Street","city","Melborne",'state','California','zip',90125);
+-------------------------------------------------------------------------+
| JSON_OBJECT("street","4 Main Street","city","Melborne",'state','California','zip',90125) |
+-------------------------------------------------------------------------+
| {"zip": 90125, "city": "Melborne", "state": "California", "street": "4 Main Street"} |
+-------------------------------------------------------------------------+
1 row in set (0.00 sec)
再次注意,函数中引号的自动转换会导致。如果您需要动态构建 JSON,这可能会很有帮助。
构造 JSON 文档还有一个有用的函数:JSON_TYPE()
函数。这个函数获取一个 JSON 文档,并将其解析成一个 JSON 值。如果值有效,它将返回该值的 JSON 类型,如果无效,它将抛出一个错误。下面显示了该函数与上述语句的用法。
MySQL localhost:33060+ ssl SQL > SELECT JSON_TYPE('[1, true, "test", 2.4]');
+-------------------------------------+
| JSON_TYPE('[1, true, "test", 2.4]') |
+-------------------------------------+
| ARRAY |
+-------------------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_TYPE('{"zip": 90125, "city": "Melborne", "state": "California", "street": "4 Main Street"}') \G
*************************** 1\. row ***************************
JSON_TYPE('{"zip": 90125, "city": "Melborne", "state": "California", "street": "4 Main Street"}'): OBJECT
1 row in set (0.00 sec)
MySQL 提供了更多的函数来处理 JSON 数据类型。我们将在后面的章节中看到更多关于这些的内容。
本节只描述了在 SQL 语句中使用 JSON 和 MySQL 的基础知识。事实上,JSON 文档的格式化也适用于文档存储。然而,有一项我们还没有谈到——如何访问 JSON 文档中的元素。
为了访问一个元素——通过它的键——我们使用一种叫做路径表达式的特殊符号。下面是一个简单的例子。注意WHERE
子句。这显示了一个路径表达式,在该表达式中,我检查了address
列是否包含用特殊符号address->'$.address.city'
引用的 JSON 键‘city’。我们在“路径表达式”一节中可以看到更多关于路径表达式的细节。
MySQL localhost:33060+ ssl SQL > SELECT id, address->'$.address.city' FROM test.addresses WHERE address->'$.address.zip' = '90125';
+----+---------------------------+
| id | address->'$.address.city' |
+----+---------------------------+
| 2 | "Melborne" |
+----+---------------------------+
1 row in set (0.00 sec)
路径表达式
如果您认为 JSON 文档可能是一组复杂的半结构化数据,并且在某个时候您需要访问文档中的某些元素,那么您可能还想知道如何从 JSON 文档中获得您想要的东西。幸运的是,有一种机制可以做到这一点,它被称为路径表达式。更具体地说,这是一种快捷表示法,您可以在 SQL 命令中(或在 X DevAPI 中)使用它来获取元素,而无需额外的编程或脚本。
正如您将看到的,这是一个非常具体的语法,虽然不是很有表现力(在英语中读起来不是很好),但这种符号可以在不需要大量额外输入的情况下获得您需要的内容。路径表达式以包含在字符串中的美元符号($
)开始。但是这个符号必须有一个上下文。在 SQL 语句中使用路径表达式时,必须使用JSON_EXTRACT(
函数,该函数允许您使用路径表达式从 JSON 文档中提取数据。这是因为,与 X DevAPI 类和方法不同,并非所有 SQL 语句都直接支持路径表达式(但正如我们将看到的,有些语句支持路径表达式)。例如,如果您想要数组中的第三项,您可以使用如下函数。
MySQL localhost:33060+ ssl SQL > SELECT JSON_EXTRACT('[1,2,3,4,5,6]', '$[2]');
+---------------------------------------+
| JSON_EXTRACT('[1,2,3,4,5,6]', '$[2]') |
+---------------------------------------+
| 3 |
+---------------------------------------+
1 row in set (0.00 sec)
注意,这是访问 JSON 数组中的数据。在这里,我们使用一个数组下标,并在索引周围加上方括号(元素从 0 开始),就像在许多编程语言中使用数组一样。
Tip
SQL 接口中路径表达式的使用仅限于其中一个 JSON 函数,或者仅用于已被修改为接受路径表达式的特定子句,如SELECT
列列表或WHERE
、HAVING
、ORDER BY
或GROUP BY
子句。
现在假设你想通过键访问一个元素。你也可以这样做。在这种情况下,我们使用美元符号后跟一个句点,然后是键名。下面显示了如何检索包含个人姓名和地址的 JSON 对象的姓氏。
MySQL localhost:33060+ ssl SQL > SELECT JSON_EXTRACT('{"name": {"first":"Billy-bob","last":"Throckmutton"},"address": {"street":"4 Main Street","city":"Melborne","state":"California","zip":"90125"}}', '$.name.first') AS Name;
+-------------+
| Name |
+-------------+
| "Billy-bob" |
+-------------+
1 row in set (0.00 sec)
请注意,我必须使用两个级别的访问权限。也就是说,我想要名为 name 的对象中名为 first 的键的值。因此,我使用了'$.name.first'
。这演示了如何使用路径表达式深入 JSON 文档。这也是为什么我们称之为路径表达式,因为我们形成表达式的方式给了我们到元素的“路径”。
现在我们已经看到了一些例子,让我们回顾一下路径表达式的完整语法;两者都在 SQL 和 NoSQL 接口中使用。除非另有说明,否则语法方面适用于两种接口。
再说一次,路径表达式以美元符号开始,后面可以有几种叫做选择器的语法形式,允许我们请求文档的一部分。这些选择器包括:
- 一个句点后跟一个键名,引用该键的值。如果不带引号的名称无效(要求引号是有效的标识符,如带空格的键名),则必须在双引号内指定键名。
- 使用带有整数索引(
[n]
)的方括号来选择数组中的元素。索引从 0 开始。 - 路径可以包含通配符*或**,如下所示。
.[*]
计算 JSON 对象中所有成员的值。[*]
计算 JSON 数组中所有元素的值。- 诸如前缀
**
后缀的序列评估以命名前缀开始并以命名后缀结束的所有路径。
- 可以使用句点作为分隔符来嵌套路径。在这种情况下,句点之后的路径在父路径上下文的上下文中进行评估。例如,
$.name.first
将名为first
的键的搜索限制在name
JSON 对象中。
如果路径表达式被评估为假或者无法定位数据项,服务器将返回null
。例如,下面返回null
,因为数组中只有 6 项。你能看出为什么吗?记住,计数从 0 开始。对于那些不熟悉使用路径表达式(或者编程语言中的数组)的人来说,这是一个常见的错误。
MySQL localhost:33060+ ssl SQL > SELECT JSON_EXTRACT('[1,2,3,4,5,6]', '$[6]');
+---------------------------------------+
| JSON_EXTRACT('[1,2,3,4,5,6]', '$[6]') |
+---------------------------------------+
| NULL |
+---------------------------------------+
1 row in set (0.00 sec)
但是等等,路径表达式还有一个更好的选择。我们可以走捷径!也就是说,当按列访问 SQL 语句中的数据时,可以使用破折号和大于号(->
)来代替JSON_EXTRACT()
函数。这有多酷?使用->
操作有时被称为内嵌路径表达式。例如,我们可以编写上面的例子,从一个表中查找 JSON 数组中的第三项,如下所示。
MySQL localhost:33060+ ssl SQL > USE test;
Query OK, 0 rows affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > CREATE TABLE ex1 (id int AUTO_INCREMENT PRIMARY KEY, recorded_data JSON);
Query OK, 0 rows affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > INSERT INTO test.ex1 VALUES (NULL, JSON_ARRAY(1,2,3,4,5,6));
Query OK, 1 row affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > INSERT INTO test.ex1 VALUES (NULL, JSON_ARRAY(7,8,9));
Query OK, 1 row affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT * FROM test.ex1 WHERE recorded_data->'$[2]' = 3;
+----+--------------------+
| id | recorded_data |
+----+--------------------+
| 1 | [1, 2, 3, 4, 5, 6] |
+----+--------------------+
1 row in set (0.00 sec)
注意,我只是使用了列名recorded_data
,并在末尾添加了->
,然后列出了路径表达式。太棒了!
这种捷径还有一种形式。如果->
操作(JSON_EXTRACT
)的结果是一个带引号的字符串,我们可以使用->>
符号(称为内嵌路径操作符)来检索不带引号的值。这在处理数值时很有帮助。下面给出了两个例子。一个例子是->
操作,同样的例子还有->>
操作。
MySQL localhost:33060+ ssl SQL > INSERT INTO test.ex1 VALUES (NULL, '{"name":"will","age":"43"}');
Query OK, 1 row affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > INSERT INTO test.ex1 VALUES (NULL, '{"name":"joseph","age":"11"}');
Query OK, 1 row affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT * FROM test.ex1 WHERE recorded_data->>'$.age' = 43;
+----+-------------------------------+
| id | recorded_data |
+----+-------------------------------+
| 3 | {"age": "43", "name": "will"} |
+----+-------------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT * FROM test.ex1 WHERE recorded_data->'$.age' = 43;
Empty set (0.00 sec)
请注意,recorded_data 值(年龄和姓名)存储为一个字符串。但是如果数据存储为整数会怎么样呢?观察。
MySQL localhost:33060+ ssl SQL > INSERT INTO test.ex1 VALUES (NULL, '{"name":"amy","age":22}');
Query OK, 1 row affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT * FROM test.ex1 WHERE recorded_data->'$.age' = 22;
+----+----------------------------+
| id | recorded_data |
+----+----------------------------+
| 5 | {"age": 22, "name": "amy"} |
+----+----------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT * FROM test.ex1 WHERE recorded_data->>'$.age' = 22;
+----+----------------------------+
| id | recorded_data |
+----+----------------------------+
| 5 | {"age": 22, "name": "amy"} |
+----+----------------------------+
1 row in set (0.00 sec)
啊哈!因此,当值必须不加引号时,->>
操作最有用。如果它们已经被取消引用(比如一个整数),那么->>
操作将返回与->
操作相同的结果。
现在,让我们再看几个路径表达式的例子。清单 3-2 展示了几个例子,没有解释。花几分钟时间浏览这些内容,并检查它所操作的数据,这样您就可以看到每个内容是如何工作的。只要有一点想象力,您就可以深入到单个数据元素!
MySQL localhost:33060+ ssl SQL > INSERT INTO test.ex1 VALUES (NULL, '{"name": {"last": "Throckmutton", "first": "Billy-bob"}, "address": {"zip": "90125", "city": "Melborne", "state": "California", "street": "4 Main Street"}}');
Query OK, 1 row affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT recorded_data FROM test.ex1 WHERE recorded_data->'$.name' IS NOT NULL \G
*************************** 1\. row ***************************
recorded_data: {"age": "43", "name": "will"}
*************************** 2\. row ***************************
recorded_data: {"age": "11", "name": "joseph"}
*************************** 3\. row ***************************
recorded_data: {"age": 22, "name": "amy"}
*************************** 4\. row ***************************
recorded_data: {"name": {"last": "Throckmutton", "first": "Billy-bob"}, "address": {"zip": "90125", "city": "Melborne", "state": "California", "street": "4 Main Street"}}
4 rows in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT recorded_data->'$.name' FROM test.ex1 WHERE recorded_data->'$.name' IS NOT NULL;
+------------------------------------------------+
| recorded_data->'$.name' |
+------------------------------------------------+
| "will" |
| "joseph" |
| "amy" |
| {"last": "Throckmutton", "first": "Billy-bob"} |
+------------------------------------------------+
4 rows in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT recorded_data->'$.name.first' as first, recorded_data->'$.name.last' as last FROM test.ex1 WHERE recorded_data->'$.name.first' IS NOT NULL;
+-------------+----------------+
| first | last |
+-------------+----------------+
| "Billy-bob" | "Throckmutton" |
+-------------+----------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > INSERT INTO test.ex1 VALUES (NULL, '{"phones": [{"work": "555-1212"}, {"cell": "555-2121"}]}');
Query OK, 1 row affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT recorded_data->>'$.phones' FROM test.ex1 WHERE recorded_data->>'$.phones' IS NOT NULL;
+----------------------------------------------+
| recorded_data->>'$.phones' |
+----------------------------------------------+
| [{"work": "555-1212"}, {"cell": "555-2121"}] |
+----------------------------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT recorded_data->'$.phones[1]' FROM test.ex1 WHERE recorded_data->>'$.phones' IS NOT NULL;
+------------------------------+
| recorded_data->'$.phones[1]' |
+------------------------------+
| {"cell": "555-2121"} |
+------------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT recorded_data->'$.phones[1].cell' FROM test.ex1 WHERE recorded_data->>'$.phones' IS NOT NULL;
+-----------------------------------+
| recorded_data->'$.phones[1].cell' |
+-----------------------------------+
| "555-2121" |
+-----------------------------------+
1 row in set (0.00 sec)
Listing 3-2Examples of Path Expressions
注意,我在 WHERE 子句中使用了路径表达式来检查结果是否不为空。这是在表格中选择包含您在文档中寻找的元素的行的一个好技巧。也就是说,您只需要包含特定数据元素的行(通过路径表达式)。
然而,快捷方式(内联路径表达式)的使用并不能直接替代 JSON_EXTRACT()函数。下面总结了这些限制。
- 数据源:在 SQL 语句中使用时,内联路径表达式仅使用指定的字段(列)。该函数可以使用任何 JSON 类型的值。
- 路径表达式字符串:内联路径表达式必须使用普通字符串;该函数可以使用任何字符串类型的值。
- 表达式的数量:内联路径表达式只能对单个字段(列)使用一个路径表达式。该函数可以对一个 JSON 文档使用多个路径表达式。
Tip
有关路径表达式的更多信息,请参见在线 MySQL 参考手册中的“JSON 数据类型”一节。
现在让我们看看可以用来处理 JSON 文档的各种 JSON 函数。
JSON 函数
在 MySQL 中使用 JSON 有几个函数。我在这一节中描述了许多可用的功能。虽然我们不会探究每个函数的细微差别,但是我们会看到处理 JSON 文档时更常用的函数。让我们以可用功能列表的形式开始概述。表 3-1 列出了 MySQL 8.0.11 中可用的 JSON 函数。
表 3-1
JSON Functions in MySQL
| 功能 | 描述和使用 | | :-- | :-- | | `JSON_ARRAY()` | 计算一列值,并返回包含这些值的 JSON 数组 | | `JSON_ARRAYAGG()` | 将结果集聚合为一个 JSON 数组,其元素由行组成 | | `JSON_ARRAY_APPEND()` | 将值追加到 JSON 文档中指定数组的末尾,并返回结果 | | `JSON_ARRAY_INSERT()` | 更新一个 JSON 文档,在文档中插入一个数组并返回修改后的文档 | | `JSON_CONTAINS()` | 返回 0 或 1 以指示特定值是否包含在目标 JSON 文档中,或者,如果给定了路径参数,则返回目标文档中的特定路径 | | `JSON_CONTAINS_PATH()` | 返回 0 或 1,以指示 JSON 文档是否包含给定路径中的数据 | | `JSON_DEPTH()` | 返回 JSON 文档的最大深度 | | `JSON_EXTRACT()` | 从 JSON 文档中返回数据,这些数据是从与路径参数匹配的文档部分中选择的 | | `JSON_INSERT()` | 将数据插入 JSON 文档并返回结果 | | `JSON_KEYS()` | 以 JSON 数组的形式返回 JSON 对象顶层值的键,或者,如果给定了路径参数,则返回所选路径的顶层键 | | `JSON_LENGTH()` | 返回 JSON 文档的长度,或者,如果给定了路径参数,则返回由路径标识的文档中的值的长度 | | `JSON_MERGE()` | 合并两个或多个 JSON 文档并返回合并结果 | | `JSON_MERGE_PATCH()` | 合并两个或多个 JSON 文档,替换重复键的值 | | `JSON_MERGE_PRESERVE()` | 合并两个或多个 JSON 文档,保存重复键的值 | | `JSON_OBJECT()` | 评估键/值对列表,并返回包含这些对的 JSON 对象 | | `JSON_OBJECTAGG()` | 接受两个列名或表达式作为参数,第一个用作键,第二个用作值,并返回包含键/值对的 JSON 对象 | | `JSON_PRETTY()` | 打印出更美观的 JSON 文档布局 | | `JSON_QUOTE()` | 通过用双引号字符将字符串括起来并转义内部引号和其他字符,将字符串作为 JSON 值引用,然后将结果作为 utf8mb4 字符串返回 | | `JSON_REMOVE()` | 从 JSON 文档中移除数据并返回结果 | | `JSON_REPLACE()` | 替换 JSON 文档中的现有值并返回结果 | | `JSON_SEARCH()` | 返回 JSON 文档中给定字符串的路径 | | `JSON_SET()` | 在 JSON 文档中插入或更新数据,并返回结果 | | `JSON_STORAGE_FREE()` | 显示部分更新后 JSON 列中剩余的空间量 | | `JSON_STORAGE_SIZE()` | 显示 JSON 值使用的存储 | | `JSON_TABLE()` | 从 JSON 文档中提取数据,并将其作为关系表返回 | | `JSON_TYPE()` | 返回表示 JSON 值类型的 utf8mb4 字符串 | | `JSON_UNQUOTE()` | 从 JSON 值中删除引号,并将结果作为 utf8mb4 字符串返回 | | `JSON_VALID()` | 返回 0 或 1 以指示值是否是有效的 JSON 文档 |Note
在 8.0.3 版本中不推荐使用JSON_MERGE()
函数(在 5.7.22 版本中也是如此)。
掌握这些函数对于使用文档存储并不重要,但是在开发混合解决方案(在 SQL 语句中使用 JSON)时会有很大帮助。
这些功能可以根据它们的使用方式进行分类。我们将看到对添加数据有用的函数,检索(搜索)数据的函数,等等。下面用简单的例子说明如何使用这些功能。
大多数函数将 JSON 文档作为第一个参数,将路径表达式和值作为第二个和第三个参数。路径表达式必须对文档有效,并且不得包含通配符*
或**
。这些函数也返回结果,因此您可以在 SQL 语句中使用它们。
创建 JSON 数据
创建 JSON 数据有几个有用的函数。我们已经看到了两个重要的功能;JSON_ARRAY()
构建 JSON 数组类型,而JSON_OBJECT()
构建 JSON 对象类型。本节讨论一些其他函数,这些函数可以用来帮助创建 JSON 文档,包括在 JSON 数组中聚合、追加和插入数据的函数。
JSON_ARRAYAGG()
函数用于从几行中创建一个 JSON 文档数组。当您想要汇总数据或合并多行数据时,它会很有帮助。该函数接受一个列名,并将行中的 JSON 数据组合到一个新数组中。清单 3-3 展示了使用该函数的例子。这个示例获取表中的行,并将它们组合起来形成一个新的 JSON 对象数组。
MySQL localhost:33060+ ssl SQL > CREATE TABLE test.favorites (id int AUTO_INCREMENT PRIMARY KEY, preferences JSON);
Query OK, 0 rows affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > INSERT INTO test.favorites VALUES (NULL, '{"color": "red"}');
Query OK, 1 row affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > INSERT INTO test.favorites VALUES (NULL, '{"color": "blue"}');
Query OK, 1 row affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > INSERT INTO test.favorites VALUES (NULL, '{"color": "purple"}');
Query OK, 1 row affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT * FROM test.favorites;
+----+---------------------+
| id | preferences |
+----+---------------------+
| 1 | {"color": "red"} |
| 2 | {"color": "blue"} |
| 3 | {"color": "purple"} |
+----+---------------------+
3 rows in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_ARRAYAGG(preferences) FROM test.favorites;
+------------------------------------------------------------+
| JSON_ARRAYAGG(preferences) |
+------------------------------------------------------------+
| [{"color": "red"}, {"color": "blue"}, {"color": "purple"}] |
+------------------------------------------------------------+
1 row in set (0.00 sec)
Listing 3-3Using the JSON_ARRAYARG Function
JSON_ARRAY_APPEND()
是一个有趣的函数,它允许您将数据附加到 JSON 数组的末尾或紧接在给定路径表达式之后。该函数将 JSON 数组、路径表达式和要插入的值(包括 JSON 文档)作为参数。清单 3-4 展示了几个例子。
MySQL localhost:33060+ ssl SQL > SET @base = '["apple","pear",{"grape":"red"},"strawberry"]';
Query OK, 0 rows affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_ARRAY_APPEND(@base, '$', "banana");
+-------------------------------------------------------------+
| JSON_ARRAY_APPEND(@base, '$', "banana") |
+-------------------------------------------------------------+
| ["apple", "pear", {"grape": "red"}, "strawberry", "banana"] |
+-------------------------------------------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_ARRAY_APPEND(@base, '$[2].grape', "green");
+--------------------------------------------------------------+
| JSON_ARRAY_APPEND(@base, '$[2].grape', "green") |
+--------------------------------------------------------------+
| ["apple", "pear", {"grape": ["red", "green"]}, "strawberry"] |
+--------------------------------------------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SET @base = '{"grape":"red"}';
Query OK, 0 rows affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_ARRAY_APPEND(@base, '$', '{"grape":"red"}');
+--------------------------------------------------+
| JSON_ARRAY_APPEND(@base, '$', '{"grape":"red"}') |
+--------------------------------------------------+
| [{"grape": "red"}, "{\"grape\":\"red\"}"] |
+--------------------------------------------------+
1 row in set (0.00 sec)
Listing 3-4Using the JSON_ARRAY_APPEND Function
请注意,第一个示例只是在数组末尾添加了一个新值。第二个示例将第三个索引中 JSON 对象的键值更改为一个数组,并添加一个新值。这是这个函数的一个有趣的副产品。在第三个例子中,我们再次看到了这一点,我们将一个基本的 JSON 对象更改为一个 JSON 对象的 JSON 数组。
JSON_ARRAY_INSERT()
函数类似,只是它在路径表达式前插入值。该函数将 JSON 数组、路径表达式和要插入的值(包括 JSON 文档)作为参数。当包含多个路径表达式和值对时,当函数计算第一个路径表达式和值并将下一个对应用于结果时,效果是累积的,依此类推。清单 3-5 展示了一些使用新函数的例子,这些例子与前面的例子相似。请注意,插入数据的位置在路径表达式之前。
MySQL localhost:33060+ ssl SQL > SET @base = '["apple","pear",{"grape":["red","green"]},"strawberry"]';
Query OK, 0 rows affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_ARRAY_INSERT(@base, '$[0]', "banana");
+------------------------------------------------------------------------+
| JSON_ARRAY_INSERT(@base, '$[0]', "banana") |
+------------------------------------------------------------------------+
| ["banana", "apple", "pear", {"grape": ["red", "green"]}, "strawberry"] |
+------------------------------------------------------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_ARRAY_INSERT(@base, '$[2].grape[0]', "white");
+-----------------------------------------------------------------------+
| JSON_ARRAY_INSERT(@base, '$[2].grape[0]', "white") |
+-----------------------------------------------------------------------+
| ["apple", "pear", {"grape": ["white", "red", "green"]}, "strawberry"] |
+-----------------------------------------------------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SET @base = '[{"grape":"red"}]';
Query OK, 0 rows affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_ARRAY_INSERT(@base, '$[0]', '{"grape":"red"}');
+-----------------------------------------------------+
| JSON_ARRAY_INSERT(@base, '$[0]', '{"grape":"red"}') |
+-----------------------------------------------------+
| ["{\"grape\":\"red\"}", {"grape": "red"}] |
+-----------------------------------------------------+
1 row in set (0.00 sec)
Listing 3-5Using the JSON_ARRAY_INSERT Function
JSON_INSERT()
函数被设计成获取一个 JSON 文档,并在指定的路径表达式中插入一个或多个值。也就是说,您可以一次传递成对的路径表达式和值。但是有一个问题。在这种情况下,路径表达式不能计算为文档中的元素。与最后一个函数一样,当包含多个路径表达式时,当该函数对第一个路径表达式求值并将下一个路径表达式应用于结果时,效果是累积的,依此类推。清单 3-6 显示了一个例子。注意,没有插入第三个路径表达式和值,因为路径表达式$[0]
的计算结果是第一个元素apple
。
MySQL localhost:33060+ ssl SQL > SET @base = '["apple","pear",{"grape":["red","green"]},"strawberry"]';
Query OK, 0 rows affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_INSERT(@base, '$[9]', "banana", '$[2].grape[3]', "white", '$[0]', "orange");
+-------------------------------------------------------------------------+
| JSON_INSERT(@base, '$[9]', "banana", '$[2].grape[3]', "white", '$[0]', "orange") |
+-------------------------------------------------------------------------+
| ["apple", "pear", {"grape": ["red", "green", "white"]}, "strawberry", "banana"] |
+-------------------------------------------------------------------------+
1 row in set (0.00 sec)
Listing 3-6Using the JSON_INSERT Function
JSON_MERGE_PATCH()
和 JSON_MERGE_PRESERVE()函数被设计成获取两个或更多 JSON 文档并组合它们。JSON_MERGE_PATH()
函数替换重复键的值,而JSON_MERGE_PRESERVE()
函数保留重复键的值。和前面的函数一样,您可以包含任意多的 JSON 文档。请注意我是如何使用这个函数从前面的例子中构建示例 JSON 文档的。清单 3-7 展示了一个使用这些方法的例子。
MySQL localhost:33060+ ssl SQL > SELECT JSON_MERGE_PATCH('["apple","pear"]', '{"grape":["red","green"]}', '["strawberry"]');
+-------------------------------------------------------------------------+
| JSON_MERGE_PATCH('["apple","pear"]', '{"grape":["red","green"]}', '["strawberry"]') |
+-------------------------------------------------------------------------+
| ["strawberry"] |
+-------------------------------------------------------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_MERGE_PRESERVE('{"grape":["red","green"]}', '{"grape":["white"]}');
+-------------------------------------------------------------------------+
| JSON_MERGE_PRESERVE('{"grape":["red","green"]}', '{"grape":["white"]}') |
+-------------------------------------------------------------------------+
| {"grape": ["red", "green", "white"]} |
+-------------------------------------------------------------------------+
1 row in set (0.00 sec)
Listing 3-7Using the JSON_MERGE_
PATCH
and JSON_MERGE_PRESERVE Functions
如果向任何 JSON 函数传递了无效的参数、无效的 JSON 文档,或者路径表达式没有找到元素,则一些函数会返回 null,而其他函数可能会返回原始的 JSON 文档。清单 3-8 显示了一个例子。在这种情况下,位置 8 没有元素,因为数组只有 4 个元素。
MySQL localhost:33060+ ssl SQL > SET @base = '["apple","pear",{"grape":"red"},"strawberry"]';
Query OK, 0 rows affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_ARRAY_APPEND(@base, '$[7]', "flesh");
+---------------------------------------------------+
| JSON_ARRAY_APPEND(@base, '$[7]', "flesh") |
+---------------------------------------------------+
| ["apple", "pear", {"grape": "red"}, "strawberry"] |
+---------------------------------------------------+
1 row in set (0.00 sec)
Listing 3-8Using the JSON_ARRAY_APPEND Function
现在让我们看看可以用来修改 JSON 数据的函数。
修改 JSON 数据
修改 JSON 数据有几个有用的函数。本节讨论了通过删除、替换和更新 JSON 文档中的元素来帮助修改 JSON 文档的函数。
JSON_REMOVE()
函数用于删除匹配路径表达式的元素。您必须提供要操作的 JSON 文档以及一个或多个路径表达式,结果将是删除了元素的 JSON 文档。当包含多个路径表达式时,当函数计算第一个路径表达式并将下一个路径表达式应用于结果时,效果是累积的,依此类推。清单 3-9 显示了一个例子。注意,我必须想象中间结果会是什么——也就是说,我使用了三次$[0]
,因为函数两次删除了第一个元素,留下 JSON 对象作为第一个元素。
MySQL localhost:33060+ ssl SQL > SET @base = '["apple","pear",{"grape":["red","white"]},"strawberry"]';
Query OK, 0 rows affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_REMOVE(@base, '$[0]', '$[0]', '$[0].grape[1]');
+-----------------------------------------------------+
| JSON_REMOVE(@base, '$[0]', '$[0]', '$[0].grape[1]') |
+-----------------------------------------------------+
| [{"grape": ["red"]}, "strawberry"] |
+-----------------------------------------------------+
1 row in set (0.00 sec)
Listing 3-9Using the JSON_REMOVE Function (Single)
这可能需要一点时间来适应,但您可以多次使用该函数或嵌套使用,如清单 3-10 中的示例所示。
MySQL localhost:33060+ ssl SQL > SET @base = '["apple","pear",{"grape":["red","white"]},"strawberry"]';
Query OK, 0 rows affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > SET @base = JSON_REMOVE(@base, '$[0]');
Query OK, 0 rows affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > SET @base = JSON_REMOVE(@base, '$[0]');
Query OK, 0 rows affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_REMOVE(@base, '$[0].grape[1]');
+-------------------------------------+
| JSON_REMOVE(@base, '$[0].grape[1]') |
+-------------------------------------+
| [{"grape": ["red"]}, "strawberry"] |
+-------------------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SET @base = '["apple","pear",{"grape":["red","white"]},"strawberry"]';
Query OK, 0 rows affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_REMOVE(JSON_REMOVE(JSON_REMOVE(@base, '$[0]'), '$[0]'), '$[0].grape[1]');
+-------------------------------------------------------------------------+
| JSON_REMOVE(JSON_REMOVE(JSON_REMOVE(@base, '$[0]'), '$[0]'), '$[0].grape[1]') |
+-------------------------------------------------------------------------+
| [{"grape": ["red"]}, "strawberry"] |
+-------------------------------------------------------------------------+
1 row in set (0.00 sec)
Listing 3-10Using the JSON_REMOVE Function (Nested)
JSON_REPLACE()
函数接受一个 JSON 文档和一对路径表达式和值,用新值替换匹配路径表达式的元素。同样,结果是累积的,并按从左到右的顺序工作。这个函数也有一个问题。它会忽略任何新值或评估为新值的路径表达式。清单 3-11 显示了一个例子。注意,第三对没有被删除,因为没有第十个元素。
MySQL localhost:33060+ ssl SQL > SET @base = '["apple","pear",{"grape":["red","white"]},"strawberry"]';
Query OK, 0 rows affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_REPLACE(@base, '$[0]', "orange", '$[2].grape[0]', "green", '$[9]', "waffles");
+-------------------------------------------------------------------------+
| JSON_REPLACE(@base, '$[0]', "orange", '$[2].grape[0]', "green", '$[9]', "waffles") |
+-------------------------------------------------------------------------+
| ["orange", "pear", {"grape": ["green", "white"]}, "strawberry"] |
+-------------------------------------------------------------------------+
1 row in set (0.00 sec)
Listing 3-11Using the JSON_REPLACE Function
JSON_SET()
函数用于修改 JSON 文档元素。与其他函数一样,您传递一个 JSON 文档作为第一个参数,然后传递一对或多对要替换的路径表达式和值。但是,该函数还会插入文档中不存在的任何元素(找不到路径表达式)。清单 3-12 显示了一个例子。注意,最后一个元素并不存在,所以它将它添加到文档中。
MySQL localhost:33060+ ssl SQL > SET @base = '["apple","pear",{"grape":["red","white"]},"strawberry"]';
Query OK, 0 rows affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_SET(@base, '$[0]', "orange", '$[2].grape[1]', "green", '$[9]', "123");
+-------------------------------------------------------------------------+
| JSON_SET(@base, '$[0]', "orange", '$[2].grape[1]', "green", '$[9]', "123") |
+-------------------------------------------------------------------------+
| ["orange", "pear", {"grape": ["red", "green"]}, "strawberry", "123"] |
+-------------------------------------------------------------------------+
1 row in set (0.00 sec)
Listing 3-12Using the JSON_SET Function
Ignore or Not Ignore, Which Does What?
JSON 函数的一个问题是,有些函数会对现有的值进行操作,有些函数会忽略现有的值,有些函数会添加尚不存在的值,等等。如果你不熟悉所有的功能,它会变得令人困惑。下面总结了那些最容易混淆的函数之间的差异。
JSON_INSERT()
:添加新值,但不替换现有值JSON_REMOVE()
:删除文档中存在的元素,忽略不存在的元素JSON_REPLACE()
:替换现有值,忽略新值JSON_SET()
:替换存在路径的值,增加不存在路径的值
如果您想使用这些函数,请务必用示例数据检查它们,直到您理解这些条件。
现在让我们看看可以用来在文档中查找元素的 JSON 函数。
搜索 JSON 数据
处理 SQL 和 JSON 数据的另一个重要操作是在 JSON 文档中搜索数据。我们在前面的章节中发现了如何用特殊符号(路径表达式)引用文档中的数据,我们还知道了可以用 JSON 函数来搜索数据。事实上,我们在上一节中看到了这两个概念一起使用。在这一节中,我们将回顾 JSON 数据搜索机制,因为您可能会比其他任何函数更多地使用这些函数,尤其是在查询中。
有四个 JSON 函数允许您搜索 JSON 文档。与前面的函数一样,这些函数使用一个或多个参数对 JSON 文档进行操作。我称它们为搜索函数,不是因为它们允许您在数据库或表格中搜索 JSON 数据,而是因为它们允许您在 JSON 文档中查找东西。这些函数包括检查文档中是否存在值或元素、路径表达式是否有效(使用它可以找到一些东西)以及从文档中检索信息的函数。
JSON_CONTAINS()
函数有两个选项:您可以使用它来返回一个值是否存在于文档中的任何地方,或者是否存在使用路径表达式的值(路径表达式是一个可选参数)。该函数返回 0 或 1,其中 0 表示未找到该值。如果文档参数不是有效的 JSON 文档,路径参数不是有效的路径表达式,或者包含*或**通配符,则会出现错误。还有一个问题。传入的值必须是有效的 JSON 字符串或文档。清单 3-13 展示了使用该函数搜索 JSON 文档的几个例子。
MySQL localhost:33060+ ssl SQL > SET @base = '{"grapes":["red","white","green"],"berries":["strawberry","raspberry","boysenberry","blackberrry"]}';
Query OK, 0 rows affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_CONTAINS(@base,'["red","white","green"]');
+------------------------------------------------+
| JSON_CONTAINS(@base,'["red","white","green"]') |
+------------------------------------------------+
| 0 |
+------------------------------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_CONTAINS(@base,'{"grapes":["red","white","green"]}');
+-----------------------------------------------------------+
| JSON_CONTAINS(@base,'{"grapes":["red","white","green"]}') |
+-----------------------------------------------------------+
| 1 |
+-----------------------------------------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_CONTAINS(@base,'["red","white","green"]','$.grapes');
+-----------------------------------------------------------+
| JSON_CONTAINS(@base,'["red","white","green"]','$.grapes') |
+-----------------------------------------------------------+
| 1 |
+-----------------------------------------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_CONTAINS(@base,'"blackberry"','$.berries');
+-------------------------------------------------+
| JSON_CONTAINS(@base,'"blackberry"','$.berries') |
+-------------------------------------------------+
| 0 |
+-------------------------------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_CONTAINS(@base,'blackberry','$.berries');
ERROR: 3141: Invalid JSON text in argument 2 to function json_contains: "Invalid value." at position 0.
MySQL localhost:33060+ ssl SQL > SELECT JSON_CONTAINS(@base,'"red"','$.grapes');
+-----------------------------------------+
| JSON_CONTAINS(@base,'"red"','$.grapes') |
+-----------------------------------------+
| 1 |
+-----------------------------------------+
1 row in set (0.00 sec)
Listing 3-13Using the JSON_CONTAINS Function
正如你所看到的,这是一个非常有用的函数,但是要正确使用它需要一点小心。也就是说,您必须确保该值是有效的字符串。在所有的例子中,除了一个例子,我在 JSON 文档中搜索 JSON 文档(这使得搜索嵌套数据更容易),或者使用路径表达式搜索单个值。记住,函数搜索的是值,而不是键。
请注意倒数第二个示例:这将返回一个错误,因为该值不是有效的 JSON 字符串。您必须用双引号将它括起来,如下例所示。
JSON_CONTAINS_PATH()
函数使用的参数策略略有不同。该函数搜索 JSON 文档以查看路径表达式是否存在,但它也允许您查找第一个或所有的匹配项。它还可以采用多个路径,并根据您作为第二个参数传递的值,将它们作为“或”或“与”条件进行评估,如下所示:
- 如果您传递
one
,如果至少找到一个路径表达式,函数将返回 1(OR)。 - 如果传递
all
,只有找到所有路径表达式,函数才会返回 1(AND)。
该函数返回 0 或 1,以指示 JSON 文档在给定的一个或多个路径中是否包含数据。请注意,如果任何路径表达式或文档为 null,它可能会返回 null。如果 JSON 文档或任何路径表达式无效,或者第二个参数不是one
或all
,则会出现错误。清单 3-14 显示了使用该功能的几个例子。
MySQL localhost:33060+ ssl SQL > SET @base = '{"grapes":["red","white","green"],"berries":["strawberry","raspberry","boysenberry","blackberrry"],"numbers":["1","2","3","4","5"]}';
Query OK, 0 rows affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_CONTAINS_PATH(@base,'one','$');
+-------------------------------------+
| JSON_CONTAINS_PATH(@base,'one','$') |
+-------------------------------------+
| 1 |
+-------------------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_CONTAINS_PATH(@base,'all','$');
+-------------------------------------+
| JSON_CONTAINS_PATH(@base,'all','$') |
+-------------------------------------+
| 1 |
+-------------------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_CONTAINS_PATH(@base,'all','$.grapes','$.berries');
+--------------------------------------------------------+
| JSON_CONTAINS_PATH(@base,'all','$.grapes','$.berries') |
+--------------------------------------------------------+
| 1 |
+--------------------------------------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_CONTAINS_PATH(@base,'all','$.grapes','$.berries','$.numbers');
+--------------------------------------------------------------------+
| JSON_CONTAINS_PATH(@base,'all','$.grapes','$.berries','$.numbers') |
+--------------------------------------------------------------------+
| 1 |
+--------------------------------------------------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_CONTAINS_PATH(@base,'all','$.grapes','$.berries','$.num');
+----------------------------------------------------------------+
| JSON_CONTAINS_PATH(@base,'all','$.grapes','$.berries','$.num') |
+----------------------------------------------------------------+
| 0 |
+----------------------------------------------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_CONTAINS_PATH(@base,'one','$.grapes','$.berries','$.num');
+----------------------------------------------------------------+
| JSON_CONTAINS_PATH(@base,'one','$.grapes','$.berries','$.num') |
+----------------------------------------------------------------+
| 1 |
+----------------------------------------------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_CONTAINS_PATH(@base,'one','$.grapes');
+--------------------------------------------+
| JSON_CONTAINS_PATH(@base,'one','$.grapes') |
+--------------------------------------------+
| 1 |
+--------------------------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_CONTAINS_PATH(@base,'all','$.grape');
+-------------------------------------------+
| JSON_CONTAINS_PATH(@base,'all','$.grape') |
+-------------------------------------------+
| 0 |
+-------------------------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_CONTAINS_PATH(@base,'one','$.berries');
+---------------------------------------------+
| JSON_CONTAINS_PATH(@base,'one','$.berries') |
+---------------------------------------------+
| 1 |
+---------------------------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_CONTAINS_PATH(@base,'all','$.berries');
+---------------------------------------------+
| JSON_CONTAINS_PATH(@base,'all','$.berries') |
+---------------------------------------------+
| 1 |
+---------------------------------------------+
1 row in set (0.00 sec)
Listing 3-14Using the JSON_CONTAINS_PATH Function
花些时间浏览这些例子,这样你就能明白它们是如何工作的。注意,在前两个命令中,我使用了一个美元符号的路径表达式。这只是整个文档的路径表达式,所以它自然存在。还要注意最后两个例子中使用one
或all
的区别。
JSON_EXTRACT()
功能是最常用的功能之一。它允许您使用一个或多个路径表达式从 JSON 文档中提取值、JSON 数组、JSON 对象等等。我们已经看到了几个例子。Recall 函数返回 JSON 文档中与路径表达式匹配的部分。清单 3-15 展示了更多使用复杂路径表达式的例子。
MySQL localhost:33060+ ssl SQL > SET@base = '{"grapes":["red","white","green"],"berries":["strawberry","raspberry","boysenberry","blackberrry"],"numbers":["1","2","3","4","5"]}';
Query OK, 0 rows affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_EXTRACT(@base,'$');
+-------------------------------------------------------------------------+
| JSON_EXTRACT(@base,'$')
+-------------------------------------------------------------------------+
| {"grapes": ["red", "white", "green"], "berries": ["strawberry", "raspberry", "boysenberry", "blackberry"], "numbers": ["1", "2", "3", "4", "5"]} |
+-------------------------------------------------------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_EXTRACT(@base,'$.grapes');
+--------------------------------+
| JSON_EXTRACT(@base,'$.grapes') |
+--------------------------------+
| ["red", "white", "green"] |
+--------------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_EXTRACT(@base,'$.grapes[*]');
+-----------------------------------+
| JSON_EXTRACT(@base,'$.grapes[*]') |
+-----------------------------------+
| ["red", "white", "green"] |
+-----------------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_EXTRACT(@base,'$.grapes[1]');
+-----------------------------------+
| JSON_EXTRACT(@base,'$.grapes[1]') |
+-----------------------------------+
| "white" |
+-----------------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_EXTRACT(@base,'$.grapes[4]');
+-----------------------------------+
| JSON_EXTRACT(@base,'$.grapes[4]') |
+-----------------------------------+
| NULL |
+-----------------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_EXTRACT(@base,'$.berries');
+-----------------------------------------------------------+
| JSON_EXTRACT(@base,'$.berries') |
+-----------------------------------------------------------+
| ["strawberry", "raspberry", "boysenberry", "blackberry"] |
+-----------------------------------------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_EXTRACT(@base,'$.berries[2]');
+------------------------------------+
| JSON_EXTRACT(@base,'$.berries[2]') |
+------------------------------------+
| "boysenberry" |
+------------------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_EXTRACT(@base,'$.berries[2]','$.berries[3]');
+---------------------------------------------------+
| JSON_EXTRACT(@base,'$.berries[2]','$.berries[3]') |
+---------------------------------------------------+
| ["boysenberry", "blackberry"] |
+---------------------------------------------------+
1 row in set (0.00 sec)
Listing 3-15Using the JSON_EXTRACT Function
注意当我们使用一元符号时会发生什么。该函数返回整个文档。此外,请注意当我们使用路径表达式时会发生什么,尽管它的语法是有效的,但它不会计算出文档中的元素(参见第五个命令)。
注意最后一个例子,我们传入了两个路径表达式。然后注意它如何返回一个 JSON 数组,而之前只有一个路径表达式的例子返回一个 JSON 字符串值。这是该函数的一个更棘手的方面。只要您记得它返回一个有效的 JSON 字符串、数组或对象,您就可以毫无问题地使用该函数。
JSON_SEARCH()
函数很有趣,因为它与JSON_EXTRACT()
函数相反。更具体地说,它接受一个或多个值,如果在文档中找到这些值,则返回这些值的路径表达式。这使得验证路径表达式或动态构建路径表达式变得更加容易。
与JSON_CONTAINS_PATH()
函数一样,JSON_SEARCH()
函数也允许您根据作为第二个参数传递的值来查找返回路径表达式的第一个或所有匹配项,如下所示:
- 如果通过
one
,函数将返回第一个匹配。 - 如果通过
all
,函数将返回所有匹配。
但是这里也有一个技巧。该函数接受第三个参数,该参数构成一个特殊的搜索字符串,在 SQL 语句中充当 LIKE 运算符。也就是说,搜索字符串参数可以像 LIKE 运算符一样使用%和 _ 字符。请注意,要将%或 _ 用作文字,必须在它前面加上(转义)字符。
该函数返回 0 或 1,以指示 JSON 文档是否包含这些值。请注意,如果任何路径表达式或文档为 null,它可能会返回 null。如果 JSON 文档或任何路径表达式无效,或者第二个参数不是one
或all
,则会出现错误。清单 3-16 显示了使用该功能的几个例子。
MySQL localhost:33060+ ssl SQL > SET @base = '{"grapes":["red","white","green"],"berries":["strawberry","raspberry","boysenberry","blackberrry"],"numbers":["1","2","3","4","5"]}';
Query OK, 0 rows affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_SEARCH(@base,'all','red');
+--------------------------------+
| JSON_SEARCH(@base,'all','red') |
+--------------------------------+
| "$.grapes[0]" |
+--------------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_SEARCH(@base,'all','gr____');
+-----------------------------------+
| JSON_SEARCH(@base,'all','gr____') |
+-----------------------------------+
| NULL |
+-----------------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_SEARCH(@base,'one','%berry');
+-----------------------------------+
| JSON_SEARCH(@base,'one','%berry') |
+-----------------------------------+
| "$.berries[0]" |
+-----------------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_SEARCH(@base,'all','%berry');
+--------------------------------------------------+
| JSON_SEARCH(@base,'all','%berry') |
+--------------------------------------------------+
| ["$.berries[0]", "$.berries[1]", "$.berries[2]"] |
+--------------------------------------------------+
1 row in set (0.00 sec)
Listing 3-16Using the JSON_SEARCH Function
现在我们来看最后一组 JSON 函数;这些工具本质上是实用的,允许您获得关于 JSON 文档的信息,并执行简单的操作来帮助处理 JSON 文档。
效用函数
最后,有几个函数可以返回关于 JSON 文档的信息,帮助添加或删除引号,甚至查找文档中的键。我们已经看到了几个实用程序JSON_TYPE()
和JSON_VALID()
函数。以下是在使用 JSON 文档时可能会发现有用的其他实用函数。
JSON_DEPTH()
函数返回 JSON 文档的最大深度。如果文档是空数组、对象或标量值,则为。该函数返回深度 1。仅包含深度为 1 的元素的数组或仅包含深度为 1 的成员值的非空对象返回深度为 2 的值。清单 3-17 展示了几个例子。
MySQL localhost:33060+ ssl SQL > SELECT JSON_DEPTH('8');
+-----------------+
| JSON_DEPTH('8') |
+-----------------+
| 1 |
+-----------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_DEPTH('[]');
+------------------+
| JSON_DEPTH('[]') |
+------------------+
| 1 |
+------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_DEPTH('{}');
+------------------+
| JSON_DEPTH('{}') |
+------------------+
| 1 |
+------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_DEPTH('[12,3,4,5,6]');
+----------------------------+
| JSON_DEPTH('[12,3,4,5,6]') |
+----------------------------+
| 2 |
+----------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_DEPTH('[[], {}]');
+------------------------+
| JSON_DEPTH('[[], {}]') |
+------------------------+
| 2 |
+------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SET @base = '{"grapes":["red","white","green"],"berries":["strawberry","raspberry","boysenberry","blackberrry"],"numbers":["1","2","3","4","5"]}';
Query OK, 0 rows affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_DEPTH(@base);
+-------------------+
| JSON_DEPTH(@base) |
+-------------------+
| 3 |
+-------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_DEPTH(JSON_EXTRACT(@base, '$.grapes'));
+---------------------------------------------+
| JSON_DEPTH(JSON_EXTRACT(@base, '$.grapes')) |
+---------------------------------------------+
| 2 |
+---------------------------------------------+
1 row in set (0.00 sec)
Listing 3-17Using the JSON_DEPTH Function
JSON_KEYS()
函数用于以 JSON 数组的形式从 JSON 对象的顶层值返回一个键列表。该函数还允许您传递路径表达式,这将产生所选路径表达式值的顶级键列表。如果 json_doc 参数不是有效的 json 文档,或者 path 参数不是有效的路径表达式,或者包含*或**通配符,则会出现错误。如果所选对象为空,则结果数组为空。
有一个限制。如果顶层值有嵌套的 JSON 对象,则返回的数组不包括这些嵌套对象的键。清单 3-18 显示了使用该功能的几个例子。
MySQL localhost:33060+ ssl SQL > SET @base = '{"grapes":["red","white","green"],"berries":["strawberry","raspberry","boysenberry","blackberrry"],"numbers":["1","2","3","4","5"]}';
Query OK, 0 rows affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_KEYS(@base);
+----------------------------------+
| JSON_KEYS(@base) |
+----------------------------------+
| ["grapes", "berries", "numbers"] |
+----------------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_KEYS(@base,'$');
+----------------------------------+
| JSON_KEYS(@base,'$') |
+----------------------------------+
| ["grapes", "berries", "numbers"] |
+----------------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_KEYS('{"z":123,"x":{"albedo":50}}');
+------------------------------------------+
| JSON_KEYS('{"z":123,"x":{"albedo":50}}') |
+------------------------------------------+
| ["x", "z"] |
+------------------------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_KEYS('{"z":123,"x":{"albedo":50}}', '$.x');
+-------------------------------------------------+
| JSON_KEYS('{"z":123,"x":{"albedo":50}}', '$.x') |
+-------------------------------------------------+
| ["albedo"] |
+-------------------------------------------------+
1 row in set (0.00 sec)
Listing 3-18Using the JSON_KEYS Function
JSON_LENGTH()
函数返回传递的 JSON 文档的长度。它还允许您传入路径表达式,如果提供了路径表达式,将返回与路径表达式匹配的值的长度。如果 json_doc 参数不是有效的 json 文档,或者 path 参数不是有效的路径表达式,或者包含*或**通配符,则会出现错误。但是,返回值有几个约束,如下所示:
- 标量的长度为 1。
- 数组的长度等于数组元素的数量。
- 对象的长度等于对象成员的数量。
然而,有一个令人惊讶的限制:返回的长度不包括嵌套数组或对象的长度。因此,使用嵌套文档的路径表达式时,必须小心使用该函数。
清单 3-19 显示了使用该函数的几个例子。
MySQL localhost:33060+ ssl SQL > SET @base = '{"grapes":["red","white","green"],"berries":["strawberry","raspberry","boysenberry","blackberrry"],"numbers":["1","2","3","4","5"]}';
Query OK, 0 rows affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_LENGTH(@base,'$');
+------------------------+
| JSON_LENGTH(@base,'$') |
+------------------------+
| 3 |
+------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_LENGTH(@base,'$.grapes');
+-------------------------------+
| JSON_LENGTH(@base,'$.grapes') |
+-------------------------------+
| 3 |
+-------------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_LENGTH(@base,'$.grapes[1]');
+----------------------------------+
| JSON_LENGTH(@base,'$.grapes[1]') |
+----------------------------------+
| 1 |
+----------------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_LENGTH(@base,'$.grapes[4]');
+----------------------------------+
| JSON_LENGTH(@base,'$.grapes[4]') |
+----------------------------------+
| NULL |
+----------------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_LENGTH(@base,'$.berries');
+--------------------------------+
| JSON_LENGTH(@base,'$.berries') |
+--------------------------------+
| 4 |
+--------------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_LENGTH(@base,'$.numbers');
+--------------------------------+
| JSON_LENGTH(@base,'$.numbers') |
+--------------------------------+
| 5 |
+--------------------------------+
1 row in set (0.00 sec)
Listing 3-19Using the JSON_LENGTH Function
注意,第四个命令返回 null,因为路径表达式虽然是有效的语法,但并不等于值或嵌套的 JSON 数组或对象。
JSON_QUOTE()
函数是一个方便的函数,可以帮助你在适当的地方添加引号。也就是说,该函数通过用双引号字符将字符串括起来并转义内部引号和其他字符来将字符串作为 JSON 字符串引用,并返回结果。注意,这个函数并不对 JSON 文档进行操作,而是只对一个字符串进行操作。
您可以使用这个函数生成一个有效的 JSON 字符串文字,以包含在 JSON 文档中。清单 3-20 展示了几个使用函数引用 JSON 字符串的简短例子。
MySQL localhost:33060+ ssl SQL > SELECT JSON_QUOTE("test");
+--------------------+
| JSON_QUOTE("test") |
+--------------------+
| "test" |
+--------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_QUOTE('[true]');
+----------------------+
| JSON_QUOTE('[true]') |
+----------------------+
| "[true]" |
+----------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_QUOTE('90125');
+---------------------+
| JSON_QUOTE('90125') |
+---------------------+
| "90125" |
+---------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_QUOTE('["red","white","green"]');
+---------------------------------------+
| JSON_QUOTE('["red","white","green"]') |
+---------------------------------------+
| "[\"red\",\"white\",\"green\"]" |
+---------------------------------------+
1 row in set (0.00 sec)
Listing 3-20Using the JSON_QUOTE Function
请注意,在最后一个示例中,函数添加了转义符(),因为传递的字符串包含引号。为什么会这样?记住,这个函数接受一个字符串,而不是一个 JSON 数组作为参数。
JSON_UNQUOTE()
功能与JSON_QUOTE()
功能相反。JSON_UNQUOTE()
函数删除引号中的 JSON 值,并将结果作为 utf8mb4 字符串返回。该函数旨在识别但不改变标记序列,如下所示:
\"
:双引号(")字符\b
:退格字符\f
:换页符\n
:换行符\r
:回车符\t
:制表符\\
:反斜杠()字符
清单 3-21 显示了使用该函数的例子。
MySQL localhost:33060+ ssl SQL > SELECT JSON_UNQUOTE("test 123");
+--------------------------+
| JSON_UNQUOTE("test 123") |
+--------------------------+
| test 123 |
+--------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_UNQUOTE('"true"');
+------------------------+
| JSON_UNQUOTE('"true"') |
+------------------------+
| true |
+------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_UNQUOTE('\"true\"');
+--------------------------+
| JSON_UNQUOTE('\"true\"') |
+--------------------------+
| true |
+--------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_UNQUOTE('9\t0\t125\\');
+-----------------------------+
| JSON_UNQUOTE('9\t0\t125\\') |
+-----------------------------+
| 9 0 125\ |
+-----------------------------+
1 row in set (0.00 sec)
Listing 3-21Using the JSON_UNQUOTE Function
JSON_PRETTY()函数格式化 JSON 文档以便于查看。您可以用它来生成一个输出发送给用户,或者让 JSON 在 shell 中看起来更好一些。清单 3-22 显示了一个没有函数的例子和一个有函数的例子。请注意,使用 JSON_PRETTY()时阅读起来要容易得多。
MySQL localhost:33060+ ssl SQL > SET @base = '{"name": {"last": "Throckmutton", "first": "Billy-bob"}, "address": {"zip": "90125", "city": "Melborne", "state": "California", "street": "4 Main Street"}}';
Query OK, 0 rows affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT @base \G
*************************** 1\. row ***************************
@base: {"name": {"last": "Throckmutton", "first": "Billy-bob"}, "address": {"zip": "90125", "city": "Melborne", "state": "California", "street": "4 Main Street"}}
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT JSON_PRETTY(@base) \G
*************************** 1\. row ***************************
JSON_PRETTY(@base): {
"name": {
"last": "Throckmutton",
"first": "Billy-bob"
},
"address": {
"zip": "90125",
"city": "Melborne",
"state": "California",
"street": "4 Main Street"
}
}
1 row in set (0.00 sec)
Listing 3-22Using the JSON_PRETTY Function
还有检查尺寸的功能;JSON_STORAGE_FREE()
和JSON_STORAGE_SIZE()
。第一个在部分更新后使用,第二个用于获取 JSON 文档的二进制表示的大小。有关这些函数的更多细节,请参见在线 MySQL 参考手册,因为它们是新的,除了在非常特殊的情况下需要考虑大小之外,并不常用。
最后,在 8.0.4 版本中发布了一个新函数,有趣的是它被命名为 JSON_TABLE()。这个函数获取一个 JSON 文档并返回一个表格数据列表。基本上,这个函数不是以 JSON 的形式返回输出,而是以结果集的形式返回行。因此,您可以在应用中需要更多传统行的地方使用这个函数。
这个函数有一些特殊的语法。它将 JSON 文档(数组)、表达式路径和列定义作为参数。后两者没有用逗号隔开(奇怪)。这种安排使得这个函数有点难以使用,但是一旦你看到一个工作示例,它就更容易理解了。所以,让我们开始吧。清单 3-23 演示了如何使用该功能。
MySQL localhost:33060+ SQL > set @phones = '[{"name":"Bill Smith","phone":"8013321033"},{"name":"Folley Finn","phone":"9991112222"},{"name":"Carrie Tonnesth","phone":"6498881212"}]';
Query OK, 0 rows affected (0.00 sec)
MySQL localhost:33060+ SQL > SELECT * FROM JSON_TABLE(@phones, "$[*]" COLUMNS(name char(20) PATH '$.name', phone char(16) PATH '$.phone')) as phone_list;
+-----------------+------------+
| name | phone |
+-----------------+------------+
| Bill Smith | 8013321033 |
| Folley Finn | 9991112222 |
| Carrie Tonnesth | 6498881212 |
+-----------------+------------+
3 rows in set (0.00 sec)
Listing 3-23Using the JSON_TABLE Function
注意,为了简单起见,我们使用了一个包含姓名和电话号码的 JSON 数组。该函数就像一个表一样使用,所以我们将它添加到一个SELECT
语句的FROM
子句中。参数是 JSON 文档,然后是路径和列定义。使用的表达式路径只是从数组中检索整个元素。如果您只想选择文档的一部分进行操作,可以在这里使用各种路径表达式。接下来是列定义,您应该对此很熟悉——就像表的列定义一样。不同之处在于我们在末尾添加了一个带有关键字PATH
的路径表达式。这只是在 JSON 文档中定位值。
可以想象,您可以形成复杂的定义,深入到您想要的元素。这个函数的需求和用例可能会增加,因为它是最近才添加的,但是如果您需要将 JSON 文档转换成结果集,这个函数可以实现这些结果,尽管需要一些创造性和路径表达式。
有关JSON_TABLE()
函数的更多信息,请参见在线 MySQL 参考手册中标题为“JSON 表函数”的部分。
Tip
有关 JSON 函数的更多信息,请参见在线 MySQL 参考手册。JSON 函数根据使用情况与其他函数一起列出。我建议在文档中搜索您想要了解更多信息的函数,或者使用名为“函数索引”的索引,该索引按字母顺序列出了所有函数。
现在我们对 JSON 有了更多的了解,“结合 SQL 和 JSON——索引 JSON 数据”一节提供了一些在 SQL 语句中使用 JSON 的高级主题。
结合 SQL 和 JSON——索引 JSON 数据
NoSQL 的一个定义是“不仅仅是 SQL ”,当您考虑可以将 JSON 文档用于关系数据时,这个名字适用于 MySQL。正如我们在描述 JSON 函数的例子中看到的,您可以向表中添加 JSON 列,并在字段中存储 JSON 数据。
但是,MySQL 不是将 JSON 文档存储为字符串,而是使用一种特殊的内部结构来存储 JSON 文档,这种结构允许 MySQL 从行数据中快速访问、查找和提取 JSON 文档元素。注意,这并不意味着 MySQL 可以索引 JSON 数据。事实上,JSON 数据列不能被索引。至少,不是直接的。在这一节中,我们将看到如何索引 JSON 数据,以帮助优化对包含 JSON 文档的行的数据元素的搜索。
What About Converting Text To Json?
如果您有一个在文本或 BLOB 字段中存储了半结构化数据的数据库,您可能想考虑将数据转换成 JSON 文档。我们在本章中看到的 JSON 函数是成功转换数据的关键,比如JSON_ARRAY()
、JSON_OBJECT()
和JSON_VALID()
。我将在第 9 章中详细讨论这个话题,包括如何转换现有数据的建议和例子。您可能还想查看各种关于将数据转换成 JSON 的博客——只是类似于“转换成 JSON”的 google 短语尽管大多数博客都是基于 Java 的,但是您可以使用它们来获得如何转换您自己的数据的想法。
有些人可能认为禁止对 JSON 列进行索引的限制是一个疏忽,但事实并非如此。考虑这样一个事实,JSON 文档是半结构化数据,不需要符合任何特定的布局。也就是说,一行可能包含一个 JSON 文档,该文档不仅有不同的键,而且可能以不同的顺序排列文档。
尽管这不一定是索引的绊脚石,尽管使用了特殊的内部机制来访问文档中的数据,但是直接索引 JSON 文档会很麻烦,并且性能可能很差。然而,并没有失去一切。MySQL 5.7 引入了一个新特性,称为生成列(有时称为虚拟列)。
生成的列是由CREATE
或ALTER TABLE
语句定义的动态解析列。有两种类型的虚拟列:按需生成的列(称为虚拟生成列),它们不使用任何额外的存储;以及可以存储在行中的那些生成的列。虚拟生成的列使用VIRTUAL
选项,存储生成的列使用CREATE
或ALTER TABLE
语句中的STORED
选项。
那么这是如何工作的呢?我们创建生成的列来从 JSON 文档中提取数据,然后使用该列来创建索引。因此,索引可用于更快地查找行。也就是说,如果您想要执行分组、排序,或者想要搜索基于 JSON 数据的行的子集,您可以为优化器创建索引,以便更快地检索数据。
让我们看一个例子。下面显示了我创建的用于在 JSON 列中存储信息的表。
CREATE TABLE `test`.`thermostats` (
`model_number` char(20) NOT NULL,
`manufacturer` char(30) DEFAULT NULL,
`capabilities` json DEFAULT NULL,
PRIMARY KEY (`model_number`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
INSERT INTO `test`.`thermostats` VALUES ('AB-90125-C1', 'Jasper', '{"rpm": 1500, "color": "beige", "modes": ["ac"], "voltage": 110, "capability": "auto fan"}');
INSERT INTO `test`.`thermostats` VALUES ('ODX-123','Genie','{"rpm": 3000, "color": "white", "modes": ["ac", "furnace"], "voltage": 220, "capability": "fan"}');
注意,这个表有一个 JSON 字段和一个型号字符字段,这也是主键。假设这些行包含 JSON 数据,如下所示。
MySQL localhost:33060+ ssl SQL > SELECT * FROM `test`.`thermostats` LIMIT 2 \G
*************************** 1\. row ***************************
model_number: AB-90125-C1
manufacturer: Jasper
capabilities: {"rpm": 1500, "color": "beige", "modes": ["ac"], "voltage": 110, "capability": "auto fan"}
*************************** 2\. row ***************************
model_number: ODX-123
manufacturer: Genie
capabilities: {"rpm": 3000, "color": "white", "modes": ["ac", "furnace"], "voltage": 220, "capability": "fan"}
2 rows in set (0.00 sec)
现在假设我们想执行查询,通过 JSON 文档中的一个或多个数据元素来选择行。例如,假设我们希望运行查询来定位风扇以 110 伏运行的行。如果表包含几十万甚至几千万行,并且没有索引,那么优化器必须读取所有行(表扫描)。但是,如果数据上有索引,优化器只需要生成虚拟生成的列,这可能更有效。
为了减轻潜在的性能问题,我们可以使用 voltage 元素在表上添加一个虚拟生成的列。下面显示了我们可以用来添加虚拟生成列的ALTER TABLE
语句。
ALTER TABLE `test`.`thermostats` ADD COLUMN voltage INT GENERATED ALWAYS AS (capabilities->'$.voltage') VIRTUAL;
ALTER TABLE `test`.`thermostats` ADD INDEX volts (voltage);
Note
如果不使用该选项,生成的列是虚拟生成的列。
如果需要,也可以重新创建表,但这需要重新加载数据。但是,我在下面展示了新的CREATE TABLE
语句,这样您就可以看到如何在创建时在表上创建一个虚拟生成的列。
CREATE TABLE `test`.`thermostats` (
`model_number` char(20) NOT NULL,
`manufacturer` char(30) DEFAULT NULL,
`capabilities` json DEFAULT NULL,
`voltage` int(11) GENERATED ALWAYS AS (json_extract(`capabilities`,'$.voltage')) VIRTUAL,
PRIMARY KEY (`model_number`),
KEY `volts` (`voltage`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
注意,我在ALTER TABLE
语句中使用了快捷键->
,但是CREATE TABLE
语句使用了JSON_EXTRACT()
函数。
如果您想知道添加虚拟生成的列和索引是否有所不同,清单 3-24 展示了优化器在添加列之前和之后如何运行查询。
MySQL localhost:33060+ ssl SQL > DROP TABLE IF EXISTS `test`.`thermostats`;
Query OK, 0 rows affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > CREATE TABLE `test`.`thermostats` (`model_number` char(20) NOT NULL,`manufacturer` char(30) DEFAULT NULL,`capabilities` json DEFAULT NULL,PRIMARY KEY (`model_number`)) ENGINE=InnoDB DEFAULT CHARSET=latin1;
Query OK, 0 rows affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > INSERT INTO `test`.`thermostats` VALUES ('ODX-123','Genie','{"rpm": 3000, "color": "white", "modes": ["ac", "furnace"], "voltage": 220, "capability": "fan"}');
Query OK, 1 row affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > INSERT INTO `test`.`thermostats` VALUES ('AB-90125-C1', 'Jasper', '{"rpm": 1500, "color": "beige", "modes": ["ac"], "voltage": 110, "capability": "auto fan"}');
Query OK, 1 row affected (0.00 sec)
# Query without virtual generated
column
.
MySQL localhost:33060+ ssl SQL > EXPLAIN SELECT * FROM thermostats WHERE capabilities->'$.voltage' = 110 \G
*************************** 1\. row ***************************
id: 1
select_type: SIMPLE
table: thermostats
partitions: NULL
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 23302
filtered: 100.00
Extra: Using where
1 row in set, 1 warning (0.00 sec)
Note (code 1003): /* select#1 */ select `test`.`thermostats`.`model_number` AS `model_number`,`test`.`thermostats`.`manufacturer` AS `manufacturer`,`test`.`thermostats`.`capabilities` AS `capabilities` from `test`.`thermostats` where (json_extract(`test`.`thermostats`.`capabilities`,'$.voltage') = 110)
MySQL localhost:33060+ ssl SQL > ALTER TABLE `test`.`thermostats` ADD COLUMN color char(20) GENERATED ALWAYS AS (capabilities->'$.color') VIRTUAL;
Query OK, 0 rows affected (0.00 sec)
# Query with virtual generated column.
MySQL localhost:33060+ ssl SQL > DROP TABLE `test`.`thermostats`;
Query OK, 0 rows affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > CREATE TABLE `thermostats` (`model_number` char(20) NOT NULL, `manufacturer` char(30) DEFAULT NULL, `capabilities` json DEFAULT NULL, `voltage` int(11) GENERATED ALWAYS AS (json_extract(`capabilities`,'$.voltage')) VIRTUAL, PRIMARY KEY (`model_number`), KEY `volts` (`voltage`)) ENGINE=InnoDB DEFAULT CHARSET=latin1;
Query OK, 0 rows affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > EXPLAIN SELECT * FROM thermostats WHERE capabilities->'$.voltage' = 110 \G
*************************** 1\. row ***************************
id: 1
select_type: SIMPLE
table: thermostats
partitions: NULL
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 1102
filtered: 100.00
Extra: Using where
1 row in set, 1 warning (0.00 sec)
Note (code 1003): /* select#1 */ select `test`.`thermostats`.`model_number` AS `model_number`,`test`.`thermostats`.`manufacturer` AS `manufacturer`,`test`.`thermostats`.`capabilities` AS `capabilities`,`test`.`thermostats`.`color` AS `color` from `test`.`thermostats` where (json_extract(`test`.`thermostats`.`capabilities`,'$.voltage') = 110)
Listing 3-24Optimizer EXPLAIN Results for Query
注意,第一个EXPLAIN
显示没有使用索引(no key
,key_len
),而第二个显示使用了索引。rows 结果显示了将读取多少行(估计)来进行比较。很明显,添加生成的列和索引可以帮助我们优化关系表中 JSON 数据的查询。酷毙了。
然而,有一件事这个例子没有涉及到。如果 JSON 数据元素是一个字符串,您必须使用JSON_UNQUOTE()
函数从字符串中删除引号。假设我们想要为颜色数据元素添加一个生成的列。如果我们添加带有ALTER TABLE
语句的列和索引,而不删除引号,我们将得到一些不寻常的结果,如清单 3-25 所示。
MySQL localhost:33060+ ssl SQL > DROP TABLE IF EXISTS `test`.`thermostats`;
Query OK, 0 rows affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > CREATE TABLE `test`.`thermostats` (`model_number` char(20) NOT NULL,`manufacturer` char(30) DEFAULT NULL,`capabilities` json DEFAULT NULL,PRIMARY KEY (`model_number`)) ENGINE=InnoDB DEFAULT CHARSET=latin1;
Query OK, 0 rows affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > INSERT INTO `test`.`thermostats` VALUES ('ODX-123','Genie','{"rpm": 3000, "color": "white", "modes": ["ac", "furnace"], "voltage": 220, "capability": "fan"}');
Query OK, 1 row affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > INSERT INTO `test`.`thermostats` VALUES ('AB-90125-C1', 'Jasper', '{"rpm": 1500, "color": "beige", "modes": ["ac"], "voltage": 110, "capability": "auto fan"}');
Query OK, 1 row affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > ALTER TABLE `test`.`thermostats` ADD COLUMN color char(20) GENERATED ALWAYS AS (capabilities->'$.color') VIRTUAL;
Query OK, 0 rows affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT model_number, color FROM thermostats WHERE color = "beige";
Empty set (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT model_number, color FROM thermostats LIMIT 2;
+--------------+---------+
| model_number | color |
+--------------+---------+
| AB-90125-C1 | "beige" |
| ODX-123 | "white" |
+--------------+---------+
2 rows in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > ALTER TABLE thermostats DROP COLUMN color;
Query OK, 0 rows affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > ALTER TABLE thermostats ADD COLUMN color char(20) GENERATED ALWAYS AS (JSON_UNQUOTE(capabilities->'$.color')) VIRTUAL;
Query OK, 0 rows affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > SELECT model_number, color FROM thermostats WHERE color = 'beige' LIMIT 1;
+--------------+-------+
| model_number | color |
+--------------+-------+
| AB-90125-C1 | beige |
+--------------+-------+
1 row in set (0.00 sec)
Listing 3-25Removing Quotes for Generated Columns on JSON Strings
注意,在第一个SELECT
语句中,没有返回任何内容。这是因为虚拟生成的列使用了带引号的 JSON 字符串。当混合 SQL 和 JSON 数据时,这通常是混淆的来源。注意,在第二个SELECT
语句中,我们看到应该有几行被返回。还要注意,在我删除该列并用JSON_UNQUOTE()
函数再次添加它之后,SELECT
返回正确的数据。
我们通常使用虚拟生成的列,这样我们就不会在行中存储任何额外的东西。这部分是因为我们可能不经常使用 JSON 数据上的索引,可能不需要维护它,但更重要的是因为对如何使用/定义存储的生成列有限制。下面总结了这些限制。
- 该表必须定义一个主键。
- 您必须使用全文索引或 RTREE 索引(而不是默认的 BTREE)。
但是,如果您有很多行,或者经常使用 JSON 数据上的索引,或者 JSON 数据上有多个索引,那么您可能希望考虑使用存储生成的列,因为在频繁访问复杂或深度嵌套的数据时,虚拟生成的列可能会计算量很大。
Tip
有关虚拟列的更多信息,请参见在线 MySQL 参考手册( https://dev.mysql.com/doc/refman/8.0/en/
)中的“创建表和生成的列”或“更改表和生成的列”一节。
摘要
MySQL 增加了 JSON 数据类型,这为我们如何使用 MySQL 带来了一个范式转变。第一次,我们可以在关系数据(表)中存储半结构化数据。这不仅为我们提供了前所未有的灵活性,还意味着我们可以利用现代编程技术来访问我们应用中的数据,而无需付出巨大努力和增加复杂性。JSON 是一种众所周知的格式,在许多应用中广泛使用。
理解 JSON 数据类型是理解文档存储的关键。这是因为 JSON 数据类型虽然是为处理关系数据而设计的,但却形成了我们在文档存储中存储数据的模式——在 JSON 文档中!我们将在后面的章节中看到更多关于文档存储的内容。
在本章中,我们更详细地探讨了 JSON 数据类型。我们看到了如何通过 MySQL 中提供的大量内置 JSON 函数来处理关系表中的 JSON 数据的例子。JSON 数据类型是允许用户开发跨越 SQL 和 NoSQL 应用的混合解决方案的关键。
在第 4 章中,我将更详细地探索 MySQL Shell,包括介绍如何使用 MySQL Shell 开发您的应用。
Footnotes 1
旧的 MySQL 客户端可以使用- xml 命令行选项来完成这项工作。
参见 https://www.w3.org/RDF/Metalog/docs/sw-easy
.
但这当然会有帮助。
四、MySQL Shell
旧的 mysql 客户端(MySQL)最大的缺失之一是没有任何形式的脚本功能。但是,可以使用旧客户端处理一批 SQL 命令,并且客户端对编写存储例程(过程和函数)的支持有限。对于那些想要创建和使用脚本来管理他们的数据库(和服务器)的人来说,到目前为止已经有了外部工具选项,包括 MySQL Workbench 和 MySQL Utilities,但是没有专门用于合并多种脚本语言的工具。
MySQL Workbench 是 Oracle 的一款非常受欢迎的产品。MySQL Workbench 是一个 GUI 工具,设计为基于工作站的管理工具。它提供了许多功能,包括数据库设计和建模工具、SQL 开发、数据库管理、数据库迁移和 Python 脚本支持。有关 MySQL Workbench 的更多信息,请参见 http://dev.mysql.com/doc/workbench/en/
。
另一方面,MySQL Utilities 是一组 Python 工具,用于帮助维护和管理 MySQL 服务器,只需一个命令就可以完成许多步骤或复杂的脚本编写。有用于管理服务器、使用复制等的工具。对于那些想要编写自己的 Python 脚本的人,包含了一个 Python 类库。有关 MySQL 实用程序的更多信息,请参见 https://dev.mysql.com/doc/mysql-utilities/1.6/en/
。
Note
MySQL 实用程序目前仅限用于 MySQL 5.7。没有适用于 MySQL 8.0 或文档存储的版本。
除了这些产品之外,向 MySQL 客户端添加脚本语言的请求还没有得到回应。也就是说,直到现在。然而,Oracle 并没有重组现有的(并且相当长寿的)MySQL 客户端工具,而是发布了一个名为 MySQL Shell 的新客户端,它支持脚本语言、X DevAPI 以及 SQL 命令等等。但是新的 Shell 远不止这些。
在这一章中,我们将更详细地探讨 MySQL Shell。我们在第 3 章中看到了 shell 的运行,但在这一章中,我们将了解更多关于它的主要特性和选项,以及如何使用新的 shell 来交互式地执行脚本。正如您将看到的,MySQL Shell 是 MySQL 未来的另一个关键元素。
我建议在自己尝试 MySQL Shell 之前,至少通读一遍本章中的示例部分。所提供的信息将帮助您适应使用新的命令和连接,在您理解这些概念之前,这些命令和连接有时会有点混乱。
Note
我使用术语 shell 来指代 MySQL Shell 支持的特性或对象。我用 MySQL Shell 来指代产品本身。
入门指南
MySQL Shell 是 MySQL 产品组合中令人激动的新成员。MySQL Shell 代表了第一个连接到 MySQL 并与之交互的现代高级客户端。shell 可以用作脚本环境,用于开发处理数据的新工具和应用。尽管它支持 SQL 模式,但它的主要目的是允许用 JavaScript 和 Python 语言访问数据。没错;您可以编写 Python 脚本,并在 shell 中以交互方式或批处理方式执行它们。酷!
回想一下第 1 章,MySQL Shell 被设计成使用新的 X 协议通过 X 插件与服务器通信。然而,shell 也可以使用旧的协议连接到服务器,尽管在脚本模式下功能有限。这意味着,shell 允许您使用关系型(SQL)和/或 JSON 文档(NoSQL)。
SQL 模式的加入为学习如何使用脚本管理数据提供了一个很好的跳板。也就是说,您可以继续使用您的 SQL 命令(或批处理),直到您将它们转换成 JavaScript 或 Python。此外,您可以使用这两者来确保您的迁移是完整的。图 4-1 展示了一个启动 MySQL Shell 的例子。请注意显示 MySQL 徽标、连接信息和模式的漂亮提示符。很好!
图 4-1
The MySQL Shell
以下各节从较高的层面介绍了 shell 的主要特性。我们不会探究每个特性或选项的每个细节,相反,本章提供了一个广泛的概述,以便您可以快速入门,更重要的是,了解关于 shell 的足够信息,以便您可以按照本书中的示例进行操作。
有关 MySQL Shell 的更多信息,请参见在线 MySQL 参考手册中标题为“MySQL Shell 用户指南”的部分。
特征
MySQL Shell 有许多特性,包括支持传统的 SQL 命令处理、脚本原型,甚至支持定制 Shell。下面我列出了 shell 的一些主要特性。大多数功能都可以通过命令行选项或特殊的 shell 命令来控制。在后面的章节中,我将深入探讨一些更重要的特性。
- 日志记录:您可以创建一个会话日志,供以后分析或保存消息记录。您可以使用
--log-level
选项设置详细程度,范围从 1(无记录)到 8(最大调试)。 - 输出格式:shell 支持三种格式选项:table (
--table
),这是您从旧客户端习惯的传统网格格式;选项卡式,使用制表符分隔显示信息,用于批处理执行;以及 JSON (--json
),它以更易于阅读的方式格式化 JSON 文档。这些是您在启动 shell 时指定的命令行选项。 - 交互式代码执行:使用 shell 的默认模式是交互式模式,它像传统的客户机一样工作,在这里输入命令并获得响应。
- 批处理代码执行:如果您想在没有交互式会话的情况下运行脚本,您可以使用 shell 以批处理模式运行脚本。但是,输出仅限于非格式化输出(但可以用
--interactive
选项覆盖)。 - 脚本语言:shell 支持 JavaScript 和 Python,尽管您一次只能使用一种。
- 会话:会话本质上是到服务器的连接。shell 允许您存储和删除会话。我们将在后面的章节中看到更多关于会话的内容。
- 启动脚本:您可以定义一个在 shell 启动时执行的脚本。您可以用 JavaScript 或 Python 编写脚本。
- 命令历史和命令完成:shell 保存您输入的命令,允许您使用上下箭头键调用它们。shell 还为已知的关键字、API 函数和 SQL 关键字提供代码补全。
- 全局变量:shell 提供了一些在交互模式下可以访问的全局变量。其中包括以下内容:
session
:全局会话对象(如果已建立)db
:通过连接建立的模式dba
:用于使用 InnoDB 集群的 AdminAPI 对象shell
:使用 Shell 的通用功能util
:与服务器一起工作的实用功能
- 定制提示:您还可以通过使用特殊格式更新名为
~/.mysqlsh/prompt.json
的配置文件或者定义名为MYSQLSH_PROMPT_THEME
的环境变量来更改默认提示。请参阅 MySQL Shell 参考手册,了解有关更改提示符的更多详细信息。 - 自动完成:从 8.0.4 开始,shell 允许用户在 SQL 模式下按 TAB 键自动完成关键字,在 JavaScript 和 Python 模式下自动完成主要的类和方法。
Shell 命令
与最初的 MySQL 客户端一样,有一些特殊的命令控制应用本身,而不是与数据交互(通过 SQL 或 X DevAPI)。要执行 shell 命令,请发出带斜杠()的命令。例如,\help
打印所有 shell 命令的帮助。表 4-1 列出了一些更常用的 shell 命令。
表 4-1
Shell Commands
| 命令 | 捷径 | 描述 | | :-- | :-- | :-- | | `\` | | 开始多行输入(仅限 SQL 模式) | | `\connect` | (`\c`) | 连接到服务器 | | `\help` | (`\?`,`\h`) | 打印帮助文本 | | `\js` | | 切换到 JavaScript 模式 | | `\nowarnings` | (`\w`) | 不显示警告 | | `\py` | | 切换到 Python 模式 | | `\quit` | (`\q`,`\exit`) | 放弃 | | `\source` | (`\.`) | 执行指定的脚本文件 | | `\sql` | | 切换到 SQL 模式 | | `\status` | (`\s`) | 打印有关连接的信息 | | `\use` | (`\u`) | 设置会话的模式 | | `\warnings` | (`\W`) | 在每个语句后显示警告 |注意,您可以使用\sql
、\js
和\py
shell 命令来动态切换模式。这使得处理 SQL 和 NoSQL 数据更加容易,因为您不必退出应用来切换模式。此外,即使使用了启动选项来设置模式,也可以使用这些 shell 命令。
Tip
要获得任何 shell 命令的帮助,请使用\help
命令。例如,要了解更多关于\connect
命令的信息,请输入\help connect
。
最后,注意您退出 shell 的方式(\q
或\quit
)。如果您像以前一样在旧客户端中键入 quit,shell 将根据您所处的模式做出不同的响应。以下是每种模式下发生的情况的示例。
MySQL SQL > quit;
ERROR: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'quit' at line 1
MySQL SQL > \js
Switching to JavaScript mode...
MySQL JS > quit
ReferenceError: quit is not defined
MySQL JS > \py
Switching to Python mode...
MySQL Py > quit
Use quit() or Ctrl-D (i.e. EOF) to exit
MySQL Py > \q
Bye!
如果您习惯了旧的 MySQL 客户端,并且不小心使用了旧的客户端命令,您可能会看到类似的奇怪现象,但是只需要经常使用它就可以提醒您要使用的正确命令。现在,让我们看看 shell 的启动选项。
Note
与需要服务器连接才能启动的旧客户端不同,当您在没有指定服务器连接的情况下启动 shell 时,shell 将会运行,但它没有连接到服务器。您必须使用\connect
shell 命令来连接到服务器。
选择
可以使用几个控制模式、连接、行为等的启动选项来启动 shell。本节介绍一些您可能想要使用的更常用的选项。我们将在后面的章节中看到更多关于连接选项的内容。表 4-2 显示了常见 Shell 选项的列表。
表 4-2
Common MySQL Shell Options
| [计]选项 | 描述 | | :-- | :-- | | `-f, --file=file` | 处理要执行的文件 | | `-e, --execute=请注意,有些选项的别名与原始客户端的用途相同。如果您有启动客户机来执行操作的脚本,这使得切换到 shell 变得更容易一些。请注意,还有一组使用安全套接字层(SSL)连接的选项。
其中大部分都是不言自明的,我们以前也见过一些。现在让我们看看可用的会话和连接以及如何使用它们。
要获得选项的完整列表,请使用- help 选项执行 shell,如下所示。
$ mysqlsh --help
MySQL Shell 8.0.11
Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners.
Usage: mysqlsh [OPTIONS] [URI]
mysqlsh [OPTIONS] [URI] -f <path> [script args...]
mysqlsh [OPTIONS] [URI] --dba [command]
mysqlsh [OPTIONS] [URI] --cluster
-?, --help Display this help and exit.
-e, --execute=<cmd> Execute command and quit.
-f, --file=file Process file.
--uri=value Connect to Uniform Resource Identifier. Format:
[user[:pass]@]host[:port][/db]
-h, --host=name Connect to host.
-P, --port=# Port number to use for connection.
-S, --socket=sock Socket name to use in UNIX, pipe name to use in
Windows (only classic sessions).
-u, --dbuser=name User for the connection to the server.
--user=name see above
-p, --password[=name] Password to use when connecting to server.
--dbpassword[=name] see above
-p Request password prompt to set the password
-D, --schema=name Schema to use.
--database=name see above
--recreate-schema Drop and recreate the specified schema.Schema
will be deleted if it exists!
-mx, --mysqlx Uses connection data to create Creating an X
protocol session.
-mc, --mysql Uses connection data to create a Classic Session.
-ma Uses the connection data to create the session
withautomatic protocol detection.
...
会话和模式
与最初的客户机以及实际上大多数 MySQL 客户机应用一样,您需要连接到 MySQL 服务器,以便可以运行命令。MySQL Shell 支持多种连接 MySQL 服务器的方式和多种与服务器交互的选项(称为会话)。在会话中,您可以更改 shell 接受命令的方式(称为模式),以包括 SQL、JavaScript 或 Python 命令。
考虑到使用服务器的所有不同的新概念,那些初学使用 shell 的人可能会发现其中的细微差别,甚至有时会感到困惑。事实上,在线 MySQL Shell 参考手册和各种博客及其他报告有时会互换使用模式和会话,但正如您将看到的,它们是不同的(无论多么微妙)。下面几节阐明了每个主要概念,包括会话、模式和连接,以便您可以更快地适应新方法。我首先用一些简单的例子介绍概念,然后用例子详细讨论如何建立连接。让我们从查看可用的会话对象开始。
会话对象
关于会话,首先要理解的是,会话是到单个服务器的连接。第二件要理解的事情是,每个会话可以使用两个会话对象中的一个来启动,这两个会话对象公开了一个特定的对象,用于使用特定的通信协议与 MySQL 服务器一起工作。也就是说,会话是到服务器的连接(定义了所有参数),会话对象是 shell 用来以几种方式之一与服务器进行交互的对象。更具体地说,MySQL Shell 会话对象简单地定义了如何与服务器交互,包括支持什么模式,甚至 Shell 如何与服务器通信。shell 支持如下两个会话对象:
- 会话:X 协议会话用于应用开发,支持 JavaScript、Python 和 SQL 模式。通常用于开发脚本或执行脚本。要使用该选项启动 shell,请使用
--mx
(--mysqlx
)选项。 - 经典会话:使用旧的服务器通信协议,对 DevAPI 的支持非常有限。对没有 X 插件或不支持 X 协议的旧服务器使用这种模式。通常用于旧服务器的 SQL 模式。要使用该选项启动 shell,请使用
--mc
(--mysqlc
)选项。
Note
经典会话仅在 MySQL Shell 中可用。它不是 X DevAPI 的一部分。通过 X DevAPI,只有通过 X 协议的会话连接是可用的。
当您使用\connect
shell 命令时,您可以通过指定-mc
用于经典会话、-mx
用于 X 协议会话或-ma
用于自动协议选择来指定要使用的会话对象(协议)。下面依次展示了其中的每一个。注意
\connect -mx <URI>
:使用 X 协议(会话)\connect -mc <URI>
:使用经典协议(经典会话)\connect -ma <URI>
:使用自动协议选择
召回会话大致等同于连接。但是,会话不仅仅是一个连接,因为它包含了用于建立连接的所有设置(包括会话对象)以及服务器使用的通信协议。因此,我们有时会遇到描述会话的术语“协议”。我们将在后面的章节中看到更多使用会话的例子。
Wait, What Session Was That???
您可能会看到使用多个名称描述的会话。特别地,正常的、默认的会话被称为会话,X 协议会话,或者更少见的,X 会话。这些是指通过 X 协议与 MySQL 通信的会话对象(连接)。较旧的服务器通信协议在称为经典会话的会话中受支持,经典,或更罕见地,旧协议。这些是指通过旧协议与 MySQL 服务器通信的会话对象(连接)。可悲的是,这些多重名字会使阅读不同的文本成为一种挑战。每当使用这些替代术语时,您应该努力阅读会话和经典会话。
有关以编程方式使用会话的更多信息,请参见在线 MySQL Shell 参考手册。
支持的模式
shell 支持三种模式(也称为语言支持或简称为活动语言);SQL、JavaScript 和 Python。回想一下,我们可以通过使用 shell 命令来启动这些模式中的任何一种。你可以随时切换模式(语言),每次都不会断线。下面列出了三种模式以及如何切换到每种模式。
\sql
:切换到 SQL 语言\js
:切换到 JavaScript 语言(默认模式)\py
:切换到 Python 语言
现在我们已经了解了会话、会话对象、模式,我们可以看看如何连接 MySQL 服务器。
连接
在 shell 中建立连接可能需要一些时间来适应与最初的 MySQL 客户端不同的做法。 1 你可以使用一个特殊格式的 URI 字符串或者通过名字使用单独的选项连接到一个服务器(像旧的客户端)。也支持 SSL 连接。可以通过启动选项、shell 命令和脚本来建立连接。但是,所有连接都需要使用密码。因此,除非您另外声明,否则如果没有给出密码,shell 将提示您输入密码。
Note
如果您想使用没有密码的连接(不推荐),您必须使用--password
选项,或者,如果使用 URI,包括一个额外的冒号来代替密码。
下面不是讨论所有可用的连接方式和选项,而是在下面的部分中给出每种连接方式的一个示例。
使用 URI
在 MySQL Shell 连接的情况下,URI 是使用以下格式编码的特殊字符串:<dbuser>[:<dbpassword>]@host[:port][/schema/]
其中< >表示各种参数的字符串值。请注意,密码、端口和模式是可选的,但用户和主机是必需的。在这种情况下,Schema 是连接时要使用的默认模式(数据库)。
Note
X 协议的默认端口是 33060。
要在启动 shell 时使用命令行上的 URI 连接到服务器,请使用如下的--uri
选项指定它。
$ mysqlsh --uri root:secret@localhost:33060
shell 假定所有连接都需要密码,如果没有提供密码,它将提示输入密码。 2 清单 4-1 显示了先前在没有密码的情况下进行的相同连接。注意 shell 是如何提示输入密码的。
Tip
world_x 数据库是一个示例数据库,您可以从 https://dev.mysql.com/doc/index-other.html
下载。
$ mysqlsh --uri root@localhost:33060/world_x
Creating a session to 'root@localhost:33060/world_x'
Enter password:
Fetching schema names for autocompletion... Press ^C to stop.
Your MySQL connection id is 13 (X protocol)
Server version: 8.0.11 MySQL Community Server (GPL)
Default schema `world_x` accessible through db.
MySQL Shell 8.0.11
Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type '\help' or '\?' for help; '\quit' to exit.
MySQL localhost:33060+ world_x JS >
Listing 4-1Connecting with a URI
注意,我还在 URI 中用/schema
选项指定了默认模式(world_x
)。
使用单个选项
您还可以使用单独的选项在 shell 命令行上指定连接。可用的连接选项如表 4-1 所示。为了向后兼容(并使向 MySQL Shell 的过渡更容易),Shell 还支持用--user
代替--dbuser
,用--password
代替--dbpassword
,用--database
代替--schema
。清单 4-2 展示了如何使用单独的选项连接到 MySQL 服务器。
$ mysqlsh --dbuser root --host localhost --port 33060 --schema world_x --py -mx
Creating an X protocol session to 'root@localhost:33060/world_x'
Enter password:
Fetching schema names for autocompletion... Press ^C to stop.
Your MySQL connection id is 14 (X protocol)
Server version: 8.0.11 MySQL Community Server (GPL)
Default schema `world_x` accessible through db.
MySQL Shell 8.0.11
Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type '\help' or '\?' for help; '\quit' to exit.
MySQL localhost:33060+ world_x Py >
Listing 4-2Connecting Using Individual Options
注意,我用--py
选项将模式(语言)改为 Python。
在脚本中使用连接
如果您计划使用 shell 来创建脚本或者仅仅作为一个原型工具,那么您也将希望在脚本中使用会话。在这种情况下,我们将创建一个变量来包含获取的会话。以这种方式创建的会话称为全局会话,因为一旦创建,它就可用于任何模式。
然而,根据我们使用的会话对象(回想一下这是经典或 X 协议),我们将使用不同的方法创建一个 X 或经典会话。我们对 X 协议会话对象使用getSession()
方法,对经典会话对象使用getClassicSession()
方法。
Tip
如果你想知道更多关于 MySQL Shell 的内部信息,包括更多关于mysql
和mysqlx
模块的信息,请看 http://dev.mysql.com/doc/dev/mysqlsh-devapi/
。
下面演示了在 JavaScript 中获取 X 协议会话对象。注意,我在 URI 中将密码指定为方法参数。
MySQL JS > var js_session = mysqlx.getSession('root@localhost:33060', 'secret')
MySQL JS > print(js_session)
<Session:root@localhost:33060>
下面演示了如何在 JavaScript 中获取经典会话对象。
MySQL JS > var js_session = mysql.getClassicSession('root@localhost:3306', 'secret')
MySQL JS > print(js_session)
<ClassicSession:root@localhost:3306>
What Happened To Port 3306?
如果您一直关注本节中的示例,您可能已经注意到我们使用的端口是 33060。这不是印刷错误。默认情况下,X 插件监听端口 33060,而不是服务器原来默认的端口 3306。事实上,端口 3306 仍然是旧协议的默认端口,您可以使用端口 3306 连接到服务器,但是您必须使用经典会话(mysqlsh-classic-ur oot-hlocalhost-port = 3306)。虽然这表明您可以使用旧的协议连接到服务器,但是回想一下,它确实限制了您可以做的事情,因为 DevAPI 在经典会话对象中并不完全受支持。
使用 SSL 连接
您还可以创建 SSL 连接,以便安全地连接到您的服务器。要使用 SSL,您必须将服务器配置为使用 SSL。要在运行 MySQL 的同一台机器上使用 SSL,可以使用--ssl-mode=REQUIRED
选项。您也可以指定 SSL 选项,如表 4-1 所示。您可以使用命令行选项在命令行上指定它们,或者将其作为\connect
shell 命令的扩展。下面显示了如何使用 SSL 和命令行选项连接到服务器。
$ mysqlsh -uroot -h127.0.0.1 --port=33060 --ssl-mode=REQUIRED
Tip
有关加密连接的更多详细信息,请参见在线 MySQL Shell 参考手册中的“使用加密连接”一节。
现在我们知道了如何连接到我们的服务器,让我们回顾一下如何设置和安装 shell,更重要的是,确保 X 插件设置正确。
设置和安装
回想一下第 2 章,我们需要将 MySQL Shell 作为独立于服务器的产品进行安装。我们还必须在服务器中启用 X 插件。以下部分演示了安装 MySQL Shell 所需的步骤,以及如何配置 X 插件以供使用。虽然我们在第二章中看到了一个关于如何安装 X 插件的简短例子,但是这一节将会更详细地介绍如何使用 MySQL Shell 自动安装 X 插件。
Caution
如果您正在安装 MySQL Shell 8 . 0 . 4 版或更高版本,以便与 MySQL Server 8 . 0 . 4 版或更高版本一起使用,您将使用新的caching_sha2_password
身份验证插件来使用 SSL 连接。默认情况下,这通常在安装过程中完成,但是如果您安装的服务器没有自动安装,或者您使用的是旧版本的服务器,则可能需要将服务器配置为使用 SSL 连接。更多信息请参见在线 MySQL 参考手册,或者更多关于认证默认值变更的信息,请阅读 https://mysqlserverteam.com/mysql-8-0-4-new-default-authentication-plugin-caching_sha2_password
的工程博客。
安装 MySQL Shell
安装 MySQL Shell 遵循与安装 MySQL 服务器相同的模式。也就是说,您可以简单地下载适用于您的平台的安装程序,并通过单击对话框面板来安装它。然而,有一个例外。在撰写本文时,MySQL Shell 的最新版本不是 MySQL Windows Installer 的一部分。
你可以在 http://dev.mysql.com/downloads/shell/
上找到安装包。只需为您的平台选择最新的版本和包(在本例中是 macOS)并安装 shell。
当您启动installer (.pkg or .dmg)
时,会出现一个欢迎对话框,其中包含您要安装的产品的名称和版本。图 4-2 显示了 MySQL Shell 安装程序的欢迎面板。
图 4-2
Installer welcome panel
请注意,在图 4-2 中,我正在安装 MySQL Shell 的发布候选版本,即版本 8.0.11。您应该安装适用于您的平台的最新版本的 shell,以确保您拥有最新的特性。
准备好后,点按“继续”。然后,您将看到如图 4-3 所示的最终用户许可协议。
图 4-3
License panel
阅读完许可证后, 3 点击继续。您将被要求接受如图 4-4 所示的许可。单击同意继续。
图 4-4
Destination folder panel
一旦您接受了许可,并同意安装在默认位置(对于 macOS 来说,这总是一个好主意),请点按“继续”。将要求您批准安装,如图 4-5 所示。准备好开始安装时,单击安装。
Tip
在 Windows 上安装时,Windows 可能会要求您批准升级安装。
图 4-5
Installation panel
这将开始将文件以及系统上的设置复制到目标位置,以确保您可以正确启动应用。根据您系统的速度,最多只需要 2 到 3 分钟就可以完成。
一旦安装完成,您将看到一个如图 4-6 所示的完成对话框。准备就绪后,单击关闭以完成安装。如果您选择启动 shell,您将看到一个新的命令窗口打开,shell 将启动。
回想一下,您可以在不指定服务器的情况下启动 shell,shell 将会运行,但它不会连接到任何 MySQL 服务器。如果您没有在命令行上指定服务器连接(URI 或单个选项),您必须使用\connect
shell 命令来连接到服务器。
图 4-6
Installation complete
现在 MySQL Shell 已经安装好了,我们需要配置 X 插件。
安装 X 插件
如果您在系统上安装了 MySQL 8.0.11 或更高版本,那么您已经安装并启用了 X 插件。然而,默认情况下,一些较旧的安装不会设置或启用 X 插件。因此,您可能需要启用插件来使用 shell 连接到您的服务器。尽管您仍然可以使用 shell 通过经典会话对象进行连接,但是在启用 X 插件之前,您将无法使用 X 协议会话对象。
此外,如果您使用 Windows Installer 在 Windows 上安装了服务器,则可以在安装过程中通过选中启用 X 协议/MySQL 作为文档存储复选框来启用 X 插件。如果你没有这样做或者安装在不同的平台上,至少有两种方法可以启用 X 插件;您可以使用新的 MySQL Shell,也可以使用旧的客户端。下面演示了每个选项。
Tip
如果在新安装的 MySQL 上连接到 MySQL 服务器有问题,请确保启用 X 插件,如本节所示。
使用 MySQL Shell 启用 X 插件
要使用 MySQL Shell 启用 X 插件,使用用户和主机的单独选项启动一个经典会话,并指定如下所示的--mysql
和--dba enableXProtocol
选项。我们使用经典的会话对象,因为我们还没有启用 X 协议。
$ mysqlsh -uroot -hlocalhost --mysql --dba enableXProtocol
Creating a Classic session to 'root@localhost'
Enter password:
Fetching schema names for autocompletion... Press ^C to stop.
Your MySQL connection id is 285
Server version: 8.0.11 MySQL Community Server (GPL)
No default schema selected; type \use <schema> to set one.
enableXProtocol: Installing plugin mysqlx...
enableXProtocol: done
使用 MySQL 客户端启用 X 插件
要使用旧的 MySQL 客户端启用 X 插件,您必须连接到服务器并手动安装插件。也就是没有新的魔法命令选项为你开启。这包括使用清单 4-3 中所示的INSTALL PLUGIN
SQL 命令。
$ mysql -uroot -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 343
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 of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> INSTALL PLUGIN mysqlx SONAME 'mysqlx.so';
Query OK, 0 rows affected (0.00 sec)
mysql> SHOW PLUGINS \G
*************************** 1\. row ***************************
Name: keyring_file
Status: ACTIVE
Type: KEYRING
Library: keyring_file.so
License: GPL
...
*************************** 43\. row ***************************
Name: mysqlx
Status: ACTIVE
Type: DAEMON
Library: mysqlx.so
License: GPL
43 rows in set (0.00 sec)
Listing 4-3Enabling the X Plugin Using the MySQL Client
注意,我使用了SHOW PLUGINS
SQL 命令来列出该命令前后安装的插件。为了清楚起见,我省略了一些冗长的输出。
Tip
您可以使用经典会话对象在 shell 中执行这些操作。对于习惯使用旧客户机的读者,我展示了使用旧客户机的命令。
有趣的是,您也可以使用如下的UNINSTALL PLUGIN
SQL 命令卸载插件。如果您需要使用 X 协议来诊断连接,或者想要使用 MySQL Shell 仅使用经典会话对象来测试脚本,这可能会很有帮助。
mysql> UNINSTALL PLUGIN mysqlx;
Query OK, 0 rows affected (0.80 sec)
现在,让我们通过在三种模式(SQL、JavaScript 和 Python)下执行一个简单任务的演示来看看 MySQL Shell 的运行情况。
教程:MySQL Shell 示例
下面几节演示了如何在这三种模式下使用 MySQL shell。这个例子是在world_x
数据库中插入新数据。将简要概述通过 shell 内置的 X DevAPI 对象,以及如何开始安装示例数据库。
本教程旨在提供一个完整的示例,展示如何使用 MySQL Shell 在所有支持的模式(语言)下解决一个任务。因此,我们将看到使用 SQL、JavaScript 和 Python 命令执行相同的任务。
任务是在数据库中插入新数据,然后进行搜索以检索满足包含新数据的条件的行。我使用关系表来说明这些概念,因为这对我们这些熟悉“普通”数据库操作的人来说更容易。然而,我们将在后面的章节中看到如何在文档存储中处理纯文档(集合)。
每个讲座都以一个示例开始,介绍如何连接到服务器,了解服务器支持什么(存在什么数据库),如何插入新数据,以及如何查询数据。正如您将看到的,有些命令非常不同,但它们都产生相同的结果。虽然所示的 SQL 命令对大多数读者来说都很熟悉,但是我在这里包含它们是为了说明如何将这些命令与您选择的脚本语言等同起来。
Note
回想一下从第 3 章开始,在 shell 中开始写脚本并不是需要成为一个 JavaScript 高手甚至是一个 python ista4T5。事实上,你需要做的大部分事情都可以通过本书和在线 MySQL Shell 参考手册中的例子找到。
我们将看到 JavaScript 和 Python 的操作与关系表上的 CRUD 操作一起工作。因此,我们不使用集合;相反,我们使用一个包含 JSON 数据类型列的关系表。我们将看到插入数据(创建)、选择数据(读取)、更新数据(更新)和删除数据(删除)的示例。
在开始我们的旅程之前,让我们花点时间安装我们将需要的示例数据库,Oracle 的world_x
示例 MySQL 数据库。
安装示例数据库
Oracle 提供了几个示例数据库,供您在测试和开发应用时使用。样本数据库可以从 http://dev.mysql.com/doc/index-other.html
下载。我们想要使用的示例数据库被命名为world_x
,以表明它包含 JSON 文档,并打算用 X DevAPI、shell 等进行测试。继续前进,导航到该页面并下载数据库。
示例数据库包含几个关系表(country
、city
和countrylanguage
)以及一个集合(countryinfo
)。在本章中我们将只使用关系表,但是在后面的章节中将会看到更多使用集合的例子。
下载完文件后,解压缩并记下文件的位置。我们进口的时候你会需要的。接下来,启动 MySQL Shell 并连接到您的服务器。使用\sql
shell 命令切换到 SQL 模式,然后使用\source
shell 命令读取world_x.sql
文件并处理其所有语句。
清单 4-4 显示了您应该看到的命令和响应的摘录。我在输出中突出显示了命令和一行,以表明这个 world 数据库确实允许在一个表中存储 JSON 文档。
MySQL JS > \connect root@localhost:33060
Creating a session to 'root@localhost:33060'
Enter password:
Your MySQL connection id is 9 (X protocol)
Server version: 8.0.11 MySQL Community Server (GPL)
No default schema selected; type \use <schema> to set one.
MySQL localhost:33060+ ssl JS > \sql
Switching to SQL mode... Commands end with ;
MySQL localhost:33060+ ssl SQL > \source /Users/cbell/Downloads/world_x-db/world_x.sql
...
Query OK, 0 rows affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > SHOW DATABASES;
+--------------------+
| Database |
+--------------------+
| animals |
| contact_list1 |
| contact_list2 |
| contact_list3 |
| greenhouse |
| information_schema |
| library_v1 |
| library_v2 |
| library_v3 |
| mysql |
| performance_schema |
| rolodex |
| sys |
| test |
| world_x |
+--------------------+
15 rows in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > USE world_x;
Query OK, 0 rows affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > SHOW TABLES;
+-------------------+
| Tables_in_world_x |
+-------------------+
| city |
| country |
| countryinfo |
| countrylanguage |
+-------------------+
4 rows in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > EXPLAIN city;
+-------------+----------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+----------+------+-----+---------+----------------+
| ID | int(11) | NO | PRI | NULL | auto_increment |
| Name | char(35) | NO | | | |
| CountryCode | char(3) | NO | | | |
| District | char(20) | NO | | | |
| Info | json | YES | | NULL | |
+-------------+----------+------+-----+---------+----------------+
5 rows in set (0.00 sec)
Listing 4-4Installing the world_x Database in SQL Mode
注意,\source
shell 命令是一种加载文件并批量执行命令的方式。这是一种非常流行的重放常用命令序列的方法,它也适用于 JavaScript 和 Python 命令。
Tip
如果文件的路径中有空格,应该用双引号将路径括起来。
还可以在命令行上使用- recreate-schema 选项安装示例数据库,如下所示。请注意,如果数据库已经存在,这将删除并重新创建数据库。这是批处理运行 SQL 命令的另一个例子。
$ mysqlsh -uroot -hlocalhost --sql --recreate-schema --schema=world_x < ~/Downloads/world_x-db/world_x.sql
Enter password:
Recreating schema world_x...
当然,您可以使用 similar source 命令在旧客户机上安装 sample 数据库,但是这有什么意思呢?
现在,让我们看看 SQL 模式下的示例任务。
结构化查询语言
我们要做的任务是在 city 表中插入两行,在每一行中添加一个 JSON 文档,然后从表中读取数据,只读取那些有额外数据的行。更具体地说,我们将向表中添加一个名胜古迹列表,以便我们稍后可以询问哪些城市有名胜古迹。你可以把它当作一种方式,来添加你自己对那些你觉得有趣并会推荐给他人的城市中你去过的地方的评论。
因为本练习是一个示例,所以我们还将看到如何删除我们添加的数据,以便将数据库恢复到其原始状态。如果您计划按照这些示例进行操作,以便完成一个示例不会影响下一个示例的尝试,那么这样做也是有帮助的。
让我们首先列出服务器上的数据库,然后列出world_x
数据库中的表。清单 4-5 显示了完成这些步骤的熟悉的 SQL 命令的副本。为了简洁起见,我省略了一些消息。请注意,我使用命令选项在 SQL 模式下启动了 shell。
$ mysqlsh -uroot -hlocalhost --sql
Creating a session to 'root@localhost'
Enter password:
Your MySQL connection id is 13 (X protocol)
Server version: 8.0.11 MySQL Community Server (GPL)
No default schema selected; type \use <schema> to set one.
MySQL Shell 8.0.11
Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type '\help' or '\?' for help; '\quit' to exit.
MySQL localhost:33060+ ssl SQL > SHOW DATABASES;
+--------------------+
| Database |
+--------------------+
| animals |
| contact_list1 |
| contact_list2 |
| contact_list3 |
| greenhouse |
| information_schema |
| library_v1 |
| library_v2 |
| library_v3 |
| mysql |
| performance_schema |
| rolodex |
| sys |
| test |
| world_x |
+--------------------+
15 rows in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > USE world_x;
Query OK, 0 rows affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > SHOW TABLES;
+-------------------+
| Tables_in_world_x |
+-------------------+
| city |
| country |
| countryinfo |
| countrylanguage |
+-------------------+
4 rows in set (0.00 sec) ... ;
Query OK, 0 rows affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > SHOW TABLES;
+-------------------+
| Tables_in_rolodex |
+-------------------+
| contacts |
+-------------------+
1 row in set (0.00 sec)
Listing 4-5Listing and Using Databases—SQL Mode
接下来,我们插入一些数据。我们将在表中插入两行;我最近去过的每个城市都有一个(北卡罗来纳州的夏洛特和佛罗里达州的代托纳)。在这一步中,我们将使用INSERT
SQL 命令来插入数据。回想一下前面的内容,我们需要仔细格式化 JSON 文档,这样我们就不会遇到错误。特别是,我们希望添加包括姓名、国家代码和地区的结构化数据,但是我们还希望添加一个 JSON 文档,其中包含人口和名胜古迹的列表(数组)。回想一下第 1 章,我们可以在INSERT
语句中通过内联创建 JSON 文档来做到这一点。下面显示了我们用来插入行的每个命令。
INSERT INTO world_x.city (Name, CountryCode, District, Info) VALUES ('Charlotte', 'USA', 'North Carolina', '{"Population": 792862, "Places_of_interest": [{"name": "NASCAR Hall of Fame"}, {"name": "Charlotte Motor Speedway"}]}');
INSERT INTO world_x.city (Name, CountryCode, District, Info) VALUES ('Daytona', 'USA', 'Florida', '{"Population": 590280, "Places_of_interest": [{"name": "Daytona Beach"}, {"name": "Motorsports Hall of Fame of America"}, {"name": "Daytona Motor Speedway"}]}');
Caution
不要在 JSON 文档的键名中使用空格。SQL 函数无法正确识别包含空格的键。
尽管这看起来有点混乱(确实如此),但是如果您仔细阅读这些语句,您会看到 JSON 文档被编码为一个字符串。例如,第一次插入的 JSON 文档的格式良好的版本如下所示。很明显,这更容易阅读。您可以使用这样的格式输入语句,但是显示的结果没有额外的格式。
注意,我们保留了表中其他行的 population 键(选择一些并查看),我们还添加了一个名为Places_of_interest
的数组来列出我们可能想去的地方。
{
"Population": 792862,
"Places_of_interest": [
{
"name": "NASCAR Hall of Fame"
},
{
"name": "Charlotte Motor Speedway"
}
]
}
Note
为了简洁起见,我从示例中截断了表格格式行(虚线)。
现在,让我们看看如果使用一个SELECT
SQL 语句,数据会是什么样子。在这种情况下,我们将只按城市名选择两行,因为它们在表中是唯一的。以下是结果摘录。
MySQL localhost:33060+ ssl SQL > SELECT * FROM city WHERE Name in ('Charlotte', 'Daytona') \G
*************************** 1\. row ***************************
ID: 3818
Name: Charlotte
CountryCode: USA
District: North Carolina
Info: {"Population": 540828}
*************************** 2\. row ***************************
ID: 4080
Name: Charlotte
CountryCode: USA
District: North Carolina
Info: {"Population": 792862, "Places_of_interest": [{"name": "NASCAR Hall of Fame"}, {"name": "Charlotte Motor Speedway"}]}
*************************** 3\. row ***************************
ID: 4081
Name: Daytona
CountryCode: USA
District: Florida
Info: {"Population": 590280, "Places_of_interest": [{"name": "Daytona Beach"}, {"name": "Motorsports Hall of Fame of America"}, {"name": "Daytona Motor Speedway"}]}
很有意思,但是没有回答我们想问的问题。也就是哪些城市有名胜古迹?为此,我们需要使用许多为 JSON 数据类型设计的特殊函数。所有函数都以名称JSON_*
开始。让我们依次来看看这三种方法,首先是如何在 JSON 文档中搜索具有特定键的行。在这种情况下,我们选择有兴趣地点的行的所有数据。
为了确定 JSON 文档是否有特定的键,我们使用了JSON_CONTAINS_PATH()
函数。回忆路径只是对文档中的键的解析。在这种情况下,我们想知道 JSON 文档是否包含Places_of_interest
的路径。因为函数在没有匹配时返回 0,在至少有一个匹配时返回 1,所以我们检查它是否等于 1。你可以省略等式,但是在试验新的特性和命令时最好是学究式的。我们还使用‘all’
选项告诉函数返回所有的匹配(值),而‘one’
只返回第一个匹配。你也可以使用稍微正确一点的IS NOT NULL
比较。
MySQL localhost:33060+ ssl SQL > SELECT * FROM city WHERE JSON_CONTAINS_PATH(info, 'all', '$.Places_of_interest') = 1 \G
*************************** 1\. row ***************************
ID: 4080
Name: Charlotte
CountryCode: USA
District: North Carolina
Info: {"Population": 792862, "Places_of_interest": [{"name": "NASCAR Hall of Fame"}, {"name": "Charlotte Motor Speedway"}]}
*************************** 2\. row ***************************
ID: 4081
Name: Daytona
CountryCode: USA
District: Florida
Info: {"Population": 590280, "Places_of_interest": [{"name": "Daytona Beach"}, {"name": "Motorsports Hall of Fame of America"}, {"name": "Daytona Motor Speedway"}]}
2 rows in set (0.00 sec)
现在,假设我们只想查看那些感兴趣的地方,而不是整个 JSON 文档。在这种情况下,我们需要使用JSON_EXTRACT()
函数从文档中提取值。特别是,我们希望在info
列中搜索数组Places_of_interest
中的所有值。尽管这看起来很复杂,但正如你在下面看到的,这并不太糟糕。
MySQL localhost:33060+ ssl SQL > SELECT Name, District, JSON_EXTRACT(info, '$.Places_of_interest') as Sights FROM city WHERE JSON_EXTRACT(info, '$.Places_of_interest') IS NOT NULL \G
*************************** 1\. row ***************************
Name: Charlotte
District: North Carolina
Sights: [{"name": "NASCAR Hall of Fame"}, {"name": "Charlotte Motor Speedway"}]
*************************** 2\. row ***************************
Name: Daytona
District: Florida
Sights: [{"name": "Daytona Beach"}, {"name": "Motorsports Hall of Fame of America"}, {"name": "Daytona Motor Speedway"}]
2 rows in set (0.00 sec)
现在,如果我们只想检索Places_of_interest
数组的值呢?在这种情况下,我们可以使用特殊格式的 JSON access 从数组中获取这些值。下面演示了这种技术。请注意以粗体突出显示的部分。
MySQL localhost:33060+ ssl SQL > SELECT Name, District, JSON_EXTRACT(info, '$.Places_of_interest[*].name') as Sights FROM city WHERE JSON_EXTRACT(info, '$.Places_of_interest') IS NOT NULL \G
*************************** 1\. row ***************************
Name: Charlotte
District: North Carolina
Sights: ["NASCAR Hall of Fame", "Charlotte Motor Speedway"]
*************************** 2\. row ***************************
Name: Daytona
District: Florida
Sights: ["Daytona Beach", "Motorsports Hall of Fame of America", "Daytona Motor Speedway"]
2 rows in set (0.00 sec)
好了,现在看起来容易多了,不是吗?这也是一个有点乱的 SQL 命令。如果这一切看起来有点痛苦,你是对的,的确如此。在 SQL 中处理 JSON 数据需要借助 JSON 函数,但是这是一个额外的步骤,在语法上可能有点混乱。有关每个 JSON_*函数的完整解释,请参阅在线 MySQL 参考手册。
如果您经常使用旧的 MySQL 客户端来查询具有宽行的数据,那么您可能已经使用了\G
选项来以垂直格式显示结果,这使得读取数据更加容易。对于 shell,我们没有这个选项,但是我们可以使用--json
选项来显示数据。虽然这个选项更容易阅读,但是它有点冗长。我们将在 Python 部分看到这一点。
最后,我们可以使用如下所示的DELETE
SQL 命令删除这些行。
MySQL localhost:33060+ ssl SQL > DELETE FROM city WHERE Name in ('Charlotte', 'Daytona');
Query OK, 3 rows affected (0.00 sec)
现在,让我们看看使用 JavaScript 执行的相同操作。
Java Script 语言
为了在 JavaScript 中执行示例任务,我们将使用 X 协议会话对象启动 shell,并传入world_x
模式来演示如何保存一个步骤。然后,我们将使用全局db
对象(有时称为变量)的getTables()
方法来获取world_x
数据库中的表列表。清单 4-6 演示了这些命令。
$ mysqlsh -uroot -hlocalhost -mx --schema=world_x
Creating an X protocol session to 'root@localhost/world_x'
Enter password:
Your MySQL connection id is 15 (X protocol)
Server version: 8.0.11 MySQL Community Server (GPL)
Default schema `world_x` accessible through db.
MySQL Shell 8.0.11
Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type '\help' or '\?' for help; '\quit' to exit.
MySQL localhost:33060+ ssl world_x JS > db
<Schema:world_x>
MySQL localhost:33060+ ssl world_x JS > db.getTables();
[
<Table:city>,
<Table:country>,
<Table:countrylanguage>
]
Listing 4-6Listing and Using Databases—JavaScript Mode
现在,让我们插入数据。注意,在清单 4-6 中,db.getTables()
方法的结果显示了三个表。我们可以使用表名通过名称来引用表对象。例如,要访问城市表,我们使用db.city
。为了插入数据,我们将使用如下所示的db.city.insert()
方法。
MySQL localhost:33060+ ssl world_x JS > db.city.insert("Name", "CountryCode", "District", "Info").values('Charlotte', 'USA', 'North Carolina', '{"Population": 792862, "Places_of_interest": [{"name": "NASCAR Hall of Fame"}, {"name": "Charlotte Motor Speedway"}]}');
Query OK, 1 item affected (0.00 sec)
MySQL localhost:33060+ ssl world_x JS > db.city.insert("Name", "CountryCode", "District", "Info").values('Daytona', 'USA', 'Florida', '{"Population": 590280, "Places_of_interest": [{"name": "Daytona Beach"}, {"name": "Motorsports Hall of Fame of America"}, {"name": "Daytona Motor Speedway"}]}');
Query OK, 1 item affected (0.00 sec)
Note
当以交互方式运行代码时,您可以省略大多数创建、读取、更新和删除操作的execute()
函数调用,因为 MySQL Shell 以交互方式显式执行这些语句。例如,insert()
函数通常需要链接 execute()函数来完成操作,但是您可以在交互模式下省略它。
现在我们有了数据,让我们用下面的代码选择行。这里我们对TableSelect
对象(从db.city.select
返回的对象)使用了db.city.select()
方法和where()
方法。请注意,我们指定了方括号内引用和列出的列的列表。在这个列表中,我们可以使用列名和特殊的->
操作符来提取一个键,从而在 JSON 文档中指定数据。在这种情况下,我们希望文档中的Places_of_interest
键(path)存储在 Info 列中。
MySQL localhost:33060+ ssl world_x JS > db.city.select(["Name", "District", "Info->'$.Places_of_interest'"]).where("Info->'$.Places_of_interest' IS NOT NULL");
+-----------+----------------+--------------------------------------------+
| Name | District | JSON_EXTRACT(`Info`,'$.Places_of_interest')|
+-----------+----------------+--------------------------------------------+
| Charlotte | North Carolina | [{"name": "NASCAR Hall of Fame"}, {"name": "Charlotte Motor Speedway"}] |
| Daytona | Florida | [{"name": "Daytona Beach"}, {"name": "Motorsports Hall of Fame of America"}, {"name": "Daytona Motor Speedway"}] |
+-----------+----------------+--------------------------------------------+
2 rows in set (0.00 sec)
注意结果中的列类型。是 JSON 函数!这意味着我们可以在代码中使用一个 JSON 函数,将结果列数据缩小到仅包含Places_of_interest
数组的值,就像我们在 SQL 示例中所做的那样,如下所示。多酷啊。
MySQL localhost:33060+ ssl world_x JS > db.city.select(["Name", "District", "JSON_EXTRACT(info, '$.Places_of_interest[*].name')"]).where("Info->'$.Places_of_interest' IS NOT NULL");
+-----------+----------------+--------------------------------------------+
| Name | District | JSON_EXTRACT(`info`,'$.Places_of_interest[*].name') |
+-----------+----------------+--------------------------------------------+
| Charlotte | North Carolina | ["NASCAR Hall of Fame", "Charlotte Motor Speedway"] |
| Daytona | Florida | ["Daytona Beach", "Motorsports Hall of Fame of America", "Daytona Motor Speedway"] |
+-----------+----------------+--------------------------------------------+
2 rows in set (0.00 sec)
现在,让我们删除为恢复数据而添加的行。
MySQL localhost:33060+ ssl world_x JS > db.city.delete().where("Name in ('Charlotte', 'Daytona')");
Query OK, 2 items affected (0.00 sec)
好吧,没那么糟。如果您认为它似乎比 SQL 更具编程性,甚至可能更直观一点,那么您就对了。如果看起来有点奇怪,不要担心。在 shell 中使用脚本越多,就会变得越容易、越自然。这也是很好的实践,因为使用 MySQL 的未来是 MySQL Shell 和脚本语言!
现在,让我们看看与 Python 执行的相同脚本。
计算机编程语言
因为我们已经看到该任务演示了两次,所以我跳过了每个步骤的执行细节,并向您展示了我的 Python 会话的脚本。
您马上会注意到的一件事是,一旦我们从 db 对象获得了表,代码就与 JavaScript 示例相同,只是函数的名称拼写有点不同。这是故意的。因为 db 对象实际上是 shell 中的一个特殊变量,所以它在两种语言中具有相同的语法。当您开始使用 X DevAPI 对象时,您只会看到不同之处,我们将在第 5 章中看到更多细节。
Note
一般规则是,当函数由多个名称组成时,JavaScript 使用 camelCase,Python 使用下划线分隔的名称。比如分别是createCluster()
和create_cluster()
。在功能是单个单词的情况下,名称是相同的,即,“select
”、“insert
”、“delete
”。
清单 4-7 展示了使用 Python 运行任务的完整脚本。请注意,唯一的区别是获取表的调用。在这种情况下,我们使用 Python 中的db.get_tables()
方法。它是具有相同功能的相同方法,只是根据典型的 Python 命名约定进行了不同的命名。
$ mysqlsh -uroot -hlocalhost -mx --py --schema=world_x
Creating an X protocol session to 'root@localhost/world_x'
Enter password:
Your MySQL connection id is 19 (X protocol)
Server version: 8.0.11 MySQL Community Server (GPL)
Default schema `world_x` accessible through db.
MySQL Shell 8.0.11
Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type '\help' or '\?' for help; '\quit' to exit.
MySQL localhost:33060+ ssl world_x Py > db
<Schema:world_x>
MySQL localhost:33060+ ssl world_x Py > db.get_tables()
[
<Table:city>,
<Table:country>,
<Table:countrylanguage>
]
MySQL localhost:33060+ ssl world_x Py > db.city.insert("Name", "CountryCode", "District", "Info").values ('Charlotte', 'USA', 'North Carolina', '{"Population": 792862, "Places_of_interest": [{"name": "NASCAR Hall of Fame"}, {"name": "Charlotte Motor Speedway"}]}')
Query OK, 1 item affected (0.00 sec)
MySQL localhost:33060+ ssl world_x Py > db.city.insert("Name", "CountryCode", "District", "Info").values('Daytona', 'USA', 'Florida', '{"Population": 590280, "Places_of_interest": [{"name": "Daytona Beach"}, {"name": "Motorsports Hall of Fame of America"}, {"name": "Daytona Motor Speedway"}]}')
Query OK, 1 item affected (0.00 sec)
MySQL localhost:33060+ ssl world_x Py > db.city.select(["Name", "District", "JSON_EXTRACT(info, '$.Places_of_interest[*].name')"]).where("Info->'$.Places_of_interest' IS NOT NULL")
+-----------+----------------+--------------------------------------------+
| Name | District | JSON_EXTRACT(`info`,'$.Places_of_interest[*].name') |
+-----------+----------------+--------------------------------------------+
| Charlotte | North Carolina | ["NASCAR Hall of Fame", "Charlotte Motor Speedway"] |
| Daytona | Florida | ["Daytona Beach", "Motorsports Hall of Fame of America", "Daytona Motor Speedway"] |
+-----------+----------------+--------------------------------------------+
2 rows in set (0.00 sec)
MySQL localhost:33060+ ssl world_x Py > db.city.delete().where("Name in ('Charlotte', 'Daytona')")
Query OK, 2 items affected (0.00 sec)
Listing 4-7Listing, Inserting, Selecting, and Deleting in Databases—Python Mode
请注意代码与 JavaScript 版本的相似之处。这使得学习 X DevAPI 更容易,因为你可以使用你最喜欢的语言,甚至当你必须使用另一种语言时,一切都很熟悉。酷。
What About Other Languages?
虽然 shell 目前只支持 JavaScript 和 Python,但是 X DevAPI 并不局限于这些语言。其实也可以用 Java,。Net,以及 C++通过适当的连接器与 X DevAPI 一起工作。参见 http://dev.mysql.com/doc/
上 X DevAPI 标题下的链接,了解更多关于使用各自的连接器用 X DevAPI 编写应用的信息。
摘要
对于 MySQL 客户端来说,MySQL Shell 是技术上的一次巨大飞跃。它不仅被设计成以更智能的方式与 MySQL 中的 SQL 一起工作;它还被设计成支持 JavaScript 和 Python 的原型。您可以使用任何您想要的语言,并在它们之间轻松切换,而不必重新启动应用或断开连接。这有多酷?
如果这还不够的话,X DevAPI 和内置对象的额外好处使得使用 shell 作为文档存储的前端意味着您不必编写单独的应用来管理您的数据。您只需选择适合您需求的模式(语言),切换到该语言,然后执行任务。正如我们在第 1 章中所了解到的,shell 还构成了最新特性的前端,包括 InnoDB 集群,为您提供了满足所有 MySQL 管理、编程和高可用性需求的一站式客户端。
在本章中,我们学习了如何使用 MySQL Shell,包括启动选项、Shell 命令、连接、会话,我们甚至学习了如何用 JavaScript 和 Python 编写一些交互式脚本。因此,本章是学习如何开始使用 MySQL Shell 以及使用 JSON 和关系数据的关键章节。虽然这一章并没有详尽地介绍 MySQL Shell 的所有特性,但是它提供了一个广泛的教程,介绍了如何使用它来完成最常见的任务。
在第 5 章中,我将更详细地探索 X DevAPI,包括更仔细地观察可用于编写应用和脚本的对象和工具。我讨论了用 JavaScript 和 Python 访问文档存储的完整脚本。
Footnotes 1
然而,如果您使用过 MySQL Fabric 或实用程序,使用 URI 进行连接会看起来非常熟悉。
尽管您可以在 URI 中指定密码,但这是一种糟糕的安全做法。
不,真的。你应该读一下。
Python 大师经常以这种方式称呼自己。不要和说“你”的骑士混淆。——https://en.wikipedia.org/wiki/Knights_who_say_Ni
五、X 开发者 API
X Developer Application Programming Interface,简称 X DevAPI,是一个类库和方法库,为 MySQL 实现了一个新的 NoSQL 接口。具体来说,X DevAPI 旨在允许与 JSON 文档和关系数据轻松交互。X DevAPI 有专门支持这两个概念的类,允许开发者在他们的应用中使用其中一个(或两个)。X DevAPI 与 X 协议、X 插件以及为公开 X DevAPI 而编写的客户端一起,形成了新的 MySQL 8 文档存储特性。
正如我们将看到的,使用 X DevAPI 有许多方面。然而,一旦掌握了连接和请求对象实例、形成表达式以及使用 JSON 文档的基础知识,X DevAPI 就非常容易学习,并且对于编写文档存储或关系数据应用来说非常有效。
在本书中,我们已经看到了几个关于关系数据的 X DevAPI 的例子,因为大多数数据库管理员都熟悉这种形式的数据库交互。然而,我们还没有看到为文档存储提供的类和方法的完整列表。本章包含了 X DevAPI 中几乎所有可用的公共类和方法(为了简洁起见,省略了一些较少使用的类)。
尽管所有的 X DevAPI 客户端连接器都支持所有的类,但是在每个客户端实现 X DevAPI 的方式上还是有一些细微的差别。特别是,类和方法的名称稍有不同,以匹配该语言的开发实践。例如,一种语言的公认样式指南可能不鼓励使用驼峰式名称,而另一种语言的样式指南可能建议使用下划线,不使用大写。
当学习使用 X DevAPI 时,回顾其他语言的例子会很有帮助。尽管命名方案可能不同,语法可能大相径庭,但是基本的类和方法非常相似,您仍然可以学习使用什么方法。这是我使用 Python 例子的主要原因。您可以使用 Python 示例来了解如何使用这些类,尽管这些方法的命名方案可能略有不同,但从一种语言到另一种语言,方法和实践是相同的。此外,Python 易于阅读,您不需要大型复杂的开发工具(例如,C++或。Net 编译器)。你所需要的只是一个 Python 解释器,它几乎适用于所有平台。
尽管这一章包含了一些来自其他章节的类似信息,但它使用了一种逐步的方法,通过一系列代码示例来演示 X DevAPI。在为文档存储应用编写自己的代码时,包含一组描述主要类及其方法的表作为参考。
我首先全面概述 X DevAPI 的特性,然后详细介绍主要的类和方法。在这个过程中,我给出了许多使用 X DevAPI 的例子。我们不会看到属于 X DevAPI 的所有可能的类或方法,但是我们会看到编写文档存储应用需要掌握的主要组件(类和方法)。如果您需要关于不常用的类和方法的更多信息,请参见“更多信息”一节中的开发者文档参考。
概观
X DevAPI 中有几个强大的特性。在前几章中,我们已经看到了其中的大部分,但是现在我们将看到 X DevAPI 提供的特性。回想一下,这些特性是通过支持 X 协议和 X DevAPI 的客户机实现的。X DevAPI 中包含的特性如下。我们将在本章的后面看到这些特性以及它们是如何实现的。
- MySQLX:一个模块,用于获取一个会话对象,该对象是通过 X 协议连接到 MySQL 服务器而产生的。
- 会话:到 MySQL 服务器的连接。
- 集合:存储 JSON 文档的组织抽象。
- 文档:JSON 文档是集合中数据的主要存储机制。
- CRUD 操作:创建、读取、更新和删除操作的简单方法。读操作简单易懂。
- 关系数据:实现传统关系数据的 CRUD 操作,包括 SQL 语句执行和结果处理。
- 表达式:使用现代实践和语法风格来摆脱传统的 SQL 字符串构建,以便在您的集合和文档中查找内容。
- 并行执行:非阻塞、异步调用遵循常见的宿主语言模式。
- 方法链接:构建 API 是为了让创建或检索(获取)对象的方法返回该对象的实例。这允许我们将几个方法结合在一起(称为方法链接)。尽管方法链接既不是新概念,也不是 X DevAPI 所独有的,但它是一种非常强大的机制,可以使我们的代码更具表现力,更易于阅读。
Note
X DevAPI 仅在使用 X 插件时可用。如果没有安装 X 插件,就不能使用 X DevAPI,只能通过支持 X 协议的客户机或数据库连接器来使用。
客户
X DevAPI 只能通过一个实现 X 协议的客户端获得。此外,要使用这些客户端中的任何一个,您还必须在您的服务器上安装和配置 X 插件。特别是以下任何一种:
- MySQL Shell:8 . 0 . 4 及以后版本(
https://dev.mysql.com/downloads/shell/
) - 连接器/J:8 . 0 . 8 及以后版本(
https://dev.mysql.com/downloads/connector/j/
) - 连接器/网络:8.0.8 及更高版本(
https://dev.mysql.com/downloads/connector/net/
) - connector/node . js:8 . 0 . 8 及更高版本(
https://dev.mysql.com/downloads/connector/nodejs/
) - 连接器/Python:8 . 0 . 5 及更高版本(
https://dev.mysql.com/downloads/connector/python/
) - 连接器/c++:8 . 0 . 6 及更高版本(
https://dev.mysql.com/downloads/connector/cpp/
)
Note
有些数据库连接器版本还没有正式发布。在这些情况下,您可以通过点击下载页面上的Development Releases
选项卡找到正确的版本。只要您不在生产中使用它们,使用 DMR 版本应该没问题。如果您没有看到想要使用的组件的 GA 版本,请务必联系您的 MySQL 销售代表以寻求帮助。
目标语言整合
当您遇到像 X DevAPI 这样的新 API 时,通常情况下,您会希望不同语言的类名和方法名是相同的。也就是说,具有名为getSomething()
的方法的类从一种语言到另一种语言的拼写是相同的。然而,遵守特定于平台和语言的命名约定是一种常见的(有些人会说是首选的)做法,这种做法牺牲了 API 中的通用性,以确保持续符合语言命名标准。如果你像我一样使用不同的编程语言,你会发现这是一个常见的地方,因此你知道对于同一个 API,从一种语言到另一种语言会有一些变化。
X DevAPI 支持这种实践,实现 API 的客户机符合它们的平台和语言标准。在大多数情况下,这可能只是名称中大写字母使用的变化,但也可能导致添加(或省略)下划线。我们已经发现,Connector/Python (C/Py)在名称中使用下划线,而不使用大写字母。Connector/Java (C/J),Connector/Node.js (C/Node.js),Connector/。Net (C/Net)和 Connector/C++ (C/C++)使用的大写略有不同。
不仅仅是方法名有不同的拼写。在处理方法结果或与对象交互的方式上也可能有细微的差别。也就是说,客户遵循这种语言对于常见构造和概念(比如迭代)的常规实践。例如,如果语言有一个返回多个项目的列表的概念(比如相对于一个数组),方法将返回一个列表。尽管随着你对 X DevAPI 的了解越来越多,这看起来有些奇怪,但它确实有好处。也就是说,您编写的结果代码符合您选择的语言标准。
为了演示这些差异,表 5-1 显示了 MySQL X 包方法名称在语言上的微小差异。注意,甚至包名在不同语言中的拼写也不同。Python 开发者会看到 Python 命名方案,并不认为它不寻常,但 Java 示例可能看起来很奇怪。
表 5-1
MySQL X Module
| 返回 | 名字 | 方法 | 语言 | 因素 | | :-- | :-- | :-- | :-- | :-- | | 会话对象 | `MysqlxSessionFactory` | `getSession()` | 爪哇 | 连接 URI 或连接属性 | | `mysqlx` | `getSession()` | Node.js | 连接 URI 或连接属性 | | `MySQLX` | `GetSession()` | 高级程序员 | 连接 URI 或连接数据对象 | | `mysqlx` | `get_session()` | 计算机编程语言 | 连接词典 |在探索 X DevAPI 时,您可能会注意到另一个不同之处。实现 API 的客户端对于如何处理数据有一些非常不同的机制。在某些情况下,如 C/Net,一切都是一个类,通常使用类来包含数据,但在 C/Py 中,更喜欢使用列表和字典。因此,客户端(特别是数据库连接器)可能会以不同的方式实现一些迭代、检索和封装机制。然而,与命名约定一样,差异是为了开发者的利益,以便 X DevAPI 在目标语言中以它应该的方式“工作”。
让我们再看一个区别的例子。表 5-2 显示了可用于处理模式的方法。我包括了四种数据库连接器的四种语言(除了组 Java 和 JavaScript)以及对每种方法的任务、参数和返回类型的简短描述。
表 5-2
Session—Create Schema Method
| 描述 | 返回 | 语言 | 方法 | 因素 | | :-- | :-- | :-- | :-- | :-- | | 创建新模式 | 模式对象 | Java/Node.js | `createSchema()` | 字符串—模式名称 | | 高级程序员 | `CreateSchema()` | 字符串—模式名称 | | 计算机编程语言 | `create_schema()` | 字符串—模式名称 |在下一节中,我们将研究 X DevAPI 的主要代码模块,名为mysqlx
。
Note
本章中的代码示例是作为使用连接器/Python 数据库连接器的脚本用 Python 编写的。因此,您需要安装连接器来使用这些示例。最后,要运行这些示例,可以使用 python 命令来执行它们,如下所示:python
./script1.py
。
mysqlx
模块(有时称为包)与会话(X 协议)一起工作。还有一个用于使用 InnoDB Cluster 的模块(名为 dba ),以及几个公共类,包括用于列、行等的类。
Note
这一章包含了很多关于对象和类的信息。对象是代码类的实例(在执行时),而类只是代码构造。
MySQL X 模块
mysqlx
模块是编写文档存储应用和与 X DevAPI 通信的入口点。我们使用该模块将连接信息以连接字符串或特定于语言的结构(例如 Python 中的字典)的形式传递给服务器,以将连接参数作为 URI 或连接字典(而不是两者)传递。回想一下,统一资源标识符(URI,一种特殊的字符串编码)使用以下格式:
ConnectURI ::= ' 'user_id' ':' 'user_password' '@' 'hostname' ':' 'port_number' '/' 'default_schema_name' '
请注意,密码、端口和模式是可选的,但用户和主机是必需的。在这种情况下,Schema 是连接时要使用的默认模式(数据库)。获取会话对象的方法如下所示。
get_session(<URI or connection dictionary>)
下面显示了使用连接选项字典获取会话对象实例和使用连接字符串(URI)获取会话对象实例的示例。
import mysqlx
mysqlx_session1 = mysqlx.get_session({'host': 'localhost', 'port': 33060, 'user': 'root', 'password': 'secret'})
mysqlx_session2 = mysqlx.get_session('root:secret@localhost:33060')
如果连接成功,结果变量将指向一个对象实例。如果失败,结果可能是一个错误或未初始化的连接。我们将在后面的章节中看到更多关于检查错误的内容。
在下一节中,我们将开始探索 X DevAPI 中的类和方法(组件)。
类别和方法
接下来的部分检查了mysqlx
模块的每个主要类及其方法(特性)。这些类只能从 Session 对象访问——从 get_session()方法返回相同的对象。因为这本书是关于文档存储的,所以我们把重点放在 mysqlx 模块的那些类上。
我们将发现包括类在内的方法,以及使用模式(数据库)、管理事务、检查或关闭连接的方法。所介绍的材料包括最常用的类和方法,按用途或应用而不是严格的等级来分组。这使得概述更简短,并遵循探索 API 的更符合逻辑的路径。如果您想查看模块和类的所有细节以及代码的原始 Doxygen 文档,请参阅本章末尾的“更多信息”一节,获取每个数据库连接器的 API 文档的链接。我在这一章中包含了一些例子来说明所介绍的许多方法。
让我们从 mysqlx 模块的简要概述开始。表 5-3 显示了模块中可用的对象。使用此表作为 X DevAPI 的快速参考指南。
表 5-3
Objects in the mysqlx Module
| 面积 | 方法 | 描述 | | :-- | :-- | :-- | | 关系 | `Session` | 支持与支持 X 协议的 MySQL 产品进行交互 | | create, read, update, and delete | `Schema` | 数据库模式的客户端表示;提供对模式内容的访问 | | `Collection` | 表示架构上的文档集合 | | `Table` | 表示模式上的数据库表 | | `View` | 表示架构上的数据库视图 | | 结果 | `ColumnMetaData` | 返回列的元数据 | | `Row` | 表示从选择查询返回的行元素 | | `Result` | 允许检索对数据库执行的非查询操作的相关信息 | | `BufferingResult` | 为缓冲结果对象提供基本功能 | | `RowResult` | 允许遍历表返回的行对象。选择操作 | | `SqlResult` | 表示 SQL 语句的结果 | | 声明 | `DbDoc` | 表示 JSON 格式的通用文档 | | `Statement` | 为语句对象提供基本功能 | | `FilterableStatement` | 与可过滤语句一起使用的语句 | | `SqlStatement` | 用于 SQL 执行的语句 | | `FindStatement` | 集合上的语句文档选择 | | `AddStatement` | 对集合进行文档添加的语句 | | `RemoveStatement` | 从集合中删除文档的语句 | | `ModifyStatement` | 集合上文档更新操作的语句 | | `SelectStatement` | 对表进行记录检索操作的语句 | | `InsertStatement` | 对表执行插入操作的语句 | | `DeleteStatement` | 删除表的语句 | | `UpdateStatement` | 对表进行记录更新操作的语句 | | `CreateCollectionIndexStatement` | 在集合上创建索引的语句 | | `ReadStatement` | 为读取操作提供基本功能 | | `WriteStatement` | 提供常见的写操作属性 | | 错误 | `DataError` | 报告已处理数据问题的错误例外 | | `DatabaseError` | 与数据库相关的错误异常 | | `Error` | 作为所有其他错误异常的基类的异常 | | `IntegrityError` | 关于关系完整性的错误异常 | | `InterfaceError` | 与接口相关的错误异常 | | `InternalError` | 内部数据库错误异常 | | `NotSupportedError` | 使用不支持的数据库功能时出现异常错误 | | `OperationalError` | 与数据库操作相关的错误异常 | | `PoolError` | 连接池相关错误的异常 | | `ProgrammingError` | 错误异常编程错误 |让我们从 Session 类开始我们的 X DevAPI 之旅。
会话类
Session 类是我们将用来开始处理文档存储的主要类。一旦我们有了连接,下一步就是获取会话对象。从那里,我们可以开始使用文档存储。下面是按领域和应用分组的类和方法的浏览。我们从模式方法开始。
模式方法
X DevAPI 使用术语 schema 来指代一组集合;集合是文档的集合。然而,当处理关系数据时,我们使用“数据库”来指代表和类似对象的集合。人们可能会认为“模式”是“数据库”的同义词,对于 MySQL 的旧版本来说,这是正确的。然而,当使用文档存储和 X DevAPI 时,应该使用“模式”,而当引用关系数据时,应该使用“数据库”
Schema or Database: Does It Matter?
从 MySQL 5.0.2 开始,这两个术语通过 SQL 命令CREATE DATABASE
和CREATE SCHEMA
成为同义词。然而,其他数据库系统做出了区分。也就是说,在某些情况下,模式是表的集合,而数据库是模式的集合。其他人认为模式是定义数据结构的东西。如果您使用其他数据库系统,请确保检查定义,以便正确使用术语。
当开始使用文档存储时,您需要做的第一件事是选择(获取)一个现有的模式,删除一个现有的模式,或者创建一个新的模式。您可能还想列出服务器上的模式。Session 类提供了几种执行这些操作的方法。表 5-4 列出了与模式相关的方法、参数和返回值。
表 5-4
Session Class—Schema Methods
| 方法 | 返回 | 描述 | | :-- | :-- | :-- | | `create_schema(str name)` | `Schema` | 在数据库上创建一个模式,并返回相应的对象 | | `get_schema(str name)` | `Schema` | 通过名称从当前会话中检索架构对象 | | `get_default_schema()` | `Schema` | 检索了配置为会话默认值的架构 | | `drop_schema(str name)` | `None` | 删除具有指定名称的架构 |清单 5-1 展示了如何使用会话对象创建模式对象的例子。当我们检查更多的类和方法时,我们将再次扩展这个例子。在这种情况下,我们使用会话对象来处理模式。
# Import the MySQL X module
import mysqlx
# Get a session with a URI
mysqlx_session = mysqlx.get_session("root:secret@localhost:33060")
# Get an unknown schema
schema1 = mysqlx_session.get_schema("not_there!")
# Does it exist?
print("Does not_there! exist? {0}".format(schema1.exists_in_database()))
# Create the schema
schema = mysqlx_session.create_schema("test_schema")
# Does it exist?
print("Does test_schema exist? {0}".format(schema.exists_in_database()))
mysqlx_session.close()
Listing 5-1Working with Schemas
记下检索不存在的模式的代码。我使用 schema 对象的一个方法来检查它是否存在,然后打印出结果。假设模式not_there!
不存在,代码将显示“False”最后,我在代码末尾创建了模式test_schema
。我们将在后面的章节中更详细地了解 schema 类,以及查看模式是否存在的更好方法。如果您将这段代码保存到一个名为listing5-1.py
的文件中并执行它,您将看到如下输出。
$ python ./listing5-1.py
Does not_there! exist? False
Does test_schema exist? True
现在让我们来看看用于执行符合 ACID 的事务的事务方法。
事务方式
事务提供了一种机制,允许一组操作作为单个原子操作执行。例如,如果为一个银行机构建立一个数据库,将资金从一个账户转移到另一个账户的宏操作将优选地被完整地执行(资金从一个账户转移到另一个账户),而不会中断。
事务允许将这些操作封装在一个原子操作中,如果在所有操作完成之前发生错误,该原子操作将取消任何更改,从而避免数据从一个表中删除,并且永远不会进入下一个表。包含在事务命令中的 SQL 语句形式的一组示例操作如下:
START TRANSACTION;
UPDATE SavingsAccount SET Balance = Balance – 100
WHERE AccountNum = 123;
UPDATE CheckingAccount SET Balance = Balance + 100
WHERE AccountNum = 345;
COMMIT;
MySQL 的 InnoDB 存储引擎(默认存储引擎)支持确保数据完整性的 ACID 事务,能够在所有操作成功的情况下提交(保存)结果更改,或者在任何一个操作失败的情况下回滚(撤消)更改。
What is Acid?
酸代表原子性、一致性、隔离性和持久性。也许是数据库理论中最重要的概念之一,它定义了数据库系统必须表现出的行为,才能被认为是可靠的事务处理。
原子性意味着对于包含多个命令的事务,数据库必须允许在“全有或全无”的基础上修改数据。也就是说,每个事务都是原子的。如果命令失败,则整个事务失败,并且事务中到该点为止的所有更改都将被丢弃。这对于在高事务环境(如金融市场)中运行的系统尤其重要。考虑一下资金转移的后果。通常,借记一个账户和贷记另一个账户需要多个步骤。如果在借记步骤后事务失败,并且没有将钱贷记回第一个帐户,该帐户的所有者将会非常生气。在这种情况下,从借方到贷方的整个事务必须成功,否则都不会成功。
一致性意味着只有有效的数据才会存储在数据库中。也就是说,如果事务中的命令违反了一致性规则之一,则整个事务将被丢弃,数据将返回到事务开始之前的状态。另一方面,如果事务成功完成,它将以遵守数据库一致性规则的方式更改数据。
隔离意味着同时执行的多个事务不会相互干扰。这是并发性的真正挑战最明显的地方。数据库系统必须处理事务不能违反数据的情况(更改、删除等)。)正在另一个事务中使用。有很多方法可以解决这个问题。大多数系统使用一种称为锁定的机制,在第一个事务完成之前,防止数据被另一个事务使用。尽管隔离属性没有规定先执行哪个事务,但它确实确保了它们不会相互干扰。
持久性意味着事务不会导致数据丢失,也不会丢失事务期间创建或更改的任何数据。耐用性通常由强大的备份和恢复维护功能提供。一些数据库系统使用日志记录来确保任何未提交的数据可以在重启时恢复。
会话类实现了用于事务处理的方法,这些方法反映了前面显示的 SQL 命令。表 5-5 列出了事务方式。
表 5-5
Transaction Methods
| 方法 | 返回 | 描述 | | :-- | :-- | :-- | | `start_transaction()` | `None` | 在服务器上启动事务上下文 | | `commit()` | `None` | 提交调用`startTransaction()`后执行的所有操作 | | `rollback()` | `None` | 放弃调用`startTransaction()`后执行的所有操作 | | `set_savepoint(str name="")` | `str` | 创建或替换具有给定名称的事务保存点 | | `release_savepoint(str name)` | `None` | 删除在事务中定义的保存点 | | `rollback_to(str name)` | `None` | 将事务回滚到指定的保存点,而不终止事务 |注意,最后三种方法允许您创建命名事务保存点,这是事务处理的一种高级形式。有关保存点和事务的更多信息,请参见在线 MySQL 参考手册。
我们将在本章后面看到一个事务的例子。现在,让我们看看与服务器连接相关的方法。
连接方法
下划线连接有两种方法。一个用于检查连接是否打开,另一个用于关闭连接。表 5-6 显示了会话类中剩余的可用实用方法。
表 5-6
Connection Methods
| 方法 | 返回 | 描述 | | :-- | :-- | :-- | | `close()` | `None` | 关闭会话 | | `is_open()` | `Bool` | 如果已知会话是打开的,则返回 true |如果您想在应用中额外检查连接,下面显示了如何使用这些方法。
# Import the MySQL X module
import mysqlx
# Get a session with a URI
mysqlx_session = mysqlx.get_session("root:secret@localhost:33060")
# Check the connection
if not mysqlx_session.is_open():
print("Connection failed!")
else:
print("Connection succeeded.")
# Close the connection
mysqlx_session.close()
Listing 5-2Working with Sessions
如果您将这段代码保存到一个名为listing5-2.py
的文件中并执行它,您将看到如下所示的输出。
$ python ./listing5-2.py
Connection succeeded.
杂项方法
Session 类中还有几个实用方法。表 5-7 列出了附加功能。有关这些方法的更多信息,请参见在线 X DevAPI 参考。
表 5-7
Miscellaneous Methods
| 方法 | 返回 | 描述 | | :-- | :-- | :-- | | `Is_open()` | 弯曲件 | 如果连接处于打开和活动状态,则为 True | | `sql(str sql)` | sqlstatesment | 创建一个`SqlStatement`对象,允许在目标 MySQL 服务器上运行收到的 SQL 语句 |CRUD 操作
X DevAPI 实现了一个创建、读取、更新和删除(CRUD)模型,用于处理模式中包含的对象。模式可以包含任意数量的集合、文档、表格、视图和其他关系数据对象(即触发器)。在本节中,我们将看到模式、集合、表(关系数据)和数据集的概述。CRUD 模型是为模式中的所有对象实现的,这些对象可以包含文档存储和关系数据的数据。
到目前为止,本书中的大多数示例都使用关系数据进行演示,因为大多数读者都熟悉使用 SQL。本章继续第 3 章的讨论,完成使用 X DevAPI 构建文档存储应用的介绍。
文档存储数据 CRUD 操作使用动词 add、find、modify 和 remove,而关系数据使用与等效 SQL 命令相匹配的术语。表 5-8 简要介绍了这些方法是如何命名的,以及每个方法的简要描述。此外,我们对文档存储数据使用 Collection 类,对关系数据使用Table
类。
表 5-8
CRUD Operations for Document Store and Relational Data
| CRUD 操作 | 描述 | 文档存储 | 关系数据 | | :-- | :-- | :-- | :-- | | 创造 | 添加新项目/对象 | `collection.add()` | `table.insert()` | | 阅读 | 检索/搜索数据 | `collection.find()` | `table.select()` | | 更新 | 修改数据 | `collection.modify()` | `table.update()` | | 删除 | 移除项目/对象 | `collection.remove()` | `table.delete()` |我们将在下面的章节中看到每个类特有的方法(Schema
、Collection
、Table
和View
)。让我们从查看 Schema 类的细节开始。
模式类
模式是存储数据的对象的容器。回想一下,这可以是文档存储数据的集合,也可以是关系数据的表或视图。就像过去处理关系数据一样,您必须选择(或使用)一个模式来存储集合、表或视图中的数据。
虽然您可以混合使用文档存储数据(集合)和关系数据(表、视图),但是为了便于记忆,我们将从文档存储方法开始依次研究与它们相关的模式类方法。
Schema 类的文档存储方法包括创建集合、使用和查找集合的方法。表 5-9 显示了使用集合和表格的文档存储方法。注意,create 和 get 方法返回一个对象的实例。例如,get_collection()
方法返回一个集合对象。这是如何使用 X DevAPI 将几个操作合并成一个语句的另一个例子。
表 5-9
Schema Class—Document Store and Table Methods
| 方法 | 返回 | 描述 | | :-- | :-- | :-- | | `get_tables()` | `List` | 返回该模式的表列表 | | `get_collections()` | `List` | 返回此架构的集合列表 | | `get_table(str name)` | `Table` | 返回该模式的给定名称的表 | | `get_collection(str name)` | `Collection` | 返回此架构的给定名称的集合 | | `get_collection_as_table(str name)` | `Table` | 返回一个代表数据库集合的 Table 对象 | | `create_collection(str name)` | `Collection` | 在当前架构中创建具有指定名称的新集合,并检索表示所创建的新集合的对象 |现在,让我们继续我们的例子,展示一些使用集合的模式方法。清单 5-3 展示了如何创建一个模式和几个集合,然后在模式中列出这些集合。注意,我使用了集合对象的 name 属性。
# Import the MySQL X module
import mysqlx
# Get a session with a URI
mysqlx_session = mysqlx.get_session("root:secret@localhost:33060")
# Check the connection
if not mysqlx_session.is_open():
print("Connection failed!")
exit(-1)
# Get the schema
schema = mysqlx_session.create_schema("test_schema")
# Create a new collection
testCol = schema.create_collection('test_collection1', True)
# Create a new collection
testCol = schema.create_collection('test_collection2', True)
# Show the collections.
collections = schema.get_collections()
for col in collections:
print(col.name)
mysqlx_session.close()
Listing 5-3Collection Methods
如果您将这段代码保存到一个名为listing5-3.py
的文件中并执行它,您将看到如下所示的输出。
$ python ./listing5-3.py
test_collection1
test_collection2
请注意,在表中有一个方法可以将文档作为关系表进行检索。这个方法,get_collection_as_table()
允许开发者将标准的 SQL 列存储在文档中,并将集合转换(造型)为表格。也就是说,可以将集合作为一个表对象提取,然后该表对象的行为就像一个普通的关系表。使用 CRUD 操作访问表对象中的数据使用以下语法。
doc->'$.field_name'
大多数连接器都支持这种语法。你也可以形成复杂的文档路径(就像我们在第三章中看到的那样)。
doc->'$.something_else.field_name.like[1].other_thing'
我们需要这种语法的原因是,作为表返回的集合会产生一个只有两个字段的表:doc
和_id,
,其中doc
是存储文档的位置,_id
是文档 id。清单 5-4 展示了如何使用这个语法。
# Import the MySQL X module
import mysqlx
# Get a session with a URI
mysqlx_session = mysqlx.get_session("root:secret@localhost:33060")
# Get the schema
schema = mysqlx_session.create_schema("test_schema")
# Create a new collection
pets = schema.create_collection("pets_json")
# Insert some documents
pets.add({'name': 'Violet', 'age': 6, 'breed':'dachshund', 'type':'dog'}).execute()
pets.add({'name': 'JonJon', 'age': 15, 'breed':'poodle', 'type':'dog'}).execute()
pets.add({'name': 'Mister', 'age': 4, 'breed':'siberian khatru', 'type':'cat'}).execute()
pets.add({'name': 'Spot', 'age': 7, 'breed':'koi', 'type':'fish'}).execute()
pets.add({'name': 'Charlie', 'age': 6, 'breed':'dachshund', 'type':'dog'}).execute()
# Fetch collection as Table
pets_tbl = schema.get_collection_as_table('pets_json')
# Now do a find operation to retrieve the inserted document
result = pets_tbl.select(["doc->'$.name'", "doc->'$.age'"]).execute()
record = result.fetch_one()
# Print the first row
print("Name : {0}, Age: {1}".format(record[0], record[1]))
# Drop the collection
schema.drop_collection("pets_json")
# Close the session
mysqlx_session.close()
Listing 5-4Collection as Table Example
如果您将这段代码保存到一个名为listing5-4.py
的文件中并执行它,您将看到如下所示的输出。
$ python ./listing5-4.py
Name : "Violet", Age: 6
集合类
Collection 类用于存储文档(数据)。您可以将它视为与关系数据中的表相同的组织概念。因此,Collection
类实现了对文档的 CRUD 操作以及一些实用方法,比如创建索引或对集合中的文档进行计数的方法。表 5-10 显示了集合类的方法。
表 5-10
Collection Class
| 方法 | 返回 | 描述 | | :-- | :-- | :-- | | `add(*values)` | `AddStatement` | 将一个或多个文档插入集合 | | `find(str search_condition)` | `FindStatement` | 从集合中检索符合指定条件的文档 | | `remove(str search_condition)` | `RemoveStatement` | 创建文档删除处理程序 | | `modify(str search_condition)` | `ModifyStatement` | 修改符合指定标准的文档 | | `drop_index(str name)` | `None` | 从集合中删除索引 | | `replace_one(str id, document doc)` | `Result` | 用新文档替换现有文档 | | `add_or_replace_one(str id, document doc)` | `Result` | 替换或添加集合中的文档 | | `remove_one(str id)` | `Result` | 移除具有给定`_id`值的文档 | | `get_one(str id)` | `Document` | 从集合中获取具有给定`_id`的文档 |注意关于这个表的一件事,每个 CRUD 操作返回一个操作的对象实例。例如,find()
方法返回一个FindStatement
对象。正如您所猜测的,这意味着产生的对象实例具有我们可以用来对语句做更多事情的方法。接下来我们将看到这些类和方法。现在,让我们看一个使用基本 CRUD 操作的例子。
既然我们已经对 X DevAPI 有了足够的了解,我们可以开始回顾更完整的例子了。也就是用数据做一些事情的例子。清单 5-5 展示了一个完整的 Python 脚本,演示了如何使用集合。我包括了会话代码和连接错误处理,就像我们之前看到的那样。这个例子是一个简单的文档存储,用于记录关于宠物的信息。
# Import the MySQL X module
import mysqlx
# Get a session with a URI
mysqlx_session = mysqlx.get_session("root:secret@localhost:33060")
# Check the connection
if not mysqlx_session.is_open():
print("Connection failed!")
exit(1)
# Create a schema.
schema = mysqlx_session.create_schema("animals")
# Create a new collection
pets = schema.create_collection("pets_json", True)
# Insert some documents
pets.add({'name': 'Violet', 'age': 6, 'breed':'dachshund', 'type':'dog'}).execute()
pets.add({'name': 'JonJon', 'age': 15, 'breed':'poodle', 'type':'dog'}).execute()
pets.add({'name': 'Mister', 'age': 4, 'breed':'siberian khatru', 'type':'cat'}).execute()
pets.add({'name': 'Spot', 'age': 7, 'breed':'koi', 'type':'fish'}).execute()
pets.add({'name': 'Charlie', 'age': 6, 'breed':'dachshund', 'type':'dog'}).execute()
# Do a find on the collection - find the fish
mydoc = pets.find("type = 'fish'").execute()
print(mydoc.fetch_one())
# Drop the collection
mysqlx_session.drop_schema("animals")
# Close the connection
mysqlx_session.close()
Listing 5-5CRUD Example Using a Collection
该脚本创建一个新的模式,然后创建一个名为animals
的新集合,并在该模式中创建一个名为pets_json
的集合。然后,该脚本将几个文档(pet)添加到集合中。为了演示查找操作,脚本调用 pets 集合上的find()
方法来查找所有的鱼。也就是说,文档的类型等于“fish”。我们将在后面的章节中看到更多关于可以在find()
方法中使用的表达式。
如果您将这段代码保存到一个名为listing5-5.py
的文件中并执行它,您将看到如下所示的输出。我们找到鱼了!
$ python ./listing5-5.py
{"breed": "koi", "age": 7, "_id": "7c3c0201f5e24bd99f586e772aad0369", "type": "fish", "name": "Spot"}
您可以通过组合一个列表(数组)中的数据来同时添加多个文档,而不是为每个文档发布一个单独的add()
方法。这就像对关系数据使用大容量插入选项一样。下面的代码相当于上面的五个add()
方法调用。
# Insert some documents
pets.add([{'name': 'Violet', 'age': 6, 'breed':'dachshund', 'type':'dog'},
{'name': 'JonJon', 'age': 15, 'breed':'poodle', 'type':'dog'},
{'name': 'Mister', 'age': 4, 'breed':'siberian khatru', 'type':'cat'},
{'name': 'Spot', 'age': 7, 'breed':'koi', 'type':'fish'},
{'name': 'Charlie', 'age': 6, 'breed':'dachshund', 'type':'dog'}]).execute()
注意在add()
方法中使用的语法。这是一种特殊的符号,所有文档存储类方法都使用它来指定 JSON 文档和列出表达式。在本例中,语法是可选语法,通常用于指定多个文档。也就是说,您将文档放在[]
中,以逗号分隔,如下所示。在本例中,我用一个方法调用添加了两个文档。因此,对于一个文档来说,[]
是可选的。
pets.add([
{'name': 'whizzy', 'age': 2, 'breed':'carp', 'type':'fish'},
{'name': 'blobby', 'age': 3, 'breed': 'carp', 'type': 'fish'},
]).execute()
虽然这稍微简化了代码,但是您可能有理由一次添加一个文档。例如,如果您需要使用从add()
方法返回的结果对象来获得更多信息或检查警告,您可能希望一次添加一个文档。
从表 5-10 中回忆一下,CRUD 方法每个都返回一个类的对象实例。这些类有几个方法,您可以使用它们来处理适合该操作的语句。表 5-11 显示了这些类及其方法。
表 5-11
Classes for CRUD Operations for Document Store Data
| 班级 | 方法 | 返回 | 描述 | | :-- | :-- | :-- | :-- | | 添加状态 | 对集合进行文档添加的语句 | | `add(*values)` | `AddStatement` | 将文档列表添加到集合中 | | `execute()` | `Result` | 执行语句 | | `get_values()` | `list` | 返回值列表 | | `is_doc_based()` | `bool` | 检查它是否基于文档 | | `is_upsert()` | `bool` | 如果是向上插入,则返回 true | | `schema` | `Schema` | 架构对象 | | `target` | `object` | 数据库对象目标 | | `upsert(val=True)` | | 将翻转标志设置为所提供值的布尔值 | | findstatesment | 在集合中查找文档 | | `bind(*args)` | `FilterableStatement` | 将值绑定到特定的占位符 | | `execute()` | `Result` | 执行语句 | | `fields(*fields)` | `FindStatement` | 设置文档字段过滤器 | | `get_binding_map()` | `dict` | 返回绑定映射字典 | | `get_bindings()` | `list` | 返回绑定列表 | | `get_grouping()` | `list` | 返回分组表达式列表 | | `get_having()` | `object` | 返回 having 表达式 | | `get_limit_offset()` | `int` | 返回极限偏移量 | | `get_limit_row_count()` | `int` | 返回限制行数 | | `get_projection_expr()` | `object` | 返回投影表达式 | | `get_sort_expr()` | `object` | 返回排序表达式 | | `get_where_expr()` | `object` | 返回 where 表达式 | | `group_by(*fields)` | `ReadStatement` | 为结果集设置分组标准 | | `having(condition)` | `ReadStatement` | 为聚合函数运算中要考虑的记录设置条件 | | | `is_doc_based()` | `bool` | 检查它是否基于文档 | | `is_lock_exclusive()` | `bool` | 如果为`EXCLUSIVE LOCK`,则返回 true | | `is_lock_shared()` | `bool` | 如果为`SHARED LOCK`,则返回 true | | `limit(row_count, offset=0)` | `FilterableStatement` | 设置要返回的记录或文档的最大数量 | | `lock_exclusive()` | `ReadStatement` | 用`EXCLUSIVE LOCK`执行读操作;一次只能有一个锁处于活动状态 | | `lock_shared()` | `ReadStatement` | 用`SHARED LOCK`执行读操作;一次只能有一个锁处于活动状态 | | `schema` | `Schema` | 架构对象 | | `sort(*sort_clauses)` | `FilterableStatement` | 设置排序标准 | | `target` | `object` | 数据库对象目标 | | `where(condition)` | `FilterableStatement` | 设置要过滤的搜索条件 | | 修改状态 | 修改集合中的文档 | | `array_append(doc_path, value)` | `ModifyStatement` | 将值插入集合文档中数组属性的特定位置 | | `array_insert(field, value)` | `ModifyStatement` | 将值插入集合文档中的指定数组 | | `bind(*args)` | `FilterableStatement` | 将值绑定到特定的占位符 | | `change(doc_path, value)` | `ModifyStatement` | 将更新添加到将字段(如果它存在于文档路径中)设置为给定值的语句中 | | `execute()` | `Result` | 执行语句。 | | `get_binding_map()` | `dict` | 返回绑定映射字典 | | `get_bindings()` | `list` | 返回绑定列表 | | `get_grouping()` | `list` | 返回分组表达式列表 | | `get_having()` | `object` | 返回 having 表达式 | | `get_limit_offset()` | `int` | 返回极限偏移量 | | `get_limit_row_count()` | `int` | 返回限制行数 | | `get_projection_expr()` | `object` | 返回投影表达式 | | `get_sort_expr()` | `object` | 返回排序表达式 | | | `get_update_ops()` | `list` | 返回更新操作的列表 | | `get_where_expr()` | `object` | 返回 where 表达式 | | `is_doc_based()` | `bool` | 检查它是否基于文档 | | `limit(row_count, offset=0)` | `FilterableStatement` | 设置要返回的记录或文档的最大数量 | | `patch(doc)` | `ModifyStatement` | 将值插入集合文档中数组属性的特定位置 | | `schema` | `Schema` | 架构对象 | | `set(doc_path, value)` | `ModifyStatement` | 设置或更新集合中文档的属性。 | | `sort(*sort_clauses)` | `FilterableStatement` | 设置排序标准。 | | `target` | `object` | 数据库对象目标 | | `unset(*doc_paths)` | `ModifyStatement` | 从集合中的文档移除属性 | | `where(condition)` | `FilterableStatement` | 设置要过滤的搜索条件 | | 移除状态 | 从集合中删除文档 | | `bind(*args)` | `FilterableStatement` | 将值绑定到特定的占位符 | | `execute()` | `Result` | 执行语句 | | `get_binding_map()` | `dict` | 返回绑定映射字典 | | `get_bindings()` | `list` | 返回绑定列表 | | `get_grouping()` | `list` | 返回分组表达式列表 | | `get_having()` | `object` | 返回 having 表达式 | | `get_limit_offset()` | `int` | 返回极限偏移量 | | `get_limit_row_count()` | `int` | 返回限制行数 | | `get_projection_expr()` | `object` | 返回投影表达式 | | `get_sort_expr()` | `object` | 返回排序表达式 | | `get_where_expr()` | `object` | 返回 where 表达式 | | `is_doc_based()` | `bool` | 检查它是否基于文档 | | `limit(row_count, offset=0)` | `FilterableStatement` | 设置要返回的记录或文档的最大数量 | | `schema` | `Schema` | 架构对象 | | `sort(*sort_clauses)` | `FilterableStatement` | 设置排序标准 | | `target` | `object` | 数据库对象目标 | | `where(condition)` | `FilterableStatement` | 设置要过滤的搜索条件 |注意,我们现在看到,除了简单地调用add()
、find()
、modify()
和remove()
方法之外,您还可以做更多的事情。因为它们都返回另一个类的对象实例,所以我们可以使用一个变量来存储对象实例,然后如果您需要为操作指定附加信息,我们可以调用新对象的适当方法。
事实上,许多返回的对象都能够链接其他方法来帮助过滤或修改搜索。表 5-12 列出了一些搜索文件的常用方法。可选方法如[]
所示。还显示了可以使用它们的方法。
表 5-12
Common Methods for Searching Documents
| 方法 | 描述 | 使用人 | | :-- | :-- | :-- | | `[.fields(...)]` | 该函数设置要从匹配查找操作标准的每个文档中检索的字段。 | `find(),` | | `[.group_by(...)[.having(searchCondition)]]` | 为结果集设置分组标准。having 子句为聚合函数运算中要考虑的记录设置了一个条件。 | `find(),` | | `[.sort(...)]` | 如果使用,该操作将返回按照定义的标准排序的记录。 | `find(), remove(), modify()` | | `[.limit(numberOfRows)` | 如果使用,操作最多返回`numberOfRows`张单据。 | `find(), remove(), modify()` | | `[.bind(placeHolder, value)[.bind(...)]]` | 将值绑定到该对象上使用的特定占位符 | `find(), remove(), modify()` | | `execute()` | 使用所有配置的选项执行操作 | `add(), find(), remove(), modify()` | | `[.set(...)]` | 将一个操作添加到修改处理程序中,以设置包含在选择过滤器和限制中的文档的属性 | `modify()` | | `[.unset(String attribute)]` | 从集合中的文档移除属性 | `modify()` | | `[.patch(...)]` | 基于补丁 JSON 对象对文档执行修改 | `modify()` | | `[.array_insert(...)]` | 在修改处理程序中添加一个操作,将一个值插入到包含在选择过滤器和限制中的文档的数组属性中 | `modify()` | | `[.array_append(...)]` | 在修改处理程序中添加一个操作,将一个值追加到选择过滤器和限制中包含的文档的数组属性中 | `modify()` |例如,假设我们想要限制清单 5-5 中使用的示例代码中find()
调用的字段。也就是说,我们只想要符合标准的宠物的名字和品种。我们可以使用 FindStatement 类的fields()
方法来投射正确的字段。清单 5-6 显示了完成这项工作的代码。
# Import the MySQL X module
import mysqlx
# Get a session with a URI
mysqlx_session = mysqlx.get_session("root:secret@localhost:33060")
# Check the connection
if not mysqlx_session.is_open():
print("Connection failed!")
exit(1)
# Create a schema.
schema = mysqlx_session.create_schema("animals")
# Create a new collection
pets = schema.create_collection("pets_json", True)
# Insert some documents
pets.add({'name': 'Violet', 'age': 6, 'breed':'dachshund', 'type':'dog'}).execute()
pets.add({'name': 'JonJon', 'age': 15, 'breed':'poodle', 'type':'dog'}).execute()
pets.add({'name': 'Mister', 'age': 4, 'breed':'siberian khatru', 'type':'cat'}).execute()
pets.add({'name': 'Spot', 'age': 7, 'breed':'koi', 'type':'fish'}).execute()
pets.add({'name': 'Charlie', 'age': 6, 'breed':'dachshund', 'type':'dog'}).execute()
# Do a find on the collection - find the fish
find = pets.find("type = 'fish'")
filterable = find.fields(['name','type'])
mydoc = filterable.execute()
print(mydoc.fetch_one())
Listing 5-6Demonstration of the FindStatement Class
注意find()
方法,这里我们再次看到使用[]
来指定一个列表。在这种情况下,它是操作的字段列表。这是您将在许多 CRUD 方法中看到的常见语法。
如果您将这段代码保存到一个名为listing5-6.py
的文件中并执行它,您将看到如下所示的输出。
$ python ./listing5-6.py
{"type": "fish", "name": "Spot"}
还要注意,我们已经设置了一个变量来接收来自每个方法的对象实例。然而,我们可以将这些方法链接成一行代码,如下所示。只需用一个链式方法调用替换清单 5-6 中的三行代码。
# Do a find on the collection - find the fish
mydoc = pets.find("type = 'fish'").fields(['name','type']).execute()
print(mydoc.fetch_one())
尽管这些新的类看起来有很多额外的工作,但是随着你越来越习惯于使用它们,它们会变得更加直观。事实上,如果您习惯于处理关系数据,有些方法在概念上可能看起来很熟悉。
还要注意,有些方法允许您传入条件,这些条件是您可以构建以形成操作标准的表达式。我们将在后面的章节中讨论表达式。现在,我们来看看Table
类。
表格类
表是关系数据的主要组织机制。在 X DevAPI 中,表是我们都熟悉的相同的关系数据结构。X DevAPI 有一个Table
(您也可以将它们用于视图)类,包含 CRUD 操作(选择、插入、更新和删除)以及用于计算行数或基对象是否是视图的其他方法。表 5-13 显示了Table
类的方法。
表 5-13
Table Class
| 方法 | 返回 | 描述 | | :-- | :-- | :-- | | `am_i_real()` | `bool` | 验证该对象是否存在于数据库中 | | `count()` | `int` | 计算表格中的行数。 | | `delete(condition=None)` | `DeleteStatement` | 创建一个新的`mysqlx.DeleteStatement`对象 | | `exists_in_database()` | `bool` | 验证该对象是否存在于数据库中 | | `get_connection()` | `Connection` | 返回基础连接 | | `get_name()` | `String` | 返回该数据库对象的名称 | | `get_schema()` | `Schema` | 返回该数据库对象的架构对象 | | `insert(*fields)` | `InsertStatement` | 创建一个新的`mysqlx.InsertStatement`对象 | | `is_view()` | `bool` | 已确定基础对象是否为视图 | | `name` | `str` | 该数据库对象的名称 | | `schema` | `Schema` | `Schema`物体 | | `select(*fields)` | `SelectStatement` | 创建一个新的`mysqlx.SelectStatement`对象 | | `update()` | `UpdateStatement` | 创建一个新的`mysqlx.UpdateStatement`对象 | | `who_am_i()` | `String` | 返回该数据库对象的名称 |注意,没有创建表的方法。我们必须使用CREATE TABLE
sql 命令或 SQL()方法来执行 SQL 语句。事实上,没有创建任何关系数据对象的方法。您必须使用 SQL 发出适当的 create 语句来创建对象。例如,为了在前面的例子中为我们的 pets 数据创建一个表,我们可以使用下面的CREATE TABLE
语句。
CREATE TABLE `animals`.`pets_sql` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` char(20) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
`breed` char(20) DEFAULT NULL,
`type` char(12) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
Tip
没有创建表或视图的创建方法。您必须将 SQL 命令传递给sql()
方法来创建这些(和其他关系数据)对象。
让我们从前面的文档存储示例中获取脚本,并将其重写为使用关系数据。在本例中,我在名为animals
的模式中创建了一个名为pets_sql
的新表,并插入几行,然后选择其中一行。清单 5-7 显示了这个例子的代码。
# Import the MySQL X module
import mysqlx
# Get a session with a URI
mysqlx_session = mysqlx.get_session("root:secret@localhost:33060")
# Check the connection
if not mysqlx_session.is_open():
print("Connection failed!")
exit(1)
# Create a schema.
schema = mysqlx_session.create_schema("animals")
# Create a new table
mysqlx_session.sql("CREATE TABLE animals.pets_sql ("
"`id` int auto_increment primary key, "
"`name` char(20), "
"`age` int, "
"`breed` char(20), "
"`type` char(12))").execute()
pets = schema.get_table("pets_sql", True)
# Insert some documents
pets.insert().values([None, 'Violet', 6, 'dachshund', 'dog']).execute()
pets.insert().values([None, 'JonJon', 15,'poodle', 'dog']).execute()
pets.insert().values([None, 'Mister', 4,'siberian khatru', 'cat']).execute()
pets.insert().values([None, 'Spot', 7,'koi', 'fish']).execute()
pets.insert().values([None, 'Charlie', 6,'dachshund', 'dog']).execute()
# Do a select (find) on the table - find el gato
mydoc = pets.select().where("type = 'cat'").execute()
print(", ".join("{0}".format(c.get_column_name()) for c in mydoc.columns))
print(", ".join("{0}".format(r) for r in mydoc.fetch_one()))
# Drop the collection
mysqlx_session.drop_schema("animals")
# Close the connection
mysqlx_session.close()
Listing 5-7CRUD Example Using a Table
如果您将这段代码保存到一个名为listing5-7.py
的文件中并执行它,您将看到如下所示的输出。
$ python ./listing5-7.py
id, name, age, breed, type
3, Mister, 4, siberian khatru, cat
虽然我将CREATE TABLE
语句放在示例代码中,但这样做并不是正常的做法。事实上,大多数开发者会在应用之外单独创建表。也就是说,他们将手动执行CREATE
SQL 语句(或者可能通过 DevOps 2 工具)并且不将它们包含在应用中。但是,使用临时表有一些争议,在这种情况下,您可能会将临时表包含在应用中,但是一般来说,永久数据库对象是与应用分开创建的。下一个示例显示了如何从现有模式中获取表。
注意,有一些有趣的新方法调用。首先,与集合的add()
方法不同,insert()
方法使用额外的链接方法。在这种情况下,我们需要values()
方法来添加值。这是因为insert()
方法返回了InsertStatement
类的一个实例。
这可能看起来很奇怪,直到您考虑 SQL INSERT
语句的语法。特别是,这些操作在 SQL 中的等效语句如下。如你所见,我们有一个VALUES
条款。
INSERT INTO animals.pets VALUES (Null, 'Violet', 6, 'dachshund', 'dog');
INSERT INTO animals.pets VALUES (Null, 'JonJon', 15,'poodle', 'dog');
INSERT INTO animals.pets VALUES (Null, 'Mister', 4,'siberian khatru', 'cat');
INSERT INTO animals.pets VALUES (Null, 'Spot', 7,'koi', 'fish');
INSERT INTO animals.pets VALUES (Null, 'Charlie', 6,'dachshund', 'dog');
对于select()
方法也是如此,该方法返回一个 SelectStatement 对象,我们在其中链接了where()
子句。正如您可能已经猜到的,同样的事情也发生在update()
和delete()
方法上。对于那些习惯使用 SQL 语句的人来说,这很自然。表 5-14 列出了与关系数据的 CRUD 操作相关的每个类的方法。
表 5-14
Classes for CRUD Operations for Relational Data
| 班级 | 方法 | 返回 | 描述 | | :-- | :-- | :-- | :-- | | 选择状态 | 对表进行记录检索操作的语句。 | | `bind(*args)` | `FilterableStatement` | 将值绑定到特定的占位符 | | `execute()` | `Result` | 执行语句 | | `get_binding_map()` | `dict` | 返回绑定映射字典 | | `get_bindings()` | `list` | 返回绑定列表 | | `get_grouping()` | `list` | 返回分组表达式列表 | | `get_having()` | `object` | 返回 having 表达式 | | `get_limit_offset()` | `int` | 返回极限偏移量 | | `get_limit_row_count()` | `int` | 返回限制行数 | | `get_projection_expr()` | `object` | 返回投影表达式 | | `get_sort_expr()` | `object` | 返回排序表达式 | | `get_sql()` | `String` | 返回生成的 SQL | | `get_where_expr()` | `object` | 返回 where 表达式 | | `group_by(*fields)` | `ReadStatement` | 为结果集设置分组标准 | | `having(condition)` | `ReadStatement` | 为聚合函数运算中要考虑的记录设置条件 | | | `is_doc_based()` | `bool` | 检查它是否基于文档 | | `is_lock_exclusive()` | `bool` | 如果为`EXCLUSIVE LOCK`,则返回 true | | `is_lock_shared()` | `bool` | 如果为`SHARED LOCK`,则返回 true | | `limit(row_count, offset=0)` | `FilterableStatement` | 设置要返回的记录或文档的最大数量 | | `lock_exclusive()` | `ReadStatement` | 用`EXCLUSIVE LOCK`执行读操作;一次只能有一个锁处于活动状态 | | `lock_shared()` | `ReadStatement` | 用`SHARED LOCK`执行读操作;一次只能有一个锁处于活动状态 | | `order_by(*clauses)` | `SelectStatement` | 按标准设置顺序。 | | `schema` | `Schema` | 架构对象 | | `sort(*sort_clauses)` | `FilterableStatement` | 设置排序标准 | | `target` | `object` | 数据库对象目标 | | `where(condition)` | `FilterableStatement` | 设置要过滤的搜索条件 | | 插入状态 | 对表执行插入操作的语句 | | `execute()` | `Result` | 执行语句 | | `get_values()` | `list` | 返回值列表 | | `is_doc_based()` | `bool` | 检查它是否基于文档 | | `is_upsert()` | `bool` | 如果是向上插入,则返回 true | | `schema` | `Schema` | 架构对象 | | `target` | `object` | 数据库对象目标 | | `upsert(val=True)` | | 将 upsert 标志设置为所提供值的布尔值;此标志的设置允许用提供的值更新匹配的行/文档 | | `values(*values)` | `InsertStatement` | 设置要插入的值 | | 更新状态 | 对表进行记录更新操作的语句 | | `bind(*args)` | `FilterableStatement` | 将值绑定到特定的占位符 | | `execute()` | `Result` | 执行语句 | | `get_binding_map()` | `dict` | 返回绑定映射字典 | | `get_bindings()` | `list` | 返回绑定列表 | | `get_grouping()` | `list` | 返回分组表达式列表 | | `get_having()` | `object` | 返回 having 表达式 | | `get_limit_offset()` | `int` | 返回极限偏移量 | | `get_limit_row_count()` | `int` | 返回限制行数 | | `get_projection_expr()` | `object` | 返回投影表达式 | | `get_sort_expr()` | `object` | 返回排序表达式 | | `get_update_ops()` | `list` | 返回更新操作的列表 | | `get_where_expr()` | `object` | 返回 where 表达式 | | `is_doc_based()` | `bool` | 检查它是否基于文档 | | `limit(row_count, offset=0)` | `FilterableStatement` | 设置要返回的记录或文档的最大数量 | | | `schema` | `Schema` | 架构对象 | | `set(field, value)` | `UpdateStatement` | 更新表中记录的列值 | | `sort(*sort_clauses)` | `FilterableStatement` | 设置排序标准 | | `target` | `object` | 数据库对象目标 | | `where(condition)` | `FilterableStatement` | 设置要过滤的搜索条件 | | 删除声明 | 删除表的语句 | | `bind(*args)` | `FilterableStatement` | 将值绑定到特定的占位符 | | `execute()` | `Result` | 执行语句 | | `get_binding_map()` | `dict` | 返回绑定映射字典 | | `get_bindings()` | `list` | 返回绑定列表 | | `get_grouping()` | `list` | 返回分组表达式列表 | | `get_having()` | `object` | 返回 having 表达式 | | `get_limit_offset()` | `int` | 返回极限偏移量 | | `get_limit_row_count()` | `int` | 返回限制行数 | | | `get_projection_expr()` | `object` | 返回投影表达式 | | `get_sort_expr()` | `object` | 返回排序表达式 | | `get_where_expr()` | `object` | 返回 where 表达式 | | `is_doc_based()` | `bool` | 检查它是否基于文档 | | `limit(row_count, offset=0)` | `FilterableStatement` | 设置要返回的记录或文档的最大数量 | | `schema` | `Schema` | 架构对象 | | `sort(*sort_clauses)` | `FilterableStatement` | 设置排序标准 | | `target` | `object` | 数据库对象目标 | | `where(condition)` | `FilterableStatement` | 设置要过滤的搜索条件 |在我们继续之前,让我们回顾一下执行本章剩余部分中的例子所需的样本数据。
What About Classicsession?
如果您已经阅读了 MySQL Shell 的文档,您可能会遇到一个名为mysqlx
的全局对象,它是 mysqlx 模块的镜像。您还可能遇到过名为ClassicSession
的会话对象,它存在于mysql
全局对象中。这个对象只能通过 MySQL Shell 获得,不要与连接器/Python 代码中名为mysql
的模块混淆,它们是不一样的。事实上,X DevAPI 没有任何名为ClassicSession
的对象。
因为这本书关注的是 MySQL 文档库和 X DevAPI,所以我们给出了一个ClassicSession
类中方法的简单列表。下面列出了常用的方法。
close():
关闭此会话对象上的 MySQL 服务器的内部连接。start_transaction():
在服务器上启动一个事务上下文。commit()
:
提交调用 startTransaction()后执行的所有操作。rollback():
放弃调用 startTransaction()后执行的所有操作。get_uri():
检索 URI 字符串。run_sql
(str query, list args=[]):
执行查询并返回对应的 ClassicResult 对象。query(str query, list args=[]):
执行查询并返回相应的 ClassicResult 对象。- 如果会话是打开的,则返回 True。
同样,这些方法是针对ClassicSession
类的,它只能通过MySQL
Shell 获得。这个简短的边栏是为了完整性和澄清类的起源。
本章中使用的示例数据
本章其余部分的示例代码使用我们在前面的示例中创建的数据。为了方便起见,我将它包含在这里。更具体地说,我包括了用于创建关系数据的 SQL 语句和一个用于创建文档存储数据的简短脚本。清单 5-8 是创建示例文档存储所需的代码。
# Create a schema.
# Import the MySQL X module
import mysqlx
# Get a session with a URI
mysqlx_session = mysqlx.get_session("root:secret@localhost:33060")
# Check the connection
if not mysqlx_session.is_open():
print("Connection failed!")
exit(1)
# Create a schema.
schema = mysqlx_session.create_schema("animals")
# Create a new collection
pets = schema.create_collection("pets_json", True)
# Insert some documents
pets.add({'name': 'Violet', 'age': 6, 'breed':'dachshund', 'type':'dog'}).execute()
pets.add({'name': 'JonJon', 'age': 15, 'breed':'poodle', 'type':'dog'}).execute()
pets.add({'name': 'Mister', 'age': 4, 'breed':'siberian khatru', 'type':'cat'}).execute()
pets.add({'name': 'Spot', 'age': 7, 'breed':'koi', 'type':'fish'}).execute()
pets.add({'name': 'Charlie', 'age': 6, 'breed':'dachshund', 'type':'dog'}).execute()
# Close the connection
mysqlx_session.close()
Listing 5-8
Sample Document Store
您可能会注意到,这类似于前面的许多清单。但是,因为从现在开始我们将使用 animals 模式,所以我们在最后省略了 drop_schema()调用。
清单 5-9 包括创建样本关系数据的 SQL 语句。
CREATE TABLE `animals`.`pets_sql` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` char(20) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
`breed` char(20) DEFAULT NULL,
`type` char(12) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
INSERT INTO animals.pets_sql VALUES (Null, 'Violet', 6, 'dachshund', 'dog');
INSERT INTO animals.pets_sql VALUES (Null, 'JonJon', 15,'poodle', 'dog');
INSERT INTO animals.pets_sql VALUES (Null, 'Mister', 4,'siberian khatru', 'cat');
INSERT INTO animals.pets_sql VALUES (Null, 'Spot', 7,'koi', 'fish');
INSERT INTO animals.pets_sql VALUES (Null, 'Charlie', 6,'dachshund', 'dog');
CREATE VIEW `animals`.`num_pets` AS
SELECT type as Type, COUNT(*) as Num
FROM animals.pets_sql
GROUP BY type;
Listing 5-9Sample Relational Data
尽管前面的示例创建了这些对象,但在本章后面的示例实验和运行示例时,您可能希望参考本节。
现在让我们来看看使用来自find()
、select()
和其他返回结果的方法的结果和数据集的类。
使用数据集
到目前为止,我们已经看到了一些处理结果的简单例子,虽然看起来所有的结果都是同一个类,但是有几个结果类。返回的结果类的对象实例取决于操作。表 5-15 显示了原点操作返回的对象实例类型以及返回的数据类型。
表 5-15
Result Classes (Object Instances) Returned
| 对象实例 | 起源 | 描述 | 返回的内容 | | :-- | :-- | :-- | :-- | | 结果 | 创建、更新、删除 | 由`add().execute()`、`modify().execute()`、`remove().execute()`返回 | `affected_item_count`、`auto_increment_value`、`last_document_id` | | SqlResult | 会议 | 由`session.sql()`返回 | `auto_increment_value`、`affected_row_count`,取数据-数据集 | | RowResult | 关系数据选择 | 由`select().execute()`返回 | 提取的数据—数据集 |请注意,内容列将结果或数据集显示为返回的内容。X DevAPI 使用术语数据集来指代从读取 CRUD 操作(find()
、select()
和sql()
方法)返回的数据,结果 3 来指代从创建、更新和删除 CRUD 操作返回的数据。
另外,请注意,每个操作类都返回不同的对象。类RowResult
和SqlResult
继承自基类(BaseResult
),因此有很多相同的方法。将这些与从创建、更新和删除操作返回的Result
类区别开来的是Result
类不支持迭代器。这是因为结果对象包含从服务器返回的与创建、更新和删除操作相关的数据,这些操作不返回任何数据,但可能返回警告和类似的元数据,并且等同于从 MySQL 中的传统 SQL INSERT
、UPDATE
和DELETE
语句返回的结果。
表 5-16 显示了您在处理数据集和结果时会遇到的所有类及其方法。
表 5-16
Classes and Methods for Working with Data Sets and Results
| 班级 | 方法 | 返回 | 描述 | | :-- | :-- | :-- | :-- | | RowResult | 允许遍历表返回的行对象。选择操作 | | `columns` | `list` | 列的列表 | | `count` | `int` | 项目总数 | | `fetch_all()` | `list` | 获取所有项目 | | `fetch_one()` | `mysqlx.Row or mysqlx.DbDoc` | 获取一个项目 | | `get_warnings()` | `list` | 返回警告 | | `get_warnings_count()` | `int` | 返回警告的数量 | | `index_of(col_name)` | `int` | 返回列的索引 | | set_closed(标志) | | 设置结果集提取是否完成 | | 集合 _ 生成 _ 标识(生成 _ 标识) | | 设置生成的 ID | | 集合 _ 有 _ 更多 _ 结果(标志) | | 如果有更多结果集,则设置 | | set_rows_affected(合计) | | 设置受影响的行数 | | SqlResult | 表示 SQL 语句的结果 | | `columns` | `list` | 列的列表 | | `count` | `int` | 项目总数 | | `fetch_all()` | `list` | 获取所有项目 | | `fetch_one()` | `mysqlx.Row or mysqlx.DbDoc` | 获取一个项目 | | `get_autoincrement_value()` | `string` | 返回插入的最后一条记录的标识符 | | `get_warnings()` | `list` | 返回警告 | | `get_warnings_count()` | `int` | 返回警告的数量 | | `index_of(col_name)` | `int` | 返回列的索引 | | `next_result()` | `bool` | 处理下一个结果 | | `set_closed(flag)` | | 设置结果集提取是否完成 | | `set_generated_id(generated_id)` | | 设置生成的 ID | | `set_has_more_results(flag)` | | 如果有更多结果集,则设置 | | BufferingResult | 为缓冲结果对象提供基本功能 | | `count` | `int` | 项目总数 | | `fetch_all()` | `list` | 获取所有项目 | | `fetch_one()` | `mysqlx.Row or mysqlx.DbDoc` | 获取一个项目 | | `get_warnings()` | `list` | 返回警告 | | `get_warnings_count()` | `int` | 返回警告的数量 | | `index_of(col_name)` | `int` | 返回列的索引 | | `set_closed(flag)` | | 设置结果集提取是否完成 | | `set_generated_id(generated_id)` | | 设置生成的 ID | | `set_has_more_results(flag)` | | 如果有更多结果集,则设置 | | `set_rows_affected(total)` | | 设置受影响的行数 | | 结果 | 允许检索对数据库执行的非查询操作的相关信息 | | `append_warning(level, code, msg)` | | 附加警告 | | `get_affected_items_count()` | `int` | 返回上一个操作中受影响的项目数 | | `get_autoincrement_value()` | `int` | 返回上次自动生成的插入 id | | `get_document_id()` | `String` | 返回插入集合中的最后一个文档的 ID | | `get_document_ids()` | `list` | 返回生成的文档 id 列表 | | `get_warnings()` | `list` | 返回警告 |有迭代器的三个类实现了两个方法:fetch_one()
和fetch_all()
。它们像您想象的那样工作,返回一个数据集或一组文档的一组对象。fetch_one()
方法返回数据集中的下一个数据项,如果没有更多的数据项,则NULL
返回所有的数据项。更具体地说,fetch_one()
一次从服务器检索一个数据项,而fetch_all()
一次从服务器检索所有数据。您将使用哪一个取决于数据集的大小以及您希望如何处理数据。
Note
一旦获取了数据项,就不能再获取它。也就是说,迭代器只能向前。
在我们研究如何访问数据集中的数据之前,让我们回顾一下文档标识符和自动递增列。
Tip
从这一点开始,在示例中,您应该按照清单 5-8 中的描述加载 JSON 数据,按照清单 5-9 中的描述加载关系数据。
文档标识符
回想一下,存储在文档存储集合中的每个文档都有一个文档标识符(文档 id 或文档 id),它是一个字符串,唯一地标识集合中的文档。 4 您不需要创建自己的文档 id,它们会自动分配给您。
有两种方法可以从Result
类中检索文档 id(为创建、更新和删除操作返回的内容)。特别是,您可以使用get_document_id()
方法检索最后一个分配的文档 id,或者使用get_document_ids()
为上述add()
方法的批量添加选项返回一个文档 id 列表。清单 5-10 演示了在添加文档时检索文档 id。
Note
从这一点开始的清单假设animals
集合不存在。如果您计划一个接一个地运行代码示例,您应该添加清单 5-5 中所示的drop_schema()
调用。
# Import the MySQL X module
import mysqlx
# Get a session with a URI
mysqlx_session = mysqlx.get_session("root:secret@localhost:33060")
# Check the connection
if not mysqlx_session.is_open():
print("Connection failed!")
exit(1)
# Drop the collection
mysqlx_session.drop_schema("animals")
# Create a schema.
schema = mysqlx_session.create_schema("animals")
# Create a new collection
pets = schema.create_collection("pets_json")
# Insert some documents and get the document ids.
res = pets.add({'name': 'Violet', 'age': 6, 'breed':'dachshund', 'type':'dog'}).execute()
print("New document id = '{0}'".format(res.get_document_id()))
res = pets.add({'name': 'JonJon', 'age': 15, 'breed':'poodle', 'type':'dog'}).execute()
print("New document id = '{0}'".format(res.get_document_id()))
res = pets.add({'name': 'Mister', 'age': 4, 'breed':'siberian khatru', 'type':'cat'}).execute()
print("New document id = '{0}'".format(res.get_document_id()))
res = pets.add({'name': 'Spot', 'age': 7, 'breed':'koi', 'type':'fish'}).execute()
print("New document id = '{0}'".format(res.get_document_id()))
res = pets.add({'name': 'Charlie', 'age': 6, 'breed':'dachshund', 'type':'dog'}).execute()
print("New document id = '{0}'".format(res.get_document_id()))# Drop the collection
mysqlx_session.drop_schema("animals")
# Close the connection
mysqlx_session.close()
Listing 5-10Getting Document Ids
如果您运行代码片段,您将看到如下所示的文档 id。
New document id = '9801A79DE0939A8311E805FB3419B12B'
New document id = '9801A79DE093B93111E805FB341CC7B5'
New document id = '9801A79DE093AD4311E805FB341CF6D9'
New document id = '9801A79DE09397AD11E805FB341D1F87'
New document id = '9801A79DE09382E911E805FB341D4568'
自动增量
如果您正在处理关系数据并且已经指定了一个自动增量字段,那么您可以使用SqlResult
和Result
类的get_autoincrement_value()
方法来检索最后一个自动增量值。此方法返回生成的自动增量值,如果您需要检索由代理主键插入的最后一行,这会很有帮助。
访问数据集中的数据
让我们考虑访问数据集中的数据。在这种情况下,我们在一个集合上发出一个find()
方法,返回几个由特定结果对象表示的文档。在这种情况下,我们有一组 DbDoc 对象要获取。
有三种方法可以访问数据项中的数据;我们可以简单地将数据项作为一个字符串(自然地),我们可以通过带有数据元素键名称的属性来访问数据元素,或者我们可以使用数组索引来查找带有键的数据元素。清单 5-11 显示了一个完整的脚本,其中包含每个机制的示例。请注意,您应该已经创建了模式和集合,并使用清单 5-8 用数据填充它。
# Import the MySQL X module
import mysqlx
# Get a session with a URI
mysqlx_session = mysqlx.get_session("root:secret@localhost:33060")
# Check the connection
if not mysqlx_session.is_open():
print("Connection failed!")
exit(1)
# Get the collection.
pets = mysqlx_session.get_schema("animals").get_collection("pets_json")
# Do a find on the collection - find the dog
find = pets.find("type = 'dog'").execute()
res = find.fetch_one()
while (res):
print("Get the data item as a string: {0}".format(res))
print("Get the data elements: {0}, {1}, {2}".format(res.name, res.age, res['breed']))
res = find.fetch_one()
# Close the connection
mysqlx_session.close()
Listing 5-11Reading Data from a Data Set
注意我是如何用find().execute()
方法检索数据集的,它返回一个我可以迭代的对象。在本例中,我获取第一个数据项,然后用 while 循环遍历这些数据项。在 while 循环中,我打印了从 fetch 返回的字符串,并演示了如何通过属性(例如,res.age
、res.name
)或通过使用键名的数组索引(例如,res['breed']
)来检索数据元素。
如果您将这段代码保存到一个名为listing5-11.py
的文件中并执行它,您将看到如下所示的输出。
$ python ./listing5-11.py
Get the data item as a string: {"breed": "dachshund", "age": 6, "_id": "9801A79DE093B2B011E805FBCB1FAC51", "type": "dog", "name": "Violet"}
Get the data elements: Violet, 6, dachshund
Get the data item as a string: {"breed": "poodle", "age": 15, "_id": "9801A79DE093B43A11E805FBCB215AFA", "type": "dog", "name": "JonJon"}
Get the data elements: JonJon, 15, poodle
Get the data item as a string: {"breed": "dachshund", "age": 6, "_id": "9801A79DE093BFD511E805FBCB21CF30", "type": "dog", "name": "Charlie"}
Get the data elements: Charlie, 6, dachshund
现在让我们看看如何从关系数据查询中获取行。
访问结果中的元数据
当使用关系数据和表或视图时select()
方法。这将返回一个 SQL 数据集,该数据集表示您期望从典型的 SQL SELECT
查询中获得的行。然后,我们可以通过将列名作为属性、将列索引号作为数组索引或者将列名作为数组索引来访问行中的数据。清单 5-12 展示了从行中获取数据的两种方法。
# Import the MySQL X module
import mysqlx
# Get a session with a URI
mysqlx_session = mysqlx.get_session("root:secret@localhost:33060")
# Check the connection
if not mysqlx_session.is_open():
print("Connection failed!")
exit(1)
# Get the collection.
pets = mysqlx_session.get_schema("animals").get_table("pets_sql")
# Do a select (find) on the table - find the dogs
res = pets.select().where("type = 'dog'").execute()
# Working with column properties
print("Get the data using column names as properties:")
for row in res.fetch_all():
for col in res.columns:
print(row.get_string(col.get_column_name())),
print("")
# Working with column indexes
print("Get the data using column index by integer:")
for row in res.fetch_all():
for i in range(0,len(res.columns)):
print(row[i]),
print("")
# Working with column names
print("Get the data using column index by name:")
for row in res.fetch_all():
for col in res.columns:
print(row[col.get_column_name()]),
print("")
# Close the connection
mysqlx_session.close()
Listing 5-12Data Set Example—Relational Data
如果您将这段代码保存到一个名为listing5-12.py
的文件中并执行它,您将看到如下输出。
$ python ./listing5-12.py
Get the data using column names as properties:
1 Violet 6 dachshund dog
2 JonJon 15 poodle dog
5 Charlie 6 dachshund dog
Get the data using column index by integer:
1 Violet 6 dachshund dog
2 JonJon 15 poodle dog
5 Charlie 6 dachshund dog
Get the data using column index by name:
1 Violet 6 dachshund dog
2 JonJon 15 poodle dog
5 Charlie 6 dachshund dog
注意我是如何用select().execute()
方法检索数据集的,它返回一个我可以迭代的对象。在这种情况下,我使用 for 循环获取项目(行)。在 for 循环中,我使用 Row 对象的get_string()
方法,该方法接受列的键名,在本例中为列名。我使用了一个小技巧来迭代嵌套 for 循环中的列。我将在下一节讨论如何处理列元数据。
列元数据
关系数据的两个结果类(RowResult
和SqlResult
)支持列的概念,正如典型的 SQL SELECT
查询所期望的那样。您可以使用columns()
方法(columns
属性)获取列,该方法返回列对象的列表。然后,您可以使用该对象中的属性来发现有关数据集中的列的更多信息。表 5-17 显示了ColumnMetaData
类及其方法。
表 5-17
ColumnMetaData Class
| 方法 | 返回 | 描述 | | :-- | :-- | :-- | | `get_schema_name()` | `str` | 检索定义该列的架构的名称 | | `get_table_name()` | `str` | 检索定义列的表名 | | `get_table_label()` | `str` | 定义列的检索表别名 | | `get_column_name()` | `str` | 检索列名 | | `get_column_label()` | `str` | 检索到的列别名 | | `get_type()` | `Type` | 检索的列类型 | | `get_length()` | `int` | 检索到的列长度 | | `get_fractional_digits()` | `int` | 如果适用,检索小数位数 | | `is_number_signed()` | `bool` | 指示数字列是否有符号 | | `get_collation_name()` | `str` | 检索排序规则名称 | | `get_character_set_name()` | `str` | 检索字符集名称 |请注意,有几种有趣的方法,包括发现类型、字符和排序、大小等的方法。注意还有一些获取列名或标签的方法。名称是操作中的名称,而标签是操作中指定的别名或替代标签。要了解区别,请考虑下面的 SQL 语句。
SELECT pet_name as name, age as years_young FROM animals.pets_sql
当您调用get_column_name()
和get_column_label()
方法时,您将获得以下值。清单 5-13 展示了如何使用这些方法。
# Import the MySQL X module
import mysqlx
# Get a session with a URI
mysqlx_session = mysqlx.get_session("root:secret@localhost:33060")
# Check the connection
if not mysqlx_session.is_open():
print("Connection failed!")
exit(1)
res = mysqlx_session.sql("SELECT name as pet_name, age as years_young FROM animals.pets_sql").execute()
cols = res.columns
for col in cols:
print "name =", col.get_column_name(), "label =", col.get_column_label()
mysqlx_session.close()
Listing 5-13Working with Column Names and Labels
如果您将这段代码保存到一个名为listing5-13.py
的文件中并执行它,您将看到如下所示的输出。
$ python ./listing5-13.py
name = name label = pet_name
name = age label = years_young
现在让我们讨论使用表达式来过滤数据。
公式
表达式是 X DevAPI 中的另一个元素,它是一个简单而强大的特性。表达式与我们在 SQL 语句中用来过滤 CRUD 语句中数据的子句同义。有几种表达形式。我们可以使用字符串、布尔表达式,或者嵌入等式或不等式等实际表达式。让我们逐一检查一下。
表达式字符串
表达式字符串是那些需要在运行时计算的字符串。通常,它们使用一个或多个变量“绑定”(称为参数绑定)到字符串中的占位符。这允许您在运行时为动态过滤赋值,而不是静态值,我们将在下一节中看到。我们将在后面的章节中看到更多关于参数绑定的内容。
清单 5-14 显示了一个例子,类似于我们在前面的例子中使用的在 pets_json 集合中寻找鱼的例子。然而,在这种情况下,我们使用一个参数来包含类型,该参数可能会在运行时被读取,从而允许我们的代码动态地过滤集合查找结果。
# Import the MySQL X module
import mysqlx
# Get a session with a URI
mysqlx_session = mysqlx.get_session("root:secret@localhost:33060")
# Check the connection
if not mysqlx_session.is_open():
print("Connection failed!")
exit(1)
# Get the collection.
pets = mysqlx_session.get_schema("animals").get_collection("pets_json")
# Do a find on the collection - find the fish with an expression string and parameter binding
fish_type = 'fish'
mydoc = pets.find("type = :mytype").bind('mytype', fish_type).execute()
print(mydoc.fetch_one())
# Close the connection
mysqlx_session.close()
Listing 5-14Expression Strings
如果您将这段代码保存到一个名为listing5-14.py
的文件中并执行它,您将看到如下所示的输出。
$ python ./listing5-14.py
{"breed": "koi", "age": 7, "_id": "9801A79DE0938FBD11E805FBCB21AB35", "type": "fish", "name": "Spot"}
布尔表达式字符串
这种形式的表达式使用一个字符串,就像我们在 SQL 语句的WHERE
子句中使用的一样。也就是说,我们使用自然语言来表达过滤器,其中比较是真还是假。清单 5-15 是前面例子中的布尔表达式字符串。第一行是一个关系数据示例,在这个示例中,我们希望结果只包括那些类型列等于“dog”的项目第二个是一个文档存储示例,我们希望结果只包括那些 type 元素的值为“fish”的项目
# Import the MySQL X module
import mysqlx
# Get a session with a URI
mysqlx_session = mysqlx.get_session("root:secret@localhost:33060")
# Check the connection
if not mysqlx_session.is_open():
print("Connection failed!")
exit(1)
# Get the collection.
pets_json = mysqlx_session.get_schema("animals").get_collection("pets_json")
# Get the table.
pets_sql = mysqlx_session.get_schema("animals").get_table("pets_sql")
res = pets_sql.select().where("type = 'dog'").limit(1).execute()
print("SQL result ="),
for row in res.fetch_all():
for i in range(0,len(res.columns)):
print("{0}".format(row[i])),
print("")
mydoc = pets_json.find("type = 'fish'").execute()
print("JSON result = {0}".format(mydoc.fetch_one()))
# Close the connection
mysqlx_session.close()
Listing 5-15Boolean Expression Strings
如果您将这段代码保存到一个名为listing5-15.py
的文件中并执行它,您将看到如下所示的输出。
$ python ./listing5-15.py
SQL result = 1 Violet 6 dachshund dog
JSON result = {"breed": "koi", "age": 7, "_id": "9801A79DE0938FBD11E805FBCB21AB35", "type": "fish", "name": "Spot"}
Tip
你可以在 https://dev.mysql.com/doc/x-devapi-userguide/en/
的 X DevAPI 用户指南中找到一套完整的表达式和方法链接的扩展巴克斯-纳尔形式 5 图。
警告和错误
我们需要花些时间了解的另一个领域是服务器发送的警告报告和 X DevAPI 的错误处理。幸运的是,X DevAPI 有获取警告的工具。然而,错误将需要更多的工作。让我们先来看看警告。
来自服务器的警告
处理警告很容易,因为 X DevAPI 内置了一种机制来帮助您获取警告信息。Warning
类有三个属性,如下所示。如果出现警告,我们可以使用这些来获取警告。
- 级别—警告的级别
- 代码—警告代码
- 消息—警告消息
Note
默认情况下,所有警告都从服务器发送到客户端。但是,您可以取消警告以节省带宽。使用 Session 类中的set_fetch_warnings()
来控制警告是在服务器上被丢弃还是被发送到客户端。使用get_fetch_warnings()
方法获取活动设置。
事实上,我们可以使用 get_warnings()方法来检查是否有需要处理的警告。但是,X DevAPI 会在每次发生警告时向客户端发送警告,因此如果您想要检查警告,必须在每次执行后进行。清单 5-16 展示了一种编写代码来处理错误的方法。这绝不是唯一的方法,但确实演示了Warning
类方法。
Note
这个例子需要建立animals
数据库。有关如何设置数据库,请参阅前面的“本章中使用的示例数据”一节。
#This method checks the result for warnings and prints them
# if any exist.
#
# result[in] result object
def process_warnings(result):
if result.get_warnings_count():
for warning in result.get_warnings():
print("WARNING: Type {0} (Code {1}): {2}".format(*warning))
else:
print "No warnings were returned."
# Import the MySQL X module
import mysqlx
# Get a session with a URI
mysqlx_session = mysqlx.get_session("root:secret@localhost:33060")
# Check the connection
if not mysqlx_session.is_open():
print("Connection failed!")
exit(1)
# Get the animals schema.
schema = mysqlx_session.get_schema("animals")
# Try to create the table using a SQL string. It should throw a warning.
res = mysqlx_session.sql("CREATE TABLE IF NOT EXISTS animals.pets_sql ("
"`id` int auto_increment primary key, "
"`name` char(20), "
"`age` int, "
"`breed` char(20), "
"`type` char(12))").execute()
process_warnings(res)
# Close the connection
mysqlx_session.close()
Listing 5-16Processing Warnings
注意,我写了一个名为process_warnings()
的方法,它接受一个结果对象,并通过调用get_warnings_count()
方法来检查是否有错误。如果这个方法返回一个正整数,这意味着有警告,如果是这样,我从警告对象中获取类型、代码和消息,并打印数据。如果没有警告,我会打印一条消息,声明没有错误(但是您可能不想知道)。
如果您将这段代码保存到一个名为listing5-16.py
的文件中并执行它,您将会看到下面的结果。请注意,如果您删除了animals
集合,您可能需要再次运行它。
$ python ./listing5-16.py
WARNING: Type 1 (Code 1050): Table 'pets_sql' already exists
现在让我们看看如何处理来自 X DevAPI 的错误。
来自 X DevAPI 的错误
正如我提到的,X DevAPI 中没有实现专门用于处理错误的东西,但是我们可以使用一些工具。在这种情况下,我们将从数据库连接器中获得一些帮助。也就是说,数据库连接器实现了特定于语言的错误处理(异常处理)机制,使得处理来自 X DevAPI 方法的错误变得很自然。换句话说,它们实现了异常处理。 6
以 Python 为例,Python 语言实现了一个 try...异常块(有时称为 try 或异常块)。这种构造允许以raise()
方法的形式“引发”异常的代码让调用代码(具有最近 try 块的代码)捕获异常。语法如下。
try:
# some operation 1
# some operation 2
# some operation 3
# some operation 4
# some operation 5
except:
# catch the exception
finally:
# do this after the success or capture
这允许我们“尝试”一个(或多个)操作,如果它们因引发异常而失败,代码将跳过 try 段中的任何剩余操作,并跳到 except 段。
让我们看看当您不使用异常处理并且代码失败时会发生什么。也就是说,X DevAPI 抛出一个异常。清单 5-17 显示了一个带有错误的简单脚本。你能认出他们吗?提示:检查密码,当您试图创建一个已经存在的表时会发生什么?
# Import the MySQL X module
import mysqlx
import getpass
# Get a session with a URI
mysqlx_session = mysqlx.get_session("root:wrongpassworddude!@localhost:33060")
# Check the connection
if not mysqlx_session.is_open():
print("Connection failed!")
exit(1)
# Get the animals schema.
schema = mysqlx_session.get_schema("animals")
# Try to create the table using a SQL string. It should throw an
# error that it already exists.
res = mysqlx_session.sql("CREATE TABLE animals.pets_sql ("
"`id` int auto_increment primary key, "
"`name` char(20), "
"`age` int, "
"`breed` char(20), "
"`type` char(12))").execute()
# Close the connection
mysqlx_session.close()
Listing 5-17
Not Handling Errors
如果您将这段代码保存到一个名为listing5-17.py
的文件中并执行它,您将会看到下面的结果(为了简洁起见,删除了无关的数据)。
$ python ./listing5-17.py
Traceback (most recent call last):
File "./listing5-17.py", line 6, in <module>
mysqlx_session = mysqlx.get_session("root:wrongpassworddude!@localhost:33060")
...
File "/Library/Python/2.7/site-packages/mysqlx/protocol.py", line 129, in read_auth_ok
raise InterfaceError(msg.msg)
mysqlx.errors.InterfaceError: Invalid user or password
哦,亲爱的,太可怕了!我们在这里得到的是一个回溯转储,这就是 Python 传达未处理异常的方式。我们应该注意的关键信息是,第一行显示了脚本中的代码行,该代码行启动了一系列方法调用,导致最后两行所示的异常抛出。这里我们看到,get_session()
调用导致连接器中的 X 协议代码抛出一个mysqlx.errors.InterfaceError
。这表明如果不使用异常处理,事情会变得多么糟糕。但是我们可以做得更好。
让我们看一个异常处理的例子。清单 5-18 显示了一个带有故意错误的脚本,这些错误将导致 X DevAPI 抛出异常。在这种情况下,将失败的是 CREATE TABLE SQL 语句。更具体地说,它将失败,因为该表已经存在。
如果您运行这个脚本并且没有失败,请确保该表已经存在。我们利用了表已经存在的事实,所以当执行 CREATE 时,我们将得到一个异常。正如您将看到的,异常也不容易理解。
# Import the MySQL X module
import mysqlx
try:
# Get a session with a URI
mysqlx_session = mysqlx.get_session("root:secret@localhost:33060")
# Check the connection
if not mysqlx_session.is_open():
print("Connection failed!")
exit(1)
# Get the animals schema.
schema = mysqlx_session.get_schema("animals")
# Try to create the table using a SQL string. It should throw an error
# that it already exists.
res = mysqlx_session.sql("CREATE TABLE animals.pets_sql ("
"`id` int auto_increment primary key, "
"`name` char(20), "
"`age` int, "
"`breed` char(20), "
"`type` char(12))").execute()
except Exception as ex:
print("ERROR: {0}:{1}".format(*ex))
# Close the connection
mysqlx_session.close()
Listing 5-18Handling Errors—Global Exception
当我们运行这段代码时,我们会得到一个更好的结果。如果您将这段代码保存到一个名为listing5-18.py
的文件中并执行它,您将会看到下面的结果。请注意,您可以从这个改进的版本中获得预期的输出。它更容易阅读,信息量也更大。
$ python ./listing5-18.py
ERROR: -1: Table 'pets_sql' already exists
尽管对于可以在异常块中放置多少内容没有可靠的规则,但是应该保持异常块较小——比方说隔离到单个概念或进程——以避免调试代码时很难知道是几十个方法调用中的哪一个触发了异常的情况。如果您使用 Python 这样的语言来抛出调用堆栈跟踪,这可能并不困难,但是如果您的语言没有调用堆栈跟踪,或者重新运行代码来创建调用堆栈跟踪是不可能的,那么保持异常块较小可以帮助您隔离出现问题的代码。
清单 5-19 展示了一个在每个 X DevAPI 语句周围包含 try 块的例子。它还演示了如何捕获引发的特定异常。也就是说,except:语法允许您指定特定的异常。在本例中,我捕获了 X DevAPI 抛出的异常。
# Import the MySQL X module
import mysqlx
import getpass
# Get a session with a URI
mysqlx_session = None
try:
mysqlx_session = mysqlx.get_session("root:wrongpassworddude!@localhost:33060")
except mysqlx.errors.InterfaceError as ex:
print("ERROR: {0} : {1}".format(*ex))
passwd = getpass.getpass("Wrong password, try again: ")
finally:
mysqlx_session = mysqlx.get_session("root:{0}@localhost:33060".format(passwd))
# Check the connection
if not mysqlx_session.is_open():
print("Connection failed!")
exit(1)
# Demostrate error from get_schema()
schema = mysqlx_session.get_schema("animal")
if (not schema.exists_in_database()):
print("Schema 'animal' doesn't exist.")
# Get the animals schema.
schema = mysqlx_session.get_schema("animals")
try:
# Try to create the table using a SQL string. It should throw an
# error that it already exists.
res = mysqlx_session.sql("CREATE TABLE animals.pets_sql ("
"`id` int auto_increment primary key, "
"`name` char(20), "
"`age` int, "
"`breed` char(20), "
"`type` char(12))").execute()
except mysqlx.errors.OperationalError as ex:
print("ERROR: {0} : {1}".format(*ex))
# Close the connection
if mysqlx_session:
mysqlx_session.close()
Listing 5-19Handling Errors—Local Exceptions
如果您将这段代码保存到一个名为listing5-19.py
的文件中并执行它,您将会看到下面的结果。出现提示时,请务必输入正确的密码。这是因为只有一个正确密码的测试。您的挑战是通过允许多次重试来确定改进代码的方法。提示:使用循环。
$ python ./listing5-19.py
ERROR: -1 : Invalid user or password
Wrong password, try again:
Schema 'animal' doesn't exist.
ERROR: -1 : Table 'pets_sql' already exists
该示例还展示了一种有趣的处理异常的方法——重试语句。通常,您会将想要重试的语句放在一个具有时间或尝试限制的循环或类似结构中。这里,我只是在提示用户输入密码时重试会话方法。
Tip
为了获得最佳结果,请使用较短的异常块封装您的代码,以便您可以轻松地隔离导致错误的代码。
现在让我们看看使用 X DevAPI 时可用的附加特性。
附加功能
既然我们已经看到了 X DevAPI 中所有可用的主要类和方法,现在让我们研究一下 X DevAPI 公开的一些特性;特别是参数绑定、链接方法、预准备语句和异步执行的例子。
Note
这个例子使用了world_x
数据库,可以从 https://dev.mysql.com/doc/index-other.html
下载。只需下载压缩文件,解压缩,然后用\source
命令或使用mysql
客户端和source
命令将其包含在 MySQL Shell 中。关于如何安装world_x
数据库的演练,请参见第 4 章中的“安装示例数据库”一节。
参数绑定
参数绑定允许我们在运行时将值应用于表达式。参数绑定通常用于过滤器,并在执行操作之前完成(因此您会经常看到.bind().execute()
)。因此,参数绑定的好处是它允许您从表达式中分离值。这是通过所有支持参数绑定的类的bind()
方法来完成的。
可以使用两种方法之一“绑定”参数:可以使用匿名参数,也可以使用命名参数。但是,对于何时可以使用每种都有限制。特别是,匿名参数只能用在 SQL 字符串(表达式)中,而命名参数用在 CRUD 操作中。让我们来看一个例子。
清单 5-20 展示了一个使用匿名参数的例子。匿名参数用问号表示。请注意我们在下面的 SQL 语句中是如何做到这一点的。
$ mysqlsh root@localhost:33060 --sql
Creating a session to 'root@localhost:33060'
Enter password:
Your MySQL connection id is 74 (X protocol)
Server version: 8.0.11 MySQL Community Server (GPL)
No default schema selected; type \use <schema> to set one.
MySQL Shell 8.0.11
Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type '\help' or '\?' for help; '\quit' to exit.
MySQL localhost:33060+ ssl SQL > PREPARE STMT FROM 'SELECT * FROM world_x.city WHERE name like ? LIMIT ?';
Query OK, 0 rows affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > SET @name_wild = 'Ar%';
Query OK, 0 rows affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > SET @numrows = 1;
Query OK, 0 rows affected (0.00 sec)
MySQL localhost:33060+ ssl SQL > EXECUTE STMT USING @name_wild, @numrows;
+----+--------+-------------+------------+------------------------+
| ID | Name | CountryCode | District | Info |
+----+--------+-------------+------------+------------------------+
| 18 | Arnhem | NLD | Gelderland | {"Population": 138020} |
+----+--------+-------------+------------+------------------------+
1 row in set (0.00 sec)
MySQL localhost:33060+ ssl SQL > \q
Bye!
Listing 5-20Parameter Binding Example (MySQL Shell)
我们可以从这个例子中得到一些东西。首先,匿名参数只在 SQL 语句中使用。第二,匿名参数按照它们在 SQL 语句中出现的顺序完成(提供值)。第三,也是最后一点,匿名参数可以用于预处理语句。 7
清单 5-21 展示了几个使用命名参数的例子。需要注意的关键点是参数是如何以冒号开头命名的。当调用bind()
方法时,我们提供命名参数(没有冒号)及其值。
# Import the MySQL X module
import mysqlx
mysqlx_session = mysqlx.get_session("root:secret@localhost:33060")
schema = mysqlx_session.get_schema("world_x")
# Collection.find() function with hardcoded values
myColl = schema.get_collection('countryinfo')
myRes1 = myColl.find("GNP >= 828").execute()
print(myRes1.fetch_one())
# Using the .bind() function to bind parameters
myRes2 = myColl.find('Name = :param1 and GNP = :param2').bind('param1','Aruba').bind('param2', '828').execute()
print(myRes2.fetch_one())
# Using named parameters
myColl.modify('Name = :param').set('GNP', '829').bind('param', 'Aruba').execute()
# Binding works for all CRUD statements except add()
myRes3 = myColl.find('Name LIKE :param').bind('param', 'Ar%').execute()
print(myRes3.fetch_one())
# Ok, now put the candle back...
myColl.modify('Name = :param').set('GNP', '828').bind('param', 'Aruba').execute()
# Close the connection
mysqlx_session.close()
Listing 5-21Parameter Binding Example
请注意我们如何传递多个要绑定的参数。在这种情况下,只要有参数要绑定,我们就调用bind()
多次。由于下一节中描述的方法链接特性,这是可能的。也就是说,bind()
方法返回它自身的一个实例,因此当我们调用下一个bind()
方法时,它会重复调用,但是使用不同的参数和值。
Tip
命名参数不能以数字开头。例如,:1test
不是有效的命名参数名。
如果您将这段代码保存到一个名为listing5-21.py
的文件中并执行它,您将会看到下面的结果。
$ python ./listing5-21.py
{"GNP": "828", "Name": "Aruba", "government": {"GovernmentForm": "Nonmetropolitan Territory of The Netherlands", "HeadOfState": "Beatrix"}, "demographics": {"LifeExpectancy": 78.4000015258789, "Population": 103000}, "_id": "ABW", "IndepYear": null, "geography": {"SurfaceArea": 193, "Region": "Caribbean", "Continent": "North America"}}
{"GNP": "828", "Name": "Aruba", "government": {"GovernmentForm": "Nonmetropolitan Territory of The Netherlands", "HeadOfState": "Beatrix"}, "demographics": {"LifeExpectancy": 78.4000015258789, "Population": 103000}, "_id": "ABW", "IndepYear": null, "geography": {"SurfaceArea": 193, "Region": "Caribbean", "Continent": "North America"}}
{"GNP": "829", "Name": "Aruba", "government": {"GovernmentForm": "Nonmetropolitan Territory of The Netherlands", "HeadOfState": "Beatrix"}, "demographics": {"LifeExpectancy": 78.4000015258789, "Population": 103000}, "_id": "ABW", "IndepYear": null, "geography": {"SurfaceArea": 193, "Region": "Caribbean", "Continent": "North America"}}
现在让我们看看方法链接和它是如何工作的。
方法链接
方法链接(也称为命名参数习惯用法)是面向对象编程中的一种设计约束,其中每个方法(支持链接)返回一个对象实例。因此,只需将调用添加到第一个方法的末尾,就可以访问(调用)返回对象上的任何方法。
例如,如果一个类 X 有一个方法 a(),它用方法 b()返回对象 Y,我们可以如下将调用链接在一起。
x = something.get_x()
res = x.a().b()
在这种情况下,x.a()
方法首先执行,然后当它返回一个 Y 对象实例时,它调用 Y 对象实例上的b()
方法。
X DevAPI 中方法链接的亮点在于关系数据方法的实现。特别是那些支持 SQL CRUD 命令的类和方法。清单 5-22 是一个复杂的表格SELECT
操作的例子。
# Import the MySQL X module
import mysqlx
mysqlx_session = mysqlx.get_session("root:secret@localhost:33060")
# Get the table
city = mysqlx_session.get_schema("world_x").get_table("city")
# Perform a complex select
res = city.select(['Name', 'District']).where("Name LIKE :param1").order_by(["District", "Name"]).bind('param1', 'X%').limit(1).execute()
# Show results
print("SQL result ="),
for row in res.fetch_all():
for i in range(0,len(res.columns)):
print("{0}".format(row[i])),
print("")
# Close the connection
mysqlx_session.close()
Listing 5-22Method Chaining
如果您将这段代码保存到一个名为listing5-22.py
的文件中并执行它,您将会看到下面的结果。
$ python ./listing5-22.py
SQL result = Xuangzhou Anhui
这里我们看到两行代码和几个使用中的对象实例以及一系列方法。在第二行代码中(忽略注释),我们使用一个 mysqlx 会话对象来获取一个模式对象,然后通过调用Schema
类方法get_table()
来链接它,该方法返回一个表对象实例。
在第三行代码中,我们使用 table 对象实例调用select()
方法,它返回 SelectStatement 对象实例,我们通过调用它的where()
方法链接它,它返回相同的 SelectStatement 对象,我们调用它的order_by()
方法,它返回相同的 SelectStatement 对象,然后我们将参数与返回相同 SelectStatement 对象的bind()
方法绑定,最后我们调用返回 SqlResult 对象的execute()
方法。哇哦!
如果您认为方法链隐藏了许多关于对象的细节,避免了在变量中存储对象实例的重复代码,那么您就对了!这正是我们正在做的。
正如你所看到的,方法链接允许我们在代码中更清楚地表达概念,旧的类风格和方法不返回对象实例(甚至旧的风格简单地返回 0 或 1 来指示成功或失败 8 )。掌握 X DevAPI 意味着掌握如何将方法链接在一起,以简化并使代码更容易阅读和理解。酷吧。
有关方法链接概念的更多信息,请参见 https://en.wikipedia.org/wiki/Method_chaining
。
CRUD 准备语句
准备好的 CRUD 语句是我们在调用execute()
方法之前想要对一个对象执行一些操作的情况。这样,我们就“准备”了对象实例(语句)来执行。也就是说,不是通过链接bind()
和execute()
或简单地execute()
来直接绑定和执行 CRUD 操作,我们可以操纵 CRUD 操作,将过滤器和其他标准之类的东西存储在一个变量中,供以后执行。
这样做的好处是,我们可以将几个参数或变量集绑定到表达式。这给了我们更好的性能,因为我们可以提前“准备”变量,稍后再执行它们。这可以让我们在执行许多类似操作时获得更好的性能。
您可能认为 CRUD 准备语句在概念上类似于 SQL 准备语句。这是真的,但与 SQL 预准备语句不同,CRUD 预准备语句是在类方法中实现的,因此可以轻松地集成到我们的代码中。
让我们看一个例子。清单 5-23 展示了一个准备 CRUD 语句的例子。在本例中,我们使用一个参数准备一个 find()语句,并将结果(FindStatement 对象)保存到一个变量中。当我们想要执行这个语句时,我们使用变量调用 bind()方法提供一个值,然后使用execute()
方法执行 FindStatement。
# Import the MySQL X module
import mysqlx
# Get a session with a URI
mysql_session = mysqlx.get_session("root:secret@localhost:33060")
# Check the connection
if not mysql_session.is_open():
print("Connection failed!")
exit(1)
# Create a schema.
schema = mysql_session.get_schema("animals")
# Create a new collection
pets = schema.get_collection("pets_json")
# Prepare a CRUD statement.
find_pet = pets.find("name = :param")
# Now execute the CRUD statement different ways.
mydoc = find_pet.bind('param', 'JonJon').execute()
print(mydoc.fetch_one())
mydoc = find_pet.bind('param', 'Charlie').execute()
print(mydoc.fetch_one())
mydoc = find_pet.bind('param', 'Spot').execute()
print(mydoc.fetch_one())
# Close the connection
mysql_session.close()
Listing 5-23
CRUD Prepared Statements
注意三个find_pet.bind(
方法调用。这里我们执行 find 语句三次;一次用于我们想要找到的每个宠物的名字。显然,这只是一个小例子,但是展示了使用 CRUD 准备语句的强大功能。
如果您将这段代码保存到一个名为listing5-23.py
的文件中并执行它,您将会看到下面的结果。
$ python ./listing5-23.py
{"breed": "poodle", "age": 15, "_id": "9801A79DE093B43A11E805FBCB215AFA", "type": "dog", "name": "JonJon"}
{"breed": "dachshund", "age": "6", "_id": "9801A79DE093BFD511E805FBCB21CF30", "type": "dog", "name": "Charlie"}
{"breed": "koi", "age": 7, "_id": "9801A79DE0938FBD11E805FBCB21AB35", "type": "fish", "name": "Spot"}
异步执行
对于那些支持异步编程的客户端,比如 C/J、C/Node.js 和 C/Net,X DevAPI 允许使用异步机制,比如回调、async()
调用等等。这些机制使得允许一个操作与其他操作并行运行成为可能。让我们看一个来自 Java 的例子。
Note
目前,C/Py 和 C/C++都不允许异步执行,但将来可能会。检查这些连接器的新版本是否有更新。
Table employees = db.getTable("employee");
// execute the query asynchronously, obtain a future
CompletableFuture<RowResult> rowsFuture = employees.select("name", "age").where("name like :name").orderBy("name").bind("name", "m%").executeAsync();
这里我们看到了executeAsync()
方法,这是 Java 连接器允许异步执行execute()
方法的方式。也就是说,select()
异步运行,当它返回(完成)时,它触发由CompletableFuture
模板/类(或者 Java 中的泛型类 9 )定义的未来。
Note
根据您使用的语言,X DevAPI 可能会实现一个类似于executeAsync()
的函数,作为对execute()
的补充或替代。查看所选连接器的 X DevAPI 文档,了解正确的方法名称和用法。
有关异步执行的更多信息,请参见 X DevAPI 指南中与您选择的语言相匹配的连接器。
更多信息
如果您想了解关于数据库连接器和 MySQL Shell 中 X DevAPI 实现的更多详细信息,请访问以下链接,获取所有类、方法、属性和帮助函数的描述和列表。这些网站以开发者为中心,可能不包括详细的解释或例子。
- MySQL Shell:有几个可用的资源,包括
- MySQL 连接器/J:
http://dev.mysql.com/doc/dev/connector-j/
- MySQL 连接器/Node.js:
http://dev.mysql.com/doc/dev/connector-nodejs/
- MySQL 连接器/网络
:
http://dev.mysql.com/doc/dev/connector-net/
- MySQL 连接器/Python:
http://dev.mysql.com/doc/dev/connector-python
- MySQL 连接器/C++:
https://dev.mysql.com/doc/dev/connector-cpp/
Note
这些组件的某些文档可能与本章开头列出的版本号不匹配。如果文档是针对较新的版本,那么您应该安装最新的版本。然而,在撰写本文时,MySQL Shell 用户指南正在更新中。定期检查以确保您使用的是最新的可用文档。
摘要
X DevAPI 是 NoSQL 与 MySQL 服务器接口简化程度的奇迹。X DevAPI 引入了一种新的、现代的、易于学习的数据处理方式。
X DevAPI 是构建文档存储应用的主要机制。虽然 X DevAPI 不是一个独立的库——您必须使用一个通过 X 协议公开 X DevAPI 的客户端——但 X DevAPI 仍然是改变您与 MySQL 交互方式的一个主要努力。现在,我们第一次同时拥有了 MySQL 的 SQL 和 NoSQL 接口。
在这一章中,我们探索了 X DevAPI,研究了用于连接 MySQL 服务器、创建集合、处理结果甚至如何处理关系数据的主要类和方法。最后,我们还看到了一组快速参考表,您可以将其用作开发文档存储应用的主要参考。
在第 6 章中,我们深入探讨了 X 插件,这将让你更好地理解 X 插件做什么,如何配置它,以及如何最好地将它作为正常数据库管理任务的一部分来管理。在这一章之后,我们将看到 X 协议的细节,以及一个文档存储应用的工作示例。
Footnotes 1
当前版本的 Connector/Python 不支持该语法。
https://en.wikipedia.org/wiki/DevOps
遗憾的是,在文档和博客中,这有时被称为结果集,这可能会引起混淆,因为结果集是关系数据中的一个常用术语,与数据集意思相同。使用 X DevAPI 时,最好将结果集和数据集视为同义词。
原则上相当于主键(如自动递增列)。
扩展的 Backus-Naar 形式是一种用于记录上下文无关语法的图表样式。 https://en.wikipedia.org/wiki/Extended_Backus%E2%80%93Naur_form
见。
https://en.wikipedia.org/wiki/Exception_handling
见。
有关匿名参数的更多信息,请参见 MySQL 在线参考手册中的选择语法一节。
我不喜欢服务器中的旧代码的一点是,大多数方法返回 0 或 1,通过指针传递对象和变量来返回数据。对于快速编写应用来说,方法链更加优雅和有用。
https://docs.oracle.com/javase/tutorial/java/generics/types.html
见
六、X 插件
X Dev API 是一种与 MySQL 交互的全新方式。正如我们所了解的,新的 NoSQL 机制建立在 X DevAPI、X 插件和 X 协议之上。您可能会有这样的印象,这些技术就在那里,一旦启用,就不再需要什么了。这在很大程度上是正确的,但是和所有好的特性一样,这个故事不仅仅是启用这个特性。
在这一章中,我们仔细看看 X 插件。正如您将看到的,它不仅仅是简单地打开它。事实上,它只是默认工作意味着它非常稳定,并适用于大多数情况。但是,您可以用几种方式来配置它,包括一个非常有趣的保护连接的选项。然而,在接下来的章节中会有更多关于这个甚至如何监控 X 插件的内容。
Note
我在这一章中使用术语“插件”来指代一般的插件,而“X 插件”来指代 X 插件的特定特性。
概观
回想一下第 2 章中的内容,X 插件是 MySQL 的一个单独编译的组件,可以在运行时加载和卸载。Oracle 将 X 插件命名为mysqlx
,并在服务器中以该名称列出。一旦加载(安装),插件将在每次服务器重启时自动启动。此外,回想一下 MySQL 中的插件特性是 Oracle 用来扩展服务器功能的主要机制,无需从头开始重新构建代码。尽管插件技术在 MySQL 中已经存在了一段时间,并且最初用于存储引擎,但它已经成为 Oracle 用来扩展和添加新功能到服务器的默认机制。
在这方面,X 插件是一个很好的例子,它展示了插件可以给服务器带来的强大力量。例如,默认情况下,服务器使用固定的协议与客户端通信,该协议通常称为 MySQL 客户端/服务器协议,简称为 MySQL 协议或旧协议。这个协议被内置到服务器中,除了在 MySQL 的生命周期中有一些小的变化;从 MySQL 4 开始就没有太大变化。x 代码库。在 X 插件出现之前,这是客户端与服务器通信的唯一方式。 1 现在,一旦你加载了 X 插件,它就为使用 X 协议的客户端和服务器启用了一个新的通信协议。
How Do MySQL Plugins Work?
在最一般的意义上,当插件被安装或在启动时启动时,服务器和插件使用特殊的插件 API 进行通信,该 API 允许插件将自己注册为服务器的一部分。例如,插件提供了处理状态变量的回调方法以及启用其功能的方法。这个协商过程就是插件如何扩展服务器的功能,而不必强制服务器重启,也不需要重新编译服务器。
也就是说,需要注意的是,插件是针对公共服务器库编译的,因此必须与特定版本和平台的服务器相匹配(例如,您不能使用针对 Windows 上的 Linux 编译的插件)。使用在插件启动期间检查的特殊版本控制机制来提供兼容性检测。大多数插件都清楚地列出了支持的服务器版本。当你决定使用一个新的插件时,一定要检查它是否与你的服务器版本兼容。有关插件的更多信息,请参见在线 MySQL 参考手册中的“MySQL 插件 API”一节。
特征
同样,X 插件的主要目的是支持与服务器通信的 X 协议,以启用 X DevAPI (NoSQL)接口。虽然这是它的主要关注点,但是有一些有趣的特性可以帮助您获得更好的体验。这些包括配置插件使用不同于服务器的安全套接字层(SSL)设置,以及使用系统变量更改插件的行为。我们将在下面几节中看到如何更改 SSL 设置以及如何更改默认端口。我们将在后面的章节中看到更多关于其他系统变量的内容。
Note
尽管文档和其他文本以大写字母显示了 X 插件的变量,但是变量在 SQL 结果中以小写字母显示。例如,您可能会看到前缀Mysqlx_
,但是服务器的输出显示为mysqlx_
。幸运的是,大多数平台上的大多数 SQL 命令都可以接受这两种版本。
安全套接字层(SSL)连接
如果您在 MySQL 服务器上使用 SSL 连接,并希望对 X 插件(和您的 NoSQL 应用)使用安全连接,您可以设置 X 插件使用不同于服务器的 SSL 选项值。这意味着您可以设置 X 插件使用一个 SSL 证书,而服务器使用另一个证书。这非常有助于确保 NoSQL 应用的安全,而无需在客户机/服务器和 X 协议之间共享 SSL 数据。
您可以将系统变量及其值放在my.cnf
文件中,或者通过服务器启动命令(命令行)传递系统变量。以这种方式使用时,系统变量通常被称为启动选项。使用以下命令可以列出系统变量及其当前值。注意,我使用 MySQL Shell 通过批处理模式获取信息。
$ mysqlsh -uroot -hlocalhost --sql -e "SHOW VARIABLES LIKE 'mysqlx_ssl%'"
Enter password:
+--------------------+-------+
| Variable_name | Value |
+--------------------+-------+
| mysqlx_ssl_ca | |
| mysqlx_ssl_capath | |
| mysqlx_ssl_cert | |
| mysqlx_ssl_cipher | |
| mysqlx_ssl_crl | |
| mysqlx_ssl_crlpath | |
| mysqlx_ssl_key | |
+--------------------+-------+
您可以在您的配置文件(my.cnf
)中设置这些变量,方法是将它们放在名为[msyqld]
的服务器部分,但是您应该省略破折号。下面的摘录展示了如何为服务器和 X 插件使用不同的 SSL 配置。
[mysqld]
...
ssl-ca=/my_ssl/certs/ca_server.pem
ssl-cert=/my_ssl/certs/server-cert.pem
ssl-key=/my_ssl/certs/server-key.pem
...
mysqlx-ssl-ca=/my_ssl/certs/ca_xplugin.pem
mysqlx-ssl-cert=/my_ssl/certs/xplugin-cert.pem
mysqlx-ssl-key=/my_ssl/certs/xplugin-key.pem
...
注意,我已经包含了两组 SSL 选项,只有 X 插件选项以前缀mysqlx_
命名。
Note
一般来说,大多数系统变量都有相应的启动选项,并且在配置文件中以相同的名称使用,只是下划线改为了破折号。例如,mysqlx_ssl_ca 系统变量的启动选项是- mysqlx-ssl-ca。然而,--mysqlx_ssl_ca
版本也适用于那些健忘的人。
要临时或作为 shell 或批处理文件的一部分更改这些值,可以在命令行上将系统变量指定为选项,如下所示。请注意,我们使用了与前面所示相同的值。
$ mysqld ... --mysqlx-ssl-ca=/my_ssl/certs/ca_xplugin.pem --mysqlx-ssl-cert=/my_ssl/certs/xplugin-cert.pem \
--mysqlx-ssl-key=/my_ssl/certs/xplugin-key.pem
虽然您可以像这样使用命令行上的选项,但这不是最好的方法。这是因为,除非您在某个地方记录新的命令行,或者在 shell 或批处理命令中使用它(即使这样),否则很容易忘记您使用了什么值,甚至是使用了哪些系统变量。因此,最好的方法是,始终将自定义系统变量更改放在 MySQL 配置文件中。
更改默认端口
回想一下,X 插件使用与服务器不同的端口。默认端口是 33060。如果您想更改默认端口,可以使用mysqlx_port
系统变量。与 SSL 选项一样,您可以将它放在my.cnf
文件中,或者在服务器启动命令(命令行)上将它作为启动选项传递。您也可以使用以下命令检查默认端口。有效值范围是 1-65535。例如,您可以设置 X 插件使用端口 3307。
$ mysqlsh -uroot -hlocalhost --sql -e "SHOW VARIABLES LIKE 'mysqlx_port'"
Enter password:
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| mysqlx_port | 3307 |
+---------------+-------+
因为mysqlx_port
系统变量只在启动时读取(原因很明显),所以更改该值需要重新启动以使用不同的端口。
与 SSL 选项一样,您可以在命令行上设置端口,如下所示。在这种情况下,我们在端口 3307 上启动服务器,X 插件在端口 3308 上监听。
$ mysqld --port=3307 --datadir... --socket=...mysql.sock --mysqlx-port=3308 --mysqlx-socket=...mysqlx.sock
同样,这也不是推荐的方法,因为命令行选项如果不放在 shell 或批处理文件中,很容易被遗忘。
更深入——探索源代码
如果你想通过检查源代码来了解 X 插件是如何工作的,你可以从 http://dev.mysql.com/downloads/mysql/
下载源代码。要下载 MySQL 8 源代码,请从平台下拉框中选择源代码,并下载与您的平台匹配的文件。如果您没有看到与您的平台相匹配的版本,并且您只想研究源代码,那么请选择通用 Linux 选项。图 6-1 显示了网站的摘录,突出显示了选项卡和下拉框。
图 6-1
Downloading the MySQL 8 source code
下载完成后,你可以在rapid/plugin/x
文件夹中找到 X 插件的源代码。您可以浏览源代码,看看它是如何工作的,甚至它是如何在启动时与服务器进行协商的。例如,要查看系统变量,打开rapid/plugin/x/src
文件夹中的xpl_plugin.cc
文件,向下滚动到大约第 240 行。你会发现一个类似清单 6-1 中的例子的结构,它列出了插件支持的变量。
...
static struct st_mysql_sys_var* xpl_plugin_system_variables[]= {
MYSQL_SYSVAR(port),
MYSQL_SYSVAR(max_connections),
MYSQL_SYSVAR(min_worker_threads),
MYSQL_SYSVAR(idle_worker_thread_timeout),
MYSQL_SYSVAR(max_allowed_packet),
MYSQL_SYSVAR(connect_timeout),
MYSQL_SYSVAR(ssl_key),
MYSQL_SYSVAR(ssl_ca),
MYSQL_SYSVAR(ssl_capath),
MYSQL_SYSVAR(ssl_cert),
MYSQL_SYSVAR(ssl_cipher),
MYSQL_SYSVAR(ssl_crl),
MYSQL_SYSVAR(ssl_crlpath),
MYSQL_SYSVAR(socket),
MYSQL_SYSVAR(bind_address),
MYSQL_SYSVAR(port_open_timeout),
MYSQL_SYSVAR(wait_timeout),
MYSQL_SYSVAR(interactive_timeout),
MYSQL_SYSVAR(read_timeout),
MYSQL_SYSVAR(write_timeout),
NULL
};
...
Listing 6-1System Variable Definition (X Plugin)
注意,有一个宏定义MYSQL_SYSVAR
,用于定义系统变量。还有按名称列出的系统变量。一旦插件启动,您可以使用清单 6-2 中的命令看到系统变量。请注意,这些变量以前缀mysqlx_
命名,所有 14 个变量都存在(主机系统运行的是 MAC OS——您的结果可能会有所不同)。
MySQL localhost:33060+ ssl SQL > SHOW VARIABLES LIKE 'mysqlx_%';
+-----------------------------------+------------------+
| Variable_name | Value |
+-----------------------------------+------------------+
| mysqlx_bind_address | * |
| mysqlx_connect_timeout | 30 |
| mysqlx_idle_worker_thread_timeout | 60 |
| mysqlx_max_allowed_packet | 1048576 |
| mysqlx_max_connections | 100 |
| mysqlx_min_worker_threads | 2 |
| mysqlx_port | 33060 |
| mysqlx_port_open_timeout | 0 |
| mysqlx_socket | /tmp/mysqlx.sock |
| mysqlx_ssl_ca | |
| mysqlx_ssl_capath | |
| mysqlx_ssl_cert | |
| mysqlx_ssl_cipher | |
| mysqlx_ssl_crl | |
| mysqlx_ssl_crlpath | |
| mysqlx_ssl_key | |
+-----------------------------------+------------------+
16 rows in set (0.00 sec)
Listing 6-2Listing the System Variables for the X Plugin
我们将在下一节中发现更多关于系统变量的内容。如果您喜欢冒险,请继续阅读该文件中的代码,以获得更多关于状态变量的线索。提示:看看名为xpl_global_status_variables.h
的文件。
选项和变量
正如我们在上一节中看到的,X 插件有几个系统变量,可以在启动时在配置文件或服务器命令行中设置。可以控制的配置项目包括默认端口、配置连接参数和建立超时限制等项目。您还可以看到 X 插件报告的关于性能、统计数据等的几个状态变量。这些状态变量可以用来监控 X 插件,以帮助您调整它的选项来匹配您的环境。我将在下面几节中探讨常用的启动选项、系统变量和状态变量。
Note
我使用变量这个术语来描述启动选项、系统变量和状态变量共有的性质和特性。
变量可以有两个范围级别:适用于所有连接的全局和仅适用于当前连接(会话)的会话,即您当前正在使用的连接。没有从您当前未使用的其他会话中捕获数据的规定。
变量还可以支持可以在运行时设置的动态值和只能在启动时设置的值。尽管您可以查看任何变量的值,而不考虑范围,但是您只能在运行时为动态变量设置值。设置全局变量时必须小心,以免对其他连接产生负面影响。
如何查看变量的值
有几种方法可以查看变量的值。我们在上一节中看到,您可以使用 SQL 命令SHOW VARIABLES
查看系统变量,使用SHOW STATUS
命令查看状态变量的值。记住,启动选项与一个系统变量相关联,所以使用SHOW VARIABLES
命令就可以看到这些选项。
您还可以通过使用特殊形式的SELECT
命令来查看系统变量的值,使用特殊的符号或快捷方式,例如在全局范围内使用@@GLOBAL
表示值,在会话范围内使用@@SESSION
表示值。虽然 X 插件目前没有会话级系统变量,但下面显示了全局系统变量mysqlx_connect_timeout
。
MySQL localhost:33060+ ssl SQL > SELECT @@GLOBAL.mysqlx_connect_timeout;
+---------------------------------+
| @@GLOBAL.mysqlx_connect_timeout |
+---------------------------------+
| 30 |
+---------------------------------+
1 row in set (0.00 sec)
您还可以使用 PERFORMANCE_SCHEMA 表(视图)查看变量的值。在这种情况下,您可以通过会话或全局范围查看状态变量。或者您可以编写一个 SQL 查询来将数据与范围结合起来,如清单 6-3 所示(您的结果可能会有所不同)。我格式化了下面的 SQL 语句,以便于阅读。
SELECT *, 'SESSION' as SCOPE FROM PERFORMANCE_SCHEMA.session_status
WHERE variable_name LIKE 'mysqlx_%'
UNION SELECT *, 'GLOBAL' as SCOPE FROM PERFORMANCE_SCHEMA.global_status
WHERE variable_name LIKE 'mysqlx_%'
MySQL localhost:33060+ ssl SQL > SELECT *, 'SESSION' as SCOPE FROM PERFORMANCE_SCHEMA.session_status WHERE variable_name LIKE 'mysqlx_%' UNION SELECT *, 'GLOBAL' as SCOPE FROM PERFORMANCE_SCHEMA.global_status WHERE variable_name LIKE 'mysqlx_%' \G
*************************** 1\. row ***************************
VARIABLE_NAME: Mysqlx_address
VARIABLE_VALUE: ::
SCOPE: SESSION
*************************** 2\. row ***************************
VARIABLE_NAME: Mysqlx_bytes_received
VARIABLE_VALUE: 1002
SCOPE: SESSION
*************************** 3\. row ***************************
VARIABLE_NAME: Mysqlx_bytes_sent
VARIABLE_VALUE: 8851
SCOPE: SESSION
*************************** 4\. row ***************************
VARIABLE_NAME: Mysqlx_connection_accept_errors
VARIABLE_VALUE: 0
SCOPE: SESSION
*************************** 5\. row ***************************
VARIABLE_NAME: Mysqlx_connection_errors
VARIABLE_VALUE: 0
SCOPE: SESSION
...
*************************** 119\. row ***************************
VARIABLE_NAME: Mysqlx_worker_threads
VARIABLE_VALUE: 2
SCOPE: GLOBAL
*************************** 120\. row ***************************
VARIABLE_NAME: Mysqlx_worker_threads_active
VARIABLE_VALUE: 1
SCOPE: GLOBAL
120 rows in set (0.00 sec)
Listing 6-3X Plugin Status Variables with Scope
请注意,我们看到了相同的变量及其范围。
Note
使用性能模式的完整描述和教程超出了本书的范围。有关性能模式的更多信息,请参见在线 MySQL 参考手册中的“MySQL 性能模式”一节。
您可能已经注意到,在前面的例子中,我使用了SHOW
SQL 命令来查看变量的值。有两个SHOW
命令:一个用于系统变量(SHOW VARIABLES
,另一个用于状态变量(SHOW STATUS
)。您可以使用 LIKE 子句来查找所有的 X 插件变量。LIKE
子句允许您指定名称的一部分并使用通配符。例如,您可以使用以下两个命令找到 X 插件的所有系统和状态变量。
SHOW VARIABLES LIKE 'mysqlx_%';
SHOW STATUS LIKE 'mysqlx_%';
注意,我使用了使用mysqlx_%
的LIKE
子句。这将显示所有以mysqlx_
开头的变量。因为所有的 X 插件变量都有这个前缀,所以我们看到了 X 插件的所有变量。
Tip
LIKE 子句在另一方面也非常方便。您可以使用它来搜索一个变量,您可能只是通过使用一个关键字就忘记了它的名称。例如,如果您想查看名称中包含dir
的所有变量,请使用LIKE '%dir%'
。
到目前为止,您可能认为我们正在使用大量的 SQL 命令。您可能想知道是否有办法使用 NoSQL 接口查看变量值。在撰写本文时,X DevAPI 或 MySQL Shell 的一部分中还没有可以用来获取变量及其值的信息的对象。 2 这就是我之前在书中提到的一些日常维护任务仍然需要 SQL 接口的原因。检查和设置变量是需要使用 SQL 命令的维护和配置任务之一。
What About Information_Schema?
如果您熟悉特殊的INFORMATION_SCHEMA
数据库,您可能想知道使用 session_和 global_表(视图)来显示变量值发生了什么。从服务器版本 5.7.6 开始,这些表(视图)已被弃用。这是因为在PERFORMANCE_SCHEMA
中它们被表格(视图)取代了。有关变更和迁移到PERFORMANCE_SCHEMA
的更多信息,请参见在线 MySQL 参考手册中的“迁移到性能模式系统和状态变量表”一节。
如何设置变量的值
我们已经发现可以在配置文件中设置系统变量,并且可以使用启动选项来设置系统变量。这些方法用于只能在启动时设置的变量。但是,对于那些可以动态设置的变量,您可以使用 set 命令和前面显示的@@SESSION 和@@GLOBAL 符号来更改它们在会话或全局范围内的值。然而,因为目前没有会话变量,我们只能为全局变量设置值,如清单 6-4 所示。
$ mysqlsh -uroot -hlocalhost --sql --json=pretty -e "SELECT @@GLOBAL.mysqlx_connect_timeout"
{
"password": "Enter password: "
}
{
"executionTime": "0.00 sec",
"warningCount": 0,
"warnings": [],
"rows": [
{
"@@GLOBAL.mysqlx_connect_timeout": 30
}
],
"hasData": true,
"affectedRowCount": 0,
"autoIncrementValue": 0
}
$ mysqlsh -uroot -hlocalhost --sql --json=pretty -e "SET @@GLOBAL.mysqlx_connect_timeout = 90"
{
"password": "Enter password: "
}
{
"executionTime": "0.00 sec",
"warningCount": 0,
"warnings": [],
"rows": [],
"hasData": false,
"affectedRowCount": 0,
"autoIncrementValue": 0
}
$ mysqlsh -uroot -hlocalhost --sql --json=pretty -e "SELECT @@GLOBAL.mysqlx_connect_timeout"
{
"password": "Enter password: "
}
{
"executionTime": "0.00 sec",
"warningCount": 0,
"warnings": [],
"rows": [
{
"@@GLOBAL.mysqlx_connect_timeout": 90
}
],
"hasData": true,
"affectedRowCount": 0,
"autoIncrementValue": 0
}
Listing 6-4Setting Global System Variables
如果引入了会话动态系统变量,可以用SET @@SESSION.<variable_name>
命令设置它们的值。
Tip
可以在运行时更改的系统变量称为动态变量。这只适用于那些在 X 插件运行时可以改变的系统变量。
现在我们知道了更多关于变量以及如何查看和设置值的知识,让我们看看 X 插件的具体变量。让我们从那些可以放在配置文件中的系统变量开始。
系统变量和启动选项
回想一下,大多数系统变量都有一个相应的选项,可以用来在启动时配置系统。也就是说,我们称可以用这种方式设置的系统变量为启动选项。其他系统变量可以在运行时更改,通常称为动态系统变量。但是,有些变量只能在配置文件或命令行中使用。正如您所猜测的,一些变量可以用作启动选项。表 6-1 列出了那些可以用作 X 插件启动选项的系统变量(也是系统变量)。我还包括哪些变量可以动态设置,以及对每个变量的简短描述。
表 6-1
System Variables and Startup Options (X Plugin)
| 名字 | 默认 | 系统瓦尔 | 动态的 | 描述 | | :-- | :-- | :-- | :-- | :-- | | `mysqlx_bind_address` | * | 是 | 不 | X 插件用于连接的网络地址。 | | `mysqlx_connect_timeout` | Thirty | 是 | 是 | 等待从新连接的客户端接收第一个数据包的秒数 | | `mysqlx_idle_worker_thread_timeout` | Sixty | 不 | 不 | 空闲工作线程终止之前的时间(秒) | | `mysqlx_max_allowed_packet` | One million forty-eight thousand five hundred and seventy-six | 不 | 是 | X 插件可以处理的网络数据包的最大大小。 | | `mysqlx_max_connections` | One hundred | 是 | 是 | X 插件可以接受的最大并发客户端连接数。 | | `mysqlx_min_worker_threads` | Two | 不 | 是 | X 插件用于处理客户端请求的最小工作线程数。 | | `mysqlx_` `port` | Thirty-three thousand and sixty | 是 | 不 | 指定 x 插件监听连接的端口 | | `mysqlx_port_open_timeout` | Zero | 是 | 不 | X 插件等待 TCP/IP 端口空闲的时间(秒)。 | | `mysqlx_socket` | 依赖于平台 | 是 | 不 | X 插件监听连接的套接字。 | | `mysqlx_ssl_ca` | | 是 | 不 | 包含可信 SSL CAs 列表的文件的路径。 | | `mysqlx_ssl_capath` | | 是 | 不 | 包含 PEM 格式的可信 SSL CA 证书的目录的路径。 | | `mysqlx_ssl_cert` | | 是 | 不 | 用于建立安全连接的 SSL 证书文件的名称。 | | `mysqlx_ssl_cipher` | | 不 | 不 | 允许用于 SSL 加密的密码列表。 | | `mysqlx_ssl_crl` | | 是 | 不 | 包含 PEM 格式的证书吊销列表的文件路径。 | | `mysqlx_ssl_crl_path` | | 是 | 不 | 包含文件的目录路径,这些文件包含 PEM 格式的证书吊销列表。 | | `mysqlx_ssl_key` | | 是 | 不 | 用于建立安全连接的 SSL 密钥文件的名称。 |正如你所看到的,我们可以为 X 插件设置很多东西,包括设置 SSL 连接,调整 X 插件的最大连接数限制,最小工作线程数,甚至设置数据包的大小(一个数据包中可以通过网络发送多少数据)。当然,我们也可以改变 X 插件使用的端口。
状态变量
召回系统变量是那些只报告插件的统计数据和其他数据的变量。状态变量不能在运行时设置。但是,每当服务器重新启动时,大多数都会被重置。也就是说,计数器会在重新启动时重置。
X 插件有相当多的状态变量来报告 X 插件中的几个区域。我们不是单独查看状态变量(如果算上会话和全局范围,有 120 多个),而是查看状态变量报告的组或区域。我们将在下一节看到更多关于特定状态变量的内容,在下一节我们将看到如何监控 X 插件。
下面列出了一些更常见的状态变量,并简要说明了为什么要检查这些值。符号mysqlx_*
表示包含多个变量的区域的状态变量。比如mysqlx_bytes_*
包括mysqlx_bytes_sent
和mysqlx_bytes_received
。
mysqlx_connections_*
:接受、拒绝和关闭的连接数。mysqlx_sessions_*
:统计已接受、已关闭、已终止、已拒绝等会话。mysqlx_stmt_*
:集合的执行、删除、列表、创建统计。
您可能想要检查一些其他的离散状态变量,包括启动时的错误(mysqlx_init_error
)和发送到客户端的行数(mysqlx_rows_sent
)。关于 X 插件可用状态变量的完整列表,请参见在线 MySQL 参考手册中的“X 插件的状态变量”一节。
现在让我们简单地看看你可以监控 X 插件的一些方法,以及你为什么要这么做。
监控 X 插件
如果您想监控 X 插件以确保一切正常工作、诊断问题、验证配置或调整性能,您可以使用 X 插件的系统变量来监控 X 插件。这需要在特定时间或事件发生时读取值。回想一下,有些状态变量同时具有会话和全局作用域。因此,您可能希望使用前面讨论过的@@符号来查询会话或全局范围值。
您可以通过几种方式查看状态变量的值,包括使用SHOW STATUS
命令以及从PERFORMANCE_SCHEMA
数据库中读取表格(视图)。清单 6-5 显示了可用于读取状态变量值的表格(视图)。
$ mysqlsh -uroot -hlocalhost --sql -e "SHOW TABLES FROM PERFORMANCE_SCHEMA LIKE '%status%'"
Enter password:
+-------------------------------------------+
| Tables_in_performance_schema (%status%) |
+-------------------------------------------+
| global_status |
| replication_applier_status |
| replication_applier_status_by_coordinator |
| replication_applier_status_by_worker |
| replication_connection_status |
| session_status |
| status_by_account |
| status_by_host |
| status_by_thread |
| status_by_user |
+-------------------------------------------+
Listing 6-5Performance Schema Views for Status Variables
请注意,有一些状态变量的表(视图),包括复制和按范围的表。只要记住在查询 X 插件的状态变量时使用LIKE
子句。然而,正如我前面提到的,使用性能模式的完整教程超出了本书的范围。幸运的是,带有@@
符号的SHOW STATUS
和SELECT
SQL 命令对于大多数应用来说足够好了。 3
尽管 X 插件有很多状态变量,但是状态变量可以组织在几个区域中。下面的列表总结了我定义的类别。
- 通信:关于发送和接收的消息和数据的信息。
- 连接:关于连接的信息,包括接受、拒绝和删除。
- CRUD 操作:创建、读取、更新和删除操作的统计数据。
- 错误和警告:关于启动时或发送到客户端的错误或警告的信息。
- 会话:关于会话的信息,包括接受、拒绝和删除。
- SSL:关于安全连接的信息。
- 语句:关于文档存储的执行、创建等的统计信息。
- Worker threads:关于 X 插件中工作线程的信息。
以下部分更详细地描述了这八个方面,包括您可能希望使用变量执行的任务的建议。每个部分还包括相关状态变量的完整列表、它们的范围和简短描述。您可以在诊断过程中使用这些章节作为探索 X 插件的指南,或者只是出于好奇。
沟通
通信类别包括状态变量,这些变量报告发送到客户端或从客户端接收的信息。您可以观察网络上一个会话或全局的通信量,查看发送到客户端的会话和全局的行数,并检查 X 协议的期望块。
当管道中有可能失败的消息时,X 协议使用期望块机制来管理情况。即在块结束之前执行的其他相关任务。期望块是确保整个块安全、可靠地失败的一种方式(想想事务)。预期障碍有几个方面,不太可能要求你去监控它们。如果您想了解更多关于期望块的信息,请参见 https://dev.mysql.com/doc/internals/en/x-protocol-expect-expectations.html
。
表 6-2 列出了通信类别的所有状态变量。
表 6-2
Communication Status Variables (X Plugin)
| 变量 | 范围 | 描述 | | :-- | :-- | :-- | | `mysqlx_bytes_received` | 两者 | 通过网络接收的字节数。 | | `mysqlx_bytes_sent` | 两者 | 通过网络发送的字节数。 | | `mysqlx_expect_close` | 两者 | 关闭的期望块数。 | | `mysqlx_expect_open` | 两者 | 打开的期望块数。 | | `mysqlx_rows_sent` | 两者 | 发送回客户端的行数。 |您可能希望使用这些状态变量的任务类型包括观察发送和接收了多少数据,以及向客户端发送了多少行(在结果集中)。你也可以看到期望块数据,但是这可能比大多数监控 X 插件时需要的更高级。
连接
连接类别包括用于检查连接状态的状态变量。您可以使用连接错误变量来查看有多少连接出现了错误。这些变量同时具有会话和全局作用域,这使得它们对于诊断单个连接问题很有意义。您还可以看到接受(打开)、关闭和拒绝(由于登录失败、权限不足、密码错误等)的连接数的统计数据。).这些状态变量只有全局范围,因此它们只显示所有连接的聚合。表 6-3 列出了连接类别的所有状态变量。
表 6-3
Connection Status Variables (X Plugin)
| 变量 | 范围 | 描述 | | :-- | :-- | :-- | | `mysqlx_connection_accept_errors` | 两者 | 导致接受错误的连接数。 | | `mysqlx_connection_errors` | 两者 | 导致错误的连接数。 | | `mysqlx_connections_accepted` | 全球的 | 已被接受的连接数。 | | `mysqlx_connections_closed` | 全球的 | 已关闭的连接数。 | | `mysqlx_connections_rejected` | 全球的 | 被拒绝的连接数。 |您可能希望使用这些状态变量的任务类型包括监控连接错误状态变量,以防出现大量失败(错误)。这可能是简单的应用使用了错误的凭据,也可能是恶意的尝试发现登录帐户和密码。
还可以使用 accepted、closed 和 rejected 系统变量来监控使用的连接数。也就是说,如果使用您的应用的用户少于 10 个,您将会看到这些状态变量的值相当低。高数值可能表示应用连接和断开太频繁(不总是一件坏事),或者应用的实例比您想象的要多。
CRUD 操作
CRUD 操作类别提供了文档存储上的创建、读取(查找)、更新和删除操作的统计信息。注意,这些是用于 X DevAPI 的计数器,而不是专门用于 SQL 语句执行的。您可以在会话或全局范围内看到每个 CRUD 操作的值。表 6-4 列出了 CRUD 操作类别的所有状态变量。
表 6-4
CRUD Status Variables (X Plugin)
| 变量 | 范围 | 描述 | | :-- | :-- | :-- | | `mysqlx_crud_create_view` | 两者 | 收到的 create view 请求数。 | | `mysqlx_crud_delete` | 两者 | 收到的删除请求数。 | | `mysqlx_crud_drop_view` | 两者 | 收到的删除视图请求数。 | | `mysqlx_crud_find` | 两者 | 收到的查找请求数。 | | `mysqlx_crud_insert` | 两者 | 收到的插入请求数。 | | `mysqlx_crud_modify_view` | 两者 | 收到的修改视图请求数。 | | `mysqlx_crud_update` | 两者 | 收到的更新请求数。 |您可能希望使用这些状态变量的任务类型包括监控文档存储应用的活动,例如发出了多少个删除请求、添加(插入)了多少个新数据项等等。因为状态变量具有会话和全局作用域,所以您可以看到特定会话的活动,并将其与全局作用域的值进行比较(总体统计)。
错误和警告
“错误和警告”类别提供了一种查看启动时发生的错误数量以及发送给客户端的通知或错误的方法。此类别中的所有状态变量都具有会话和全局范围,因此可用于检查单个连接(会话)的统计数据或所有会话的聚合值。
通知是 X 协议在会话或全局范围向客户机发送附加信息的一种方式。当在会话级别(在内部手册中称为本地)发送时,它们可以包括提交的事务标识符、事务状态更改、SQL 警告和变量更改的列表。在全局级别发送时,可能包括服务器关闭、组复制中的连接断开、表删除等。请记住,状态变量只是计数器,因此尽管您看不到消息(通知)本身,但您可以看到发送了多少消息,以及它们是信息性的(警告)还是对错误或其他严重事件的响应。有关 X 协议中通知的更多信息,请参见 http://dev.mysql.com/doc/internals/en/x-protocol-notices-notices.html
。
表 6-5 列出了错误和警告类别的所有状态变量。
表 6-5
Errors and Warnings Status Variables (X Plugin)
| 变量 | 范围 | 描述 | | :-- | :-- | :-- | | `mysqlx_errors_sent` | 两者 | 发送到客户端的错误数。 | | `mysqlx_init_error` | 两者 | 初始化过程中的错误数。 | | `mysqlx_notice_other_sent` | 两者 | 发送回客户端的其他类型通知的数量。 | | `mysqlx_notice_warning_sent` | 两者 | 发送回客户端的警告通知的数量。 |您可能希望使用这些状态变量的任务类型包括检查会话是否有过多的错误,这可能表明应用(或用户的使用)有问题。通知状态变量可能有助于收集数据,用于诊断发送给客户端的错误和警告。也就是说,它可能表示您可能希望在日志中查找其他数据。例如,这些变量在会话级别的高计数可能表明应用正在尝试做一些它不应该做的事情,或者执行操作过于频繁。
然而,当开始使用 X 插件或改变其配置时,这一类别中最重要的状态变量是mysqlx_init_error
状态变量。检查这个变量以确保在启动(初始化)时没有错误,如果有问题,跟踪它们以确保所有的配置都是正确的。虽然有时一个错误可能是好的,但一般来说,您不应该看到任何为初始化而注册的错误。
会议
会话类别提供了一种方法来跟踪有多少会话已被创建(接受)、关闭、由于错误导致关闭、被随意终止或由于登录或建立会话时的其他错误而被拒绝。所有可用的状态变量都只有全局范围。表 6-6 列出了会话类别的所有状态变量。
表 6-6
Session Status Variables (X Plugin)
| 变量 | 范围 | 描述 | | :-- | :-- | :-- | | `mysqlx_sessions` | 全球的 | 已打开的会话数。 | | `mysqlx_sessions_accepted` | 全球的 | 已被接受的会话尝试次数。 | | `mysqlx_sessions_closed` | 全球的 | 已关闭的会话数。 | | `mysqlx_sessions_fatal_error` | 全球的 | 因致命错误而关闭的会话数。 | | `mysqlx_sessions_killed` | 全球的 | 已被终止的会话数。 | | `mysqlx_sessions_rejected` | 全球的 | 被拒绝的会话尝试次数。 |您可能希望使用这些状态变量的任务类型包括检查有多少会话失败(msyqlx_sessions_fatal_error
)、被管理员之类的人终止(mysqlx_sessions_killed
),以及有多少会话成功打开或关闭。与连接尝试一样,您可以使用该类别中的状态变量来监控创建和使用会话的频率和数量。太多可能意味着会话比您最初计划的要多,广泛使用增加了,等等。每当您发现或认为创建会话可能有问题时,或者当会话开始频繁失败时,请检查这些状态变量。
加密套接字协议层
SSL 类别是最大的类别之一,包括许多用于监控安全连接的状态变量。这一点非常重要,因为信息技术专家必须保持持续的警惕,以保护系统和数据不被意外使用、误用或利用。如果您决定使用 SSL 连接,您将需要检查这些状态变量,以确保您的 SSL 连接设置正常工作。您可以检查证书状态的有效性,查看密码列表、使用的 SSL 版本等等。表 6-7 列出了 SSL 类别的所有状态变量。
表 6-7
SSL Status Variables (X Plugin)
| 变量 | 范围 | 描述 | | :-- | :-- | :-- | | `mysqlx_ssl_accepts` | 全球的 | 接受的 SSL 连接数 | | `mysqlx_ssl_active` | 两者 | 如果 SSL 处于活动状态 | | `mysqlx_ssl_cipher` | 两者 | 当前的 SSL 密码(对于非 SSL 连接为空) | | `mysqlx_ssl_cipher_list` | 两者 | 可能的 SSL 密码列表(非 SSL 连接为空) | | `mysqlx_ssl_ctx_verify_depth` | 两者 | ctx 中当前设置的证书验证深度限制 | | `mysqlx_ssl_ctx_verify_mode` | 两者 | ctx 中当前设置的证书验证模式 | | `mysqlx_ssl_finished_accepts` | 全球的 | 与服务器的成功 SSL 连接数 | | `mysqlx_ssl_server_not_after` | 全球的 | SSL 证书有效的最后日期 | | `mysqlx_ssl_server_not_before` | 全球的 | SSL 证书有效的第一个日期 | | `mysqlx_ssl_verify_depth` | 全球的 | SSL 连接的证书验证深度 | | `mysqlx_ssl_verify_mode` | 全球的 | SSL 连接的证书验证模式 | | `mysqlx_ssl_version` | 两者 | 用于连接 ssl 的协议的名称 |您可能希望使用这些状态变量的任务类型包括检查以确保为一个会话或所有会话打开 SSL(mysqlx_ssl_active
)、查看接受的 SSL 连接数(mysqlx_ssl_finished_accepts
)以及有效 SSL 证书的日期。这最后一个操作可以把你从一大堆兔子洞诊断 4 追着奇怪的错误信息中解救出来。
请注意,有些变量同时具有会话和全局作用域,因此您可以使用这些变量来帮助在会话级别诊断 SSL 连接问题。例如,如果客户端无法使用 SSL 正确连接到 X 插件,需要很长时间才能连接,或者在连接过程中出现错误。
有关这些状态变量的更多信息,可以参阅在线 MySQL 参考手册中的“使用安全连接”一节。因为这些状态变量中的大多数与服务器使用的相同,所以应用了相同的技术和描述。
声明
语句类别是一个非常有趣的类别,在诊断或观察与 X DevAPI 相关的操作时非常方便。特别是,有一些状态变量可以计算集合创建和删除的数量、集合索引、执行事件的数量、列出客户端的数量等等。
回想一下,在 X DevAPI 的说法中,语句是一个执行一个或多个 CRUD 操作的动作。尽管 CRUD 操作是这类状态变量的主要焦点,但我们也将该术语用于 SQL 命令,SQL 语句也有状态变量。可用的状态变量具有会话和全局作用域,因此它们可用于监控会话活动或聚合详细信息。表 6-8 列出了声明类别的所有状态变量。
表 6-8
Statement Status Variables (X Plugin)
| 变量 | 范围 | 描述 | | :-- | :-- | :-- | | `mysqlx_stmt_create_collection` | 两者 | 收到的 create collection 语句数。 | | `mysqlx_stmt_create_collection_index` | 两者 | 收到的 create collection index 语句的数目。 | | `mysqlx_stmt_disable_notices` | 两者 | 收到的禁用通知语句的数量。 | | `mysqlx_stmt_drop_collection` | 两者 | 收到的 drop collection 语句数。 | | `mysqlx_stmt_drop_collection_index` | 两者 | 收到的删除集合索引语句的数目。 | | `mysqlx_stmt_enable_notices` | 两者 | 收到的启用通知语句数。 | | `mysqlx_stmt_ensure_collection` | 两者 | 收到的确保集合语句的数量。 | | `mysqlx_stmt_execute_mysqlx` | 两者 | 命名空间设置为 mysqlx 时接收的 StmtExecute 消息数。 | | `mysqlx_stmt_execute_sql` | 两者 | 为 SQL 命名空间接收的 StmtExecute 请求数。 | | `mysqlx_stmt_execute_xplugin` | 两者 | 为 X 插件命名空间接收的 StmtExecute 请求数。 | | `mysqlx_stmt_kill_client` | 两者 | 收到的 kill client 语句数。 | | `mysqlx_stmt_list_clients` | 两者 | 收到的 list client 语句数。 | | `mysqlx_stmt_list_notices` | 两者 | 收到的列表通知语句的数量。 | | `mysqlx_stmt_list_objects` | 两者 | 收到的 list object 语句数。 | | `mysqlx_stmt_ping` | 两者 | 收到的 ping 语句数。 |您可能希望使用这些状态变量的任务类型包括监控文档存储以创建和删除集合及相关索引。如果您正在监控文档存储应用如何使用集合,这可能会有所帮助。也就是说,频繁的集合创建可能表明数据没有被经常保存或者是动态生成的。这可能会让您发现改进应用使用数据方式的方法。
其他任务包括监控通知(消息)、发送客户机终止请求的次数(不一定成功执行),以及列出通知、客户机和对象。这些状态变量中的大多数都超出了正常监控的范围。事实上,这些状态变量中的一些只是在文档中被简单地引用,除了源代码本身,很少在其他地方被引用。
最后一个可能有用的状态变量是mysqlx_stmt_ping
状态变量,用于查看客户机检查服务器的次数,以确定它是否处于活动状态。此处的高值可能表示潜在的网络连接问题。
工作线程
工作线程是 X 插件用来执行任务的线程。该类别中只有两个状态变量,允许您查看可用工作线程的总数(仅限全局)和当前活动线程的数量(也仅限全局)。您可以使用系统变量mysqlx_min_worker_threads
增加工作线程的最小数量。表 6-9 列出了线程类别的所有状态变量。
表 6-9
Worker Threads Status Variables (X Plugin)
| 变量 | 范围 | 描述 | | :-- | :-- | :-- | | `mysqlx_worker_threads` | 全球的 | 可用的工作线程数 | | `mysqlx_worker_threads_active` | 全球的 | 当前使用的工作线程数。 |您可能希望使用这些状态变量的任务类型包括当存在与较慢的执行有关的性能问题时。如果活动的工作线程数量超过了系统可以处理的数量,或者没有足够的工作线程用于所有连接和任务执行请求,就会发生这种情况。
随着 X 插件的成熟,可能会有更多的任务需要您去执行,比如诊断问题、调整性能,或者简单地配置插件。如果您对监控 X 插件感兴趣,请务必查看在线 MySQL 参考手册,因为 MySQL 8 的每个新版本都会发布状态变量的更新以及监控 X 插件的任务。
摘要
X 插件是 MySQL 服务器的扩展,可以动态加载。这非常重要,因为 X 插件启用了文档存储特性,允许存储和检索 JSON 文档。具体来说,X 插件允许服务器和客户机之间使用 X 协议进行通信,并与 X DevAPI 进行交互,以允许符合 ACID 的存储。此外,使用 X DevAPI,您可以使用类似 NoSQL 的语法对文档存储执行 CRUD 操作。正是 X 插件将所有的功能联系在一起,将 MySQL 服务器变成了一个文档库。
在这一章中,我们学习了更多关于 X 插件及其工作原理。特别是,我们看到了如何配置 X 插件,比如改变端口和通过 SSL 启用独立于服务器的安全连接。我们还发现了其他系统变量以及一长串状态变量,您可以用它们来监控 X 插件。最后,我们发现了一些关于 X 插件的有趣的内部事实,比如它是如何注册系统变量的。
如果您仍然对 X 插件及其内部工作方式感到好奇,那么没有比源代码本身更好的文档了。虽然对门外汉来说可能不太容易,但研究源代码就像阅读希腊原著一样。
在下一章中,我将仔细研究新的 X 协议是如何工作的,包括服务器如何与客户机交换数据包。正如您将看到的,它与旧协议有很大不同。这主要是由于用于设计和实现新协议的构件。
Footnotes 1
应该注意的是,MySQL 复制使用了内置于原始协议中的扩展。
如果我们有这样的对象,它将使与服务器的交互更加容易。
有些人可能会说 SQL 命令更容易使用。
我称之为兔子洞诊断,因为它经常令人沮丧,很少导致正确的诊断。SSL 证书过期就是其中一个原因。
源代码是用 C++写的,而且是真正的 C++形式(可悲的是)代码几乎没有内联文档。
七、X 协议
X 协议代表了 MySQL 中第一个与现有的客户机/服务器协议的重大偏离。X 协议被设计成可扩展的,最大化安全性,并确保良好的性能。当 X 协议被设计时,这三个类别都是必须具备的特性和需求的首要条件。
尽管 X 协议主要被包装(实现它)的客户端(如 X 插件和数据库连接器)隐藏在一个抽象层之后,但如果您计划使用 X 协议实现自己的应用,了解它的工作方式是很重要的。我们将在第 8 章和第 9 章中介绍。即使您从未打算开发 MySQL 客户端,仔细研究 X 协议也会发现并进一步强调 MySQL 8 中技术飞跃的一个例子。
在这一章中,我们将探索 X 协议并发现它是如何工作的。我们还将了解如何通过数据库连接器开始使用 X 协议。我们看到了一些通过连接器/Python 库用 Python 编写小脚本与 X 协议交互的例子。让我们从 X 协议及其起源的详细概述开始。
Note
我提出了许多我们在前面章节中发现的概念,为了简洁起见,我只在需要清楚的地方重复了一些信息。
概观
如果您曾经编写过从头开始设计的通信协议,或者如果您不得不编写代码来实现通信协议,那么您会意识到以毫不动摇的精度处理数据交换的复杂性和严格要求。当从一个系统到另一个系统交换消息时,根本没有“足够好”的质量。发送到另一个系统或从另一个系统接收的数据必须按照约定的格式进行安排,包括数据对齐(先进行什么)和数据表示方式(编码)。如果做得不好,可能会导致灾难。
较老的客户机/服务器 MySQL 协议是从头开始设计的通信协议的一个很好的例子。虽然它已经使用了几十年,只有相对较小的变化,但有一段时间它限制了 MySQL 工程师。由于旧的客户机/服务器协议不可扩展,他们在尝试实现新功能时反复挣扎。
然而,在协议的发展过程中,添加新功能并不是唯一需要处理的问题。对于 MySQL 中的客户机/服务器协议,安全性是一个主要问题。尽管 SSL 扩展被添加到协议中,但默认情况下并不强制实施安全性。也就是说,除了登录密码的交换之外,客户端/服务器消息不需要加密。因此,如果没有启用 SSL 或其他形式的加密,有人就有可能发现发送到服务器或从服务器接收的数据。
性能是为特定的、有限的命令和消息集设计的现有协议可能受到影响的另一个方面。也就是说,更新的技术已经表明,如果使用类似流水线的技术来设计协议交换,则有可能实现更好的性能。
将这些特性添加到现有的客户机/服务器协议中是不可行的。更具体地说,工程师们知道,要扩展客户机/服务器协议,每个系统(客户机、应用、服务器等)。)必须被更新或修改以与新的扩展一起工作。这很严重,因为你不能指望 MySQL 的每个用户都突然更新他们的 MySQL 工具、定制应用、脚本等的每个版本,以符合协议的新扩展。由于这个原因和许多类似的原因,在过去,改变客户机/服务器协议是被禁止的,并且仅限于那些确保现有客户机尽管改变也能继续工作的改变。
尽管有这样的要求,在客户机/服务器协议的发展过程中还是有一些小的变化。最近一次发生在 5.7 版本开发发行期间,涉及 Ok 消息的返回。但即使是这种微小的变化也是为了确保向后兼容性。迄今为止,客户机/服务器协议继续支持 Ok 前和 Ok 后的消息协议更改。这是长期通信协议的祸根:总是不得不以牺牲进步为代价来保持某种程度的向后兼容性。
当工程师们开始设计 MySQL 中现在的文档存储库,包括新的 MySQL Shell、X Plugin 和 X DevAPI 时,很明显是时候实现一个可以增强新特性的新协议了。更具体地说,很明显,现有的客户机/服务器协议不足以满足 MySQL 8 特性和产品的所有目标。因此,我们需要一个新的协议,它被称为 X 协议,以遵循新的命名约定。 1
X 协议已经集成到大多数 MySQL 产品套件中,包括以下产品。我提供了一个链接,可以下载列出的每一个产品。注意,这里包含了几个数据库连接器(使用客户机/服务器协议或 X 协议与 MySQL 服务器交互的特定语言的库)。未来寻找更多实现 X 协议的产品。
- x 插件:集成在 MySQL 服务器中( https://
dev.mysql.com/downloads/mysql/
) - shell:8 . 0 . 4 或更高版本(
https://dev.mysql.com/downloads/shell/
) - 连接器/J:8 . 0 . 8 及以后版本(
https://dev.mysql.com/downloads/connector/j/
) - 连接器/网络:8.0.8 及以后版本
(
https://dev.mysql.com/downloads/connector/net/
) - connector/node . js:8 . 0 . 8 及更高版本(
https://dev.mysql.com/downloads/connector/nodejs/
) - 连接器/Python:8 . 0 . 5 及更高版本(
https://dev.mysql.com/downloads/connector/python/
)
Note
连接器产品通常缩写为 C/J、C/Net、C/Node.js 和 C/Py。
在后面的章节中,我们将会看到一个连接器/Python 连接器如何实现和公开 X 协议的例子。现在让我们看看开发和实现 X 协议的目标和动机。
X 协议的目标
如上所述,X 协议旨在解决的三个主要领域(称为设计约束或简称目标)包括可扩展性、安全性和性能。接下来的几个部分展示了 X 协议的三个主要设计约束的一些驱动力。
Tip
如果你想看一些用于设计 X 协议的实际工程文档,请看 http://dev.mysql.com/worklog/task/?id=8639
项目的工作日志 2 。
What About the Client/Server Protocol?
你可能想知道 X 协议是否只适用于所有的 X。也就是说,它不适用于旧的协议。答案是 X 协议也支持客户机/服务器协议。这就是 MySQL Shell 不需要使用中间库就可以连接到旧服务器的方式。更具体地说,X 协议包括一个使用旧的客户机/服务器协议进行通信的选项。
展开性
当软件被称为具有可扩展性的目标时,它意味着软件必须能够被修改以添加新的特性,而不需要大的返工或重组。尽管组织可能对返工的含义有稍微不同的定义或示例,但在客户端/服务器协议的情况下,它是不可扩展的,因为在不对代码进行重大更改的情况下,扩展协议以包括新消息、命令和数据的空间非常小,并且可能与旧产品不兼容。
工程师们希望确保新协议从一开始就考虑到可扩展性。在这种情况下,可扩展性包括添加功能和特性的能力,而不会导致现有产品失败或返工以适应变化。
X 协议需要可扩展性的一些领域包括能够添加新的消息、添加新的特性(例如,确保协议支持诸如流水线之类的东西以减少往返行程)、允许添加新的认证机制、改变或添加新的加密和压缩设施等等。
安全性
在这个物联网的现代世界中,随着现代文明人口的快速增长,系统越来越安全变得越来越重要。也就是说,提供最佳选项来保护数据和用户免受意外或故意的利用。
Tip
有关物联网和 MySQL 的更多信息,请参见我的书《物联网的 MySQL》,查尔斯·贝尔(Apress 2016) https://www.apress.com/us/book/9781484212943
。
甲骨文的工程师非常重视安全性。事实上,它是几乎所有设计、评审和质量控制机制的关键方面。在甲骨文公司,安全性至关重要。因此,当开发新协议时,安全机制比客户机/服务器协议有了很大的改进。特别地,X 协议中的安全缺省仅使用可信的、经过验证的标准,例如传输层安全(TLS) 3 和简单认证和安全层(SASL)。 4
表演
与安全性一样,性能是 Oracle 用来评估产品质量的另一个关键领域。在这种情况下,性能必须使系统能够适当地执行其任务,而没有不必要的等待时间、延迟或长时间运行的任务。与安全性不同,性能通常是以主观和轶事的方式进行评估的。也就是说,新版本的运行速度不能慢于以前的版本。
在 X 协议的情况下,通过使用可靠的基础技术和利用诸如流水线之类的特性来确保性能目标,流水线允许一次传递多个消息,减少往返次数(往返于服务器和客户端),并且在发送多个命令时不等待来自服务器的响应,从而不占用客户端来等待响应。
在下一节中,我们将通过研究设计的基础来了解 X 协议的基础。
x 协议和协议缓冲区
MySQL 工程师想要克服的最大问题之一是从头开始开发协议机制的各个方面需要很长的时间。特别是,工程师们希望利用成熟的、记录良好的、卓越的技术。毕竟,创建一个可扩展的、安全的、高性能的通信协议的问题已经被许多人解决了,并取得了不同程度的成功。
虽然对几种选择进行了评估和讨论,但重要的是该技术必须是成熟的和开源的。此外,该技术必须支持快速实施,很少或没有第三方依赖性,独立于语言和平台,并且不需要重新装备开发工具和流程来使用它。
被选中的技术叫做谷歌的协议缓冲区( https://developers.google.com/protocol-buffers/
)。Google Protocol Buffers 被亲切地命名为 protobuf,它是一个可扩展的、独立于语言和平台的机制,用于序列化结构化数据。它是为速度、紧凑和简单而设计的。Protobuf 允许您快速轻松地定义消息交换协议。在这方面,protobuf 与 XML 和其他变体有点类似。Protobuf 可用于多种语言,包括 C++、C#、Go、Java 和 Python。最新版本的 protobuf(版本 3)支持其他语言,比如 Ruby。
然而,这种意义上的语言支持意味着有一个编译器选项可以将 protobuf 定义文件翻译成该语言可以使用的特定于语言的代码。例如,要在 C++中使用 protobuf,您必须将 protobuf 定义文件从它们的本机 protobuf 定义编译成可由 C++编译器读取和编译的文件。
Protobuf 本质上是一种组织数据的方法,这样就可以用结构化的方式定义数据(称为消息)。也就是说,我们可以定义如何表示数据的精确集合。这允许你以约定的结构发送和接收数据。这听起来可能没什么大不了的,除非考虑到可扩展性方面,即使有消息的新版本,旧消息仍然有效。大多数语言都支持结构化数据机制,但具有不同程度的类型严格性。然而,这些很少是可扩展的,对结构的任何改变都会导致格式不兼容(大多数情况下)。Protobuf 旨在允许您扩展数据组织,而无需重新构建。
为了理解 protobuf 的强大,我们来看一个简短的例子。在这种情况下,我们将使用前面章节中联系人的 rolodex 示例的变体。我们需要两条消息(数据结构);存储联系人姓名和电话号码的方法(每个联系人可能不止一个),以及存储所有联系人的消息。正如您将看到的,这允许我们编写一些非常简单的代码来读写数据。
Note
虽然完整的 protobuf 教程已经超出了本书的范围,但是下面还是会给你一个 protobuf 的鸟瞰图。然而,如果您需要了解更多关于 protobuf 的信息,Google 已经提供了大量的文档。
安装 Protobuf 编译器
我们需要安装两件东西。我们必须安装 protobuf 编译器和 protobuf 库。
可以从 https://github.com/google/protobuf/releases/tag/v3.0.0
下载 protobuf 编译器。向下滚动到页面底部,下载与您的平台匹配的文件。大多数都是压缩文件的形式,你可以下载和解压缩。对于大多数平台,不需要安装。您可以从下载的 bin 文件夹中运行 protobuf 编译器(名为protoc
)。例如,我为 macOS 下载了名为protoc-3.0.2-osx-x86_64.zip
的文件,因此可以作为./protoc-3.0.2-osx-x86_64/bin/protoc
运行 protobuf 编译器。或者,你可以在你的路径中放置protoc
的位置。
有几种方法可以安装 protobuf 库。有关如何安装 protobuf 的说明,请参见 https://github.com/google/protobuf/#protobuf-runtime-installation
中针对您的语言的运行时安装说明。对于 Linux 和 macOS 平台,可以使用 PyPi (pip)安装 protobuf 库,如下所示。请注意,如果您使用提升的权限(例如 sudo)安装 pip,您可能需要指定 sudo 来安装 protobuf。
$ pip install protobuf
Collecting protobuf
Downloading protobuf-3.5.1-py2.py3-none-any.whl (388kB)
100% |█████████████████████| 389kB 1.0MB/s
Requirement already satisfied: setuptools in /System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python (from protobuf)
Requirement already satisfied: six>=1.9 in /Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/six-1.10.0-py2.7.egg (from protobuf)
Installing collected packages: protobuf
Successfully installed protobuf-3.5.1
Note
您还必须在系统上安装 Python。请参见 https://www.python.org/
在您的系统上下载并安装 Python。本章中的示例脚本是为 Python 版编写的,并可在 Python 版中正确执行。如果您使用的是 Python 3.0 或更高版本,您可能需要对代码进行微小的更改。
Protobuf 示例
让我们先来看看 protobuf 定义文件。Protobuf 文件以扩展名.proto
命名。我们将把我们的 protobuf 定义文件命名为contacts.proto
。清单 7-1 显示了 protobuf 文件contacts.proto
的内容。将这个文件放在一个文件夹中,因为我们将添加额外的文件来编译和测试 protobuf 定义。这是您将在其他文档中看到的标准示例模式——一个数据项定义后跟一个包含数据项的数组(或列表)。
syntax = "proto2";
message Contact {
required string first = 1;
required int32 id = 2;
optional string last = 3;
message PhoneNumber {
required string number = 1;
}
repeated PhoneNumber phones = 5;
}
message Contacts {
repeated Contact list = 1;
}
Listing 7-1Contacts Protobuf Definition
这里我们看到的代码看起来很像 C++。这不是偶然的,之所以选择它是因为几种语言使用相似的语法,这让大多数开发者都很熟悉。我们看到的第一行是 protobuf 编译器使用该语言版本 2(版本 3 是当前版本)的指令。MySQL 也使用版本 2。
在名为Contact
的第一条消息中,我们定义了两个必填字段,一个 id 和一个名字。Id
是一个整数,first
名是一个字符串。我们还可以为last
名称定义一个可选字段。在该消息中还有另一条名为PhoneNumber
的消息,它存储了电话number
的必填字段。但是,因为这是一条消息,所以我们添加了另一个名为phones
的字段来存储 0 个或多个电话号码。也就是说,重复声明表明它可以包含 0 个或多个消息。注意每个数据项的= N
。这是一个必需的标签,必须是唯一的。大多数人只是用一个从 1 开始的数字。最后,我们看到一个名为Contacts
的消息,我们存储了 0 个或多个名为list
的联系人。
要使用新的 protobuf 定义,我们必须编译它。对于这个例子,我将编译它以用于 Python。要使用的命令如下。这会生成一个名为contacts_pb2.py
的文件,我们可以在 Python 脚本中导入它。我们用选项--python_out
告诉编译器两件事:1)我们要为 Python 编译;以及 2)我们希望编译器的输出出现在当前文件夹(。).您将看不到该命令的任何附加输出—它都被写入文件。确保您的路径上有协议可执行文件的位置,或者使用如下所示的位置(路径)直接调用它。
$ protoc-3.0.2-osx-x86_64/bin/protoc --python_out=. contacts.proto
回想一下,protobuf 支持几种语言。下面列出了编译时支持的语言和使用的正确选项(<out dir>
是结果源文件的输出目录)。如您所见,有几个选项涵盖了当今使用的大多数编程语言。如果要用另一种编程语言实现此示例,请使用下面显示的适用于您的编程语言的选项。
- C++:
--cpp_out=<out_dir>
- C#
--csharp_out=<out_dir>
- Java:
--java_out=<out_dir>
- Java Nano
--javanano_out=<out_dir>
- JavaScript:
--js_out=<out_dir>
- 目标 C:
--objc_out=<out_dir>
- Python:
--python_out=<out_dir>
- 露比:
--ruby_out=<out_dir>
contacts_pb2.py 文件的内容不是很有趣。其实挺复杂的。更有趣的是我们如何使用新协议。因为这是用于存储联系人的数据结构,所以让我们编写一个脚本,使用新消息将几个联系人写入一个文件。清单 7-2 显示了一个简单的 Python 脚本,用于将两个联系人写入一个二进制文件。为什么是二进制?因为 protobuf 的设计允许我们在保留类型化(二进制)数据的同时快速轻松地序列化数据。和书中前面的例子一样,如果你不了解 Python,也不用太担心。这是一种非常简单的脚本语言(详见本章后面的边栏)。
import contacts_pb2
# Open the file
f = open("my_contacts", "wb")
# Create a contacts class instance
contacts = contacts_pb2.Contacts()
# Create a new contact message
new_contact = contacts.list.add()
new_contact.id = 90125
new_contact.first = "Andrew"
# Add phone numbers
phone_number = new_contact.phones.add()
phone_number.number = '212-555-1212'
phone_number = new_contact.phones.add()
phone_number.number = '212-555-1213'
# Create a new contact message
new_contact = contacts.list.add()
new_contact.id = 90126
new_contact.first = "William"
new_contact.last = "Edwards"
# Add phone numbers
phone_number = new_contact.phones.add()
phone_number.number = '301-555-1111'
phone_number = new_contact.phones.add()
phone_number.number = '301-555-3333'
# Write the data
f.write(contacts.SerializeToString())
# Close the file
f.close()
Listing 7-2Writing Contacts to a File (Protobuf Example)
我在这里使用了内联编码风格,而不是循环,向您展示了如何使用 protobuf 中的add()
方法添加新消息。但是,首先要注意,我们必须导入用 protobuf 编译器(contacts_pbs2
)创建的文件。然后我们为 protobuf 编译器生成的Contacts
类创建一个实例。回想一下,这是一个类型为Contact
的数组(列表)。当调用add()
方法时,我们得到一个Contact
结构的实例,我们可以使用字段名给它赋值。因此,我设置了 id、名字,然后通过引用名为phones
的嵌套消息创建一个新的电话号码结构并填充它来添加电话号码。请注意,每次想要添加新消息时,都必须调用add()
。最后,我使用SerializeToString()
方法序列化我在内存中构建的所有消息,并将其写入名为my_contacts
的文件。花一些时间通读代码,直到你理解它是如何工作的。
Tip
不要太担心次要的细节或改进代码的方法。我包含了示例代码来演示 protobuf,而不是使用 Python 来演示。我们将在后面的章节中看到更多关于 Python 的内容。
如果您想运行代码,创建一个名为write_contacts.py
的文件,输入代码,保存它,然后用下面的命令执行它。这里您也看不到任何输出,因为它创建了文件my_contacts
。
$ python ./write_contacts.py
如果您想知道这些数据在文件中是什么样子,下面显示了文件 my_contacts 的十六进制转储。注意,它确实是一个二进制文件。
$ hexdump -C my_contacts
00000000 0a 2c 0a 06 41 6e 64 72 65 77 10 8d c0 05 2a 0e |.,..Andrew....*.|
00000010 0a 0c 32 31 32 2d 35 35 35 2d 31 32 31 32 2a 0e |..212-555-1212*.|
00000020 0a 0c 32 31 32 2d 35 35 35 2d 31 32 31 33 0a 36 |..212-555-1213.6|
00000030 0a 07 57 69 6c 6c 69 61 6d 10 8e c0 05 1a 07 45 |..William......E|
00000040 64 77 61 72 64 73 2a 0e 0a 0c 33 30 31 2d 35 35 |dwards*...301-55|
00000050 35 2d 31 31 31 31 2a 0e 0a 0c 33 30 31 2d 35 35 |5-1111*...301-55|
00000060 35 2d 33 33 33 33 |5-3333|
00000066
现在,让我们看看如何从文件中读取联系人。这段代码要短得多,也更容易阅读。我们再次导入contacts_pb2
文件,然后打开该文件进行读取。然而,在本例中,我们创建了 Contacts 类的一个新实例,然后使用ParseFromString()
方法从文件中读取。这将在内存中创建联系人列表,然后我们可以遍历并打印数据。下面显示了读取联系人列表的完整代码。
import contacts_pb2
contacts = contacts_pb2.Contacts()
# Read the existing contacts.
with open("my_contacts", "rb") as f:
contacts.ParseFromString(f.read())
# Print out the contacts
for contact in contacts.list:
print contact
f.close()
与 write 示例中一样,我们可以执行这段代码,但在本例中,我们将看到联系人列表被打印出来。清单 7-3 显示了输出。请注意,我们看到一个类似 C++(和 JSON 有点像)的格式良好的输出。
$ python ./read_contacts.py
first: "Andrew"
id: 90125
phones {
number: "212-555-1212"
}
phones {
number: "212-555-1213"
}
first: "William"
id: 90126
last: "Edwards"
phones {
number: "301-555-1111"
}
phones {
number: "301-555-3333"
}
Listing 7-3Reading the Contact List (protobuf example)
当然,您可以编写代码来使用点语法访问单个字段。例如,您可以用下面的示例代码只打印出名字和姓氏。
# Print out the contacts
for contact in contacts.list:
print contact.first, contact.last,
for phone in contact.phones:
print phone.number,
print
当您执行这个文件时,您会看到如下所示的输出。
$ python ./read_contacts.py
Andrew 212-555-1212 212-555-1213
William Edwards 301-555-1111 301-555-3333
正如您所看到的,使用 protobuf 使读写结构化数据变得更加容易,并且比我们自己编写结构要简单得多。如果这个例子很有意思,我鼓励你去尝试一下,并随心所欲地修饰它。如果您想了解更多关于 protobuf 的信息,包括如何开始构建您自己的消息和协议,请参阅位于 https://developers.google.com/protocol-buffers/docs/overview
的在线文档。
那么,MySQL protobuf 称为 X 协议是什么呢?不应该被命名为“MySQL 协议缓冲区”吗?Recall protobuf 是一种可以用来设计协议的技术。因此,X 协议是使用 protobuf 定义组成新协议的消息、命令等的产物。因此,X 协议是使用 protobuf 语言的通信协议的定义。酷吧。
既然我们对 X 协议有了更多的了解,知道了它是如何(以及为什么)被设计的,那么让我们更仔细地看看它在代码和 protobuf 级别是如何工作的。
x 协议:引擎盖下
虽然开发者不太可能需要编写直接与 X 协议接口的低级代码,但是浏览一下 X 协议是如何实现的还是很有帮助的。为了简洁起见,在开始详细研究一个数据库连接器如何实现 X 协议之前,我们将只看 X 协议的一小部分。如果你是一个代码狂热者,你现在可以采取你最好的编码姿势。 6
让我们先来看看定义 MySQL X 协议的 protobuf 定义文件。
Protobuf 实现
MySQL protobuf 定义文件可以在任何实现 X 协议的产品的源代码下载中找到。例如,您可以在 MySQL 服务器的源代码中的rapid/plugin/x/protocol
文件夹中找到它们,该文件夹以前缀mysqlx
和文件扩展名.proto
命名。也可以在 https://github.com/mysql/mysql-server/blob/5.7/rapid/plugin/x/protocol
从 GitHub 看到并下载 X 协议 protobuf 定义文件。
我展示 Github 存储库,而不是让您下载服务器代码,因为您可以使用 Github 存储库深入查看文件,而不必下载任何东西。只需使用之前的 URL 并点击 mysqlx.proto 文件链接。图 7-1 显示了在 Github 中查看文件的示例。
图 7-1
The mysqlx.proto file (Github)
但是,如果您喜欢下载服务器代码,您可以。只需访问 https://dev.mysql.com/downloads/mysql/
,在选择操作系统下拉列表中选择源代码条目,为您的平台选择一个文件,并下载它。一旦您解压缩了文件,您就可以在您自己的 PC 上探索服务器源代码。
这些是未编译的原始 protobuf 定义文件。表 7-1 列出了组成 X 协议的 protobuf 定义文件,包括文件名和简短描述。注意,文件名与 X DevAPI 中的主要概念相关联,显示了 protobuf 到 X 协议的清晰映射。
表 7-1
Protobuf Definition Files (X Protocol)
| 文件 | 描述 | | :-- | :-- | | `mysqlx.proto` | 定义客户端、服务器以及常规 Ok 和错误消息。这是导入所有其他文件的主文件。 | | `mysqlx_connection.proto` | 定义用于在连接协商过程中确定服务器功能的消息(见下文) | | `mysqlx_crud.proto` | 定义用于处理 CRUD 操作的消息 | | `mysqlx_datatypes.proto` | 定义使用标量数据类型的消息 | | `mysqlx_expect.proto` | 定义用于处理管道消息的消息 | | `mysqlx_expr.proto` | 定义使用表达式的消息 | | `mysqlx_notice.proto` | 定义用于发布通知(如会话和变量状态更改)的消息 | | `mysqlx_resultset.proto` | 为包括行和列的结果集定义消息;这个文件是 X 协议的关键组件,展示了 protobuf 的强大功能。 | | `mysqlx_sql.proto` | 定义用于执行语句的消息 | | `mysqlx_session.proto` | 定义管理会话的消息 |为了让您对文件包含的内容有所了解,清单 7-4 显示了来自mysqlx.proto
文件的错误消息。
...
// generic Error message
//
// A ``severity`` of ``ERROR`` indicates the current message sequence is
// aborted for the given error and the session is ready for more.
//
// In case of a ``FATAL`` error message the client should not expect
// the server to continue handling any further messages and should
// close the connection.
//
// :param severity: severity of the error message
// :param code: error-code
// :param sql_state: SQL state
// :param msg: human readable error message
message Error {
optional Severity severity = 1 [ default = ERROR ];
required uint32 code = 2;
required string sql_state = 4;
required string msg = 3;
enum Severity {
ERROR = 0;
FATAL = 1;
};
}
...
Listing 7-4Generic Error Message (mysqlx.proto)
请注意,该消息定义得非常好,包含了您在查看客户机/服务器协议时所期望看到的内容。特别是,我们看到一个可选的严重性设置、错误代码、SQL 状态代码(字符串)和一条错误消息(字符串)。严重性是一个枚举值,当前可以设置为错误(0)或失败(1)。酷吧。
您可能想知道 protobuf 编译器在编译时对这段代码做了什么。让我们来看看由此产生的 Python 代码。清单 7-5 显示了通用错误消息的编译代码。为了简洁起见,我省略了一些代码。
...
_ERROR = _descriptor.Descriptor(
name='Error',
full_name='Mysqlx.Error',
filename=None,
file=DESCRIPTOR,
containing_type=None,
fields=[
_descriptor.FieldDescriptor(
name='severity', full_name='Mysqlx.Error.severity', index=0,
number=1, type=14, cpp_type=8, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
options=None),
_descriptor.FieldDescriptor(
name='code', full_name='Mysqlx.Error.code', index=1,
number=2, type=13, cpp_type=3, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
options=None),
...
],
extensions=[
],
nested_types=[],
enum_types=[
_ERROR_SEVERITY,
],
options=None,
is_extendable=False,
syntax='proto3',
extension_ranges=[],
oneofs=[
],
serialized_start=872,
serialized_end=1001,
)
...
Listing 7-5Python Generic Error Message (mysqlx_pb2.proto)
啊!这一点也不简单,也不容易读懂。这是一个很好的例子,展示了 protobuf 可以为我们做多少事情。显然,用 protobuf 定义消息比用 Python 定义消息要重要得多(相对而言)。如果您感到好奇,用其他语言编译 protobuf 定义文件同样会产生复杂且看似难以理解的代码。但是不用担心;我们不需要直接读取编译后的文件!这很好,不是吗?
为了了解 X 协议的复杂性(和完整性),让我们看看 Connector/Python 是如何实现 X 协议的。在下一节中,我们将通过几个简单的例子,包括连接过程,来看看 X 协议是如何工作的。
Tip
我鼓励您探索其他* .proto
文件,看看它们定义的消息。
x 协议示例
我们探索 X 协议的两个实例:1)概述如何从协商、认证开始建立连接,然后是命令;以及 2)如何处理 SQL 插入。这些例子很容易理解,如果你好奇的话,可以很容易地在 protobuf 定义文件中找到。
示例 1:身份验证
为简单起见,让我们假设我们想要使用旧的身份验证连接到服务器。这将让您很好地了解通信协议是如何工作的,而不像我们在新机制中看到的那样费力。目标是通过举例来理解典型的通信协议是如何工作的。毕竟,您不太可能构建自己的身份验证协议(但您可以通过构建自己的身份验证插件)。
该过程的生命周期从协商阶段开始,在这个阶段,客户机使用CapabilitiesGet()
方法向服务器请求认证(和其他)能力。服务器用CapabilitiesGet
消息响应(在mysqlx_connection.proto
文件中定义)。客户端然后设置功能(比如设置 TLS 之类的认证扩展),通过CapabilitiesSet()
方法发送完整的消息。假设数据是正确的,服务器回复Ok
消息。
然后,客户端使用AuthenticateStart()
方法启动身份验证。然后,服务器可以发出一个AuthenticateContinue()
方法调用,向客户机请求更多数据。然后,客户端可以用相同的AuthenticateContinue()
方法调用进行响应,一旦认证完成,服务器就用AuthenticateOk()
方法调用进行响应。从那里,客户端可以启动命令。图 7-2 显示了消息传输方向的生命周期示例(执行相关方法的结果)。
图 7-2
X Protocol connection procedure (Courtesy of Oracle)
我们来看一下CapabilitiesSet
的消息。清单 7-6 显示了来自mysqlx_connection.proto
文件的摘录。
...
// a Capability
//
// a tuple of a ``name`` and a :protobuf:msg:`Mysqlx.Datatypes::Any`
message Capability {
required string name = 1;
required Mysqlx.Datatypes.Any value = 2;
}
// Capabilities
message Capabilities {
repeated Capability capabilities = 1;
}
...
// :precond: active sessions == 0
// :returns: :protobuf:msg:`Mysqlx::Ok` or :protobuf:msg:`Mysqlx::Error`
message CapabilitiesSet {
required Capabilities capabilities = 1;
};
...
Listing 7-6CapabilitiesSet Message (mysqlx_connection.proto)
注意,我们看到CapabilitiesSet
消息有一个名为Capabilities
类型消息的能力的字段。这是一个占位符,供客户端用数据完成消息并将其发送回服务器。其他值包括SCALAR
(1)、OBJECT
(2)或ARRAY
(3),可以在mysqlx_datatypes.proto
文件中找到。
示例 2:简单插入
在本例中,我们将检查发出 SQL 语句时会发生什么。特别是,对一个简单的表执行两个INSERT
语句。此时,我们正在处理一个 SQL 对象和位于名称奇怪的mysqlx_sql.proto
文件中的StmtExecute
消息。
该过程从客户端使用Sql.StmtExecute()
方法向服务器发送语句开始。然后,服务器可以用Sql.StmtExecuteOk()
方法进行响应。如图 7-3 所示,对下一条 INSERT 语句重复该过程。
图 7-3
X Protocol simple inserts (Courtesy of Oracle)
我们来看一下Sql.StmtExecute
的消息。清单 7-7 显示了来自mysqlx_sql.proto
文件的摘录。
...
// execute a statement in the given namespace
//
// .. uml::
//
// client -> server: StmtExecute
// ... zero or more Resultsets ...
// server --> client: StmtExecuteOk
//
// Notices:
// This message may generate a notice containing WARNINGs generated by its execution.
// This message may generate a notice containing INFO messages generated by its execution.
//
// :param namespace: namespace of the statement to be executed
// :param stmt: statement that shall be executed.
// :param args: values for wildcard replacements
// :param compact_metadata: send only type information for :protobuf:msg:`Mysqlx.Resultset::ColumnMetadata`, skipping names and others
// :returns:
// * zero or one :protobuf:msg:`Mysqlx.Resultset::` followed by :protobuf:msg:`Mysqlx.Sql::StmtExecuteOk`
message StmtExecute {
optional string namespace = 3 [ default = "sql" ];
required bytes stmt = 1;
repeated Mysqlx.Datatypes.Any args = 2;
optional bool compact_metadata = 4 [ default = false ];
}
...
Listing 7-7Sql.StmtExecute Message (mysqlx_sql.proto)
注意,我们有用于namespace
(默认设置为 SQL)的字段,SQL 语句存储在stmt
中。注意,它的类型是 byte,所以我们可以处理任何字符集,包括二进制数据。然后,我们可以有零个或多个参数(args
),以允许参数化查询。最后,我们可以有一个可选的compact_metadata
设置,允许服务器只将类型信息发送回客户端。
正如你所看到的,X 协议有很多内幕。然而,我们不需要了解 X 协议的所有知识就可以使用它。事实上,使用 X 协议的最佳方式是通过 MySQL Shell,这一点我们在第 4 章中有详细介绍,或者通过支持 X 协议的数据库连接器。让我们看看一个数据库连接器是如何实现 X 协议的。
Wait! Where’s the Rest of the Code?
如果您花时间检查 protobuf 定义文件,您可能会注意到缺少了两个主要的东西。Protobuf 是一种协议定义语言(API ),但它不支持通过网络直接传输消息,也不支持加密、压缩和其他数据传输技术。
因此,X 协议是所有这些代码存在的地方。现在你可以明白为什么 X 协议不仅仅是一个 protobuf 实现了。X 协议还实现了其他一些不属于 protobuf 消息定义的功能。这些包括与服务器握手、错误消息定义等等。
x 协议演练
为了更好地理解 X 协议的强大和优雅,我们将研究一个数据库连接器是如何实现 X 协议的。这在 protobuf 定义文件上提供了一个抽象层,这让我们了解了 protobuf,这是一件非常好的事情。正如您将看到的,连接器使得使用 X 协议非常容易,从而延续了 protobuf 的目标,使通信协议易于创建和使用。
我们在本节和下一节中使用的数据库连接器是 Connector/Python,C/Py。我再次选择 C/Py 是因为它的简单性和可读性。如果您想继续学习并查看上下文中的代码,可以在 http://dev.mysql.com/downloads/connector/python/
下载 Connector/Python 8 . 0 . 5 版或更高版本的源代码。请注意,您可能需要单击开发版本选项卡,然后从下拉列表中选择独立于平台的条目。
我们看一下上一节中每个例子的 C/Py 代码。因此,我们将看到连接到服务器并执行 SQL INSERT
语句的代码。
示例 1:身份验证
我们在位于/lib/mysqlx
文件夹中的名为connection.py
的 C/Py 源代码文件中找到了认证过程的代码。清单 7-8 显示了实现该过程的源代码(方法)的摘录。为了简洁起见,我省略了收集和传递连接信息的细节。重点关注的起点是Connection
类中的connect()
方法。
...
def connect(self):
# Loop and check
error = None
while self._can_failover:
try:
self.stream.connect(self._connection_params())
self.reader_writer = MessageReaderWriter(self.stream)
self.protocol = Protocol(self.reader_writer)
self._handle_capabilities()
self._authenticate()
return
except socket.error as err:
error = err
if len(self._routers) <= 1:
raise InterfaceError("Cannot connect to host: {0}".format(error))
raise InterfaceError("Failed to connect to any of the routers.", 4001)
def _handle_capabilities(self):
if self.settings.get("ssl-mode") == SSLMode.DISABLED:
return
if self.stream.is_socket:
if self.settings.get("ssl-mode"):
_LOGGER.warning("SSL not required when using Unix socket.")
return
data = self.protocol.get_capabilites().capabilities
if not (get_item_or_attr(data[0], "name").lower() == "tls"
if data else False):
self.close_connection()
raise OperationalError("SSL not enabled at server.")
is_ol7 = False
if platform.system() == "Linux":
# pylint: disable=W1505
distname, version, _ = platform.linux_distribution()
try:
is_ol7 = "Oracle Linux" in distname and version.split(".")[0] == "7"
except IndexError:
is_ol7 = False
if sys.version_info < (2, 7, 9) and not is_ol7:
self.close_connection()
raise RuntimeError("The support for SSL is not available for "
"this Python version.")
self.protocol.set_capabilities(tls=True)
self.stream.set_ssl(self.settings.get("ssl-mode", SSLMode.REQUIRED),
self.settings.get("ssl-ca"),
self.settings.get("ssl-crl"),
self.settings.get("ssl-cert"),
self.settings.get("ssl-key"))
def _authenticate(self):
auth = self.settings.get("auth")
if (not auth and self.stream.is_secure()) or auth == Auth.PLAIN:
self._authenticate_plain()
elif auth == Auth.EXTERNAL:
self._authenticate_external()
else:
self._authenticate_mysql41()
...
Listing 7-8Connection Methods for Authenticate Procedure (C/Py)
注意,在 connect()方法中,我们看到发生了几件事情。首先,我们看到 C/Py 打开了一个到服务器的流连接(通过 _connection_params()方法,该方法先前返回了数据集),然后代码创建了一个到读取器/写入器的实例。这就是连接器向/从服务器传输邮件的方式。
接下来,代码实例化协议类的一个实例,这是 X 协议的抽象。稍后我们将看到该代码的更多细节。
现在,关注 connect()方法中的最后两条语句。这里我们看到 _handle_capabilities()中对 CapabilitiesGet/Set 方法的方法调用和 _authenticate()中的身份验证阶段。花一些时间通读代码,这样您就可以看到图 7-1 中的所有步骤都显示出来了。
Protocol
类的 CapabilitiesGet/Set 方法可以在 C/Py 源代码的/lib/mysqlx 文件夹中的 protocol.py 文件中找到,如清单 7-9 所示。
...
def get_capabilites(self):
msg = Message("Mysqlx.Connection.CapabilitiesGet")
self._writer.write_message(
mysqlxpb_enum("Mysqlx.ClientMessages.Type.CON_CAPABILITIES_GET"),
msg)
return self._reader.read_message()
def set_capabilities(self, **kwargs):
capabilities = Message("Mysqlx.Connection.Capabilities")
for key, value in kwargs.items():
capability = Message("Mysqlx.Connection.Capability")
capability["name"] = key
capability["value"] = self._create_any(value)
capabilities["capabilities"].extend([capability.get_message()])
msg = Message("Mysqlx.Connection.CapabilitiesSet")
msg["capabilities"] = capabilities
self._writer.write_message(
mysqlxpb_enum("Mysqlx.ClientMessages.Type.CON_CAPABILITIES_SET"),
msg)
return self.read_ok()
)
...
Listing 7-9CapabilitiesGet/Set Methods for Authenticate Procedure (C/Py)
在这一点上,我们可以看到通过由 protobuf 编译器生成的MySQLx*
类对 protobuf 代码的调用。
示例 2:简单插入
这个例子更容易理解,所以我们将比上一个例子更深入一些。我们在 C/Py 源代码的/lib/mysqlx
文件夹中名为statement.py
的 C/Py 源代码文件中找到了认证过程的代码。清单 7-10 显示了源代码的摘录,它实现了一个用于执行INSERT
SQL 语句的类。
...
class InsertStatement(WriteStatement):
"""A statement for insert operations on Table.
Args:
table (mysqlx.Table): The Table object.
*fields: The fields to be inserted.
"""
def __init__(self, table, *fields):
super(InsertStatement, self).__init__(table, False)
self._fields = flexible_params(*fields)
def values(self, *values):
"""Set the values to be inserted.
Args:
*values: The values of the columns to be inserted.
Returns:
mysqlx.InsertStatement: InsertStatement object.
"""
self._values.append(list(flexible_params(*values)))
return self
def execute(self):
"""Execute the statement.
Returns:
mysqlx.Result: Result object.
"""
return self._connection.send_insert(self)
...
Listing 7-10SQL INSERT Class (C/Py)
如您所见,代码很容易阅读。首先要注意的是该类是从一个名为 WriteStatement 的基类派生而来的(也在statement.py
中)。这个基类有一个名为execute()
的抽象(虚拟)方法,这个派生类实现了这个方法。然而,在这种情况下,它从连接类(在connection.py
中)调用send_insert()
方法。下面展示了send_insert()
的方法。
@catch_network_exception
def send_insert(self, statement):
self.protocol.send_insert(statement)
ids = None
if isinstance(statement, AddStatement):
ids = statement._ids
return Result(self, ids)
如您所见,这用清单 7-11 所示的语句调用了protocol.py
文件中的协议类方法send_insert()
。
...
def send_insert(self, stmt):
data_model = mysqlxpb_enum("Mysqlx.Crud.DataModel.DOCUMENT"
if stmt._doc_based else
"Mysqlx.Crud.DataModel.TABLE")
collection = Message("Mysqlx.Crud.Collection",
name=stmt.target.name,
schema=stmt.schema.name)
msg = Message("Mysqlx.Crud.Insert", data_model=data_model,
collection=collection)
if hasattr(stmt, "_fields"):
for field in stmt._fields:
expr = ExprParser(field, not stmt._doc_based) \
.parse_table_insert_field()
msg["projection"].extend([expr.get_message()])
for value in stmt._values:
row = Message("Mysqlx.Crud.Insert.TypedRow")
if isinstance(value, list):
for val in value:
row["field"].extend([build_expr(val).get_message()])
else:
row["field"].extend([build_expr(value).get_message()])
msg["row"].extend([row.get_message()])
msg["upsert"] = stmt._upsert
self._writer.write_message(
mysqlxpb_enum("Mysqlx.ClientMessages.Type.CRUD_INSERT"), msg)
...
Listing 7-11The send_insert() Method in the Protocol Class (C/Py)
和前面的例子一样,我们现在可以看到 protobuf 接口,并按照代码来查看图 7-2 中概述的步骤。
Tip
如果你想了解更多关于 X 协议是如何工作的,请看位于 https://dev.mysql.com/doc/internals/en/x-protocol.html
的 MySQL 内部文档。
既然我们对 X 协议有了更多的了解,并且能够理解 X 插件和 Shell 以及数据库连接器提供的抽象,那么让我们看看如何编写利用 MySQL 连接器提供的 X 协议的客户端应用。在这种情况下,我们将继续通过使用 Connector/Python 来掌握 X 协议。
创建 X 客户端
创建使用 X 协议的 MySQL 客户端应用最好是使用 MySQL Shell 或者最终使用一个数据库连接器,并在服务器上安装 X 插件。在本节中,我们将看到两个独立客户端的示例。一个是使用 MySQL 作为文档存储编写的,另一个只使用关系数据模型。
我们将使用的编程语言是一种非常简单的脚本语言,叫做 Python。正如您将看到的,这些命令非常直观,非常有表现力。出于本演示的目的,您不需要成为这种语言的专家。我会提供所有的代码和命令,你需要我们一起去。
Python? Isn’t That a Snake?
Python 编程语言是一种高级语言,旨在尽可能接近阅读英语,同时简单、易学且非常强大。皮托尼斯塔斯会告诉你设计师们确实达到了这些目标。
Python 在使用前不需要编译步骤。相反,Python 应用(其文件名以.py
结尾)是动态解释的。这是非常强大的;但是除非使用 Python 开发环境,否则一些语法错误(比如不正确的缩进)直到应用执行后才会被发现。幸运的是,Python 提供了一个健壮的异常处理机制。
如果您从未使用过 Python,或者您想了解更多,下面是几本介绍这种语言的好书。互联网上也有很多资源,包括位于 http://www.python.org/doc/
的 Python 文档页面:
- 《树莓派编程》,西蒙·蒙克著(麦格劳-希尔出版社,2013 年)。
- Python 入门,从新手到专业人士,第 2 版。马格努斯·李·赫特兰德著。
- 大卫·比兹利和布莱恩·k·琼斯的《Python 食谱》(O'Reilly Media,2013 年)。
有趣的是,Python 是以英国喜剧团 Monty Python 而不是爬行动物命名的。当你学习 Python 的时候,你可能会遇到对 Monty Python 剧集的无聊引用。我对巨蟒小组情有独钟,觉得这些参考资料很有趣。当然,您的里程可能会有所不同。
首先,您可以输入示例中所示的代码,或者从本书的网站下载源代码。在编写 Python 脚本时,您可以使用任何想要的代码编辑器。我们从如何设置环境来运行示例的简短描述开始。
Tip
有很多可用的,包括 JetBrains 的一个非常强大的 IDE,名为 PyCharm ( http://www.jetbrains.com/pycharm/
)。如果您想要一个优秀的 Python 开源软件,请查看 PyCharm 社区版。
示例的设置
要使用本节中的示例,您需要安装一些东西。您必须下载 Google Protocol Buffers Python 库并安装编程语言运行时。您还必须下载 C/Py 的源代码。
回想一下,我们需要安装 protobuf 编译器和 protobuf 库。如果您还没有这样做,请参考上一节“安装 Protobuf 编译器”
特定语言的运行时库可以从 https://github.com/google/protobuf
下载。您应该通过单击克隆或下载按钮来下载整个包。下载完成后,您会看到一个名为protobuf-master.zip
的文件,您可以解压缩它。要安装所选语言的库,请导航到以该语言命名的文件夹,并阅读 README.md 文件以获取特定的安装说明。例如,我们将在本章中使用 Python。这个文件夹被命名为/protobuf-master/python
。要在 macOS 上安装 Python,可以运行以下命令。
$ python ./setup.py build
$ sudo python ./setup.py install
在其他系统上安装 Python 库是类似的。在 Windows 上安装它的唯一区别是你不需要使用 sudo(超级用户)。然而,在我的系统上,定位 protobuf 编译器有一个问题。我收到了类似如下的错误。
protoc is not installed nor found in ../src. Please compile it or install the binary package.
一旦我将 protobuf 编译器可执行文件(protoc
)放在指定的目录(../src
)中,我就可以用前面的命令安装 Python protobuf 库了。您可能会在其他平台上遇到类似的问题。
Tip
向下滚动到页面底部的 https://github.com/google/protobuf
,点击表格中的链接,查看安装其他语言的 protobuf 库的说明。
如果您还没有,您必须从 http://dev.mysql.com/downloads/connector/python/
下载 C/Py 8 . 0 . 5 版或更高版本的源代码。确保从下拉列表中下载独立于平台的选项。在我们的例子中,我们将使用 C/Py 源文件树中的一些源文件。
我选择这样做是为了帮助您了解 protobuf 如何与 Python 一起工作以及 C/Py 如何实现 X 协议的细节。尽管这些示例将展示 C/Py 中的 X 协议抽象层,但是您可以使用您最喜欢的调试器或 Python IDE 来深入研究代码,看看事情是如何工作的。因此,我为我们当中的好奇者树立了这个榜样。然而,如果你不想走那么远,你可以不走那么远。相反,您可以专注于示例是如何工作的,以便更好地理解如何通过数据库连接器使用新的 X 协议。
也许更重要的是,因为我们使用的 C/Py 示例是一个开发里程碑版本(考虑 beta 版),复制源代码不会影响系统上 C/Py 的任何其他安装,从而允许您运行这些示例,而不必安装连接器的开发里程碑版本。
我们需要的文件在/lib/mysqlx
文件夹里。但是首先,在你的系统上创建一个新的文件夹。随便你怎么命名比如xclient
。接下来,将 C/Py 档案中的mysqlx
文件夹复制到xclient
文件夹中。当你为下面的例子创建文件时,把它们保存在xclient
文件夹中。例如,我将文档存储示例命名为xclient_json.p
y,将关系数据示例命名为xclient_sql.py
。
Tip
如果出现找不到一个或多个库的错误,请确保将 mysqlx 文件夹复制到与 xclient_json.py 和 xclient_sql.py 文件相同的文件夹中。
文档存储示例
这个例子创建了一个简单的客户机来演示如何使用 C/Py 中的 X 协议抽象。这个例子使用了我们在第 1 章中遇到的联系人的概念。在这种情况下,代码将连接到服务器,在模式中创建一个模式和集合,并用文档填充集合。然后,代码将检索所有文档并打印出来。但是我们不仅仅打印原始文档。该代码演示了如何在集合上执行查找操作,并遍历文档,为找到的每个联系人文档打印电话号码。
下面简要描述了代码部分。我强调了相关的代码语句,以引起您对 X 协议抽象方法的注意。大多数调用对你来说都很熟悉,因为我们在第 5 章和本书的其他地方遇到过它们。因此,我保持解释简短。如果你需要更多关于例子中使用的类和方法的信息,请参考第 5 章。
我们需要做的第一件事是导入mysqlx
库。回想一下,这是从 C/Py 下载的一组文件。它包含了我们前面看到的 X 协议文件的 C/Py 抽象。如果您检查该文件夹,您会注意到.proto
文件丢失了。这是因为我们只需要运行 protobuf 编译器时生成的.py
文件。幸运的是,所有这些文件都存在于mysqlx
文件夹中。
接下来,我们要求用户提供登录凭证(用户 id、密码、主机和端口)。我们使用这些信息来打开一个到服务器的会话(连接)。为此,我们使用get_session()
方法,并将会话对象的结果实例赋给变量mysqlx_session
。如果发生了我们无法连接的情况,我们会检查会话的状态,如果会话没有打开,就退出。注意,我们在这个例子中使用 X 会话,因为我们只执行 CRUD 操作,不需要任何 SQL 支持。
接下来,我们使用mysqlx_session
对象实例,并尝试用get_schema()
方法获取模式。 8 这将设置默认模式,以便当我们创建集合(或者其他对象)时;它们将包含在模式中。我使用一个常量来存储模式名和集合名。如果模式不在服务器上,我们用create_schema()
方法创建它。无论哪种方式,我们都会得到一个 schema 对象实例,我们可以用这个实例用create_collection()
方法创建集合,这个方法为我们提供了一个集合的对象实例。注意,我使用了remove()
方法来清空集合。这允许我们在不复制数据的情况下重新运行代码(我没有检查文档 id)。
在继续之前,让我们看一下代码。清单 7-12 显示了完整的代码。花一些时间通读代码,以便您可以看到到目前为止描述的所有方法和操作。您应该对 contacts.remove()调用之前的所有代码都很熟悉。如果您想执行这段代码来看看它做了什么,您可以将这段代码放在一个名为 xclient_json.py 的文件中。
#
# Introducing the MySQL 8 Document Store - xclient_json
#
# This file contains and example of how to read a collection from a MySQL
# server using the X Protocol via a Session object
#
# Dr. Charles Bell, 2018
#
import getpass
import mysqlx
# Declarations
TEST_SCHEMA = "rolodex"
TEST_COL = "contacts"
# Get user information
print("Please enter the connection information.")
user = raw_input("Username: ")
passwd = getpass.getpass("Password: ")
host = raw_input("Hostname [localhost]: ") or 'localhost'
port = raw_input("Port [33060]: ") or '33060'
# Get a session object using a dictionary of terms
mysqlx_session = mysqlx.get_session({'host': host, 'port': port, 'user': user, 'password': passwd})
# Check to see that the session is open. If not, quit.
if not mysqlx_session.is_open():
exit(1)
# Get the schema and create it if it doesn't exist
schema = mysqlx_session.get_schema(TEST_SCHEMA)
if not schema.exists_in_database():
schema = mysqlx_session.create_schema(TEST_SCHEMA)
# Create a collection or use it if it already exists
contacts = schema.create_collection(TEST_COL)
# Empty the collection
contacts.remove()
# Insert data with inline JSON
contacts.add({"name": {"first": "Allen"},
"phones": [{"work": "212-555-1212"}]}).execute()
contacts.add({"name": {"first": "Joe", "last": "Wheelerton"},
"phones": [{"work": "212-555-1213"}, {"home": "212-555-1253"}],
"address": {"street": "123 main", "city": "oxnard", "state": "ca", "zip": "90125"},
"notes": "Excellent car detailer. Referrals get $20 off next detail!"}).execute()
# Get all of the data
doc_results = contacts.find().execute()
# Show the results
print("\nList of Phone Numbers")
document = doc_results.fetch_one()
while document:
print("{0}:\t".format(document.name['first'])),
for phone in document.phones:
for key, value in phone.iteritems():
print("({0}) {1}".format(key, value)),
print("")
document = doc_results.fetch_one()
# Drop the collection
schema.drop_collection(TEST_COL)
# Drop the schema
mysqlx_session.drop_schema(TEST_SCHEMA)
# Close the session
mysqlx_session.close()
Listing 7-12
X Client Source Code (JSON)
Tip
如果您使用的是 Python 3.0 或更高版本,您需要将raw_input()
调用改为input()
,将iteritems()
改为items()
。这是因为在 Python 的后续版本中不再支持raw_input()
和iteritems()
。
接下来,我们可以添加一些联系人。我们使用集合对象实例的add()
方法来实现这一点。在本例中,我们添加了几个文档;一个是我们只知道他们的名字和电话号码的人,另一个是我们知道他们的全名、几个电话号码和一些我们做的笔记的人。这说明了使用文档存储的强大之处:存储您需要的东西,不要强迫数据遵守严格的结构或存储机制!
一旦添加了文档,我们就对集合使用find()
方法,而不使用任何表达式。我们用execute()
方法链接查找操作。这只是以文档结果对象实例的形式返回集合中的所有文档。然后我们可以用这个对象通过fetch_one()
方法迭代文档。请注意,这将返回一个文档对象实例,我们可以使用该实例通过命名属性(一个强大的表达式)直接获取数据元素。花点时间通读一下获取文档的代码。注意,当收集结束时,fetch_one()
返回None
,while 循环终止。
最后,我们用drop_collection()
方法删除集合,用drop_schema()
方法删除模式,这样我们就可以重新运行代码并避免重复。但是,您可能会注意到,我添加了代码来防止意外执行。例如,如果您使用调试器并在结束前终止代码,代码顶部的语句将使用该架构(如果它已经存在)并清空集合。
现在让我们看看脚本的运行情况。在这种情况下,我们希望只看到 rolodex 中的人的名字和电话号码列表(在这种情况下只有两个条目)。
$ python ./xclient_json.py
Please enter the connection information.
Username: root
Password:
Hostname [localhost]:
Port [33060]:
List of Phone Numbers
Joe: (work) 212-555-1213 (home) 212-555-1253
Allen: (work) 212-555-1212
如果您想知道这是否是一个精心策划的诡计,我们创建的集合和文档不知何故存储在 MySQL 的其他地方,如果您禁用了drop_*()
调用并再次运行该程序,您可以登录到服务器并查看底层表的构造,如清单 7-13 所示。
$ mysqlsh root@localhost:33060 --sql --json=pretty --schema=rolodex -e "EXPLAIN contacts"
{
"password": "Enter password: "
}
{
"executionTime": "0.00 sec",
"warningCount": 0,
"warnings": [],
"rows": [
{
"Field": "doc",
"Type": "json",
"Null": "YES",
"Key": "",
"Default": null,
"Extra": ""
},
{
"Field": "_id",
"Type": "varchar(32)",
"Null": "NO",
"Key": "PRI",
"Default": null,
"Extra": "STORED GENERATED"
}
],
"hasData": true,
"affectedRowCount": 0,
"autoIncrementValue": 0
}
Listing 7-13Definition of the Contacts Collection
如果您运行一个SELECT
语句从该表中获取所有数据,您将看到类似于清单 7-14 中所示的结果。结果的顺序可能有所不同,但您应该在结果中看到相同的数据。注意,文档 id 被添加到每个 JSON 文档中。
$ mysqlsh root@localhost:33060 --sql --json=pretty --schema=rolodex -e "SELECT * FROM contacts"
{
"password": "Enter password: "
}
{
"executionTime": "0.00 sec",
"warningCount": 0,
"warnings": [],
"rows": [
{
"doc": "{\"_id\": \"9801A79DE09382A811E806BFAD2FA2CF\", \"name\": {\"first\": \"Allen\"}, \"phones\": [{\"work\": \"212-555-1212\"}]}",
"_id": "9801A79DE09382A811E806BFAD2FA2CF"
},
{
"doc": "{\"_id\": \"9801A79DE0938DFD11E806BFAD314DE1\", \"name\": {\"last\": \"Wheelerton\", \"first\": \"Joe\"}, \"notes\": \"Excellent car detailer. Referrals get $20 off next detail!\", \"phones\": [{\"work\": \"212-555-1213\"}, {\"home\": \"212-555-1253\"}], \"address\": {\"zip\": \"90125\", \"city\": \"oxnard\", \"state\": \"ca\", \"street\": \"123 main\"}}",
"_id": "9801A79DE0938DFD11E806BFAD314DE1"
}
],
"hasData": true,
"affectedRowCount": 0,
"autoIncrementValue": 0
}
Listing 7-14Results of SELECT Statement for Contacts Collection
那很酷,不是吗?当我们探索一个完整的文档存储应用示例时,我们将在第 8 章中看到更多这样的代码。但是首先,让我们看一个使用 X 协议执行 SQL 命令的 Connector/Python 示例。
关系数据示例
现在让我们看一个使用 X 协议的关系数据例子。我们将使用与上一个示例相同的 C/Py 代码,只是这次我们将执行 SQL 语句,而不是处理数据。我选择这个简单的例子是因为,如果不是一开始,最终您的 MySQL 文档存储应用将使用越来越少的 SQL 操作。即便如此,如果您想检查变量、状态或类似的服务器操作,您可能需要不时地执行一条 SQL 语句。
这个示例使用一个会话连接到服务器,并执行 SQL 语句SHOW VARIABLES LIKE
,来检索 X 插件的所有系统变量。这与我们在第 6 章中看到的 SQL 语句相同。尽管我们没有访问任何数据,但是从SHOW VARIABLES
语句返回的结果集与查询表时返回的结果集是相同的。因此,我们将看到如何处理来自 SQL 命令的结果集,而不需要创建任何示例数据。
和上一个例子一样,我们从导入mysqlx
库开始,并提示用户输入登录凭证。请注意,我演示了如何为用户输入使用默认值。接下来,我们用get_session()
方法得到一个会话。这将返回一个会话对象实例。然后,我们检查连接是否打开,如果不是(例如,连接失败),我们退出。清单 7-15 显示了这个例子的完整代码。花点时间通读一下,这样你就可以看到到目前为止讨论的所有概念。
#
# Introducing the MySQL 8 Document Store - xclient_sql
#
# This file contains an example of how to read a database (SQL) from a MySQL
# server using the X Protocol via a Session object
#
# Dr. Charles Bell, 2018
#
import getpass
import mysqlx
# Get user information
print("Please enter the connection information.")
user = raw_input("Username: ")
passwd = getpass.getpass("Password: ")
host = raw_input("Hostname [localhost]: ") or 'localhost'
port = raw_input("Port [33060]: ") or '33060'
# Get a session object since we want to execute SQL statements
mysqlx_session = mysqlx.get_session({'host': host, 'port': port, 'user': user, 'password': passwd})
# Check to see that the session is open. If not, quit.
if not mysqlx_session.is_open():
exit(1)
# Get an SqlStatements object
sql_stmt = mysqlx_session.sql("SHOW VARIABLES LIKE 'mysqlx_%'")
# Execute and get a SqlResult object
sql_result = sql_stmt.execute()
print("\nVariables for the X Plugin:")
# Print the column labels (names)
for col in sql_result.columns:
print("{0}\t".format(col.get_column_name())),
print("\n-------------------------------------------")
# Print the rows
for row in sql_result.fetch_all():
for col in row:
print("{0}\t".format(col)),
print("")
# Close the session
mysqlx_session.close()
Listing 7-15
X Client Source Code (SQL)
Tip
如果您使用的是 Python 3.0 或更高版本,您可能需要将raw_input()
调用改为input()
。这是因为在 Python 的后续版本中不再支持raw_input()
。
为了执行 SQL 语句,我们需要通过传入我们想要执行的 SQL 语句,向会话请求 SQL statement 对象实例。我们通过调用会话对象实例的 sql()方法来实现这一点。我们可以使用该对象来执行语句,并获得一个结果对象实例作为回报。
接下来,我们可以迭代结果集中的列,打印它们的名称。这说明了如何捕获结果集中的列名。
接下来,我们使用fetch_all()
方法获取列表中的所有行,在 for 循环中遍历它们,并打印找到的每一列的值。注意,我们在这里使用“行”和“列”,因为这不是一个被返回的文档——它是一个老式的 SQL 结果集(嗯,通过 X 协议)。最后,我们结束会议。清单 7-16 展示了脚本运行的一个例子。您应该能够将输出等同于源代码中的print()
语句。请注意,MySQL 的更高版本可能会有额外的变量,一些默认值可能会有所不同。
$ python ./xclient_sql.py
Please enter the connection information.
Username: root
Password:
Hostname [localhost]:
Port [33060]:
Variables for the X Plugin:
Variable_name Value
-------------------------------------------
mysqlx_bind_address *
mysqlx_connect_timeout 30
mysqlx_idle_worker_thread_timeout 60
mysqlx_max_allowed_packet 1048576
mysqlx_max_connections 100
mysqlx_min_worker_threads 2
mysqlx_port 33060
mysqlx_port_open_timeout 0
mysqlx_socket /tmp/mysqlx.sock
mysqlx_ssl_ca
mysqlx_ssl_capath
mysqlx_ssl_cert
mysqlx_ssl_cipher
mysqlx_ssl_crl
mysqlx_ssl_crlpath
mysqlx_ssl_key
Listing 7-16X Client Results (SQL)
注意这里我们看到了 X 插件的所有系统变量(那些以mysqlx_
开头的变量)。我们还可以看到每个系统变量的值。SSL 条目没有任何值,因为示例中使用的连接没有通过安全连接进行连接。
如您所见,即使使用像 Python 这样的语言,也很容易编写利用 X 协议和 X DevAPI 的客户端。当然,这在 Connector/Python 中都是可能的,它实现了 X 协议。有关 X 协议的更多信息,请参见在线 MySQL 内部参考手册 https://dev.mysql.com/doc/internals/en/
中的“X 协议”部分。有关使用连接器编写客户端的具体信息,请参见位于 https://dev.mysql.com/doc
的单个连接器在线文档。你可以在 https://dev.mysql.com/doc/dev/connector-python/
找到关于使用 X DevAPI 和 Connector/Python 的信息。
摘要
X 协议是 MySQL 中一个革命性的新特性,它克服了旧的客户机/服务器协议的许多限制。X 协议是为可扩展性而设计的,因此它可以在不影响依赖它的客户端的情况下进行扩展。X 协议也被设计成具有更高的安全性和更好的性能。几十年来,MySQL 客户端第一次可以使用现代、可靠的技术与服务器连接和交互,并有望成为未来更多新功能的催化剂。
在这一章中,我们从创建 X 协议的动机、设计的主要原则或目标以及如何使用 protobuf 作为基础来实现 X 协议开始进行了研究。我们还看到了 X 协议的一些部分如何为简单用例工作的演练。然后我们看了如何在我们的应用中使用 protobuf 在代码中移动数据(消息)(在磁盘上,通过网络等)。),说明了 protobuf 的强大。
我们还通过检查实际 C/Py 源代码的一部分,简短地介绍了 C/Py 如何实现 X 协议。然后,我们在独立的 Python 脚本中使用 C/Py 中的 X 协议抽象层来演示 X 协议是如何工作的——它易于实现,也是本书中到目前为止介绍的技术的一个具体示例。
与 X 插件一样,我们也发现 X 协议不仅仅是一个特性,它是一个精心制作和良好抽象的机制,是 MySQL 未来的基础之一。即使我们知道在使用支持 X 协议的连接器时,我们使用的是 X 协议,X 协议也能正常工作,而且工作得非常好。
在第 8 章中,我提供了一个使用 X DevAPI 编写应用的教程,我们现在知道它是通过 X 插件和 X 协议实现的。该项目将使用 MySQL 文档存储来构建一个基于 Python web 的解决方案,用于存储有关书籍的信息。
Footnotes 1
那么,为什么是 MySQL 8 而不是 MySQL X 呢?
工作日志是一个内部文档,用于捕获在 MySQL 中实现特性的设计和需求。
SSL 的一种演变: https://en.wikipedia.org/wiki/Transport_Layer_Security
.
一个认证和数据安全的框架: https://en.wikipedia.org/wiki/Simple_Authentication_and_Security_Layer
.
充其量是渐进成功;总有美中不足的地方。
换句话说,把你的椅子放在半倾斜的状态,放上你最喜欢的音乐,确保手边有足够的你最喜欢的饮料。
或者像我有时被指责的那样“没有把事情做好”。有罪。我从小就在拆东西。有时我会把它们放回一起!
在 SQL 术语中使用它。
八、库应用:用户界面
现在我们已经了解了什么是 MySQL 文档存储以及如何通过 MySQL Shell 使用它,我们可以探索一个更复杂的示例,演示所描述的三种形式的数据存储:一个纯关系数据库解决方案,一个混合解决方案,其中我们使用 X DevAPI 的 SQL 特性来使用一个或多个 JSON 字段,以及一个纯文档存储解决方案,它专门使用 X DevAPI(NoSQL 解决方案)。因此,我们将看到应用在三个独立的实现中实现。
但是,我们必须首先了解示例应用是如何设计的,以及它是如何工作的。毕竟,最好的例子应该是读者可以在自己的环境中使用的。因此,这个例子必须足够复杂和完整才有意义。
为了延续前面章节中代码的可理解性,我们将在应用中使用 Python,因为 Python 非常容易学习,代码阅读起来比其他语言更清晰。但是如果你喜欢另一种语言,也不用担心。您可以很容易地将本章中的代码改写成支持 X DevAPI 的连接器的任何语言。
另一方面,用户界面使事情变得有点复杂。我们可以通过使用熟悉的用户界面设计来缓解这一问题。为此,我们将使用一个 web 应用。不幸的是,用纯 Python 编写一个 web 应用是单调乏味的,并且需要更多关于 web 应用如何工作的知识,这超出了人们对这种规模的工作的期望。
为了克服这个挑战,我们将使用一个流行的 Python web 应用框架。在这种情况下,我们将使用 Flask,包括入门、教程和用户界面代码的演练。正如您将看到的,Flask 也很容易学习,只需要学习少量的细微差别和概念。Flask 最初是由阿明·罗纳彻开发的,已经被证明是 Python 最简单、最稳定的 web 平台之一。
在第 9 章中,我们将添加前面描述的数据库访问方法来完成应用。
入门指南
如果您想继续并实现示例项目,您需要在您的计算机上安装一些东西。本节将帮助您为计算机准备所需的工具:您需要安装什么以及如何配置您的环境。我们还将看到一个关于用户界面工具的简短介绍。让我们从更详细的应用描述开始。
库应用
本章中的示例应用是一个相当简单的应用,旨在演示概念。它是完整的,因为它支持对数据的创建、读取、更新和删除(CRUD)操作。错误处理和用户界面组件不太复杂,以便将重点放在与数据的交互上。也就是说,我们将看到如何使用 Flask 在 Python 中实现一个健壮且美观的 web 界面。
应用的数据是一个简单的图书数据库。我们将存储书籍的基本信息,如 ISBN、书名、出版商等等。我们还会有一个笔记区,这样我们可以在书上做笔记。我在我的许多研究论文甚至一些更高级的项目中使用了类似的东西。操作的概念是记录每本书的书目信息以及关于内容的注释,以便以后可以使用它来创建参考文献列表。例如,如果一本书包含了与论文中某个主题相关的信息,我会添加一个注释,指明主题,列出页码和其他重要信息。笔记中的信息因我记录的内容而异,所以只需要在一个简单的文本字段中进行搜索。
与我用于研究的允许存储书籍、杂志、文章、博客等信息的应用不同,这一章的应用被简化为只存储书籍。这使得项目足够小,可以在没有不必要的细节的情况下进行讨论。本章的重点是研究迁移到文档存储的好处,而不是如何最好地实现媒体参考应用。
因此,基本操作将是存储和检索关于书籍、作者和出版商的信息。用户界面被设计为呈现数据库中所有书籍的列表,并具有编辑列表中任何书籍的选项。默认视图是 books,但是该应用的第一个版本(1 和 2)将允许您查看作者和出版商的列表。用户还可以创建新书(作者和出版商),编辑和删除书籍。
当我们看到改变数据存储和检索方式对应用设计的影响时,应用的每个版本的行为都会稍有不同。每个项目的更详细的解释包含在后面讨论项目版本的章节中。
现在,让我们看看如何设置我们的计算机来运行示例应用项目。
设置您的环境
对你的环境的改变并不困难,也不漫长。我们将安装 Flask 和一些扩展,这是应用用户界面所需要的。Flask 是可以与 Python 一起使用的几个 web 库之一。这些 web 库使得用 Python 开发 web 应用比使用原始的 HTML 代码并为请求编写自己的处理程序和代码要容易得多。另外,Flask 并不难学。
我们需要安装的库如表 8-1 所示。该表列出了库/扩展的名称、简短描述以及产品文档的 URL。
表 8-1
List of Libraries Required
| 库 | 描述 | 文件 | | :-- | :-- | :-- | | 瓶 | Python Web API | [`http://flask.pocoo.org/docs/0.12/installation/`](http://flask.pocoo.org/docs/0.12/installation/) | | 烧瓶脚本 | Flask 的脚本支持 | [`https://flask-script.readthedocs.io/en/latest/`](https://flask-script.readthedocs.io/en/latest/) | | 烧瓶自举 | 用户界面的改进和增强 | [`https://pythonhosted.org/Flask-Bootstrap/`](https://pythonhosted.org/Flask-Bootstrap/) | | 烧瓶-WTF | WTForms 集成 | [`https://flask-wtf.readthedocs.io/en/latest/`](https://flask-wtf.readthedocs.io/en/latest/) | | WTForms | 表单验证和呈现 | [`https://wtforms.readthedocs.io/en/latest/`](https://wtforms.readthedocs.io/en/latest/) |Note
根据您的系统配置,您可能会看到为本节安装的组件安装了更多或更少的组件。
当然,您应该已经在系统上安装了 Python。如果没有,请务必下载并安装最新版本的 2。x 或 3。x 版。本章中的示例代码是用 Python 2.7.10 和 Python 3.6.0 测试的。
要安装这些库,我们可以使用 Python 包管理器pip
,从命令行安装这些库。大多数 Python 发行版中都包含了pip
实用程序,但是如果您需要安装它,可以在 https://pip.pypa.io/en/latest/installing/
查看安装文档。
如果需要在 Windows 上安装 pip,需要下载一个安装程序,get-pip.py
( https://pip.pypa.io/en/stable/installing/#installing-with-get-pip-py
),然后将安装目录的路径添加到PATH
环境变量中。有几篇文章更详细地记录了这个过程。你可以谷歌一下“在 Windows 10 上安装 pip”,找到包括 https://matthewhorne.me/how-to-install-python-and-pip-on-windows-10/
在内的几个,都是最准确的。
Note
如果您的系统上安装了多个版本的 Python,那么pip
命令将安装到默认的 Python 版本环境中。要使用pip
安装到特定版本,请使用pipN
,其中N
是版本。例如,pip3
在 Python 3 环境中安装包。
pip
命令非常方便,因为它使得安装注册的 Python 包——那些在 Python 包索引中注册的包,缩写为 PyPI1(https://pypi.python.org/pypi
)——非常容易。pip
命令将使用一个命令下载、解压和安装。让我们来看看如何安装我们需要的每个包。
Caution
一些系统可能需要使用提升的权限运行 pip,例如sudo
(Linux、macOS),或者在命令窗口中以管理员用户身份运行(Windows 10)。如果安装由于权限问题而无法复制文件,您将知道是否需要提升权限。
安装烧瓶
清单 8-1 演示了如何使用命令pip install flask
安装 Flask。请注意,该命令下载必要的组件,提取它们,然后运行每个组件的安装程序。在这种情况下,我们看到 Flask 由几个组件组成,包括 Werkzeug、MarkupSafe 和 Jinja2。我们将在“烧瓶初级读本”一节中了解更多。
$ pip3 install flask
Collecting flask
Using cached Flask-0.12.2-py2.py3-none-any.whl
Collecting Werkzeug>=0.7 (from flask)
Downloading Werkzeug-0.14.1-py2.py3-none-any.whl (322kB)
100% |████████████████████████████████| 327kB 442kB/s
Collecting Jinja2>=2.4 (from flask)
Using cached Jinja2-2.10-py2.py3-none-any.whl
Collecting itsdangerous>=0.21 (from flask)
Using cached itsdangerous-0.24.tar.gz
Collecting click>=2.0 (from flask)
Downloading click-6.7-py2.py3-none-any.whl (71kB)
100% |████████████████████████████████| 71kB 9.4MB/s
Collecting MarkupSafe>=0.23 (from Jinja2>=2.4->flask)
Using cached MarkupSafe-1.0.tar.gz
Installing collected packages: Werkzeug, MarkupSafe, Jinja2, itsdangerous, click, flask
Running setup.py install for MarkupSafe ... done
Running setup.py install for itsdangerous ... done
Successfully installed Jinja2-2.10 MarkupSafe-1.0 Werkzeug-0.14.1 click-6.7 flask-0.12.2 itsdangerous-0.24
Listing 8-1Installing Flask
安装烧瓶-脚本
清单 8-2 展示了如何使用命令pip install flask-script
安装 Flask-Script。请注意,在这种情况下,我们看到安装检查先决条件及其版本。
$ pip3 install flask-script
Collecting flask-script
Using cached Flask-Script-2.0.6.tar.gz
Requirement already satisfied: Flask in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from flask-script)
Requirement already satisfied: click>=2.0 in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from Flask->flask-script)
Requirement already satisfied: Jinja2>=2.4 in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from Flask->flask-script)
Requirement already satisfied: Werkzeug>=0.7 in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from Flask->flask-script)
Requirement already satisfied: itsdangerous>=0.21 in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from Flask->flask-script)
Requirement already satisfied: MarkupSafe>=0.23 in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from Jinja2>=2.4->Flask->flask-script)
Installing collected packages: flask-script
Running setup.py install for flask-script ... done
Successfully installed flask-script-2.0.6
Listing 8-2Installing Flask-Script
安装烧瓶-引导程序
清单 8-3 展示了如何使用命令pip install flask-bootstrap
安装 Flask-Bootstrap。我们再次看到安装检查先决条件及其版本,以及依赖组件的安装。
$ pip3 install flask-bootstrap
Collecting flask-bootstrap
Downloading Flask-Bootstrap-3.3.7.1.tar.gz (456kB)
100% |████████████████████████████████| 460kB 267kB/s
Requirement already satisfied: Flask>=0.8 in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from flask-bootstrap)
Collecting dominate (from flask-bootstrap)
Downloading dominate-2.3.1.tar.gz
Collecting visitor (from flask-bootstrap)
Downloading visitor-0.1.3.tar.gz
Requirement already satisfied: click>=2.0 in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from Flask>=0.8->flask-bootstrap)
Requirement already satisfied: Jinja2>=2.4 in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from Flask>=0.8->flask-bootstrap)
Requirement already satisfied: Werkzeug>=0.7 in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from Flask>=0.8->flask-bootstrap)
Requirement already satisfied: itsdangerous>=0.21 in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from Flask>=0.8->flask-bootstrap)
Requirement already satisfied: MarkupSafe>=0.23 in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from Jinja2>=2.4->Flask>=0.8->flask-bootstrap)
Installing collected packages: dominate, visitor, flask-bootstrap
Running setup.py install for dominate ... done
Running setup.py install for visitor ... done
Running setup.py install for flask-bootstrap ... done
Successfully installed dominate-2.3.1 flask-bootstrap-3.3.7.1 visitor-0.1.3
Listing 8-3Installing Flask-Bootstrap
安装烧瓶-WTF
清单 8-4 展示了如何使用命令pip install flask-wtf
安装 Flask-WTF。
$ pip3 install flask-wtf
Collecting flask-wtf
Downloading Flask_WTF-0.14.2-py2.py3-none-any.whl
Requirement already satisfied: WTForms in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from flask-wtf)
Requirement already satisfied: Flask in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from flask-wtf)
Requirement already satisfied: Jinja2>=2.4 in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from Flask->flask-wtf)
Requirement already satisfied: click>=2.0 in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from Flask->flask-wtf)
Requirement already satisfied: Werkzeug>=0.7 in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from Flask->flask-wtf)
Requirement already satisfied: itsdangerous>=0.21 in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from Flask->flask-wtf)
Requirement already satisfied: MarkupSafe>=0.23 in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from Jinja2>=2.4->Flask->flask-wtf)
Installing collected packages: flask-wtf
Successfully installed flask-wtf-0.14.2
Listing 8-4Installing Flask-WTF
安装 WTForms
下面演示了如何使用命令pip install WTforms
安装 WTForms。在这种情况下,安装很简单,因为我们只需要一个包。
$ pip3 install wtforms
Collecting wtforms
Using cached WTForms-2.1.zip
Installing collected packages: wtforms
Running setup.py install for wtforms ... done
Successfully installed wtforms-2.1
Using Python Virtual Environments
使用 Python 的一个好处是,你可以使用虚拟环境来尝试一些东西。虚拟环境是 Python 的本地(认为是私有的)安装,您可以安装软件包并对 Python 环境进行更改,而不会影响系统上的全局 Python 安装。因此,例如,如果您使用虚拟环境安装 Flask,它只对该虚拟环境可用,不会影响任何其他虚拟环境或全局 Python 安装。
要使用虚拟环境,您必须安装virtualenv
应用。不是所有的系统都有这个功能,事实上也不是所有的平台都支持这个功能(但是很多平台都支持)。要在 Linux 上安装虚拟环境,可以使用命令sudo apt-get install python-virtualenv
。要在 macOS 上安装虚拟环境,请使用命令sudo easy_install virtualenv
。要在 Windows 10 上安装虚拟环境,必须从 https://github.com/pypa/setuptools
下载ez_setup.py
(setuptools
的一部分)。下载完成后,使用管理权限打开命令窗口,然后输入命令python ez_setup.py
安装easy_install
,然后输入命令easy_install virtualenv
安装虚拟环境。
要创建和使用虚拟环境,发出命令virtualenv project1
。这会创建一个名为project1
的文件夹,其中包含虚拟环境文件,这些文件会跟踪在该环境中所做的所有更改。要激活环境,使用source
命令。请注意,我们正在新的虚拟环境文件夹中调用一个脚本。这将改变您的提示,以表明您正在使用虚拟环境。要停用环境,在虚拟环境激活时使用deactivate
命令。这将使您的 Python 环境返回到全局默认值。下面演示了 macOS 上的这些命令。
$ mkdir virtual_environments
$ cd virtual_environments
$ virtualenv project1
New python executable in /virtual_environments/project1/bin/python
Installing setuptools, pip, wheel...done.
$ source ./project1/bin/activate
[Do something Python related here. Changes apply only to the active virtual environment.]
(project1) $ deactivate
删除虚拟环境只需删除环境文件夹(在停用它之后):
$ deactivate
$ rm -r /virtual_environments/project1
有些人建议在尝试 Python 中的新事物时总是使用虚拟环境,对于一些东西,比如不受信任或未经尝试的库或者与现有安装的库冲突的库,这是一个好的实践。然而,对于主流项目,如 Flask 及其支持库,并不需要它。如果你想在进行的项目中使用虚拟环境,请随意。只需记住在发出任何 Python 命令之前激活它,并在完成后停用它。
要了解更多关于虚拟环境的信息,请参见 https://virtualenv.pypa.io/en/stable/
。
您还应该安装 MySQL 连接器/Python 8.0.5 或更高版本的数据库连接器。如果没有,从 https://dev.mysql.com/downloads/connector/python/
下载并安装。如果您安装了多个版本的 Python,请确保将其安装在您想要使用的所有 Python 环境中。否则,在启动代码时,您可能会看到如下错误。
$ python3 ./mylibrary_v1.py runserver -p 5001
Traceback (most recent call last):
File "./mylibrary_v1.py", line 18, in <module>
from database.library_v1 import Library, Author, Publisher, Book
File ".../Ch08/version1/database/library_v1.py", line 15, in <module>
import mysql.connector
ModuleNotFoundError: No module named 'mysql'
Pip 也可以用来安装 MySQL 连接器/Python。下面显示了如何使用 PIP 安装连接器。
$ pip3 install mysql-connector-python
Collecting mysql-connector-python
Downloading mysql_connector_python-8.0.6-cp36-cp36m-macosx_10_12_x86_64.whl (3.2MB)
100% |████████████████████████████████| 3.2MB 16.9MB/s
Installing collected packages: mysql-connector-python
Successfully installed mysql-connector-python-8.0.6
如果您手动或从源代码安装了 MySQL Connector/Python,您可能还需要安装 Protobuf。你可以使用pip
来安装它,如下图所示。
$ pip3 install protobuf
Collecting protobuf
Downloading protobuf-3.5.1-py2.py3-none-any.whl (388kB)
100% |████████████████████████████████| 389kB 414kB/s
Requirement already satisfied: setuptools in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages (from protobuf)
Requirement already satisfied: six>=1.9 in /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/six-1.10.0-py3.6.egg (from protobuf)
Installing collected packages: protobuf
Successfully installed protobuf-3.5.1
现在我们的计算机已经安装好了,让我们上一堂关于 Flask 及其相关扩展的速成课。
弗拉斯克第一
Flask 是与 Python 一起使用的几个 web 应用库(有时称为框架或应用编程接口——API)之一。Flask 在众多选择中是独一无二的,因为它很小,一旦你熟悉了它的工作原理,就很容易使用。也就是说,一旦编写了初始化代码,使用 Flask 的大部分工作将局限于创建网页、重定向响应和编写功能代码。
Flask 被认为是一个微型框架,因为它体积小、重量轻,而且它不会强迫你进入一个专门编写代码来与框架交互的盒子中。它提供了您需要的一切,让您自己选择在代码中使用什么。
Flask 由提供基本功能的两个主要组件组成:一个 Web 服务器网关接口(WSGI ),处理托管网页的所有工作;以及一个模板库,用于简化 web 页面开发,减少了学习 HTML 的需要,删除了重复的结构,并为 HTML 代码提供了脚本功能。WSGI 组件被命名为 Werkzeug,它是从德语中大致翻译过来的意思,“工作的东西”( http://werkzeug.pocoo.org/
)。模板组件被命名为 Jinja2,并模仿 Django ( http://jinja.pocoo.org/docs/2.10/
)。两者都是由 Flask 的创始人开发和维护的。最后,当您安装 Flask 时,这两个组件都会被安装。
Flask 也是一个可扩展的库,允许其他开发者创建基本库的附件(扩展)来添加功能。在上一节中,我们看到了如何安装 Flask 可用的一些扩展。我们将在本章中使用脚本、引导和 WTForms 扩展。能够挑选您想要的扩展意味着您可以保持您的应用尽可能小,只添加您需要的。
您可能认为 flask“缺少”的组件之一是与其他服务(如数据库系统)交互的能力。这是一个有目的的设计,像这样的功能可以通过扩展来实现。事实上,Flask 有几个可用的数据库扩展,包括那些允许您使用 MySQL 的扩展。但是,因为我们想要使用 X DevAPI,所以我们必须使用 Oracle 提供的连接器 MySQL Connector/Python。这不仅是可能的,也说明了你在使用 Flask 时的自由度;我们不局限于数据库服务器访问这样的特定功能,我们可以使用我们想要或需要的任何其他 Python 库。 2
Tip
如果你对 MySQL 对 Flask 的支持很好奇,请看 http://flask-mysql.readthedocs.io/en/latest/
。
Flask 和前面描述的扩展一起,提供了用 Python 制作 Web 应用所需的所有连接和管道。它消除了编写 web 应用所需的几乎所有负担,例如解释客户机响应包、路由、HTML 表单处理等等。如果您曾经用 Python 编写过 web 应用,您将会体会到创建健壮的 web 页面的能力,而无需编写 HTML 和样式表的复杂性。一旦你熟悉如何使用 Flask,它将允许你专注于你的应用的代码,而不是花大量的时间编写用户界面。
现在,让我们开始学习 Flask!如果您不着急,尝试一下示例应用,您的第一个 Flask 应用将在第一次尝试时就能工作。学习 Flask 最难的部分已经过去了——安装 Flask 及其扩展。剩下的就是学习在 Flask 中编写应用的概念。在此之前,让我们了解一下 Flask 中的术语,以及如何设置我们将用来初始化本章中使用的应用实例的基本代码。
Tip
如果你想进一步了解 Flask,你应该考虑阅读在线文档、用户指南和 http://flask.pocoo.org/docs/0.12/
的例子。
术语
Flask 旨在简化编写 web 应用的繁琐过程。按照 Flask 的说法,使用代码的两个部分来呈现一个 web 页面:一个视图,它是在 HTML 文件中定义的;一个路由,它处理来自客户端的请求。回想一下,我们可以看到两个请求中的一个:一个是请求加载网页的GET
请求(从客户端的角度读取),另一个是从客户端通过网页向服务器发送数据的POST
请求(从客户端的角度写入)。这两个请求都在 Flask 中使用您定义的函数进行处理。
然后,这些函数呈现网页,并将其发送回客户端以满足请求。Flask 调用函数视图函数(或简称视图)。Flask 知道调用哪个方法的方式是使用识别 URL 路径(在 Flask 中称为路由)的装饰器。你可以用一条或多条路线来装饰一个功能,这样就可以提供多种到达视图的方式。用的装饰师是@app.route(<path>)
。以下显示了查看功能的多条路线的示例。
@app.route('/book', methods=['GET', 'POST'])
@app.route('/book/<string:isbn_selected>', methods=['GET', 'POST'])
def book(isbn_selected=None):
notes = None
form = BookForm()
form.publisher.choices = []
form.authors.choices = []
new_note = ""
if request.method == 'POST':
pass
return render_template("book.html", form=form, notes=notes)
注意这里有多个装饰者。第一个是 book,它允许我们使用一个 URL,比如localhost:5000/book
,这使得 Flask 将执行路由到book()
函数。第二个是book/<isbn_selected>
,演示了如何使用变量向视图传递信息。在这种情况下,如果用户(应用)使用 URL localhost:5000/book/978-1-4842-1294-3
,Flask 将值978-1-4842-1294-3
放在isbn_selected
变量中。这样,我们可以动态地将信息传递给我们的视图。
还要注意,路由指定了每条路由允许的方法。在这个应用中,我们可以为任何一个路由设置一个GET
或POST
。如果你离开装饰器,默认是GET
只让网页只读。
最后,请注意,在函数的末尾,我们返回了对render_template()
函数的调用(从 flask 模块导入),该函数告诉 flask 返回(刷新)带有我们获取或分配的数据的网页。网页,book.html,虽然视图的一部分在 Flask 中被称为表单。我们将使用这个概念从数据库中检索信息并将其发送给用户。我们可以返回一个简单的 HTML 字符串(或整个文件)或所谓的表单。因为我们使用 Flask-WTF 和 WTForms 扩展,所以我们可以返回一个呈现为表单类的模板。我们将在后面的章节中讨论表单、表单类以及章节项目的其他路径和视图。正如您将看到的,模板是另一个强大的功能,它使创建网页变得很容易。
What’s a Decorator?
在 Python 中,我们可以通过使用 decorators 来指定特殊的处理参数。装饰器只是改变函数行为的一种方式。例如,您可以使用 decorators 来添加更强的类型检查、定义宏以及在执行前后调用函数。Flask for routing 中的 decorator 是正确使用 decorator 的一些最好的例子。要了解更多关于装饰者的信息,请参见 https://www.python.org/dev/peps/pep-0318
。
Flask 构建了一个应用中所有路径的列表,使得应用在被请求时可以很容易地将执行路由到正确的函数。但是,如果请求了一条路由,但它不在应用中,会发生什么情况呢?默认情况下,您将得到一个类似于“Not Found. The requested URL was not found on the server.
”的一般错误消息。我们将在后面的小节中看到如何添加我们自己的自定义错误处理路线。
现在我们已经了解了 Flask 中使用的术语以及它是如何与网页一起工作的,让我们来看看一个典型的 Flask 应用是如何构造的,它带有我们需要的扩展。
初始化和应用实例
Flask 及其扩展为您的 web 应用提供了入口点。Flask 会为您完成这些工作,而不是自己编写所有繁重的代码!我们将在本章使用的 Flask 扩展包括 Flask-Script、Flask-Bootstrap、Flask-WTF 和 WTForms。以下各节简要介绍了每一种方法。
烧瓶脚本
Flask-Script 通过添加命令行解析器(显示为manager
)来启用 Flask 应用中的脚本,您可以使用该解析器链接到您编写的函数。这可以通过用@manager.command
修饰函数来实现。理解这为我们做了什么的最好方法是通过一个例子。
下面是一个基本的原始 Flask 应用,它什么也不做。它甚至不是一个“hello,world”示例,因为没有显示任何内容,也没有托管任何网页——它只是一个原始的 Flask 应用。
from flask import Flask # import the Flask framework
app = Flask(__name__) # initialize the application
if __name__ == "__main__": # guard for running the code
app.run() # launch the application
注意这个app.run()
调用。这称为服务器启动,在我们使用 Python 解释器加载脚本时执行。当我们运行这段代码时,我们看到的只是来自 Flask 的默认消息,如下所示。请注意,我们无法查看帮助,因为没有这样的选项。我们还看到代码使用 web 服务器的缺省值启动(如果需要,我们可以在代码中更改)。例如,我们可以改变服务器监听的端口。
$ python ./flask-ex.py --help
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
使用 Flask-Script,我们不仅添加了帮助选项,还添加了控制服务器的选项。下面的代码显示了添加语句来启用 Flask-Script 是多么容易。新语句以粗体突出显示。
from flask import Flask # import the Flask framework
from flask_script import Manager # import the flask script manager class
app = Flask(__name__) # initialize the application
manager = Manager(app) # initialize the script manager class
# Sample method linked as a command-line option
@manager.command
def hello_world():
"""Print 'Hello, world!'"""
print("Hello, world!")
if __name__ == "__main__": # guard for running the code
manager.run() # launch the application via manager class
当运行这段代码时,我们可以看到还有其他选项可用。请注意,文档字符串(紧跟在方法定义之后)显示为所添加命令的帮助文本。
$ python ./flask-script-ex.py --help
usage: flask-script-ex.py [-?] {hello_world,shell,runserver} ...
positional arguments:
{hello_world,shell,runserver}
hello_world Print 'Hello, world!'
shell Runs a Python shell inside Flask application context.
runserver Runs the Flask development server i.e. app.run()
optional arguments:
-?, --help show this help message and exit
注意,我们看到了我们添加的命令行参数(命令),hello_world
,但是我们也看到了 Flask-Script 提供的两个新参数;Shell 和runserver
。启动服务器时,您必须选择其中一个命令。shell 命令允许您在 Python 解释器或类似工具中使用代码,而runserver
执行代码来启动 web 服务器。
我们不仅可以获得关于命令和选项的帮助,Flask-Script 还提供了从命令行对服务器的更多控制。事实上,我们可以通过添加如下所示的--help
选项来查看每个命令的所有选项。
$ python ./flask-script-ex.py runserver --help
usage: flask-script-ex.py runserver [-?] [-h HOST] [-p PORT] [--threaded]
[--processes PROCESSES]
[--passthrough-errors] [-d] [-D] [-r] [-R]
[--ssl-crt SSL_CRT] [--ssl-key SSL_KEY]
Runs the Flask development server i.e. app.run()
optional arguments:
-?, --help show this help message and exit
-h HOST, --host HOST
-p PORT, --port PORT
--threaded
--processes PROCESSES
--passthrough-errors
-d, --debug enable the Werkzeug debugger (DO NOT use in production
code)
-D, --no-debug disable the Werkzeug debugger
-r, --reload monitor Python files for changes (not 100% safe for
production use)
-R, --no-reload do not monitor Python files for changes
--ssl-crt SSL_CRT Path to ssl certificate
--ssl-key SSL_KEY Path to ssl key
请注意,我们可以控制服务器的所有方面,包括端口、主机,甚至它是如何执行的。
最后,我们可以执行我们修饰为命令行选项的方法,如下所示。
$ python ./flask-script-ex.py hello_world
Hello, world!
因此,Flask-Script 仅用几行代码就提供了一些非常强大的功能。你一定会喜欢的!
烧瓶自举
Flask-Bootstrap 最初由 Twitter 开发,用于制作统一、美观的 web 客户端。幸运的是,他们已经把它扩展成了一个 Flask,这样每个人都可以利用它的特性。Flask-Bootstrap 是一个独立的框架,它提供了更多的命令行控制和用户界面组件,以获得干净、漂亮的网页。它也兼容最新的网络浏览器。
该框架在幕后发挥了神奇的作用,它是一个级联样式表(CSS)和脚本的客户端库,这些样式表和脚本是从 Flask 中的 HTML 模板(通常称为 HTML 文件或模板文件)调用的。我们将在后面的章节中学习更多关于模板的知识。因为是客户端,所以在主应用中初始化我们不会看到太多。不管怎样,下面显示了如何将 Flask-bootstrap 添加到我们的应用代码中。这里,我们看到我们有一个框架,其中初始化并配置了 Flask-Script 和 Flask-Bootstrap。
from flask import Flask # import the Flask framework
from flask_script import Manager # import the flask script manager class
from flask_bootstrap import Bootstrap # import the flask bootstrap extension
app = Flask(__name__) # initialize the application
manager = Manager(app) # initialize the script manager class
bootstrap = Bootstrap(app) # initialize the bootstrap extension
if __name__ == "__main__": # guard for running the code
manager.run() # launch the application via manager class
WTForms
WTForms 是我们需要用来支持 Flask-WTF 扩展的一个组件。它提供了 Flask-WTF 组件所提供的大部分功能(因为 Flask-WTF 组件是 WTForms 的 Flask 特定包装器)。因此,我们只需要安装它作为 Flask-WTF 的先决条件,我们将在 Flask-WTF 的上下文中讨论它。
Note
Flask-WTF 的一些包装装置可以包括 WTForms。
烧瓶-WTF
Flask-WTF 扩展是一个有趣的组件,提供了几个非常有用的附加功能:最值得注意的是与 WTForms(一个框架不可知组件)的集成,它允许创建表单类,并以跨站点请求伪造(CSRF)保护的形式提供了额外的 web 安全性。这两个特性允许您将 web 应用提升到更高的复杂程度。
表单类
表单类提供了一个类的层次结构,使得定义网页更加符合逻辑。使用 Flask-WTF,您可以使用两段代码定义表单;一个从 FormForm 类(从 Flask framework 导入)派生的特殊类,用于使用一个或多个提供对数据的编程访问的附加类以及一个用于呈现网页的 HTML 文件(或模板)来定义字段。这样,我们在 HTML 文件上看到了一个抽象层(表单类)。我们将在下一节看到更多关于 HTML 文件的内容。
使用 form 类,您可以定义一个或多个字段,例如 TextField 表示文本,StringField 表示字符串,等等。更好的是,您可以定义允许您以编程方式描述数据的验证器。例如,您可以为文本字段定义最小和最大字符数。如果提交的字符数超出范围,将生成一条错误消息。是的,你可以定义一个错误信息!下面列出了一些可用的验证器。查看 http://wtforms.readthedocs.io/en/latest/validators.html
查看验证器的完整列表。
DataRequired
:确定输入栏是否为空Email
:确保该字段遵循电子邮件 ID 约定IPAddress
:验证 IP 地址Length
:确保文本长度在给定范围内NumberRange
:确保文本是数字,并且在给定的范围内URL
:验证 URL
为了形成类,我们必须导入类和任何我们想在应用的序言中使用的字段类。下面显示了一个导入表单类和表单域类的示例。在这个例子中,我们还导入了一些验证器,用于自动验证数据。
from flask_wtf import FlaskForm
from wtforms import (HiddenField, TextField, TextAreaField, SelectField,
SelectMultipleField, IntegerField, SubmitField)
from wtforms.validators import Required, Length
要定义一个表单类,我们必须从FlaskForm
派生一个新类。从那里,我们可以构造我们想要的类,但是它允许您定义字段。FlaskForm
父类包括 Flask 需要实例化和使用 form 类的所有必要代码。
让我们看一个简单的例子。下面显示了作者网页的 form 类。作者表包含三个字段,我们将通过 view 函数将它链接到这段代码;自动递增字段(authorid
)、作者的名(firstname
)和作者的姓(lastname
)。因为用户不需要看到 author id 字段,所以我们将该字段设置为隐藏字段,其他字段是TextField()
类的派生字段。请注意这些是如何在清单中用名称(标签)作为第一个参数来定义的。
class AuthorForm(FlaskForm):
authorid = HiddenField('AuthorId')
firstname = TextField('First name', validators=[
Required(message=REQUIRED.format("Firstname")),
Length(min=1, max=64, message=RANGE.format("Firstname", 1, 64))
])
lastname = TextField( 'Last name', validators=[
Required(message=REQUIRED.format("Lastname")),
Length(min=1, max=64, message=RANGE.format("Lastname", 1, 64))
])
create_button = SubmitField('Add')
del_button = SubmitField('Delete')
还要注意,我们以从 WTForms 组件为字段导入的函数调用的形式定义了一个验证器数组。在每种情况下,我们都为消息使用字符串,以使代码更容易阅读,更统一。这些字符串包括以下内容。
REQUIRED = "{0} field is required."
RANGE = "{0} range is {1} to {2} characters."
我们使用Required()
验证器来指示字段必须有一个值。我们用字段的名称增加了默认的错误消息,使用户更容易理解。我们还使用了一个Length()
验证函数,它定义了字段数据的最小和最大长度。我们再次增加了默认的错误消息。验证器只应用于POST
操作(当提交事件发生时)。
接下来,我们看到有两个SubmitField()
实例:一个用于创建(添加)按钮,另一个用于删除按钮。正如您可能猜测的那样,按照 HTML 的说法,这些字段被呈现为类型为“submit
”的<input>
字段。
最后,为了使用一个表单类,我们在一个视图函数中实例化这个类。下面显示了作者视图函数的存根。注意,我们实例化了名为AuthorForm()
的表单类,并将其分配给名为 form 的变量,该变量被传递给render_template()
函数。
@app.route('/author', methods=['GET', 'POST'])
@app.route('/author/<int:author_id>', methods=['GET', 'POST'])
def author(author_id=None):
form = AuthorForm()
if request.method == 'POST':
pass
return render_template("author.html", form=form)
有几个字段类可供使用。表 8-2 显示了最常用的字段类(也称为 HTML 字段)的示例。您还可以从这些字段派生来创建自定义字段类,并为可以显示在字段旁边的标签提供文本(例如,作为按钮文本)。我们将在后面的章节中看到一个这样的例子。
表 8-2
WTForms Field Classes
| 字段类 | 描述 | | :-- | :-- | | `BooleanField` | 具有真值和假值的复选框 | | `DateField` | 接受日期值 | | `DateTimeField` | 接受日期时间值 | | `DecimalField` | 接受十进制值 | | `FileField` | 文件上传字段 | | `FloatField` | 接受浮点值 | | `HiddenField` | 隐藏文本字段 | | `IntegerField` | 接受整数值 | | `PasswordField` | 密码(屏蔽)文本字段 | | `RadioField` | 单选按钮列表 | | `SelectField` | 下拉列表(选择一个) | | `SelectMultipleField` | 选项下拉列表(选择一项或多项) | | `StringField` | 接受简单文本 | | `SubmitField` | 表单提交按钮 | | `TextAreaField` | 多行文本字段 |CSRF 保护
CSRF 保护是一种允许开发者用加密密钥签署网页的技术,这使得黑客更难欺骗GET
或POST
请求。这是通过首先在应用代码中放置一个特殊的键,然后在每个 HTML 文件中引用这个键来实现的。下面显示了一个应用序言的示例。注意,我们需要做的就是用一个短语给app.config
数组的SECRET_KEY
索引赋值。这应该是一个不容易猜到的短语。
from flask import Flask # import the Flask framework
from flask_script import Manager # import the flask script manager class
from flask_bootstrap import Bootstrap # import the flask bootstrap extension
app = Flask(__name__) # initialize the application
app.config['SECRET_KEY'] = "He says, he's already got one!"
manager = Manager(app) # initialize the script manager class
bootstrap = Bootstrap(app) # initialize the bootstrap extension
if __name__ == "__main__": # guard for running the code
manager.run() # launch the application via manager class
要激活网页中的 CSRF,我们只需将form.csrf_token
添加到 HTML 文件中。这是一个特殊的隐藏字段,Flask 使用它来验证请求。我们将在后面的部分中看到更多关于在哪里放置它的信息。但首先,我们来看看 Flask 的一个很酷的功能,叫做 flash。
信息闪烁
Flask 有很多很酷的功能。Flask 扩展的创建者似乎已经考虑到了一切——甚至是错误消息。考虑一个典型的 web 应用。你如何向用户传达错误?你是否重定向到一个新页面,【3】弹出, 4 或者在页面上显示错误?Flask 有一个解决方案,叫做消息闪烁。
消息闪烁是使用 Flask 框架中的 flash()方法完成的。我们只需将它导入到代码的序言中,然后当我们想要显示一条消息时,我们调用 flash()函数传入我们想要看到的错误消息。Flask 将在表单顶部的一个格式良好的框中显示错误。它没有取代表单,也不是弹出窗口,但是它允许用户关闭消息。您可以使用 flash messaging 向用户传达错误、警告甚至状态更改。图 8-1 显示了一个闪光信息的例子。在本例中,我们看到两条 flash 消息,表明您可以同时显示多条消息。请注意用于消除图像的消息右侧的小 X。
图 8-1
Example flash message
在下一节中,我们将看到一种将 flash 消息传递构建到所有网页中的机制。
HTML 文件和模板
让我们回顾一下到目前为止的旅程。我们发现了如何用各种组件初始化应用,了解了 Flask 如何通过 decorators 使用路由来为应用创建一组 URL,这些路由被定向到一个视图函数,该函数实例化了 form 类。下一个难题是如何将 HTML 网页链接到 form 类。
回想一下,这是通过render_template()
函数完成的,在这里我们传入一个 HTML 文件的名称进行处理。template 出现在名称中的原因是因为我们可以使用 Jinja2 模板组件来简化 web 页面的编写。更具体地说,HTML 文件包含 HTML 标记和 Jinja2 模板构造。
Note
所有 HTML 文件(模板)必须存储在与主应用代码相同的位置的templates
文件夹中。例如,如果你的代码在一个名为my-flask-app.py
的文件中,那么在与my-flask-app.py
相同的文件夹中应该有一个templates
文件夹。如果你把它们放在其他地方,Flask 将找不到 HTML 文件。
模板和表单类是设计用户界面的地方。简而言之,模板用于包含表示逻辑,HTML 文件用于包含表示数据。这些主题可能是一些人需要花一些时间来尝试如何使用它们的领域。下面几节将通过工作示例的演示,向您简要介绍 Jinja2 模板以及如何在我们的 HTML 文件中使用它们。有关更多详细信息,请参见在线烧瓶文档。
Jinja2 模板概述
Jinja2 模板(或称模板)用于包含任何表示逻辑,如遍历数据数组、决定显示什么,甚至格式化和表示设置。如果您熟悉其他 web 开发环境,您可能已经看到过这种封装在脚本中或通过嵌入式脚本(如 JavaScript)实现的功能。
回想一下,我们在主代码中呈现了网页。这个函数告诉 Flask 读取指定的文件,并将模板结构转换(渲染)成 HTML。也就是说,Flask 会将模板结构扩展并编译成 web 服务器可以呈现给客户机的 HTML。
有几种模板结构可以用来控制执行流、循环甚至注释。每当你想使用一个模板结构(想想脚本语言),你用前缀和后缀{% %}
把它括起来。这使得 Flask 框架能够将该构造识别为模板操作,而不是 HTML。
然而,看到模板结构与 HTML 标记混杂在一起并不罕见,也很正常。事实上,这正是你应该做的。毕竟,您将创建的文件被命名为. html。它们只是碰巧包含模板构造。这是否意味着在使用 Flask 时只能使用模板?不,当然不是。如果你愿意,你可以渲染一个纯 HTML 文件!
起初,查看模板可能会令人望而生畏。但也没那么难。只需查看所有将{%
和%}
作为“代码”部分的行。 5 你也可以看到以{# #}
前缀和后缀形式出现的评论。
Caution
所有模板构造都要求在{%
之后和%}
之前有一个空格。
如果你看看模板,你会看到构造和标签,并使用两个空格缩进格式化。在标签和构造之外,缩进和空白通常无关紧要。然而,大多数开发者会使用某种形式的缩进来使文件更容易阅读。事实上,大多数编码指南都要求缩进。
模板除了构造(想想代码)之外的一个很酷的特性是创建模板层次结构的能力。这允许您创建一个其他模板可以使用的“基础”模板。例如,您可以创建一个模板构造和 HTML 标记的样板文件,这样您的所有网页看起来都一样。
回想一下 Flask-Bootstrap,Bootstrap 提供了几个很好的格式化特性。其中一个功能是创建一个外观漂亮的导航栏。很自然,我们希望它出现在我们所有的网页上。我们可以通过在基本模板中定义它并在我们的其他模板(HTML)文件中扩展它来做到这一点。让我们看一下库应用的基本模板。清单 8-5 显示了库应用的基本模板。为了便于讨论,添加了行号。
01 {% extends "bootstrap/base.html" %}
02 {% block title %}MyLibrary{% endblock %}
03 {% block navbar %}
04 <div class="navbar navbar-inverse" role="navigation">
05 <div class="container">
06 <div class="navbar-header">
07 <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
08 <span class="sr-only">Toggle navigation</span>
09 <span class="icon-bar"></span>
10 <span class="icon-bar"></span>
11 <span class="icon-bar"></span>
12 </button>
13 <a class="navbar-brand" href="/">MyLibrary Base</a>
14 </div>
15 <div class="navbar-collapse collapse">
16 <ul class="nav navbar-nav">
17 <li><a href="/list/book">Books</a></li>
18 </ul>
19 <ul class="nav navbar-nav">
20 <li><a href="/list/author">Authors</a></li>
21 </ul>
22 <ul class="nav navbar-nav">
23 <li><a href="/list/publisher">Publishers</a></li>
24 </ul>
25 </div>
26 </div>
27 </div>
28 {% endblock %}
29
30 {% block content %}
31 <div class="container">
32 {% for message in get_flashed_messages() %}
33 <div class="alert alert-warning">
34 <button type="button" class="close" data-dismiss="alert">×</button>
35 {{ message }}
36 </div>
37 {% endfor %}
38 {% block page_content %}{% endblock %}
39 </div>
40 {% endblock %}
Listing 8-5Sample Base Template
哇,这里发生了很多事情!注意第一行。这告诉我们,我们正在继承(扩展)另一个名为bootstrap/base.html
template 的模板。这是在你安装 Flask-Bootstrap 时免费提供给你的,正是这个模板包含了对 Bootstrap 导航栏特性的支持。这是为 Flask 应用构建一组 HTML 文件的一种非常常见的方法,我们将在本节的后面看到。
让我们从鸟瞰图开始我们的旅行。请注意,有两个“块”用{ % block <> %}
和{% endblock %}
表示(第 2、3、28、30、38 和 40 行)。这些是逻辑部分,我们可以在其中对块内的标记和构造应用格式。用编码术语来说,这就像一个代码块。第一个块定义页面的标题。在本例中是 MyLibrary,这是库应用的可执行名称。
第二块定义了应用的导航栏(思考菜单)。请注意,第 5–27 行定义了简单的 HTML <div>
标签,这些标签构成了导航栏上的项目。值得注意的是第 13 行,它指定了用作应用名称的文本,该文本出现在导航栏的左侧,类似于“home”链接。第 15–24 行定义了三个网页(表单)的导航栏项目(提交按钮)。另请注意 collapse 关键字。这表明可以折叠导航栏。那么,导航栏是什么样子的呢?图 8-2 显示了正常、折叠和展开模式下的库应用的导航栏。当无法显示导航项目标签时,正常和折叠模式基于折叠的浏览器窗口的大小进行操作。当用户在折叠模式下点击右边的按钮时,展开模式开始工作。酷吧。
图 8-2
Bootstrap navigation bar demonstration
第 30–39 行的最后一个块定义了模板结构和 flash 消息的 HTML 标签。让我们更深入地看看这段代码(为了方便起见,这里重复了一遍)。
30 {% block content %}
31 <div class="container">
32 {% for message in get_flashed_messages() %}
33 <div class="alert alert-warning">
34 <button type="button" class="close" data-dismiss="alert">×</button>
35 {{ message }}
36 </div>
37 {% endfor %}
38 {% block page_content %}{% endblock %}
39 </div>
40 {% endblock %}
这里,我们看到另一个包含按钮的标签。这是我们用来关闭简讯的按钮。注意,这个标签被放在一个用{% for ... %}
指定的 for 循环中,并以{% endfor %}
结束。在本例中,我们循环从get_flashed_messages()
函数返回的消息,这些消息是由应用代码中的flash()
函数收集的。这告诉我们几件事:我们可以在我们的模板中使用循环,模板允许显示多个图像(我们前面看到过),模板可以调用函数!这是模板威力的一个例子。
Note
模板不需要以任何方式格式化。也就是说,空白在 HTML 标记或模板构造之外不做任何事情。
最后,注意我们在第 32 行的 for 循环中定义的变量。这个变量 message 被定义在它所在的块的本地(在本例中是 for 循环),并且可以在任何时候通过将它包含在{{ }}
中来引用。例如,我们在第 35 行看到,我们在<div>
标签中使用了{{ message }}
,这意味着这个文本将出现在客户机上,由 Flask 就地呈现。当我们讨论如何用模板构建用户界面时,变量的使用将变得更加重要。
模板语言结构
Jinja2 模板有很多特性,对所有特性的完整讨论超出了本书的范围。然而,快速参考 Jinja2 的主要结构是很方便的。下面给出了一些常用的构造,包括我们在上一节中发现的一些(为了完整性)。每一个都有一个简短的例子,说明这个构造在模板中是如何出现的。在本章后面探索库应用或编写自己的 Flask 应用时,请随意参考本节。
评论
您可以在模板中嵌入自己的注释。您可能希望这样做,以确保您充分解释您正在做什么,并作为一个提醒,以防您以后重用代码。 6 下面是一个在模板中使用注释的例子。回想一下,注释以{#
开始,以#}
结束,可以跨多行。
{# This is a line comment written by Dr. Charles Bell on Dec. 12, 2017\. #}
{#
Introducing the MySQL 8 Document Store
This template defines the base template used for all of the HTML forms and
responses in the MyLibrary application. It also defines the menu for the
basic operations.
Dr. Charles Bell, 2017
#}
包括
如果您的模板文件增长了,并且您发现有些部分是可重用的,比如一个<div>
标签,您可以将标签和模板构造保存在一个单独的文件中,并使用{% include %}
构造将它包含在其他模板中。{% include %}
构造将您想要包含的文件的名称作为参数。与模板一样,它们必须位于 templates 文件夹中。这样,我们避免了重复和维护重复代码的麻烦和容易出错的任务。
{# Include the utilities common tags for a list. #}
{% include 'utilities.html' %}
宏指令
减少重复代码的另一种形式是创建一个在模板中使用的宏(想想函数)。在这种情况下,我们使用{% macro … %}
和{% endmacro %}
构造来定义一个宏,稍后我们可以在代码中调用(使用)它。下面显示了一个定义简单宏并在循环中使用它的示例。请注意我们是如何将变量传递给宏来操作数据的。
{# Macro definition #}
{% macro bold_me(data) %}
<b>{{ data }}</b>
{% endmacro %}
{# Invoke the macro #}
{% for value in data %}
{{ bold_me(value) }}
{% endfor %}
导入
使用宏的最好方法之一是将它们放在一个单独的代码文件中,从而进一步增强可重用性。为了使用单独文件中的宏,我们使用{% import … %}构造来提供要导入的文件的名称。下面显示了一个在单独的文件中导入先前定义的宏的示例。与 include 一样,该文件必须位于 templates 文件夹中。注意,我们可以使用别名,并使用点符号来引用宏。
{% import 'utilities.html' as utils %}
...
{{ utils.bold_me(value) }}
扩展(继承)
我们可以通过继承(扩展)模板来使用模板的层次结构。我们在前面检查基本模板时看到了这一点。在这种情况下,我们使用{% extend … %}构造来提供我们想要扩展的模板的名称。下面显示了先前基本模板中的一个示例。
{% extends "base.html" %}
阻碍
块用于隔离执行和范围(对于变量)。每当我们想要隔离一组模板构造时,我们就使用块(想象一个代码块)。{% block … %}
构造与{% endblock %}
构造一起用于定义块。这些构件允许您命名块。下面显示了一个示例。
{% block if_true %}
...
{% endblock if_true %}
环
循环是多次执行同一个块的一种方式。我们用{% for <variable> in <data_array> %}
构造来做这件事。在这种情况下,循环将迭代数组,用数组的每个索引中的值替换<variable>
中的值。这种结构非常适合遍历数组来创建表格、显示数据列表以及类似的演示活动。下面显示了在构造表时使用的 for 循环。注意,我们使用了两个 for 循环:一个循环遍历名为 columns 的数组中的列,另一个循环遍历名为 rows 的数组中的行。
<table border="1" cellpadding="1" cellspacing="1">
<tr>
<td style="width:80px"><b>Action</b></td>
{% for col in columns %}
{{ col|safe }}
{% endfor %}
</tr>
{% for row in rows %}
<tr>
<td><a href="{{ '/%s/%s'%(kind,row[0]) }}">Modify</a></td>
{% for col in row[1:] %}
<td> {{ col }} </td>
{% endfor %}
</tr>
{% endfor %}
</table>
此时,您可能想知道列和行中的数据是如何到达模板的。调用render_template(
功能。如果想要将数据传递给模板,只需在呈现模板时将数据列在参数中。在这种情况下,我们将按如下方式传递列和行。在这种情况下,row_data
和col_data
是在视图函数中定义的变量,并通过赋值传递给模板中的rows
和columns
变量。酷吧。
render_template("list.html", form=form, rows=row_data, columns=col_data)
条件式
条件或“if”语句(在 Jinja2 文档中称为测试)允许您在模板中做出决定。我们使用{% if <condition> %}
构造,它以{% endif %}构造结束。如果你想要一个“else ”,你可以使用{% else %}
结构。更进一步,你可以用{% elif
来连锁条件<condition> %}
。您通常在条件中使用变量或表单元素,并且可以使用通用比较器(有关测试列表,请参见 http://jinja.pocoo.org/docs/2.10/templates/#builtin-tests
)。
例如,您可能希望根据某个事件更改提交字段的标签。您可能希望定义一个提交按钮来添加或更新数据。也就是说,当网页用于添加新的数据项时,文本应该显示为“add ”,但是当您使用相同的网页更新数据时,我们希望文本显示为“update”。这是为GET
和POST
请求(读和写)重用模板的关键之一。下面显示了以这种方式使用的条件的一个示例。
{% if form.create_button.label.text == "Update" %}
{{ form.new_note.label }}
{{ form.new_note(rows='2',cols='100') }}
{% endif %}
{% if form.del_button %}
{{ form.del_button }}
{% endif %}
在这个例子中有两个条件。第一个示例演示如何检查窗体上标签的文本。注意,这里我们用form.create_button
引用表单上的元素,这是我们在表单类中定义的字段类的名称,它在呈现模板之前被实例化(我们将在后面的部分中看到如何做到这一点)。表单变量在render_template("book.html", form=form)
调用中被传递给模板。在这种情况下,如果按钮文本被设置为“更新”,我们只显示new_note
字段及其标签
第二个例子显示了一个简单的测试,如果表单上的delete_button
是活动的(没有隐藏或删除),我们就显示它。这是一个如何显示可选提交字段的例子。
变量和变量过滤器
变量是保存数据值供以后处理的一种方式。变量最常见的用途是引用从视图函数传递到模板的数据(通过render_template()
函数)。我们还可以在模板中使用变量来保存数据,比如计数器、循环数据值等等。回想一下,我们通过用花括号{{ variable }}
将变量括起来来引用它,或者在 for 循环的情况下,在 for 循环结构中定义它。请注意,当在 HTML 标记中引用时,构造中的空格将被忽略。
您还可以在模板中使用过滤器来更改变量中的值。变量筛选器是一种以编程方式更改值的方法,以便在表示逻辑中使用。您可以更改大小写,删除空白,甚至去掉 HTML 标签或直接使用原始文本。在最后一种情况下,我们使用安全过滤器,它告诉模板使用文本,即使它有 HTML 标签。这有点棘手,因为它可能会为攻击打开方便之门,但是如果您使用 WTForms 的特殊安全特性(见下一节),通常可以这样做,但是要谨慎。表 8-3 显示了常用的可变滤波器。
表 8-3
Variable Filters
| 过滤器 | 描述 | | :-- | :-- | | `Capitalize` | 将文本的第一个字符转换为大写 | | `Lower` | 将文本转换为小写字符 | | `Safe` | 呈现文本,不转义特殊字符 | | `Striptags` | 从文本中删除 HTML 标签 | | `Title` | 将字符串中的每个单词大写 | | `Trim` | 删除前导和尾随空白 | | `Upper` | 将文本转换为大写 |Tip
要更深入地了解 Jinja2 模板构造,请参见 http://jinja.pocoo.org/
。
现在我们已经对模板的工作原理有了一个大致的了解,并且已经为库应用定义了一个基本模板,让我们看看如何使用这个基本模板来为我们的 web 页面形成 HTML 文件。正如您将看到的,它涉及到我们一直在讨论的三个概念,并将把讨论引向 Flask 在构建网页并将它们发送给客户端时如何工作的结论。我们将在后面的小节中研究如何从客户端获取数据。
使用模板的 HTML 文件
现在,我们准备看看如何体现我们在表单类中定义的字段类。让我们从如何在库应用中显示发布者数据的演练开始讨论。我们从定义给视图函数的表单类和字段类开始,视图函数呈现模板,最后是模板本身。
回想一下,表单类是我们定义一个或多个表单字段的地方。我们将使用这些字段类实例来访问视图函数和模板中的数据。清单 8-6 显示了表单类(没有数据库访问)。
class PublisherForm(FlaskForm):
publisherid = HiddenField('PublisherId')
name = TextField('Name', validators=[
Required(message=REQUIRED.format("Name")),
Length(min=1, max=128, message=RANGE.format("Name", 1, 128))
])
city = TextField('City', validators=[
Required(message=REQUIRED.format("City")),
Length(min=1, max=32, message=RANGE.format("City", 1, 32))
])
url = TextField('URL/Website')
create_button = SubmitField('Add')
del_button = SubmitField('Delete')
Listing 8-6Publisher Form Class (No Database Code)
请注意,form 类创建了三个字段:一个用于发布者姓名(name
),一个用于发布者所在的城市(city
),另一个用于发布者 URL ( url
)。我们还看到两个提交字段(按钮):一个用于创建新的发布者数据(create_button
,另一个用于删除发布者数据(del_button
)。我们还有一个隐藏的发布者 id 字段。
在视图函数中实例化表单数据之后,当呈现表单数据时,我们将表单数据传递给模板。清单 8-7 显示了发布者数据的查看功能。这里,我们首先实例化 publisher form 类,然后将其传递给模板。
#
# Publisher
#
# This page allows creating and editing publisher records.
#
@app.route('/publisher', methods=['GET', 'POST'])
@app.route('/publisher/<int:publisher_id>', methods=['GET', 'POST'])
def publisher(publisher_id=None):
form = PublisherForm()
if request.method == 'POST':
pass
return render_template("publisher.html", form=form)
Listing 8-7
Publisher View Function
请注意,这里我们看到了为视图定义的路线。还要注意,我们已经为请求设置了包含GET
和POST
的方法。我们可以检查这个请求是否是一个POST
(提交数据)。在这种情况下,我们可以从 form 类实例中检索数据,并将其保存到数据库中。当我们添加数据库功能时,我们会更深入地了解这一点。
最后,请注意,我们实例化了 publisher form 类(form)的一个实例,然后将其作为参数传递给render_template("publisher.html", form=form)
调用。在这种情况下,我们现在渲染存储在templates
文件夹中的publisher.html
模板。
好了,现在我们有了表单类和视图函数。现在的焦点是当我们呈现 HTML 模板文件时会发生什么。清单 8-8 显示了发布者数据的 HTML 文件(模板)。
{#
Introducing the MySQL 8 Document Store
This template defines the publisher template for use in the MyLibrary application
using the base template.
Dr. Charles Bell, 2017
#}
{% extends "base.html" %}
{% block title %}MyLibrary Search{% endblock %}
{% block page_content %}
<form method=post> {{ form.csrf_token }}
<fieldset>
<legend>Publisher - Detail</legend>
{{ form.hidden_tag() }}
<div style=font-size:20pz; font-weight:bold; margin-left:150px;s>
{{ form.name.label }} <br>
{{ form.name(size=64) }} <br>
{{ form.city.label }} <br>
{{ form.city(size=48) }} <br>
{{ form.url.label }} <br>
{{ form.url(size=75) }} <br><br>
{{ form.create_button }}
{% if form.del_button %}
{{ form.del_button }}
{% endif %}
</div>
</fieldset>
</form>
{% endblock %}
Listing 8-8
Publisher HTML File
注意,模板从扩展(继承)我们之前讨论过的base.html
模板文件开始。我们看到一个块定义了标题,另一个块定义了页面内容。在这个块中,我们看到了如何从表单类实例(form
)中引用字段类实例来定义页面上的字段。实际上,请注意,我们引用了字段的标签和数据。当您声明字段类和数据是存储值的位置时,就定义了标签。当我们想要填充表单(GET
)时,我们将数据元素设置为值,当我们想要读取数据(POST
)时,我们引用数据元素。
注意,为了安全起见,我们还添加了 CSRF 令牌,用form.hidden_tag()
函数呈现隐藏字段,并通过包含删除提交字段(del_button
)来有条件地包含提交字段。
咻!这就是 Flask 呈现网页的方式。一旦您习惯了,这是一种很好的方式来分离几层功能,并使从用户那里获取数据或呈现给用户变得容易。
现在,让我们看看如何在我们的应用中构建定制的错误处理程序,以及稍后如何将应用中的控制重定向到正确的视图函数。
错误处理程序
回想一下,我提到过可以为应用中的错误创建自己的错误处理机制。您应该考虑建立两种这样的错误机制:一种用于 404(未找到)错误,另一种用于 500(应用错误)。为了定义每一个,我们首先创建一个用@app.errorhandler(num)
修饰的视图函数、一个视图函数和一个 HTML 文件。让我们看看每个例子。
未找到(404)错误
为了处理 404(未找到)错误,我们创建了一个带有特殊错误处理程序路由函数的视图函数,该函数呈现 HTML 文件。Flask 会自动将所有未找到的错误条件定向到此视图。下面显示了 404 未找到错误处理程序的视图函数。如你所见,这很简单。
@app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
相关的错误处理程序 HTML 代码位于名为 404.html 的文件中,如下所示。请注意,我们从 base.html 文件中继承了它,因此生成的 web 页面看起来与应用中的任何其他页面一样,都包含来自引导组件的菜单。注意,我们还可以定义错误消息的文本和标题。随意修饰你自己的错误处理程序,让你的用户更感兴趣。 7
{% extends "base.html" %}
{% block title %}MyLibrary ERROR: Page Not Found{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Page not found.</h1>
</div>
{% endblock %}
应用(500)错误
要处理 500 个(应用)错误,请遵循与前面相同的模式。下面是应用错误的错误处理程序。
@app.errorhandler(500)
def internal_server_error(e):
return render_template('500.html'), 500
相关的错误处理程序 HTML 代码在名为500.html
的文件中,如下所示。请注意,我们从base.html
文件中继承了它,因此生成的网页看起来与应用中的任何其他网页一样,都包含来自 bootstrap 组件的菜单。
{% extends "base.html" %}
{% block title %}MyLibrary ERROR{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>OOPS! Application error.</h1>
</div>
{% endblock %}
强烈建议所有 Flask 应用创建这些基本的错误处理程序。在开发应用时,您可能会发现应用错误处理程序非常有用。您甚至可以扩充代码,以提供要在网页中显示的调试信息。
重新寄送
此时,您可能想知道 Flask 应用如何以编程方式将执行从一个视图定向到另一个视图。答案是 Flask 中的另一个简单构造:重定向。我们使用带有 URL 的redirect()
函数(从 flask 模块导入)将控制重定向到另一个视图。例如,假设您有一个列表表单,根据用户单击的按钮(通过 POST 提交表单),您希望显示不同的 web 页面。下面演示了如何使用redirect()
函数来实现这一点。
if kind == 'book' or not kind:
if request.method == 'POST':
return redirect('book')
return render_template("list.html", form=form, rows=rows,
columns=columns, kind=kind)
elif kind == 'author':
if request.method == 'POST':
return redirect('author')
return render_template("list.html", form=form, rows=rows,
columns=columns, kind=kind)
elif kind == 'publisher':
if request.method == 'POST':
return redirect('publisher')
return render_template("list.html", form=form, rows=rows,
columns=columns, kind=kind)
这里,我们看到在一个POST
请求之后有三个重定向。在每种情况下,我们都使用应用中定义的一个路由来告诉 Flask 调用相关的视图函数。这样,我们可以创建一个菜单或一系列提交字段,允许用户从一个页面移动到另一个页面。
redirect()
函数需要一个有效的路径,在大多数情况下,它只是您在装饰器中提供的文本。但是,如果需要形成一个复杂的 URL 路径,可以在重定向之前使用url_for()
函数来验证路由。如果您重组或更改路线,该功能还有助于避免断开链接。例如,您可以使用redirect(url_for(“author”))
来验证路线并为其形成一个 URL。
附加功能
除了我们在这个速成班中所看到的,Flask 还有更多的内容。您可能有兴趣进一步了解一些未讨论的内容,包括以下内容(这只是其中的一部分)。如果您对这些感兴趣,可以考虑在在线文档中查找它们。
- 应用和请求上下文:有一些变量可以用来捕获应用上下文,比如会话、全局、请求等等。更多信息请参见
http://flask.pocoo.org/docs/0.12/appcontext/
。 - Cookies:如果你需要,你可以使用 cookies。更多信息请参见
http://flask.pocoo.org/docs/0.12/quickstart/#cookies
。 - Flask-Moment—日期和时间的本地化:如果需要进行日期和时间的本地化,请参见
https://github.com/miguelgrinberg/Flask-Moment
中的 Flask-Moment 扩展。
烧瓶审查:样品应用
现在我们已经对 Flask 有了一个简单的了解,让我们看看所有这些是如何工作的。在这一节中,我们将回顾我们所学的典型 Flask web 应用的基本布局。在本章的后面,我们将把它作为编写库应用的指南。不要太担心执行这段代码,因为它并没有做太多事情,只是作为章节项目的一个开始。然而,它确实演示了如何将我们所学的所有部分组合在一起,使 Flask web 应用在没有定义表单的情况下运行。
清单 8-9 显示了库应用的样例应用布局。花点时间通读一遍。您应该可以找到我们到目前为止讨论过的所有主题,包括字段类、表单类和视图函数的占位符。
#
# Introducing the MySQL 8 Document Store - Template
#
# This file contains a template for building Flask applications. No form
# classes, routes, or view functions are defined but placeholders for each
# are defined in the comments.
#
# Dr. Charles Bell, 2017
#
from flask import Flask, render_template, request, redirect, flash
from flask_script import Manager
from flask_bootstrap import Bootstrap
from flask_wtf import FlaskForm
from wtforms import (HiddenField, TextField, TextAreaField, SelectField,
SelectMultipleField, IntegerField, SubmitField)
from wtforms.validators import Required, Length
#
# Setup Flask, Bootstrap, and security.
#
app = Flask(__name__)
app.config['SECRET_KEY'] = "He says, he's already got one!"
manager = Manager(app)
bootstrap = Bootstrap(app)
#
# Utility functions
#
def flash_errors(form):
for error in form.errors:
flash("{0} : {1}".format(error, ",".join(form.errors[error])))
#
# Customized fields for skipping prevalidation
#
<custom field classes go here>
#
# Form classes - the forms for the application
#
<form classes go here>
#
# Routing functions - the following defines the routing functions for the
# menu including the index or "home", book, author, and publisher.
#
<routing functions (view functions) go here>
#
# Error handling routes
#
@app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
@app.errorhandler(500)
def internal_server_error(e):
return render_template('500.html'), 500
#
# Main entry
#
if __name__ == '__main__':
manager.run()
Listing 8-9Sample Flask Application Template
注意,在这个模板中有一件事我们还没有谈到——效用函数。这些是你自己的函数来支持你的应用。您可能想考虑在所有 Flask 应用中包含的一个函数是循环遍历表单上的错误并在 flash 消息中显示它们的函数。召回简讯在网页上显示为弹出框。为了清楚起见,下面给出了效用函数。注意,我们使用 for 循环来遍历显示每条消息的表单实例的 errors 数组。这允许您在网页上显示多条消息。
def flash_errors(form):
for error in form.errors:
flash("{0} : {1}".format(error, ",".join(form.errors[error])))
创建自己的 Flask 应用时,可以随意使用这个模板。我们还将在下一节中使用它来定义库应用的用户界面。
Tip
关于 Flask 以及如何使用它和它的相关包的更多信息,下面的书是关于这个主题的很好的参考:Flask Web Development:用 Python 开发 Web 应用第 2 版。,米格尔·格林伯格,(奥莱利传媒,2018)。
既然我们已经设置了 Flask 环境并发现了 Flask 及其扩展,那么让我们来看一下这个应用的三个版本共有的用户界面。
库应用用户界面设计
现在,我们对 Flask 以及如何构建 Flask 应用有了更多的了解,让我们看看库应用的用户界面。正如您可能猜测的那样,我们将数据库访问构建为一组独立的类,但是用户界面几乎完全可以在没有它的情况下构建。将用户界面与数据库访问机制分开研究,可以更容易地关注每个部分。我们将在下一节讨论数据库访问机制。
库应用的用户界面对于应用的所有三个版本都是相同的代码,只是对代码进行了一些修改以适应不同的数据库机制。特别是,我们在版本 1(关系数据库)中提供了完整的界面,在版本 2 中提供了简化的用户界面(混合:带有 JSON 的关系数据库),版本 3 将更加简洁(文档存储)。因此,我们将为用户界面中托管的所有网页编写表单类、视图函数和模板。
然而,在我们开始为应用编写表单类、视图函数和模板之前,我们需要创建几个目录。
准备目录结构
在我们开始实现库应用的三个版本之前,我们需要创建几个文件夹(目录)。回想一下 Flask Primer,我们需要文件夹来包含.html
文件(表单模板)。我们还将与 MySQL 交互的代码放在一个名为 database 的文件夹中。最后,我们需要为应用的每个版本创建一个单独的文件夹。清单 8-10 显示了你需要的文件夹结构。您可以随意命名版本文件夹,但数据库和模板文件夹必须按如下所示命名。注意,我们还有一个名为“base”的文件夹,它包含基本的用户界面设计,但没有数据库文件夹,这将在下一节讨论。
root folder
|
+- base
| |
| +-- templates
|
+- version1
| |
| +-- database
| |
| +-- templates
|
+- version2
| |
| +-- database
| |
| +-- templates
|
+- version3
|
+-- database
|
+-- templates
Listing 8-10
Directory Structure
用户界面功能
library 应用将托管三种类型的数据:书籍、作者和出版商,将它们链接起来形成一个库。该应用的默认视图将是一个图书列表,它以简短书目的形式显示所有的图书。还会有所有作者和所有出版商的视图。用户还可以查看特定书籍、作者或出版商的数据,允许他们更新或删除数据项。因此,库应用演示了数据的基本创建、读取、更新和删除(CRUD)操作。
回想一下,我们将使用 bootstrap 导航栏,它为每个视图都提供了菜单项:图书、作者和出版商。让我们来看看默认视图——图书列表。图 8-3 显示了默认视图(无数据)。请注意,导航栏和每个视图的选项。还记得我们指定默认视图(通过单击 MyLibrary Base 到达)是书籍的相同视图。换句话说,它是典型的index.html
或其他网络应用的大本营。
图 8-3
MyLibrary application book list (default view)
虽然这个例子中没有数据,但是我们将编写代码为列表中的每个项目创建一个链接,用户可以单击该链接来编辑行中的数据。您将在后面的小节中看到这是如何实现的。还要注意New
按钮。用户可以用它来创建一个如图 8-4 所示的新视图。这使用相同的表单类、视图函数和模板来查看和编辑数据。还记得我们将在视图上放置一个删除按钮,允许用户在编辑数据时选择删除数据。这个额外的步骤——首先编辑,然后删除——是避免“你确定吗?”大多数验证删除操作的应用的常见问题。这种方式允许用户在删除数据之前对其进行编辑和查看。你来判断它是否比“你确定吗?”提示。
图 8-4
Book detail view
请注意,表单上有一个选择(下拉)字段。该字段由数据库中发布者的名称填充。同样,还有一个多选字段,允许用户在数据库中选择一个或多个作者。正如您将在我们讨论数据库设计时看到的,当使用关系数据时,这种布局在某种程度上是强加给我们的。我们将在视图函数中填充这些列表。请注意,我们还可以看到Add
和Delete
提交字段(按钮)。回想一下,我们将禁用模板中的Delete
按钮——在添加新数据项时,它通常不会被启用。
接下来是作者观点。与 books 视图一样,这里是数据库中的作者列表,包括用于编辑行的链接和用于创建新作者的新按钮。图 8-5 显示了作者视图。
图 8-5
Author list view
当用户单击 New(或稍后,列表中的 edit 链接)时,将显示 author detail 视图。图 8-6 显示了作者详细视图。
图 8-6
Author detail view
注意表格很短。它只显示了两个字段以及 Add 和Delete
按钮,它们将在模板中被控制。
最后,我们有 publisher 视图,它显示了数据库中所有发布者的列表。图 8-7 显示了发布者视图。
图 8-7
Publisher view
最后,当用户点击列表中的新建或编辑链接时,将显示发布者详细视图,如图 8-8 所示。注意,这里有三个发布者数据字段以及添加和删除按钮。
图 8-8
Publisher detail view
现在我们已经了解了基本的用户界面,让我们看看如何为详细视图的三个表单类和一个用于显示列表的表单类构建表单类,它使用继承和一些模板构造在所有三个列表视图之间共享表单类和模板。酷吧。
表单类
library 应用的表单类需要三个表单类。作者、出版商和图书视图各有一个,可重用列表视图有一个表单类。让我们从最简单的表单类(author)开始,然后向更复杂的(book)前进。
作者表单类
author 表单非常简单,只需要三个字段:一个使用HiddenField
字段类存储行的主键,一个存储名,一个存储姓。两个名称字段都使用一个TextField
字段类。name 字段的验证设置为 required(提示:它们在数据库表中被定义为NOT NULL
)以及最小和最大长度检查。我们还需要两个SubmitField
字段类:一个用于Add
,另一个用于Delete
。回想一下,我们将以编程方式控制模板中的删除按钮。清单 8-11 显示了AuthorForm
表单类。
class AuthorForm(FlaskForm):
authorid = HiddenField('AuthorId')
firstname = TextField('First name', validators=[
Required(message=REQUIRED.format("Firstname")),
Length(min=1, max=64, message=RANGE.format("Firstname", 1, 64))
])
lastname = TextField( 'Last name', validators=[
Required(message=REQUIRED.format("Lastname")),
Length(min=1, max=64, message=RANGE.format("Lastname", 1, 64))
])
create_button = SubmitField('Add')
del_button = SubmitField('Delete')
Listing 8-11AuthorForm Class
Publisher 表单类
publisher 表单也非常简单,只需要四个字段:一个使用HiddenField
字段类存储行的主键,一个存储出版商名称,一个存储出版商的原籍城市,另一个存储出版商的 URL。所有三个可见字段都使用一个TextField
字段类。name 和 city 字段的验证都设置为 required(提示:它们在数据库表中被定义为NOT NULL
)以及最小和最大长度检查。URL 字段没有验证器,因为它是数据的可选字段(它可以是数据库表中的NULL
)。我们还看到了用于Add
和Delete
按钮的两个SubmitFields()
。清单 8-12 显示了PublisherForm
表单类。
class PublisherForm(FlaskForm):
publisherid = HiddenField('PublisherId')
name = TextField('Name', validators=[
Required(message=REQUIRED.format("Name")),
Length(min=1, max=128, message=RANGE.format("Name", 1, 128))
])
city = TextField('City', validators=[
Required(message=REQUIRED.format("City")),
Length(min=1, max=32, message=RANGE.format("City", 1, 32))
])
url = TextField('URL/Website')
create_button = SubmitField('Add')
del_button = SubmitField('Delete')
Listing 8-12PublisherForm Class
图书表单类
book 表单稍微复杂一点,有许多数据字段。实际上有 10 个字段。表 8-4 列出了 book form 类所需的字段。包括字段名称、使用的字段类和有效性选项。
表 8-4
Field Classes for the Book Form Class
| 字段名 | 字段类 | 确认 | | :-- | :-- | :-- | | `ISBN` | `TextField()` | `Required()`,`Length()` | | `Title` | `TextField()` | `Required()` | | `Year` | `IntegerField()` | `Required()` | | `Edition` | `IntegerField()` | 没有人 | | `Language` | `TextField()` | `Required()`,`Length()` | | `Publisher` | `NewSelectField()` | `Required()` | | `Authors` | `NewSelectMultipleField()` | `Required()` | | `create_button` | `SubmitField()` | 不适用的 | | `del_button` | `SubmitField()` | 不适用的 | | `new_note` | `TextAreaField()` | 没有人 |在我们讨论图书细节视图的表单类之前,有一个小问题需要解决。使用SelectField()
和SelectMultipleField()
字段类时有一个众所周知的问题。如果没有选择默认值或者您以编程方式设置了默认值,预验证代码在验证时可能会出现一些不寻常的结果。为了克服这些限制,我们可以创建自己的派生字段类并覆盖预验证代码。清单 8-13 显示了用于创建这些字段类的定制版本以克服限制的代码。如果要使用这些字段类中的任何一个,就需要将它们放在代码中。
class NewSelectMultipleField(SelectMultipleField):
def pre_validate(self, form):
# Prevent "not a valid choice" error
pass
def process_formdata(self, valuelist):
if valuelist:
self.data = ",".join(valuelist)
else:
self.data = ""
class NewSelectField(SelectField):
def pre_validate(self, form):
# Prevent "not a valid choice" error
pass
def process_formdata(self, valuelist):
if valuelist:
self.data = ",".join(valuelist)
else:
self.data = ""
Listing 8-13Creating Custom Field Classes
请注意,在每种情况下,我们都覆盖了pre_validate()
和process_formdata()
方法,从而允许我们忽略预先验证,并使更新值变得更容易。现在让我们看看 book form 类的代码。清单 8-14 显示了BookForm
表单类的代码。注意,我们为 authors 和 publisher 字段使用了新的字段类。我们还看到两个SubmitFields()
用于Add
和Delete
按钮。
class BookForm(FlaskForm):
isbn = TextField('ISBN ', validators=[
Required(message=REQUIRED.format("ISBN")),
Length(min=1, max=32, message=RANGE.format("ISBN", 1, 32))
])
title = TextField('Title ',
validators=[Required(message=REQUIRED.format("Title"))])
year = IntegerField('Year ',
validators=[Required(message=REQUIRED.format("Year"))])
edition = IntegerField('Edition ')
language = TextField('Language ', validators=[
Required(message=REQUIRED.format("Language")),
Length(min=1, max=24, message=RANGE.format("Language", 1, 24))
])
publisher = NewSelectField('Publisher ',
validators=[Required(message=REQUIRED.format("Publisher"))])
authors = NewSelectMultipleField('Authors ',
validators=[Required(message=REQUIRED.format("Author"))])
create_button = SubmitField('Add')
del_button = SubmitField('Delete')
new_note = TextAreaField('Add Note')
Listing 8-14BookForm Class
列表表单类
为了节省重复的代码,我们将创建一个简单的 form 类,用于以 HTML 表格的形式创建一个简单的行列表。这样做的代码非常简单,因为所有的表示代码都在模板文件中。下面是ListForm
表单类的代码。
class ListForm(FlaskForm):
submit = SubmitField('New')
既然我们已经看到了库应用的所有表单类,现在我们来研究相关的视图函数。
查看功能
视图函数是 Flask 应用指导执行的地方和方式。加上我们定义的路由,我们可以构建没有循环或轮询的应用。让我们从最简单的视图函数开始。我们将看到定义了路由的视图函数(通过 decorators)。作者、出版商和图书视图功能的基本代码是相同的,不需要额外讨论。唯一的区别是图书视图功能中的路径和人口或选择和多选字段。每个函数分别显示在清单 8-15 (名为author
)、清单 8-16 (名为publisher
)和清单 8-17 (名为book
)中。
@app.route('/author', methods=['GET', 'POST'])
@app.route('/author/<int:author_id>', methods=['GET', 'POST'])
def author(author_id=None):
form = AuthorForm()
if request.method == 'POST':
pass
return render_template("author.html", form=form)
Listing 8-15
Author View Function
@app.route('/publisher', methods=['GET', 'POST'])
@app.route('/publisher/<int:publisher_id>', methods=['GET', 'POST'])
def publisher(publisher_id=None):
form = PublisherForm()
if request.method == 'POST':
pass
return render_template("publisher.html", form=form)
Listing 8-16Publisher View Function
@app.route('/book', methods=['GET', 'POST'])
@app.route('/book/<string:isbn_selected>', methods=['GET', 'POST'])
def book(isbn_selected=None):
notes = None
form = BookForm()
form.publisher.choices = []
form.authors.choices = []
new_note = ""
if request.method == 'POST':
pass
return render_template("book.html", form=form, notes=notes)
Listing 8-17Book View Function
列表视图功能更复杂。回想一下,我们想要创建一个可以重用的列表。因此,我们需要能够创建一个 HTML 表,其中包含我们想要显示的列名和行。我们可以使用render_template()
函数中的参数传递列和行。我们还想定义列的大小。我们可以通过向模板传递 HTML 代码来做到这一点。在这种情况下,我们将它们定义为列名的 HTML 标记,并在模板中使用安全过滤器显示它而不进行翻译。
我们还想为每一行创建一个包含该行主键的链接,我们将把它作为每一行的第一个数据项来传递。对于作者和出版商,它是自动增量主键。对于书籍,它是 ISBN。因此,ISBN 将在该行中列出两次。为了确定我们需要哪些数据,我们在 list route 中使用了一个变量。例如,如果我们想要书,我们的 URL 应该是 localhost:5000/list/book。酷。
最后,因为这个视图函数是默认视图,所以路径很简单:默认(索引)和列表。清单 8-18 显示了名为simple_list
的列表视图函数的完整代码。花些时间通读一下,这样你就能理解代码了。
@app.route('/', methods=['GET', 'POST'])
@app.route('/list/<kind>', methods=['GET', 'POST'])
def simple_list(kind=None):
rows = []
columns = []
form = ListForm()
if kind == 'book' or not kind:
if request.method == 'POST':
return redirect('book')
columns = (
'<td style="width:200px">ISBN</td>',
'<td style="width:400px">Title</td>',
'<td style="width:200px">Publisher</td>',
'<td style="width:80px">Year</td>',
'<td style="width:300px">Authors</td>',
)
kind = 'book'
# Here, we get all books in the database
return render_template("list.html", form=form, rows=rows,
columns=columns, kind=kind)
elif kind == 'author':
if request.method == 'POST':
return redirect('author')
# Just list the authors
columns = (
'<td style="width:100px">Lastname</td>',
'<td style="width:200px">Firstname</td>',
)
kind = 'author'
# Here, we get all authors in the database
return render_template("list.html", form=form, rows=rows,
columns=columns, kind=kind)
elif kind == 'publisher':
if request.method == 'POST':
return redirect('publisher')
columns = (
'<td style="width:300px">Name</td>',
'<td style="width:100px">City</td>',
'<td style="width:300px">URL/Website</td>',
)
kind = 'publisher'
# Here, we get all publishers in the database
return render_template("list.html", form=form, rows=rows,
columns=columns, kind=kind)
else:
flash("Something is wrong!")
return
Listing 8-18
List View Function
现在我们已经看到了表单类和视图函数的代码,让我们来看看这个难题的剩余部分:模板。
模板
模板是我们放置用于构建网页的所有 HTML 和模板构造的地方(在数据库应用、数据视图或简单的视图 8 )的上下文中。这些模板提供了简短的描述,供您参考,以便您可以一起查看所有部分。因为我们有四个视图函数,所以我们将创建四个模板文件,它们都将使用前面解释的基本模板。回想一下,基本模板定义了引导导航栏和 for 循环,用于显示 flash 消息的数组。
Note
记住,模板文件在templates
文件夹中,按照惯例命名为XXX.html
。
作者模板
作者模板创建用于查看、编辑和创建作者数据的表单。因此,我们给页面一个图例,托管隐藏字段(用于自动增量主键),并将标签和表单字段放在每个字段的表单上。我们通过垂直列出字段来保持简单(但是您可以使用您想要的任何格式)。我们还使用表单字段默认函数来设置字段的大小。例如,要将名字字段的大小设置为 75 个字符,我们使用form.firstname(size=75)
。最后,我们看到了打开 delete 按钮的逻辑(如果它被定义的话)(我们将在后面看到如何禁用它)。清单 8-19 显示了作者数据的完整模板(名为author.html
)。
{% extends "base.html" %}
{% block title %}MyLibrary Search{% endblock %}
{% block page_content %}
<form method=post> {{ form.csrf_token }}
<fieldset>
<legend>Author - Detail</legend>
{{ form.hidden_tag() }}
<div style=font-size:20pz; font-weight:bold; margin-left:150px;s>
{{ form.firstname.label }} <br>
{{ form.firstname(size=75) }} <br>
{{ form.lastname.label }} <br>
{{ form.lastname(size=75) }} <br><br>
{{ form.create_button }}
{% if form.del_button %}
{{ form.del_button }}
{% endif %}
</div>
</fieldset>
</form>
{% endblock %}
Listing 8-19Author Template (author.html)
发布者模板
publisher 模板创建用于查看、编辑和创建 publisher 数据的表单。因此,我们给页面一个图例,托管隐藏字段(用于自动增量主键),并将标签和表单字段放在每个字段的表单上。我们通过垂直列出字段来保持简单(但是您可以使用您想要的任何格式)。我们还设置了字段的大小。最后,我们看到了打开 delete 按钮的逻辑(如果它被定义的话)(我们将在后面看到如何禁用它)。清单 8-20 显示了发布者数据(名为publisher.html
)的完整模板。
{% extends "base.html" %}
{% block title %}MyLibrary Search{% endblock %}
{% block page_content %}
<form method=post> {{ form.csrf_token }}
<fieldset>
<legend>Publisher - Detail</legend>
{{ form.hidden_tag() }}
<div style=font-size:20pz; font-weight:bold; margin-left:150px;s>
{{ form.name.label }} <br>
{{ form.name(size=64) }} <br>
{{ form.city.label }} <br>
{{ form.city(size=48) }} <br>
{{ form.url.label }} <br>
{{ form.url(size=75) }} <br><br>
{{ form.create_button }}
{% if form.del_button %}
{{ form.del_button }}
{% endif %}
</div>
</fieldset>
</form>
{% endblock %}
Listing 8-20Publisher Template (publisher.html)
书籍模板
图书模板稍微复杂一点。我们从图例和隐藏标签开始,它存储当前数据的 ISBN,然后构建列出标签和字段的表单,并在过程中垂直设置字段的大小。到目前为止,这与我们构建作者和发布者模板的方式类似。
当我们试图为选择字段设置字段大小时,事情变得更有趣了。在这种情况下,我们需要使用以像素为单位传入width
参数的style
参数。这是 Flask 模板为数不多的细微差别之一,可能有点棘手,因为size
参数不适用于选择字段(但现在您知道如何绕过它)。与前面的模板一样,如果定义了 delete 按钮,我们可以看到打开它的逻辑(稍后我们将看到如何禁用它)。
之后,我们会看到一些处理笔记的附加逻辑。笔记功能允许用户在图书创建后添加笔记。因此,我们既需要显示任何现有的注释,也需要提供添加新注释的方法,但只有在页面用于更新操作时才需要。您可以在文件的底部看到这是如何完成的。
清单 8-21 显示了发布者数据(名为book.html
)的完整模板。花一些时间通读文件,直到你确信你理解它是如何工作的。
{% extends "base.html" %}
{% block title %}MyLibrary Search{% endblock %}
{% block page_content %}
<form method=post> {{ form.csrf_token }}
<fieldset>
<legend>Book - Detail</legend>
{{ form.hidden_tag() }}
<div style=font-size:20pz; font-weight:bold; margin-left:150px;>
{{ form.isbn.label }} <br>
{{ form.isbn(size=32) }} <br>
{{ form.title.label }} <br>
{{ form.title(size=100) }} <br>
{{ form.year.label }} <br>
{{ form.year(size=10) }} <br>
{{ form.edition.label }} <br>
{{ form.edition(size=10) }} <br>
{{ form.language.label }} <br>
{{ form.language(size=34) }} <br>
{{ form.publisher.label }} <br>
{{ form.publisher(style="width: 300px;") }} <br>
{{ form.authors.label }} <br>
{{ form.authors(style="width: 300px;") }}
{# Show the new note text field if this is an update. #}
{% if form.create_button.label.text == "Update" %}
<br>{{ form.new_note.label }} <br>
{{ form.new_note(rows='2',cols='100') }}
{% endif %}
<br><br>
{{ form.create_button }}
{% if form.del_button %}
{{ form.del_button }}
{% endif %}
<br><br>
</div>
{# Show the list of existing notes if there is a list. #}
{% if notes %}
<div>
<table border="1" cellpadding="1" cellspacing="1">
<tr><td><b>Notes</b></td></tr>
{% for note in notes %}
<tr><td style="width:600px"> {{ note }} </td></tr>
{% endfor %}
</table>
<br>
</div>
{% endif %}
</fieldset>
</form>
{% endblock %}
Listing 8-21Book Template (book.html)
列表模板
尽管列表特性的视图功能相当复杂,但是列表视图的模板相当简单。我们只需在顶部添加新按钮(submit field ),提供一个图例,然后使用 view 函数中的 columns 数组格式化表格。然后,我们使用视图函数提供的行构建 HTML 表。列表 8-22 显示了列表数据的完整模板(名为list.html
)。
{% extends "base.html" %}
{% block title %}MyLibrary Query Results{% endblock %}
{% block page_content %}
<form method=post> {{ form.csrf_token }}
<fieldset>
{{ form.submit }} <br><br>
</fieldset>
</form>
<legend>Query Results</legend>
<table border="1" cellpadding="1" cellspacing="1">
<tr>
<td style="width:80px"><b>Action</b></td>
{% for col in columns %}
{{ col|safe }}
{% endfor %}
</tr>
{% for row in rows %}
<tr>
<td><a href="{{ '/%s/%s'%(kind,row[0]) }}">Modify</a></td>
{% for col in row[1:] %}
<td> {{ col }} </td>
{% endfor %}
</tr>
{% endfor %}
</table>
{% endblock %}
Listing 8-22List Template (list.html)
其他模板
回想一下,还有三个我们之前见过的模板,我们将使用它们:404 和 500 错误处理程序(404.html
,500.html
),如“错误处理程序”一节中所述,以及基本模板(base.html
),如清单 8-5 所示。
应用代码
现在,让我们将这些概念放在应用代码中,完成前面介绍的基本布局。清单 8-23 显示了库应用的应用代码。因为这是库应用的基础版本,我们将该文件命名为mylibrary_base.py
。我们可以将它作为三个版本的库应用(名为mylibrary_v1.py
、mylibrary_v2.py
和mylibrary_v3.py
)的基础。
该列表是为了完整性,没有额外的讨论。前面讨论的代码部分用[...]
占位符标记,以避免重复。更确切地说,该清单是为后面讨论这三个版本的部分提供的参考。请随意通读代码,以确保您理解代码的所有部分,并在阅读不同版本的部分时参考它。
#
# Introducing the MySQL 8 Document Store - Base
#
# This file contains the sample Python + Flask application for demonstrating
# how to build a simple relational database application. Thus, it relies on
# a database class that encapsulates the CRUD operations for a MySQL database
# of relational tables.
#
# Dr. Charles Bell, 2017
#
from flask import Flask, render_template, request, redirect, flash
from flask_script import Manager
from flask_bootstrap import Bootstrap
from flask_wtf import FlaskForm
from wtforms import (HiddenField, TextField, TextAreaField, SelectField,
SelectMultipleField, IntegerField, SubmitField)
from wtforms.validators import Required, Length
#
# Strings
#
REQUIRED = "{0} field is required."
RANGE = "{0} range is {1} to {2} characters."
#
# Setup Flask, Bootstrap, and security.
#
app = Flask(__name__)
app.config['SECRET_KEY'] = "He says, he's already got one!"
manager = Manager(app)
bootstrap = Bootstrap(app)
#
# Utility functions
#
def flash_errors(form):
[...]
#
# Customized fields for skipping prevalidation
#
class NewSelectMultipleField(SelectMultipleField):
[...]
class NewSelectField(SelectField):
[...]
#
# Form classes - the forms for the application
#
class ListForm(FlaskForm):
[...]
class PublisherForm(FlaskForm):
[...]
class AuthorForm(FlaskForm):
[...]
class BookForm(FlaskForm):
[...]
#
# Routing functions - the following defines the routing functions for the
# menu including the index or "home", book, author, and publisher.
#
#
# Simple List
#
# This is the default page for "home" and listing objects. It reuses a
# single template "list.html" to show a list of rows from the database.
# Built into each row is a special edit link for editing any of the rows,
# which redirects to the appropriate route (form).
#
@app.route('/', methods=['GET', 'POST'])
@app.route('/list/<kind>', methods=['GET', 'POST'])
def simple_list(kind=None):
[...]
#
# Author
#
# This page allows creating and editing author records.
#
@app.route('/author', methods=['GET', 'POST'])
@app.route('/author/<int:author_id>', methods=['GET', 'POST'])
def author(author_id=None):
[...]
#
# Publisher
#
# This page allows creating and editing publisher records.
#
@app.route('/publisher', methods=['GET', 'POST'])
@app.route('/publisher/<int:publisher_id>', methods=['GET', 'POST'])
def publisher(publisher_id=None):
[...]
#
# Book
#
# This page allows creating and editing book records.
#
@app.route('/book', methods=['GET', 'POST'])
@app.route('/book/<string:isbn_selected>', methods=['GET', 'POST'])
def book(isbn_selected=None):
[...]
#
# Error handling routes
#
@app.errorhandler(404)
def page_not_found(e):
[...]
@app.errorhandler(500)
def internal_server_error(e):
[...]
#
# Main entry
#
if __name__ == '__main__':
manager.run()
Listing 8-23Base MyLibrary Application Code
既然我们已经有了 Flask 的坚实基础以及用户界面的设计方式,我们就可以开始为应用的每个版本编写代码了,从关系数据库版本开始。
摘要
使用一个好的框架来构建 MySQL 应用不仅是为了数据库访问,更重要的是为了用户界面。决定使用哪种语言和平台有时会变成一个科学项目,甚至是一次学术活动,或者是一项不可逾越的命令。用示例表示概念(如文档存储)可能会更加复杂,因为您必须选择一种易于使用和理解的语言和框架。也许更具挑战性的是,选择一个以有意义的方式阐释概念的应用。 9
在本书中,这些技术的选择是 Python、Flask 框架,当然还有 MySQL 连接器/Python 数据库连接器和 X DevAPI。Python 易于阅读,任何人——甚至那些没有写很多代码的人——都能理解它。另外,它是一种非常强大的语言。然而,Python 中的用户界面仅限于命令行(终端)输出,除非您使用用户界面框架。再次,选择一个可能是一个挑战。然而,web 应用的框架只是帮助构建一个看起来不错的示例的入场券,读者可以使用它作为自己的实验和应用的基础。
在这一章中,我们学习了一个名为 Flask 的新的 Python web 应用库。我们还看到了 Flask 是如何作为一个可扩展的框架构建的,该框架可以很容易地用组件进行扩充,从而使您的应用更加健壮。我们还介绍了基于我们对 Flask 的了解而构建的库应用的用户界面。
在下一章中,我将介绍应用的三个版本:使用旧协议的纯关系数据库解决方案、使用 X DevAPI 和 SQL 语句的 JSON(混合)关系数据库,以及纯文档存储版本。每个版本都提供了如何使用不同的数据库访问机制构建应用的基础。正如你将看到的,从旧的到新的有一个深刻的转变。
Footnotes 1
也叫奶酪店,参考了巨蟒剧团《飞行马戏团》( https://en.wikipedia.org/wiki/Cheese_Shop_sketch
)中的奶酪店小品。
如果您使用了足够多的框架,您最终会遇到那些不可扩展的框架,并迫使您使用它们的数据库功能,这些功能通常过于有限,可能无法满足您的需求。发现了一个新的框架,却发现无法访问数据,或者必须重构数据库才能在框架中使用数据,这是多么可悲啊。
我发现这在输入数据时特别烦人,因为当你返回页面时数据经常会丢失。请不要用这种方法。
如果你为了更好的安全锁定了你的浏览器,允许弹出窗口可能会有问题。
很少有人会用这个词来描述模板构造,尽管不准确,但如果它有助于学习如何使用 Jinja2 模板,那么可以将其视为类似代码的组件。
随着年龄的增长,你越来越经常地阅读代码并问:“这是谁写的?”可悲的是,往往是你自己的代码!这里或那里的一些评论将有助于记住你在做什么(以及为什么)。
Github 上有一个很好的定制错误处理程序的例子。他们有一个自定义的背景和样式表,让其他网站无聊的 404 错误相形见绌。
这很不幸,因为它很容易与 Flask view 函数混淆。
遗憾的是,大多数文档丰富的教程很少有可以实际使用的例子。“你好,世界!”例子终究只能到此为止。
九、库应用:数据库实现
既然我们已经有了 Flask 的坚实基础以及用户界面的设计方式,我们就可以开始为应用的每个版本编写代码了,从关系数据库版本开始。
正如您将看到的,应用从纯关系模型到文档模型的演变演示了我们如何避免使用关系数据的一些混乱方面——即使是在混合示例中。可能会让您感到惊讶的一个因素是,文档存储版本的代码长度和复杂性要短得多,也更容易理解。还有什么更好的理由来考虑使用 MySQL 文档库编写未来的应用呢!
以下部分描述了库应用的三个版本。因为它们都使用相同的用户界面代码,所以为了简洁起见,我们省略了对用户界面的讨论,并且仅在适当的地方呈现应用执行的快照来说明差异。以下简要概述了这些版本。每个版本都实现了不同形式的数据存储。
- 版本 1—关系数据库:仅使用非文档存储特性实现传统的关系数据库模型。
- 版本 2——关系数据库+ JSON 字段(混合):实现一个用 JSON 字段扩充的关系数据库模型。
- 版本 3—文档存储:实现一个纯文档存储(NoSQL)解决方案。
以下部分介绍了每个版本的数据库组件的完整代码,以及对用户界面的适当更改。每个版本的完整代码都是为了清晰和完整而呈现的,而不是呈现可能与上下文无关的代码片段。因此,这一章有点长。
Note
回想一下第 8 章中,我们使用了目录结构来组织代码。例如,对于应用的每个版本,我们都有名为version1
、version2
和version3
的文件夹。如果您正在进行,请确保将讨论的文件放在适当的文件夹中。
版本 1:关系数据库
这个版本实现了一个传统的关系数据库解决方案,其中我们基于视图或数据项对数据进行建模。出于演示的目的,我们将在一个代码模块中实现数据库代码,我们可以将该代码模块导入到应用代码中。这个代码模块将使用旧的 MySQL 连接器/Python 协议和 API。也就是说,我们将不使用 X DevAPI,而是依赖 SQL 语句来处理数据。
让我们从数据库设计的简要概述开始。因为对 MySQL 文档存储感兴趣的大多数读者都熟悉关系数据库,所以我们将跳过任何冗长的讨论,通过简单介绍和查看 SQL CREATE
语句来展示数据库。
数据库设计
这个版本的数据库名为 library_v1。本着良好的关系数据库设计的精神,我们将创建一个表来存储作者、出版商和书籍的离散表中的数据。我们还将创建一个单独的表来存储注释,因为这些数据很少被引用,并且可能是很长的字符串。我们将使用外键来确保这三个表之间的一致性。因为每本书可以有多个作者,所以我们需要创建一个连接表来管理书和作者之间的多对多关系。因此,我们将总共创建五个表。图 9-1 显示了带有索引和外键的 library_v1 数据库的实体关系图(ERD)。
图 9-1
ERD diagram—library database (version 1)
我们还需要一种方法,通过ISBN
列从 authors 表中检索给定书籍的主键。当我们在数据库中查询给定书籍的数据时,会使用这些数据。为了便于维护,我们将创建一个存储例程(函数)来检索 authors 表中以逗号分隔的列AuthorId
的列表。我们用它来填充图书模板中的SelectMultipleField
。最后,我们需要另一个存储例程(函数),通过ISBN
列检索给定书籍的作者姓名。然后,我们将使用这些数据来填充 books 表的列表视图。
清单 9-1 显示了所有七个对象的CREATE
语句。如果您想在阅读时继续构建这个版本的应用,那么您应该创建一个名为library_v1.sql
的文件,以便在以后需要时可以重新创建数据库。数据库只使用表和存储的例程来保持讨论简短。
CREATE DATABASE `library_v1`;
CREATE TABLE `library_v1`.`authors` (
`AuthorId` int(11) NOT NULL AUTO_INCREMENT,
`FirstName` varchar(64) DEFAULT NULL,
`LastName` varchar(64) DEFAULT NULL,
PRIMARY KEY (`AuthorId`)
) ENGINE=InnoDB;
CREATE TABLE `library_v1`.`publishers` (
`PublisherId` int(11) NOT NULL AUTO_INCREMENT,
`Name` varchar(128) NOT NULL,
`City` varchar(32) DEFAULT NULL,
`URL` text,
PRIMARY KEY (`PublisherId`)
) ENGINE=InnoDB;
CREATE TABLE `library_v1`.`books` (
`ISBN` char(32) NOT NULL,
`Title` text NOT NULL,
`Year` int(11) NOT NULL DEFAULT '2017',
`Edition` int(11) DEFAULT '1',
`PublisherId` int(11) DEFAULT NULL,
`Language` char(24) NOT NULL DEFAULT 'English',
PRIMARY KEY (`ISBN`),
KEY `pub_id` (`PublisherId`),
CONSTRAINT `books_ibfk_1` FOREIGN KEY (`PublisherId`)
REFERENCES `library_v1`.`publishers` (`publisherid`)
) ENGINE=InnoDB;
CREATE TABLE `library_v1`.`notes` (
`NoteId` int(11) NOT NULL AUTO_INCREMENT,
`ISBN` char(32) NOT NULL,
`Note` text,
PRIMARY KEY (`NoteId`,`ISBN`),
KEY `ISBN` (`ISBN`),
CONSTRAINT `notes_fk_1` FOREIGN KEY (`ISBN`)
REFERENCES `library_v1`.`books` (`isbn`)
) ENGINE=InnoDB;
CREATE TABLE `library_v1`.`books_authors` (
`ISBN` char(32) NOT NULL,
`AuthorId` int(11) DEFAULT NULL,
KEY `auth_id` (`AuthorId`),
KEY `isbn_id` (`ISBN`),
CONSTRAINT `books_authors_fk_1` FOREIGN KEY (`ISBN`)
REFERENCES `library_v1`.`books` (`isbn`),
CONSTRAINT `books_authors_fk_2` FOREIGN KEY (`AuthorId`)
REFERENCES `library_v1`.`authors` (`authorid`)
) ENGINE=InnoDB;
DELIMITER //
CREATE FUNCTION `library_v1`.`get_author_ids`(isbn_lookup char(32))
RETURNS varchar(128) DETERMINISTIC
RETURN (
SELECT GROUP_CONCAT(library_v1.authors.AuthorId SEPARATOR ', ') AS author_ids
FROM library_v1.books_authors JOIN library_v1.authors
ON books_authors.AuthorId = authors.AuthorId
WHERE ISBN = isbn_lookup GROUP BY library_v1.books_authors.ISBN
)//
CREATE FUNCTION `library_v1`.`get_author_names`(isbn_lookup char(32))
RETURNS varchar(128) DETERMINISTIC
RETURN (
SELECT GROUP_CONCAT(library_v1.authors.LastName SEPARATOR ', ') AS author_names
FROM library_v1.books_authors JOIN library_v1.authors
ON books_authors.AuthorId = authors.AuthorId
WHERE ISBN = isbn_lookup GROUP BY library_v1.books_authors.ISBN
)//
DELIMITER ;
Listing 9-1Library Version 1 Database Create Script (library_v1.sql)
既然已经创建了数据库,让我们看看数据库类的代码。
Tip
在本书的示例代码中,每个版本都有一个数据库创建脚本。请访问 Apress 网站下载这本书的源代码。
数据库代码
处理数据库的代码放在名为library_v1.py
的文件中,该文件位于version1
文件夹下的database
文件夹中,如第 8 章“准备目录结构”一节中所述因为大部分代码是使用 MySQL 连接器/Python 连接器的旧 Python 应用所共有的,所以我们只讨论代码每一部分的要点。
也就是说,代码实现了四个类:每个数据视图(作者、出版商、图书)一个类,另一个类用于与服务器接口。这些等级分别被命名为Author
、Publisher
、Book
和Library,
。
Note
要使用位于database
文件夹中的库,您必须在database
文件夹中创建一个名为__init__.py
的空文件。
sql 字符串
为了使代码更易于维护,并在需要对 SQL 语句进行任何更改时对其进行修改,我们将这些语句作为字符串放在代码模块的序言中,以便在代码中引用。这样做还有助于缩短代码行的长度。清单 9-2 显示了library_v1.py
代码模块的序言。注意,它从导入 MySQL 连接器/Python 库开始。
import mysql.connector
ALL_BOOKS = """
SELECT DISTINCT book.ISBN, book.ISBN, Title, publisher.Name as Publisher,
Year, library_v1.get_author_names(book.ISBN) as Authors
FROM library_v1.books As book
INNER JOIN library_v1.publishers as publisher ON
book.PublisherId=publisher.PublisherId
INNER JOIN library_v1.books_authors as book_author ON
book.ISBN = book_author.ISBN
INNER JOIN library_v1.authors as a ON book_author.AuthorId = a.AuthorId
ORDER BY book.ISBN DESC
"""
GET_LASTID = "SELECT @@last_insert_id"
#
# Author SQL Statements
#
INSERT_AUTHOR = """
INSERT INTO library_v1.authors (LastName, FirstName) VALUES ('{0}','{1}')
"""
GET_AUTHORS = "SELECT AuthorId, LastName, FirstName FROM library_v1.authors {0}"
UPDATE_AUTHOR = """
UPDATE library_v1.authors SET LastName = '{0}',
FirstName='{1}' WHERE AuthorId = {2}
"""
DELETE_AUTHOR = """
DELETE FROM library_v1.authors WHERE AuthorId = {0}
"""
#
# Publisher SQL Statements
#
INSERT_PUBLISHER = """
INSERT INTO library_v1.publishers (Name, City, URL) VALUES ('{0}','{1}','{2}')
"""
GET_PUBLISHERS = "SELECT * FROM library_v1.publishers {0}"
UPDATE_PUBLISHER = "UPDATE library_v1.publishers SET Name = '{0}'"
DELETE_PUBLISHER = "DELETE FROM library_v1.publishers WHERE PublisherId = {0}"
#
# Book SQL Statements
#
INSERT_BOOK = """
INSERT INTO library_v1.books (ISBN, Title, Year, PublisherId, Edition,
Language) VALUES ('{0}','{1}','{2}','{3}',{4},'{5}')
"""
INSERT_BOOK_AUTHOR = """
INSERT INTO library_v1.books_authors (ISBN, AuthorId) VALUES ('{0}', {1})
"""
INSERT_NOTE = "INSERT INTO library_v1.notes (ISBN, Note) VALUES ('{0}','{1}')"
GET_BOOKS = "SELECT * FROM library_v1.books {0}"
GET_NOTES = "SELECT * FROM library_v1.notes WHERE ISBN = '{0}'"
GET_AUTHOR_IDS = "SELECT library_v1.get_author_ids('{0}')"
UPDATE_BOOK = "UPDATE library_v1.books SET ISBN = '{0}'"
DELETE_BOOK = "DELETE FROM library_v1.books WHERE ISBN = '{0}'"
DELETE_BOOK_AUTHOR = "DELETE FROM library_v1.books_authors WHERE ISBN = '{0}'"
DELETE_NOTES = "DELETE FROM library_v1.notes WHERE ISBN = '{0}'"
Listing 9-2Initialization and SQL Statements (library_v1.py)
这是一个很大的 SQL,不是吗?如果这看起来令人望而生畏,请考虑大多数关系数据库应用都有一组类似的 SQL 语句。还要考虑到这个示例应用是故意小而有限的。考虑到这些,想象一下一个更大的应用的 SQL 语句的数量和复杂性。哇哦。
接下来,我们来看看Author
类。
作者类别
Author
类是最不复杂的,它为其他数据类的构造提供了一个模型。特别是,我们通过构造函数保存了一个Library
类的实例,并在执行查询时引用这个实例(或者使用Library
类中的任何方法)。然后我们构建四个函数——创建、读取、更新和删除各一个。清单 9-3 显示了Author
类代码。如果主键作为参数传递,read 操作将返回一行,如果没有提供参数,则返回所有行。
注意,我们使用库函数sql()
来执行查询。例如,self.library.sql(“COMMIT”)
执行COMMIT
SQL 命令。我们使用之前使用format()
函数创建的字符串来填充可选参数。我们将在本节的后面更详细地了解这个函数。花一些时间通读代码,以确保你理解它。
class Author(object):
"""Author class
This class encapsulates the authors table permitting CRUD operations
on the data.
"""
def __init__(self, library):
self.library = library
def create(self, LastName, FirstName):
assert LastName, "You must supply a LastName for a new author."
assert FirstName, "You must supply a FirstName for a new author."
query_str = INSERT_AUTHOR
last_id = None
try:
self.library.sql(query_str.format(LastName, FirstName))
last_id = self.library.sql(GET_LASTID)
self.library.sql("COMMIT")
except Exception as err:
print("ERROR: Cannot add author: {0}".format(err))
return last_id
def read(self, AuthorId=None):
query_str = GET_AUTHORS
if not AuthorId:
# return all authors
query_str = query_str.format("")
else:
# return specific author
query_str = query_str.format("WHERE AuthorId = '{0}'".format(AuthorId))
return self.library.sql(query_str)
def update(self, AuthorId, LastName, FirstName):
assert AuthorId, "You must supply an AuthorId to update the author."
assert LastName, "You must supply a LastName for the author."
assert FirstName, "You must supply a FirstName for the author."
query_str = UPDATE_AUTHOR
try:
self.library.sql(query_str.format(LastName, FirstName, AuthorId))
self.library.sql("COMMIT")
except Exception as err:
print("ERROR: Cannot update author: {0}".format(err))
def delete(self, AuthorId):
assert AuthorId, "You must supply an AuthorId to delete the author."
query_str = DELETE_AUTHOR.format(AuthorId)
try:
self.library.sql(query_str)
self.library.sql("COMMIT")
except Exception as err:
print("ERROR: Cannot delete author: {0}".format(err))
Listing 9-3Author Class (library_v1.py)
接下来,我们来看看Publisher
类。
发布者类别
Publisher
类与Author
类非常相似,并且以相同的方式实现。唯一的区别在于使用的 SQL 语句。为了完整起见,清单 9-4 显示了Publisher
类的完整代码。
class Publisher(object):
"""Publisher class
This class encapsulates the publishers table permitting CRUD operations
on the data.
"""
def __init__(self, library):
self.library = library
def create(self, Name, City=None, URL=None):
assert Name, "You must supply a Name for a new publisher."
query_str = INSERT_PUBLISHER
last_id = None
try:
self.library.sql(query_str.format(Name, City, URL))
last_id = self.library.sql(GET_LASTID)
self.library.sql("COMMIT")
except Exception as err:
print("ERROR: Cannot add publisher: {0}".format(err))
return last_id
def read(self, PublisherId=None):
query_str = GET_PUBLISHERS
if not PublisherId:
# return all authors
query_str = query_str.format("")
else:
# return specific author
query_str = query_str.format(
"WHERE PublisherId = '{0}'".format(PublisherId))
return self.library.sql(query_str)
def update(self, PublisherId, Name, City=None, URL=None):
assert PublisherId, "You must supply a publisher to update the author."
query_str = UPDATE_PUBLISHER.format(Name)
if City:
query_str = query_str + ", City = '{0}'".format(City)
if URL:
query_str = query_str + ", URL = '{0}'".format(URL)
query_str = query_str + " WHERE PublisherId = {0}".format(PublisherId)
try:
self.library.sql(query_str)
self.library.sql("COMMIT")
except Exception as err:
print("ERROR: Cannot update publisher: {0}".format(err))
def delete(self, PublisherId):
assert PublisherId, "You must supply a publisher to delete the publisher."
query_str = DELETE_PUBLISHER.format(PublisherId)
try:
self.library.sql(query_str)
self.library.sql("COMMIT")
except Exception as err:
print("ERROR: Cannot delete publisher: {0}".format(err))
Listing 9-4Publisher Class (library_v1.py)
接下来,看一下图书类。
图书类
Book 类具有与后两个类相同的方法,但是创建、更新和删除的代码稍微复杂一些。这是因为我们必须执行多个语句来处理数据。因此,我们在 try 块中隐式启动一个事务,如果任何查询失败,我们将回滚该事务。这在关系数据库解决方案中很常见。清单 9-5 显示了Book
类的完整代码。花点时间通读代码,了解它是如何构造的。
class Book(object):
"""Book class
This class encapsulates the books table permitting CRUD operations
on the data.
"""
def __init__(self, library):
self.library = library
def create(self, ISBN, Title, Year, PublisherId, Authors=[], Edition=1,
Language='English'):
assert ISBN, "You must supply an ISBN for a new book."
assert Title, "You must supply Title for a new book."
assert Year, "You must supply a Year for a new book."
assert PublisherId, "You must supply a PublisherId for a new book."
assert Authors, "You must supply at least one AuthorId for a new book."
last_id = ISBN
#
# We must do this as a transaction to ensure all tables are updated.
#
try:
self.library.sql("START TRANSACTION")
query_str = INSERT_BOOK.format(ISBN, Title, Year, PublisherId,
Edition, Language)
self.library.sql(query_str)
query_str = INSERT_BOOK_AUTHOR
for AuthorId in Authors.split(","):
self.library.sql(query_str.format(ISBN, AuthorId))
self.library.sql("COMMIT")
except Exception as err:
print("ERROR: Cannot add book: {0}".format(err))
self.library.sql("ROLLBACK")
return last_id
def read(self, ISBN=None):
query_str = GET_BOOKS
if not ISBN:
# return all authors
query_str = query_str.format("")
else:
# return specific author
query_str = query_str.format("WHERE ISBN = '{0}'".format(ISBN))
return self.library.sql(query_str)
def read_notes(self, ISBN):
assert ISBN, "You must supply an ISBN to get the notes."
query_str = GET_NOTES.format(ISBN)
return self.library.sql(query_str)
def read_author_ids(self, ISBN):
assert ISBN, "You must supply an ISBN to get the list of author ids."
query_str = GET_AUTHOR_IDS.format(ISBN)
return self.library.sql(query_str)
def update(self, old_isbn, ISBN, Title=None, Year=None, PublisherId=None,
Authors=None, Edition=None, Language=None, Note=None):
assert ISBN, "You must supply an ISBN to update the book."
last_id = None
#
# Build the book update query
#
book_query_str = UPDATE_BOOK.format(ISBN)
if Title:
book_query_str += ", Title = '{0}'".format(Title)
if Year:
book_query_str += ", Year = {0}".format(Year)
if PublisherId:
book_query_str += ", PublisherId = {0}".format(PublisherId)
if Edition:
book_query_str += ", Edition = {0}".format(Edition)
book_query_str += " WHERE ISBN = '{0}'".format(old_isbn)
#
# We must do this as a transaction to ensure all tables are updated.
#
try:
self.library.sql("START TRANSACTION")
#
# If the ISBN changes, we must remove the author ids first to
# avoid the foreign key constraint error.
#
if old_isbn != ISBN:
self.library.sql(DELETE_BOOK_AUTHOR.format(old_isbn))
self.library.sql(book_query_str)
last_id = self.library.sql(GET_LASTID)
if Authors:
# First, clear the author list.
self.library.sql(DELETE_BOOK_AUTHOR.format(ISBN))
query_str = INSERT_BOOK_AUTHOR
for AuthorId in Authors:
self.library.sql(query_str.format(ISBN,AuthorId))
if Note:
self.add_note(ISBN, Note)
self.library.sql("COMMIT")
except Exception as err:
print("ERROR: Cannot update book: {0}".format(err))
self.library.sql("ROLLBACK")
return last_id
def delete(self, ISBN):
assert ISBN, "You must supply a ISBN to delete the book."
#
# Here, we must cascade delete the notes when we delete a book.
# We must do this as a transaction to ensure all tables are updated.
#
try:
self.library.sql("START TRANSACTION")
query_str = DELETE_NOTES.format(ISBN)
self.library.sql(query_str)
query_str = DELETE_BOOK_AUTHOR.format(ISBN)
self.library.sql(query_str)
query_str = DELETE_BOOK.format(ISBN)
self.library.sql(query_str)
self.library.sql("COMMIT")
except Exception as err:
print("ERROR: Cannot delete book: {0}".format(err))
self.library.sql("ROLLBACK")
def add_note(self, ISBN, Note):
assert ISBN, "You must supply a ISBN to add a note for the book."
assert Note, "You must supply text (Note) to add a note for the book."
query_str = INSERT_NOTE.format(ISBN, Note)
try:
self.library.sql(query_str)
self.library.sql("COMMIT")
except Exception as err:
print("ERROR: Cannot add publisher: {0}".format(err))
Listing 9-5Book Class (library_v1.py)
最后,我们看看库类。
库类
回想一下,Library 类用于封装 MySQL 服务器的工作。因此,我们创建了处理连接的函数(connect
、disconnect
、is_connected
)。我们还创建了一个可以用来执行查询的函数。这主要是为了方便,一般不需要。该函数名为 sql(),根据需要处理返回的结果集或错误。最后一个函数用于返回数据库中图书的缩略数据集,该数据集用于填充图书列表页面。清单 9-6 显示了库类的代码。正如您将看到的,它也非常简单。
class Library(object):
"""Library master class
Use this class to interface with the library database. It includes
utility functions for connections to the server as well as running
queries.
"""
def __init__(self):
self.db_conn = None
def connect(self, username, passwd, host, port, db=None):
config = {
'user': username,
'password': passwd,
'host': host,
'port': port,
'database': db,
}
try:
self.db_conn = mysql.connector.connect(**config)
except mysql.connector.Error as err:
print("CONNECTION ERROR:", err)
self.db_conn = None
raise
#
# Return the connection for use in other classes
#
def get_connection(self):
return self.db_conn
#
# Check to see if connected to the server
#
def is_connected(self):
return (self.db_conn and (self.db_conn.is_connected()))
#
# Disconnect from the server
#
def disconnect(self):
try:
self.db_conn.disconnect()
except:
pass
#
# Execute a query and return any results
#
# query_str[in] The query to execute
# fetch Execute the fetch as part of the operation and
# use a buffered cursor (default is True)
# buffered If True, use a buffered raw cursor (default is False)
#
# Returns result set or cursor
#
def sql(self, query_str, fetch=True, buffered=False):
# If we are fetching all, we need to use a buffered
if fetch:
cur = self.db_conn.cursor(buffered=True)
else:
cur = self.db_conn.cursor(raw=True)
try:
cur.execute(query_str)
except Exception as err:
cur.close()
print("Query error. Command: {0}:{1}".format(query_str, err))
raise
# Fetch rows (only if available or fetch = True).
if cur.with_rows:
if fetch:
try:
results = cur.fetchall()
except mysql.connector.Error as err:
print("Error fetching all query data: {0}".format(err))
raise
finally:
cur.close()
return results
else:
# Return cursor to fetch rows elsewhere (fetch = false).
return cur
else:
return cur
#
# Get list of books
#
def get_books(self):
try:
results = self.sql(ALL_BOOKS)
except Exception as err:
print("ERROR: {0}".format(err))
raise
return results
Listing 9-6Library Class (library_v1.py)
现在我们有了数据库代码模块,让我们看看应用代码。
应用代码
在我们之前看到的基础代码中,应用代码中只有几个地方需要添加更多的代码。这包括为数据库代码模块添加 import 语句,设置Librar
y 类实例,以及向 author、publisher 和 book view 函数添加代码以使用数据库代码模块中的类。幸运的是,我们在用户界面讨论中创建的模板文件无需修改即可使用。
要构建这个版本的应用,您应该将base/mylibrary_base.py
文件复制到 v ersion1/mylibrary_v1.py
中,然后输入下面的代码或者从 Apress book 网站上检索。
尽管代码看起来很长,但并不复杂。此外,除了图书视图功能之外,作者和出版商视图的逻辑是相同的。图书视图有更多的逻辑来启用添加注释功能。以下部分讨论了每个领域所需的更改。回想一下,我们需要在前面章节中看到的mylibrary_base.py
代码。
设置和初始化
设置和初始化Library
类的代码很简单。我们只需要从代码模块导入类,然后创建一个Library
类的实例并调用connect()
函数,如下所示。import 语句位于其他 import 语句的末尾,库设置代码可以位于其后的任何地方。在示例代码中,此代码放在第一个 form 类函数之前。
from database.library_v1 import Library, Author, Publisher, Book
[...]
#
# Setup the library database class
#
library = Library()
# Provide your user credentials here
library.connect(<user>, <password>, 'localhost', 3306)
Note
确保修改<user>
和<password>
条目以匹配您的 MySQL 配置。这些是用户帐户和密码的占位符。
列表视图功能
列表视图功能只需要一些修改。我们将使用Library
类实例(名为 l ibrary
)从数据库中获取数据,并显示在页面的列表中。对于书籍,这只是简单地调用library.get_books()
函数。对于作者,我们实例化了Author
和Publisher
类的一个实例,然后不带任何参数调用read()
函数。回想一下前面的部分,这会导致读取表中的所有行。清单 9-7 显示了simple_list()
视图功能所需的更改。新的代码行以粗体显示。如您所见,我们只添加了五行代码。简单!
def simple_list(kind=None):
rows = []
columns = []
form = ListForm()
if kind == 'book' or not kind:
if request.method == 'POST':
return redirect('book')
columns = (
'<td style="width:200px">ISBN</td>',
'<td style="width:400px">Title</td>',
'<td style="width:200px">Publisher</td>',
'<td style="width:80px">Year</td>',
'<td style="width:300px">Authors</td>',
)
kind = 'book'
# Here, we get all books in the database
rows = library.get_books()
return render_template("list.html", form=form, rows=rows,
columns=columns, kind=kind)
elif kind == 'author':
if request.method == 'POST':
return redirect('author')
# Just list the authors
columns = (
'<td style="width:100px">Lastname</td>',
'<td style="width:200px">Firstname</td>',
)
kind = 'author'
# Here, we get all authors in the database
author = Author(library)
rows = author.read()
return render_template("list.html", form=form, rows=rows,
columns=columns, kind=kind)
elif kind == 'publisher':
if request.method == 'POST':
return redirect('publisher')
columns = (
'<td style="width:300px">Name</td>',
'<td style="width:100px">City</td>',
'<td style="width:300px">URL/Website</td>',
)
kind = 'publisher'
# Here, we get all publishers in the database
publisher = Publisher(library)
rows = publisher.read()
return render_template("list.html", form=form, rows=rows,
columns=columns, kind=kind)
else:
flash("Something is wrong!")
return
Listing 9-7List View Function (Version 1)
作者视图功能
作者视图功能的变化稍微复杂一些。因为 author、publisher 和 book view 函数遵循相同的模式,所以我们将首先讨论一般的模式,然后查看 view each 函数的代码。因为GET
和POST
操作的概念对您来说可能是新的,所以我们将花一点时间来讨论它们的区别。
我们希望对GET
和POST
操作都使用这个视图函数。特别是,当用户单击列表中的作者时,我们希望在表中显示该行的数据。或者,如果用户单击 New 按钮,我们希望呈现一个空的 HTML 表单供用户完成。到目前为止,这些都是GET
行动。如果用户点击提交字段(或者是Add
、Update
或者是Delete
),我们希望从用户那里获取数据,或者创建、更新或者删除数据。这些是POST
操作。这在视图函数中是如何工作的并不明显。但是,一旦习惯了就有道理了。让我们浏览一下调用视图函数的条件。表 9-1 列出了不同的条件(或模式)。
表 9-1
Operations (Modes) for View Functions
| 操作 | 类型 | 行动 | | :-- | :-- | :-- | | 增加 | 得到 | 显示一个空表单,并提供一个名为 Add 的提交字段 | | 创造 | 邮政 | 将一个数据项的数据保存到数据库 | | 阅读 | 得到 | 显示一个数据项的数据库中的数据,并提供名为 Update 和 Delete 的提交字段 | | 更新 | 邮政 | 将现有数据项的更新数据保存到数据库 | | 删除 | 邮政 | 从数据库中删除数据项 |注意,有两个GET
操作和三个POST
操作。GET
操作要么呈现一个空表单,要么从表中的一行读取。POST
操作是当用户点击一个提交字段时发生的事件,导致创建、更新或删除。
返回到作者视图函数,我们需要为上面列出的操作添加代码。清单 9-8 展示了作者视图函数的完整代码,而不是详细讨论代码然后再展示。添加了行号,以便更容易看到所讨论的代码行。清单后面包含了对数据库代码的详细讨论。
01 def author(author_id=None):
02 author = Author(library)
03 form = AuthorForm()
04 # Get data from the form if present
05 form_authorid = form.authorid.data
06 firstname = form.firstname.data
07 lastname = form.lastname.data
08 # If the route with the variable is called, change the create button to update
09 # then populate the form with the data from the row in the table. Otherwise,
10 # remove the delete button because this will be a new data item.
11 if author_id:
12 form.create_button.label.text = "Update"
13 # Here, we get the data and populate the form
14 data = author.read(author_id)
15 if data == []:
16 flash("Author not found!")
17 form.authorid.data = data[0][0]
18 form.firstname.data = data[0][1]
19 form.lastname.data = data[0][2]
20 else:
21 del form.del_button
22 if request.method == 'POST':
23 # First, determine if we must create, update, or delete when form posts.
24 operation = "Create"
25 if form.create_button.data:
26 if form.create_button.label.text == "Update":
27 operation = "Update"
28 if form.del_button and form.del_button.data:
29 operation = "Delete"
30 if form.validate_on_submit():
31 # Get the data from the form here
32 if operation == "Create":
33 try:
34 author.create(LastName=lastname, FirstName=firstname)
35 flash("Added.")
36 return redirect('/list/author')
37 except Exception as err:
38 flash(err)
39 elif operation == "Update":
40 try:
41 author.update(AuthorId=form_authorid, LastName=lastname,
42 FirstName=firstname)
43 flash("Updated.")
44 return redirect('/list/author')
45 except Exception as err:
46 flash(err)
47 else:
48 try:
49 author.delete(form_authorid)
50 flash("Deleted.")
51 return redirect('/list/author')
52 except Exception as err:
53 flash(err)
54 else:
55 flash_errors(form)
56 return render_template("author.html", form=form)
Listing 9-8Author View Function (Version 1)
我们做的第一件事是添加 Author 类的实例,并将其传递给 Library 类实例,如第 2 行所示。接下来,为了涵盖需要表单数据的操作,我们将代码放在视图函数的顶部,将数据从表单复制到局部变量,如第 4–7 行所示。这确保了如果再次调用视图函数进行POST
操作,我们可以捕获用户输入的任何数据。如果我们没有这样做,我们就不能对新的和现有的数据使用视图功能。
接下来,我们必须覆盖我们传入主键的路线(在本例中是author_id
)。如果author_id
存在,我们将其中一个提交字段(add)的标签更改为Update
。我们还知道我们必须从数据库中读取数据,这是通过第 14 行的author.read(author_id)
调用来完成的,如果我们检索到的行没有错误,我们就将表中的数据放入第 17-19 行的字段中。如果author_id
变量不存在,我们删除第 21 行的删除提交字段。
至此,我们已经介绍了表 9-1 中所示的添加和读取操作。只有当请求是一个POST
时,才执行创建、更新和删除操作。为了确定这一点,我们检查第 22 行的request.method
属性的值。如果是POST
,那么我们必须决定哪个行动是有效的。我们可以通过检查提交字段的文本来做到这一点。我们使用默认值 create,但根据单击的提交字段将其更改为 update 或 delete。您可以在第 24–29 行看到这些操作。
特别是,如果在POST
上点击一个提交字段,那么data
属性将是True
。因此,我们可以根据单击了哪个按钮来查看需要执行哪个操作。对于 create 按钮,我们知道它是 create,除非标签被更改为 update,在这种情况下,操作与 update 匹配。另一方面,如果删除按钮没有被删除而是被点击了,那么操作就是删除。这是一种在多个操作中重用视图函数的方法。
既然我们知道哪个操作是活动的,我们就执行这个操作。但是,只有当所有字段都通过了验证检查时,我们才会这样做。如果所有字段都被验证,第 30 行的代码将返回True
。因此,只有当表单域被验证时,我们才执行活动操作。
第 33–38 行显示了创建操作。注意,我们使用 try 块来检测错误。要创建一个新作者,我们只需用表单中的数据调用author.create()
函数。同样,更新操作如第 40–45 行所示。我们再次使用一个try
块来检测错误。为了更新现有的作者,我们用表单中的数据调用author.update()
函数。最后,删除操作如第 46–53 行所示。同样,我们使用一个try
块来检测错误。要删除一个现有的作者,我们从表单中调用带有author_id
的author.delete()
函数。
现在,让我们看一下 publisher view 函数,它非常类似。
发布者视图功能
因为 publisher view 函数与 author view 函数非常相似(是相同的设计或模式),所以我只总结了详细描述数据库操作的代码。清单 9-9 显示了发布者视图函数的完整代码。添加了行号,以便更容易看到讨论的代码行。清单后面包含了对数据库代码的详细讨论。
01 def publisher(publisher_id=None):
02 publisher = Publisher(library)
03 form = PublisherForm()
04 # Get data from the form if present
05 form_publisherid = form.publisherid.data
06 name = form.name.data
07 city = form.city.data
08 url = form.url.data
09 # If the route with the variable is called, change the create button to update then populate the form with the data from the
10 # row in the table. Otherwise, remove the delete button because
11 # this will be a new data item.
12 if publisher_id:
13 # Here, we get the data and populate the form
14 form.create_button.label.text = "Update"
15 # Here, we get the data and populate the form
16 data = publisher.read(publisher_id)
17 if data == []:
18 flash("Publisher not found!")
19 form.publisherid.data = data[0][0]
20 form.name.data = data[0][1]
21 form.city.data = data[0][2]
22 form.url.data = data[0][3]
23 else:
24 del form.del_button
25 if request.method == 'POST':
26 # First, determine if we must create, update, or delete when form posts.
27 operation = "Create"
28 if form.create_button.data:
29 if form.create_button.label.text == "Update":
30 operation = "Update"
31 if form.del_button and form.del_button.data:
32 operation = "Delete"
33 if form.validate_on_submit():
34 # Get the data from the form here
35 if operation == "Create":
36 try:
37 publisher.create(Name=name, City=city, URL=url)
38 flash("Added.")
39 return redirect('/list/publisher')
40 except Exception as err:
41 flash(err)
42 elif operation == "Update":
43 try:
44 publisher.update(PublisherId=form_publisherid, Name=name,
45 City=city, URL=url)
46 flash("Updated.")
47 return redirect('/list/publisher')
48 except Exception as err:
49 flash(err)
50 else:
51 try:
52 publisher.delete(form_publisherid)
53 flash("Deleted.")
54 return redirect('/list/publisher')
55 except Exception as err:
56 flash(err)
57 else:
58 flash_errors(form)
59 return render_template("publisher.html", form=form)
Listing 9-9Publisher View Function (Version 1)
使用 author view 函数,第 2 行实例化了一个Publisher
类的实例,第 4–8 行从表单中获取数据供以后使用。第 12 行开始从数据库读取数据,第 14 行将 add submit 按钮的标签更改为 update,第 16–22 行将数据存储在表单中。最后,第 27–32 行确定了一个POST
请求的活动操作,第 33 行确保在我们执行数据库操作之前表单字段被验证。
第 36–41 行显示了创建操作。要创建一个新的发布者,我们只需用表单中的数据调用publisher.create()
函数。同样,更新操作如第 43–49 行所示。为了更新现有的发布者,我们使用表单中的数据调用publisher.update()
函数。最后,删除操作如第 51–56 行所示。要删除一个现有的发布者,我们从表单中调用带有publisher_id
的publisher.delete()
函数。
Pop Quiz
您是否注意到我们处理 publisher_id 的方式有所不同?我们从表单的隐藏字段中获取发布者 id,而不是使用 route 中的变量。这样做是为了展示将数据保存到表单的另一种方法。
但是有一个很好的理由来使用这种技术,即使它复制了一小部分数据。例如,用户可能想要更改 ISBN。因为 ISBN 是表的主键,所以如果我们使用 GET 请求中的 ISBN (/ book/978-1-4842-2724-4 路径),数据库操作将无法定位该行,因为表单上的 ISBN 已被更改。
这也演示了代理主键(如自动增量字段)如何帮助您避免这种潜在的数据地雷。
现在,让我们看看 book view 函数,它遵循相同的模式,但是需要更多的逻辑。
图书视图功能
图书视图功能比作者或出版商视图功能更复杂,原因有三:1)它有更多的字段,2)有需要填充的选择字段,以及 3)有一个用于更新操作的附加功能,以便向图书的数据库添加注释。
但是,代码遵循与前面的视图函数相同的模式。清单 9-10 显示了完整的图书视图功能代码。再次添加了行号以增强可读性,代码的讨论在清单后面。
01 def book(isbn_selected=None):
02 notes = None
03 book = Book(library)
04 form = BookForm()
05 # Get data from the form if present
06 isbn = form.isbn.data
07 title = form.title.data
08 year = form.year.data
09 authorids = form.authors.data
10 publisherid = form.publisher.data
11 edition = form.edition.data
12 language = form.language.data
13 #
14 # Here, we get the choices for the select lists
15 #
16 publisher = Publisher(library)
17 publishers = publisher.read()
18 publisher_list = []
19 for pub in publishers:
20 publisher_list.append((pub[0], '{0}'.format(pub[1])))
21 form.publisher.choices = publisher_list
22 author = Author(library)
23 authors = author.read()
24 author_list = []
25 for author in authors:
26 author_list.append((author[0],'{0}, {1}'.format(author[2], author[1])))
27 form.authors.choices = author_list
28 new_note = form.new_note.data
29 # If the route with the variable is called, change the create button to update then populate the form with the data from
30 # the row in the table. Otherwise, remove the delete button
31 # because this will be a new data item.
32 if isbn_selected:
33 # Here, we get the data and populate the form
34 data = book.read(isbn_selected)
35 if data == []:
36 flash("Book not found!")
37
38 #
39 # Here, we populate the data
40 #
41 form.isbn.data = data[0][0]
42 form.title.data = data[0][1]
43 form.year.data = data[0][2]
44 form.edition.data = data[0][3]
45 form.publisher.process_data(data[0][4])
46 form.language.data = data[0][5]
47 #
48 # Here, we get the author_ids for the authors
49 #
50 author_ids = book.read_author_ids(isbn_selected)[0][0]
51 form.authors.data = set(author_ids)
52
53 # We also must retrieve the notes for the book.
54 all_notes = book.read_notes(isbn_selected)
55 notes = []
56 for note in all_notes:
57 notes.append(note[2])
58 form.create_button.label.text = "Update"
59 else:
60 del form.del_button
61 if request.method == 'POST':
62 # First, determine if we must create, update, or delete when form posts.
63 operation = "Create"
64 if form.create_button.data:
65 if form.create_button.label.text == "Update":
66 operation = "Update"
67 if form.del_button and form.del_button.data:
68 operation = "Delete"
69 if form.validate_on_submit():
70 # Get the data from the form here
71 if operation == "Create":
72 try:
73 book.create(ISBN=isbn, Title=title, Year=year,
74 PublisherId=publisherid, Authors=authorids,
75 Edition=edition, Language=language)
76 flash("Added.")
77 return redirect('/list/book')
78 except Exception as err:
79 flash(err)
80 elif operation == "Update":
81 try:
82 book.update(isbn_selected, ISBN=isbn, Title=title, Year=year,
83 PublisherId=publisherid, Authors=authorids,
84 Edition=edition, Language=language,
85 Note=new_note)
86 flash("Updated.")
87 return redirect('/list/book')
88 except Exception as err:
89 flash(err)
90 else:
91 try:
92 book.delete(isbn)
93 flash("Deleted.")
94 return redirect('/list/book')
95 except Exception as err:
96 flash(err)
97 else:
98 flash_errors(form)
99 return render_template("book.html", form=form, notes=notes)
Listing 9-10Book View Function (Version 1)
首先,您可能会注意到我们有一个名为 notes 的新变量,它被设置为 None。我们在这里这样做是因为我们将使用这个变量来包含从数据库中读取的该书的所有注释。稍后会详细介绍。
与 author 和 publisher 视图函数一样,第 3 行实例化了 Book 类的一个实例,第 6–12 行从表单中获取数据供以后使用。接下来是用数据库中的值填充选择字段的代码。我们这样做是因为 book 表依赖于authors
(技术上是通过books_authors
连接表和publishers
表。因此,我们需要从两个表中提取行来填充下拉列表和多个选择列表。
第 16–21 行是发布者数据。这里,我们首先实例化一个Publisher
类的实例,然后从表中检索所有数据。接下来,我们遍历这些行,将发布者 id 和名称添加到一个列表中,然后将该列表分配给选择字段选择属性的数据属性(form.publisher.choices
)。为什么我们包括发行者 id?因为出版商 id 只存储在 book 表中。
同样,第 22–27 行对 author 数据做了同样的事情,创建了一个 Author 类的实例,检索所有行,然后遍历数据添加作者 id,并连接姓和名。与 select 字段一样,我们用新数组填充字段数据。此时,我们已经填充了两个选择字段。接下来是如何设置值以匹配行,以及从数据库中检索数据。
第 32 行开始从数据库读取数据。第 34–57 行从数据库中检索数据并填充表单。对于选择字段,设置数据属性可确保选择值。对于发布者,我们设置选择字段数据,默认情况下匹配的项目被选中。在选择多个字段的情况下,我们传递一个逗号分隔的列表,如第 50–51 行所示,在这里我们从数据库中检索作者 id 的列表。接下来,我们检索这本书的注释并填充一个数组,我们在模板中使用该数组来填充 HTML 表。
哇哦!那是许多工作,不是吗?所有的工作就是为添加和读取操作设置表单。幸运的是,创建、更新和删除设置与其他视图功能相同。您可以在第 63–9 行看到这一点。
令人欣慰的是,数据库操作是熟悉的。第 72–79 行显示了创建操作。要创建一本新书,我们只需用表单中的数据调用book.create()
函数。同样,更新操作如第 81–89 行所示。为了更新现有的书籍,我们使用表单中的数据调用book.update()
函数。最后,删除操作如第 91–96 行所示。要删除现有的书,我们从表单中调用带有isbn
的book.delete()
函数。
模板
基本版本的模板文件没有任何变化。你所需要做的就是创建一个新的文件夹,并从基础文件夹中复制文件。特别是将base/templates/*
复制到version1/templates/
。
您需要做的唯一更改是将 base.html 文件中的“base”文本更改为“V1 ”,如下面的差异示例所示,其中删除了带“-”的行,添加了带“+”的行。
- <a class="navbar-brand" href="/">MyLibrary Base</a>
+ <a class="navbar-brand" href="/">MyLibrary v1</a>
现在我们已经更新了代码,让我们看看它是如何工作的!
执行代码
现在代码写好了,我们来试驾一下。确保首先创建数据库和任何必要的表。如果您将前面的 SQL 语句保存在名为 library_v1.sql 的文件中,您可以在 mysql 客户端中使用SOURCE
命令,如下所示。
mysql> SOURCE <path>/version1/library_v1.sql;
要执行应用,可以用 Python 解释器指定runserver
命令来启动它。下面显示了一个执行应用的示例。注意,我们使用了 port 选项来指定端口。您应该从version1
文件夹中输入这个命令。注意,我们将端口指定为 5001。我们将对版本 2 使用 5002,对版本 3 使用 5003。这将允许您同时运行所有三个版本。
$ cd version1
$ python ./mylibrary_v1.py runserver -p 5001
* Running on http://127.0.0.1:5001/ (Press CTRL+C to quit)
应用将启动并运行,但是数据库中还没有任何数据。你应该从拿几本你喜欢的书开始,首先输入作者和出版商,然后输入图书数据。现在还不要担心音符。一旦你添加了几本书,你应该在默认视图中看到它们(通过点击导航栏中的MyLibrary v1
或Books
)。图 9-2 显示了一个你应该看到的例子。其他视图是相似的,留给读者去探索。
图 9-2
Library application book list (version 1)
接下来,尝试一下 notes 特性。单击图书列表中某本图书的修改链接,然后添加注释并单击更新。当您下一次通过单击修改链接来查看数据时,您将看到注释出现。图 9-3 显示了一本书的注释列表摘录。
图 9-3
Notes list example (version 1)
在我们继续讨论版本 2 之前,让我们花点时间来讨论一下关于这个版本的应用的一些观察结果。
观察
以下是对该应用版本的一些观察。有些是数据库设计的结果,有些是代码的结果,还有一些是我们为了让应用变得更好而想要改变的。观察结果以无序列表的形式呈现。如果您想试验这个版本的应用,您可以考虑其中一些对改进应用的挑战。否则,可以考虑在下一个版本中使用这个列表。
- 冗长的代码:应用的代码相当长(超过 400 行)。
- 冗长的数据库代码模块:数据库代码模块的代码也很长(超过 400 行)。
- 过度设计的表:多对多表是不必要的复杂,这使得使用 SQL 更加困难。
- 数据库设计可以改进:精明的数据库管理员无疑会发现数据库设计中可以改进的地方。例如,视图的使用可以代替在
Library
类的get_books()
函数中使用的查询。 - 过度分析的数据:关系数据库设计的一个障碍是在可用性面前过度使用范式。在这种情况下,用户不太可能关心作者列表,因为没有其他有意义的信息——只有作者的名和姓。
- 简单读取:查看数据的默认机制是一个列表。虽然这对于作者和出版商来说很好,但对于书籍来说是有限制的,因为你必须点击
Modify
链接才能看到这本书的注释。这可以通过简单的只读模式而不是更新来改善。 - 旧协议:没有 X DevAPI 集成。
现在,让我们看看应用的下一个版本。
版本 2:关系数据库+ JSON 字段(混合)
这个版本实现了一个增加了 JSON 字段的关系数据库。我们基于视图或数据项对数据建模,但是使用 JSON 字段来消除传统关系数据库解决方案的一个问题:多对多连接。出于演示的目的,我们将在一个代码模块中实现数据库代码,我们可以将该代码模块导入到应用代码中。尽管我们将像在版本 1 中一样使用 MySQL Connector/Python,但是我们将使用 X DevAPI 使用 SQL 语句来处理数据。目标是演示如何迁移到使用 X DevAPI 但保留 SQL 接口。因此,这个版本提供了一个混合解决方案。
版本 1 数据库中的多对多关系使得我们可以将一本书链接到一个或多个作者,并且我们可能有同一作者的多本书。然而,像大多数应用一样,数据库设计揭示了一个我们拥有比所需更多的复杂性的情况。特别是,我们有一个作者表,但发现我们只存储(或关心)名和姓。此外,应用的使用表明,除了将作者数据与图书一起列出之外,我们没有查询作者数据的用例。
因此,我们可以消除在 JSON 字段中存储作者姓名列表的多对多关系。这导致了其他一些小的变化,比如存储的例程和其他的添加。
让我们从变更后的数据库设计的简要概述开始。因为该数据库与版本 1 相同,只是稍有改动,所以我们将只针对不同之处进行简要概述。
数据库设计
该版本的数据库名为 library_v2。因为目标是移除多对多关系,所以我们移除了books_authors
连接表,代之以 books 表中的 JSON 字段,并移除了authors
表。因此,我们将数据库从五个表减少到三个。图 9-4 显示了带有索引和外键的 library_v2 数据库的 ERD。
图 9-4
ERD diagram—library database (version 2)
通过消除多对多关系,我们可以删除图书视图中作者的选择多个字段。我们可以用一个简单的逗号分隔的列表来代替,这个列表很容易转换成 JSON。因此,我们需要一种方法从 JSON 字段中检索名称,返回逗号分隔的列表。我们可以用一个存储的例程(函数)来做这件事。
清单 9-11 显示了所有对象的CREATE
语句。如果您想在阅读的时候继续构建这个版本的应用,那么您应该创建一个名为library_v2.sql
的文件,以便以后可以重新创建数据库。
CREATE DATABASE `library_v2`;
CREATE TABLE `library_v2`.`publishers` (
`PublisherId` int(11) NOT NULL AUTO_INCREMENT,
`Name` varchar(128) NOT NULL,
`City` varchar(32) DEFAULT NULL,
`URL` text,
PRIMARY KEY (`PublisherId`)
) ENGINE=InnoDB;
CREATE TABLE `library_v2`.`books` (
`ISBN` char(32) NOT NULL,
`Title` text NOT NULL,
`Year` int(11) NOT NULL DEFAULT '2017',
`Edition` int(11) DEFAULT '1',
`PublisherId` int(11) DEFAULT NULL,
`Language` char(24) NOT NULL DEFAULT 'English',
`Authors` JSON NOT NULL,
PRIMARY KEY (`ISBN`),
KEY `Pub_id` (`PublisherId`),
CONSTRAINT `books_fk_1` FOREIGN KEY (`PublisherId`)
REFERENCES `library_v2`.`publishers` (`publisherid`)
) ENGINE=InnoDB;
CREATE TABLE `library_v2`.`notes` (
`NoteId` int(11) NOT NULL AUTO_INCREMENT,
`ISBN` char(32) NOT NULL,
`Note` text,
PRIMARY KEY (`NoteId`,`ISBN`),
KEY `ISBN` (`ISBN`),
CONSTRAINT `notes_fk_1` FOREIGN KEY (`ISBN`)
REFERENCES `library_v2`.`books` (`isbn`)
) ENGINE=InnoDB;
DELIMITER //
CREATE FUNCTION `library_v2`.`get_author_names`(isbn_lookup char(32))
RETURNS text DETERMINISTIC
BEGIN
DECLARE j_array varchar(255);
DECLARE num_items int;
DECLARE i int;
DECLARE last char(20);
DECLARE first char(20);
DECLARE csv varchar(255);
SET j_array = (SELECT JSON_EXTRACT(Authors,'$.authors')
FROM library_v2.books WHERE ISBN = isbn_lookup);
SET num_items = JSON_LENGTH(j_array);
SET csv = "";
SET i = 0;
author_loop: LOOP
IF i < num_items THEN
SET last = CONCAT('$[',i,'].LastName');
SET first = CONCAT('$[',i,'].FirstName');
IF i > 0 THEN
SET csv = CONCAT(csv,", ",JSON_UNQUOTE(JSON_EXTRACT(j_array,last)),' ',
JSON_UNQUOTE(JSON_EXTRACT(j_array,first)));
ELSE
SET csv = CONCAT(JSON_UNQUOTE(JSON_EXTRACT(j_array,last)),' ',
JSON_UNQUOTE(JSON_EXTRACT(j_array,first)));
END IF;
SET i = i + 1;
ELSE
LEAVE author_loop;
END IF;
END LOOP;
RETURN csv;
END//
DELIMITER ;
Listing 9-11Library Version 2 Database Create Script (library_v2.sql)
请注意名为get_author_names()
的新函数。该函数从与 ISBN 匹配的行中检索 JSON 文档,并创建一个逗号分隔的作者列表。这用于呈现作者数据,使用户更容易查看。
现在我们已经创建了数据库,让我们看看数据库类的代码。
数据库代码
处理数据库的代码放在名为library_v2.py
的文件中,该文件位于version2
文件夹下的database
文件夹中,如第 8 章“准备目录结构”一节中所述代码基于版本 1,转换为使用 X DevAPI,不再需要 authors 表的类。也就是说,代码实现了三个类:一个用于数据视图——publisher 和 book——另一个用于与服务器接口的类。这些等级分别被命名为Publisher
、Book
和Library
。
然而,因为代码是基于版本 1 的,所以我讨论的是变化,而不是另一个关于类和它们如何工作的冗长讨论。下面总结了这些变化。
ALL_BOOKS
查询要短得多,也更容易维护。- 添加了一个新的
GET_PUBLISHER_NAME
查询来填充图书列表。 INSERT_BOOK
查询需要为作者的 JSON 文档增加一列。- 对 authors 表的所有查询都被删除。
- 我们将
GET_AUTHOR_IDS
改为GET_AUTHOR_NAMES
,因为我们只处理 JSON 文档中的名字。 - 数据库名称从
library_v1
变为library_v2
。
要创建文件,只需将文件从version1/database/library_v1.py
复制到version2/database/library_v2.py
。
代码已删除
首先删除 author 表的Authors
类和 SQL 语句。他们将不被需要。
sql 字符串
因为这个版本也使用 SQL 语句,所以将这些语句作为字符串放在代码模块的序言中,以便稍后在代码中引用。清单 9-12 显示了library_v2.py
代码模块的序言,它取代了第一个版本中使用的代码。注意,它从导入 MySQL 连接器/Python X DevAPI 库开始。前面列出的更改(除了版本 1 到 2 的重命名)在清单中以粗体显示。
import mysqlx
ALL_BOOKS = """
SELECT DISTINCT book.ISBN, book.ISBN, Title, PublisherId, Year,
library_v2.get_author_names(book.ISBN) as Authors
FROM library_v2.books As book
ORDER BY book.ISBN DESC
"""
GET_PUBLISHER_NAME = """
SELECT Name
FROM library_v2.publishers
WHERE PublisherId = {0}
"""
GET_LASTID = "SELECT @@last_insert_id"
INSERT_PUBLISHER = """
INSERT INTO library_v2.publishers (Name, City, URL) VALUES ('{0}','{1}','{2}')
"""
GET_PUBLISHERS = "SELECT * FROM library_v2.publishers {0}"
UPDATE_PUBLISHER = "UPDATE library_v2.publishers SET Name = '{0}'"
DELETE_PUBLISHER = "DELETE FROM library_v2.publishers WHERE PublisherId = {0}"
INSERT_BOOK = """
INSERT INTO library_v2.books (ISBN, Title, Year, PublisherId, Edition,
Language, Authors) VALUES ('{0}','{1}','{2}','{3}',{4},'{5}','{6}')
"""
INSERT_NOTE = "INSERT INTO library_v2.notes (ISBN, Note) VALUES ('{0}','{1}')"
GET_BOOKS = "SELECT * FROM library_v2.books {0}"
GET_NOTES = "SELECT * FROM library_v2.notes WHERE ISBN = '{0}'"
GET_AUTHOR_NAMES = "SELECT library_v2.get_author_names('{0}')"
UPDATE_BOOK = "UPDATE library_v2.books SET ISBN = '{0}'"
DELETE_NOTES = "DELETE FROM library_v2.notes WHERE ISBN = '{0}'"
DELETE_BOOK = "DELETE FROM library_v2.books WHERE ISBN = '{0}'"
Listing 9-12Initialization and SQL Statements (library_v2.py)
如果您还记得版本 1 中相同代码的长度,请注意我们已经大大减少了字符串的数量。这主要是因为删除了 authors 表和多对多关系。所以,添加一个 JSON 字段产生了巨大的影响!
在我们讨论对Publisher
和Book
类的更改之前,让我们讨论一下对Library
类的更改。
库类
library 类基于版本 1,但是因为我们使用的是 X DevAPI,所以工作方式会有很大不同。特别是,我们将打开一个会话,在端口 33060(X 协议的默认端口)上连接到 MySQL 服务器,并且我们将使用一个 S QLStatement
对象来执行 SQL 语句。下面总结了对Library
类的更改。
下面列出了 Library 类的更改摘要。
- 我们使用会话对象而不是连接对象。
- 将
connect()
函数改为从mysql_x
库中检索一个会话。 sql()
函数被大大简化,只返回来自session.sql()
—SQLStatment
对象的结果。- 我们添加了一个
make_rows()
函数,将来自SQLStatement
对象的行结果转换成一个数组。 get_books()
函数调用链接SQLStatement execute()
函数的make_rows()
函数(作为参数传递)。
Note
对Library
类的更改旨在演示如何从旧协议迁移到使用 X DevAPI。正如您将看到的,通过使用相同的方法,但使用不同的数据库访问方法,可以最小化对现有数据库库和相关代码的更改。
清单 9-13 显示了修改后的库类。版本 1 中的变化以粗体显示。请注意,我们使用了与第一个版本相同的方法,但是我们没有使用连接对象,而是使用会话对象和重命名的函数来获取和检查会话。这些都是很好的函数,因为在开发更高级的特性时,您可能会需要它们。
class Library(object):
"""Library master class
Use this class to interface with the library database. It includes
utility functions for connections to the server and returning a
SQLStatement object.
"""
def __init__(self):
self.session = None
#
# Connect to a MySQL server at host, port
#
# Attempts to connect to the server as specified by the connection
# parameters.
#
def connect(self, username, passwd, host, port):
config = {
'user': username,
'password': passwd,
'host': host,
'port': port,
}
try:
self.session = mysqlx.get_session(**config)
except Exception as err:
print("CONNECTION ERROR:", err)
self.session = None
raise
#
# Return the session for use in other classes
#
def get_session(self):
return self.session
#
# Check to see if connected to the server
#
def is_connected(self):
return (self.session and (self.session.is_open()))
#
# Disconnect from the server
#
def disconnect(self):
try:
self.session.close()
except:
pass
#
# Get an SQLStatement object
#
def sql(self, query_str):
return self.session.sql(query_str)
#
# Build row array
#
# Here, we cheat a bit and give an option to substitute the publisher name
# for publisher Id column.
#
def make_rows(self, sql_res, get_publisher=False):
cols = []
for col in sql_res.columns:
cols.append(col.get_column_name())
rows = []
for row in sql_res.fetch_all():
row_item = []
for col in cols:
if get_publisher and (col == 'PublisherId'):
query_str = GET_PUBLISHER_NAME.format(row.get_string(col))
name = self.session.sql(query_str).execute().fetch_one()[0]
row_item.append("{0}".format(name))
else:
row_item.append("{0}".format(row.get_string(col)))
rows.append(row_item)
return rows
#
# Get list of books
#
def get_books(self):
try:
sql_stmt = self.sql(ALL_BOOKS)
results = self.make_rows(sql_stmt.execute(), True)
except Exception as err:
print("ERROR: {0}".format(err))
raise
return results
Listing 9-13Library Class (library_v2.py)
请注意sql()
函数与版本 1 相比缩短了多少。回想一下,版本 1 中的sql()
函数有 30 行长。使用SQLStatement
对象实例为我们节省了大量编码!我们将看到这个主题在版本 3 中继续。其实我们看到的get_books()
功能也短了一点。不错。
如前所述,有一个新功能。函数make_rows()
获取结果对象,获取所有的行,并将其转换为一个列表。可能有更有效的方法来做到这一点,但是这演示了您可能需要做的一些事情来转换您的现有代码以使用 X DevAPI。
接下来,我们来看看Publisher
类。
发布者类别
Publisher
类与版本 1 的代码几乎相同,除了我们将它修改为与 X DevAPI 一起使用。特别是,因为我们正在获取从Library
类中的sql()
函数返回的SQLStatement
对象,所以我们可以将它与SQLStatement
实例的execute()
函数链接起来并获得结果。我们还利用Library
类的make_rows()
函数为结果中的行创建一个数组。清单 9-14 显示了Publisher
类的完整代码,为清晰起见,更改以粗体显示。
class Publisher(object):
"""Publisher class
This class encapsulates the publishers table permitting CRUD operations
on the data.
"""
def __init__(self, library):
self.library = library
def create(self, Name, City=None, URL=None):
assert Name, "You must supply a Name for a new publisher."
query_str = INSERT_PUBLISHER
last_id = None
try:
self.library.sql(query_str.format(Name, City, URL)).execute()
last_id = self.library.make_rows(
self.library.sql(GET_LASTID).execute())[0][0]
self.library.sql("COMMIT").execute()
except Exception as err:
print("ERROR: Cannot add publisher: {0}".format(err))
return last_id
def read(self, PublisherId=None):
query_str = GET_PUBLISHERS
if not PublisherId:
# return all authors
query_str = query_str.format("")
else:
# return specific author
query_str = query_str.format(
"WHERE PublisherId = '{0}'".format(PublisherId))
sql_stmt = self.library.sql(query_str)
return self.library.make_rows(sql_stmt.execute())
def update(self, PublisherId, Name, City=None, URL=None):
assert PublisherId, "You must supply a publisher to update the author."
query_str = UPDATE_PUBLISHER.format(Name)
if City:
query_str = query_str + ", City = '{0}'".format(City)
if URL:
query_str = query_str + ", URL = '{0}'".format(URL)
query_str = query_str + " WHERE PublisherId = {0}".format(PublisherId)
try:
self.library.sql(query_str).execute()
self.library.sql("COMMIT").execute()
except Exception as err:
print("ERROR: Cannot update publisher: {0}".format(err))
def delete(self, PublisherId):
assert PublisherId, "You must supply a publisher to delete the publisher."
query_str = DELETE_PUBLISHER.format(PublisherId)
try:
self.library.sql(query_str).execute()
self.library.sql("COMMIT").execute()
except Exception as err:
print("ERROR: Cannot delete publisher: {0}".format(err))
Listing 9-14Publisher Class (library_v2.py)
正如您所看到的,变化很小,再次证明了将代码移植到新的 X DevAPI 是多么容易。
现在让我们看一下Book
类,它有一个类似的简短变化列表。
图书类
Book 类和Publisher
类一样,与版本 1 的代码相比几乎没有什么变化。我们对使用 X DevAPI 进行了同样的修改,但是我们还需要处理将逗号分隔的作者列表转换成 JSON 文档。为此,我们将使用一个助手函数。我们还通过删除连接表来降低代码的复杂性。下面总结了这个版本的Book
类的变化。
- 我们使用库类的
sql()
函数链接execute()
函数来执行 SQL 语句。 - 在调用库类的
make_rows()
函数之前,我们准备好了SQLStatement
对象实例。 - 我们添加了一个函数
make_authors_json()
来将逗号分隔的作者姓名列表转换成 JSON 文档。 - 我们删除了使用
books_authors
表的代码。
清单 9-15 显示了Book
类的完整代码,为了清晰起见,更改以粗体显示。正如您将看到的,尽管添加了更多用于处理 JSON 文档的代码行,但代码还是比前一版本短了一些。
class Book(object):
"""Book class
This class encapsulates the books table permitting CRUD operations
on the data.
"""
def __init__(self, library):
self.library = library
def make_authors_json(self, author_list=None):
from json import JSONEncoder
if not author_list:
return None
author_dict = {"authors":[]}
authors = author_list.split(",")
for author in authors:
try:
last, first = author.strip(' ').split(' ')
except Exception as err:
last = author.strip(' ')
first = ''
author_dict["authors"].append({"LastName":last,"FirstName":first})
author_json = JSONEncoder().encode(author_dict)
return author_json
def create(self, ISBN, Title, Year, PublisherId, Authors=[], Edition=1,
Language='English'):
assert ISBN, "You must supply an ISBN for a new book."
assert Title, "You must supply Title for a new book."
assert Year, "You must supply a Year for a new book."
assert PublisherId, "You must supply a publisher for a new book."
assert Authors, "You must supply at least one Author for a new book."
query_str = INSERT_BOOK
last_id = ISBN
try:
author_json = self.make_authors_json(Authors)
self.library.sql(query_str.format(ISBN, Title, Year, PublisherId,
Edition, Language,
author_json)).execute()
self.library.sql("COMMIT").execute()
except Exception as err:
print("ERROR: Cannot add book: {0}".format(err))
self.library.sql("ROLLBACK").execute()
return last_id
def read(self, ISBN=None):
query_str = GET_BOOKS
if not ISBN:
# return all authors
query_str = query_str.format("")
else:
# return specific author
query_str = query_str.format("WHERE ISBN = '{0}'".format(ISBN))
sql_stmt = self.library.sql(query_str)
return self.library.make_rows(sql_stmt.execute())
#
# Get the notes for this book
#
def read_notes(self, ISBN):
assert ISBN, "You must supply an ISBN to get the notes."
query_str = GET_NOTES.format(ISBN)
sql_stmt = self.library.sql(query_str)
return self.library.make_rows(sql_stmt.execute())
#
# Get the authors for this book
#
def read_authors(self, ISBN):
assert ISBN, "You must supply an ISBN to get the list of author ids."
query_str = GET_AUTHOR_NAMES.format(ISBN)
sql_stmt = self.library.sql(query_str)
return self.library.make_rows(sql_stmt.execute())
def update(self, old_isbn, ISBN, Title=None, Year=None, PublisherId=None,
Authors=None, Edition=None, Language=None, Note=None):
assert ISBN, "You must supply an ISBN to update the book."
last_id = None
#
# Build the book update query
#
book_query_str = UPDATE_BOOK.format(ISBN)
if Title:
book_query_str += ", Title = '{0}'".format(Title)
if Year:
book_query_str += ", Year = {0}".format(Year)
if PublisherId:
book_query_str += ", PublisherId = {0}".format(PublisherId)
if Edition:
book_query_str += ", Edition = {0}".format(Edition)
if Authors:
author_json = self.make_authors_json(Authors)
book_query_str += ", Authors = '{0}'".format(author_json)
book_query_str += " WHERE ISBN = '{0}'".format(old_isbn)
#
# We must do this as a transaction to ensure all tables are updated.
#
try:
self.library.sql("START TRANSACTION").execute()
self.library.sql(book_query_str).execute()
if Note:
self.add_note(ISBN, Note)
self.library.sql("COMMIT").execute()
except Exception as err:
print("ERROR: Cannot update book: {0}".format(err))
self.library.sql("ROLLBACK").execute()
return last_id
def delete(self, ISBN):
assert ISBN, "You must supply a ISBN to delete the book."
#
# Here, we must cascade delete the notes when we delete a book.
# We must do this as a transaction to ensure all tables are updated.
#
try:
self.library.sql("START TRANSACTION").execute()
query_str = DELETE_NOTES.format(ISBN)
self.library.sql(query_str).execute()
query_str = DELETE_BOOK.format(ISBN)
self.library.sql(query_str).execute()
self.library.sql("COMMIT").execute()
except Exception as err:
print("ERROR: Cannot delete book: {0}".format(err))
self.library.sql("ROLLBACK").execute()
#
# Add a note for this book
#
def add_note(self, ISBN, Note):
assert ISBN, "You must supply a ISBN to add a note for the book."
assert Note, "You must supply text (Note) to add a note for the book."
query_str = INSERT_NOTE.format(ISBN, Note)
try:
self.library.sql(query_str).execute()
self.library.sql("COMMIT").execute()
except Exception as err:
print("ERROR: Cannot add note: {0}".format(err))
Listing 9-15Book Class (library_v2.py)
请注意新的函数 make_author_json(),它演示了如何构建 json 文档。在本例中,这是一个使用 Python JSON 编码器构建的简单 JSON 数组。我们还可以在 update()函数中看到如何将 JSON 文档合并到我们的 UPDATE SQL 语句中。太好了。
那还不算太糟,是吗?现在,让我们看看应用代码的变化。
应用代码
与我们之前看到的版本 1 代码相比,应用代码有一些小的变化。这包括修改用户界面以删除 authors 视图并将 authors 列表添加到 book 视图表单中。幸运的是,版本 1 中的大部分代码无需修改就可以使用。
要构建这个版本的应用,您应该将version1/mylibrary_v1.py
文件复制到version2/mylibrary_v2.py
中,然后输入下面的代码或者从 Apress book 网站上检索。下面列出了应用代码的更改。虽然这看起来像一个很长的列表,但大多数都是微不足道的变化。以下部分将更详细地描述这些变化。
- 从 import 语句中删除
Author
类。 - 将端口从 3306 更改为 33060。
- 移除
NewSelectMultipleField
类,因为不再需要它了(它在图书视图表单中用于显示可供选择的作者列表)。 - 删除作者视图功能和模板。
- 将工作簿详细信息页面上的多选字段替换为文本字段。
- 从列表视图功能中删除作者列表。
- 更改代码以读取作者姓名列表,而不是 id。
- 添加作者列表来创建和更新对
Book
类的调用。 - 将新文本字段中的作者姓名列表传递给图书视图函数中的
render_template()
函数。 - 发布者视图功能、表单类或模板不需要任何更改。
- 列表表单类或模板不需要任何更改。
- 基本模板已更改,以指示应用的版本 2。
我们从设置和初始化的变化开始看变化。
设置和初始化
对设置和初始化的改变是微不足道的。我们只需要从导入中删除 Author 类,将library_v1
改为library_v2
,并在connect()
函数中更改默认端口,如下所示。
from database.library_v2 import Library, Publisher, Book
...
library.connect(<user>, <password>, 'localhost', 33060)
表单类
首先,我们可以删除AuthorForm
和NewSelectMultipleField
类,因为我们不需要它们。幸运的是,PublisherForm
类不需要任何改变。甚至BookForm
类也只有一个微小的变化,将多选字段切换为文本字段。清单 9-16 用粗体显示了修改后的BookForm
类代码。正如您将看到的,只需更改一行代码。
class BookForm(FlaskForm):
isbn = TextField('ISBN ', validators=[
Required(message=REQUIRED.format("ISBN")),
Length(min=1, max=32, message=RANGE.format("ISBN", 1, 32))
])
title = TextField('Title ',
validators=[Required(message=REQUIRED.format("Title"))])
year = IntegerField('Year ',
validators=[Required(message=REQUIRED.format("Year"))])
edition = IntegerField('Edition ')
language = TextField('Language ', validators=[
Required(message=REQUIRED.format("Language")),
Length(min=1, max=24, message=RANGE.format("Language", 1, 24))
])
publisher = NewSelectField('Publisher ',
validators=[Required(message=REQUIRED.format("Publisher"))])
authors = TextField('Authors (comma separated by LastName FirstName)',
validators=[Required(message=REQUIRED.format("Author"))])
create_button = SubmitField('Add')
del_button = SubmitField('Delete')
new_note = TextAreaField('Add Note')
Listing 9-16Book Form Class (Version 2)
查看功能
首先,我们可以删除author()
视图功能,因为不再需要它了。幸运的是,发布者视图功能不需要任何更改。
但是,我们需要修改simple_list()
视图函数来删除作者列表选项。清单 9-17 显示了修改后的模板,其中代码被删除的区域以粗体显示。
def simple_list(kind=None):
rows = []
columns = []
form = ListForm()
if kind == 'book' or not kind:
if request.method == 'POST':
return redirect('book')
columns = (
'<td style="width:200px">ISBN</td>',
'<td style="width:400px">Title</td>',
'<td style="width:200px">Publisher</td>',
'<td style="width:80px">Year</td>',
'<td style="width:300px">Authors</td>',
)
kind = 'book'
# Here, we get all books in the database
rows = library.get_books()
return render_template("list.html", form=form, rows=rows,
columns=columns, kind=kind)
elif kind == 'publisher':
if request.method == 'POST':
return redirect('publisher')
columns = (
'<td style="width:300px">Name</td>',
'<td style="width:100px">City</td>',
'<td style="width:300px">URL/Website</td>',
)
kind = 'publisher'
# Here, we get all publishers in the database
publisher = Publisher(library)
rows = publisher.read()
return render_template("list.html", form=form, rows=rows,
columns=columns, kind=kind)
else:
flash("Something is wrong!")
return
Listing 9-17List View Function (Version 2)
我们还需要修改图书视图功能。这一部分还需要更多的修改,因为一本书的作者现在是一个 JSON 文档,我们使用逗号分隔的列表在 book detail 表单中指定他们。下面列出了这段代码所需的更改。
- 我们将 id 的列表
authorids
改为author_list
以包含逗号分隔的列表。 - 我们删除了
Author()
类代码。 - 我们将获取作者 id 列表改为获取逗号分隔列表。
- 我们不需要模板文件的作者列表。
清单 9-18 显示了对图书视图功能的更改,更改以粗体显示。
def book(isbn_selected=None):
notes = None
book = Book(library)
form = BookForm()
# Get data from the form if present
isbn = form.isbn.data
title = form.title.data
year = form.year.data
author_list = form.authors.data
publisherid = form.publisher.data
edition = form.edition.data
language = form.language.data
#
# Here, we get the choices for the select lists
#
publisher = Publisher(library)
publishers = publisher.read()
publisher_list = []
for pub in publishers:
publisher_list.append((pub[0], '{0}'.format(pub[1])))
form.publisher.choices = publisher_list
new_note = form.new_note.data
# If the route with the variable is called, change the create button to update
# then populate the form with the data from the row in the table. Otherwise,
# remove the delete button because this will be a new data item.
if isbn_selected:
# Here, we get the data and populate the form
data = book.read(isbn_selected)
if data == []:
flash("Book not found!")
#
# Here, we populate the data
#
form.isbn.data = data[0][0]
form.title.data = data[0][1]
form.year.data = data[0][2]
form.edition.data = data[0][3]
form.publisher.process_data(data[0][4])
form.language.data = data[0][5]
form.authors.data = book.read_authors(isbn_selected)[0][0]
# We also must retrieve the notes for the book.
all_notes = book.read_notes(isbn_selected)
notes = []
for note in all_notes:
notes.append(note[2])
form.create_button.label.text = "Update"
else:
del form.del_button
if request.method == 'POST':
# First, determine if we must create, update, or delete when form posts.
operation = "Create"
if form.create_button.data:
if form.create_button.label.text == "Update":
operation = "Update"
if form.del_button and form.del_button.data:
operation = "Delete"
if form.validate_on_submit():
# Get the data from the form here
if operation == "Create":
try:
book.create(ISBN=isbn, Title=title, Year=year,
PublisherId=publisherid, Authors=author_list,
Edition=edition, Language=language)
flash("Added.")
return redirect('/list/book')
except Exception as err:
flash(err)
elif operation == "Update":
try:
book.update(isbn_selected, isbn, Title=title, Year=year,
PublisherId=publisherid, Authors=author_list,
Edition=edition, Language=language,
Note=new_note)
flash("Updated.")
return redirect('/list/book')
except Exception as err:
flash(err)
else:
try:
book.delete(isbn)
flash("Deleted.")
return redirect('/list/book')
except Exception as err:
flash(err)
else:
flash_errors(form)
return render_template("book.html", form=form, notes=notes,
authors=author_list)
Listing 9-18Book View Function (Version 2)
应用代码不需要额外的更改。再说一次,这还不算太糟。我们还没完呢。模板需要做一些小的改动。
模板
对模板文件的更改很小。如果您还没有这样做,请将模板从版本 1 复制到版本 2。例如,将所有文件从version1/templates/*
复制到version2/templates
。一旦复制,你可以删除author.html
模板,因为我们不再需要它。
我们还需要对 base.html 文件做两个小的修改,以更改版本号并从导航栏中删除作者列表。清单 9-19 显示了 base.html 文件的摘录,更改以粗体显示。
<div class="navbar navbar-inverse" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">MyLibrary v2</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a href="/list/book">Books</a></li>
</ul>
<ul class="nav navbar-nav">
<li><a href="/list/publisher">Publishers</a></li>
</ul>
</div>
</div>
</div>
Listing 9-19Base Template (Version 2)
我们还必须对book.html
模板做两个小的修改,以显示一个以逗号分隔的作者列表的文本字段。清单 9-20 用粗体显示了修改后的模板的摘录。
{% extends "base.html" %}
{% block title %}MyLibrary Search{% endblock %}
{% block page_content %}
<form method=post> {{ form.csrf_token }}
<fieldset>
<legend>Book - Detail</legend>
{{ form.hidden_tag() }}
<div style=font-size:20pz; font-weight:bold; margin-left:150px;>
{{ form.isbn.label }} <br>
{{ form.isbn(size=32) }} <br>
{{ form.title.label }} <br>
{{ form.title(size=100) }} <br>
{{ form.year.label }} <br>
{{ form.year(size=10) }} <br>
{{ form.edition.label }} <br>
{{ form.edition(size=10) }} <br>
{{ form.language.label }} <br>
{{ form.language(size=34) }} <br>
{{ form.publisher.label }} <br>
{{ form.publisher(style="width: 300px;") }} <br><br>
{{ form.authors.label }} <br>
{{ form.authors(size=100) }} <br>
{# Show the new note text field if this is an update. #}
{% if form.create_button.label.text == "Update" %}
<br>{{ form.new_note.label }} <br>
{{ form.new_note(rows='2',cols='100') }}
{% endif %}
...
Listing 9-20Book Template (Version 2)
好了,这就是我们所做的改动,现在让我们来看看代码的运行情况。
执行代码
现在我们已经写好了代码,让我们试一试。要执行应用,可以用 Python 解释器指定runserver
命令来启动它。下面显示了一个执行应用的示例。注意,我们使用了 port 选项来指定端口。您应该从version2
文件夹中输入这个命令。
$ cd version2
$ python ./mylibrary_v2.py runserver -p 5002
* Running on http://127.0.0.1:5002/ (Press CTRL+C to quit)
应用将启动并运行,但是数据库中还没有任何数据。你应该从拿几本你喜欢的书开始,首先输入作者和出版商,然后输入图书数据。现在还不要担心音符。一旦你添加了几本书,你应该在默认视图中看到它们(通过点击导航栏中的MyLibrary v2
或Books
)。图 9-5 显示了一个你应该看到的例子。其他视图是相似的,留给读者去探索。
图 9-5
Library application book list (version 2)
请注意,我们删除了导航栏中的 author 条目,因为我们不再拥有详细的视图。相反,作者列表存储在书籍的一个 JSON 文档中。图 9-6 显示了新的表格。
图 9-6
Book detailed view (version 2)
请注意,作者条目现在是一个文本字段,而不是一个多选列表。有些人可能认为这更直观,而其他人可能觉得多选列表更好。选择逗号分隔的列表是出于演示的目的,但是您可以随意尝试自己的想法,了解如何为一本书收集和显示关于作者的信息。
publisher 视图与版本 1 相比没有变化。
在我们继续讨论版本 3 之前,让我们花点时间来讨论一下关于这个版本的应用的一些观察结果。
观察
以下是对该应用版本的一些观察。有些是数据库设计的结果,有些是代码的结果,还有一些是我们为了让应用变得更好而想要改变的。观察结果以无序列表的形式呈现。如果您想试验这个版本的应用,您可以考虑其中的一些挑战来改进应用。否则,可以考虑在下一个版本中使用这个列表。
- 使用 JSON 进一步简化数据库:notes 表也可以转换成 books 表中的 JSON 字段,因为不需要在没有查看 book 的上下文的情况下查询 notes 表,notes 表中的一行与 books 表中的一行匹配。
- 数据库代码更短:我们在数据库代码模块中需要更少的代码来实现应用。
- 应用代码更短:我们在应用中需要更少的代码。
- JSON 需要一些转换代码:尽管 Python 提供了一个使用 JSON 的库,并且可以在 Python 中直接使用 JSON 文档作为数据结构,但是我们需要添加代码来将 JSON 转换成更易于阅读的形式。在这种情况下,它使用的是作者姓名列表。
- 作者列表可能需要改进:虽然是为演示目的而设计的,但逗号分隔的列表可能不是新手用户的最佳选择。
现在,让我们看看应用的最新版本。
版本 3:文档存储
这个版本实现了数据的纯文档存储版本。出于演示的目的,我们将在一个代码模块中实现数据库代码,我们可以将该代码模块导入到应用代码中。我们将使用 X DevAPI 管理集合来存储和检索数据。目标是演示如何迁移到使用 JSON 文档而不是 SQL 接口。
为此,我们将把数据库从多个表简化为一个文档集合,更具体地说,是一个图书集合。让我们从数据库设计的简要概述开始。
数据库设计
称之为数据库设计有点过时,因为我们不是在逻辑上使用数据库,而是使用 X DevAPI 术语中的模式。从实现的角度来看,它仍然是 MySQL 中的一个数据库,并且将在 SHOW DATABASES 命令中显示,如下面的 MySQL Shell 输出中的(library_v3
)所示。
$ mysqlsh root@localhost:33060 -mx --sql
Creating an X protocol session to 'root@localhost:33060'
...
MySQL localhost:33060+ ssl SQL > SHOW DATABASES;
+--------------------+
| Database |
+--------------------+
| animals |
| information_schema |
| library_v1 |
| library_v2 |
| library_v3 |
| mysql |
| performance_schema |
| sys |
+--------------------+
8 rows in set (0.00 sec)
数据库(模式)只包含一个表,该表是作为集合创建的。您可以使用 MySQL Shell 中显示的命令来做到这一点。
$ mysqlsh root@localhost:33060 -mx --py
Creating an X protocol session to 'root@localhost:33060'
...
MySQL localhost:33060+ ssl Py > import mysqlx
MySQL localhost:33060+ ssl Py > session = mysqlx.get_session('root:password@localhost:33060')
MySQL localhost:33060+ ssl Py > schema = session.create_schema('library_v3')
MySQL localhost:33060+ ssl Py > collection = schema.create_collection('books')
注意,我们得到一个会话,然后创建模式,最后创建集合。这个新集合将作为名为books
的表出现在library_v3
数据库中,但是它的CREATE
语句看起来非常不同。下面显示了该表的CREATE
语句。您应该永远不需要使用这个语句,而应该总是使用 MySQL Shell 和 X DevAPI 来创建模式、集合或 X DevAPI 万神殿中的任何对象。
MySQL localhost:33060+ ssl SQL > SHOW CREATE TABLE library_v3.books \G
*************************** 1\. row ***************************
Table: books
Create Table: CREATE TABLE `books` (
`doc` json DEFAULT NULL,
`_id` varchar(32) GENERATED ALWAYS AS (json_unquote(json_extract(`doc`,_utf8mb4'$._id'))) STORED NOT NULL,
PRIMARY KEY (`_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
1 row in set (0.00 sec)
不要担心CREATE
语句看起来很奇怪。它应该是这样的。毕竟,它是作为包含文档 id 和 JSON 字段的行表实现的集合。请注意,这里定义了一个主键,这样可以通过 id 快速查找。酷毙了。
现在我们已经创建了模式(数据库)和集合(表),让我们看看数据库类的代码。
数据库代码
这个版本的应用对数据库代码的更改比上一个版本稍长。尽管我们在版本 2 中切换到使用 X DevAPI,但我们仍然使用 SQL 接口。这个版本使用纯 X DevAPI 接口。
处理数据库的代码放在名为library_v3.py
的文件中,该文件位于version3
文件夹下的database
文件夹中,如第 8 章“准备目录结构”一节中所述代码基于版本 2,转换为使用 X DevAPI,我们不再需要 publishers 表的类。然而,因为代码是基于版本 2 的,我们将讨论这些变化,而不是另一个关于类和它们如何工作的冗长讨论。下面总结了这些变化。
- 添加
JSONEncoder
导入语句。 - 删除所有 SQL 语句(是!).
- 移除
Publisher
类。 - 将
Book
类重命名为Books
。 - 将
Books
类更改为使用会话、模式和集合对象实例。 - 移除
make_authors_json()
功能。 - 移除
Library
类,将实用函数移动到Books
类。 - 数据库名称从
library_v2
变为library_v3
。
要创建文件,只需将文件从version2/database/library_v2.py
复制到version3/database/library_v3.py
。
代码已删除
首先删除Publishers
类和所有 SQL 语句。我们不需要这些。您还需要删除Library
类,但我们仍将使用该类中的一些方法。有关更多详细信息,请参见 Books 类一节。
设置和初始化
删除 SQL 语句后,我们需要添加一个导入语句来导入 Python JSON 编码器类。下面的代码应该放在 imports 部分的末尾。
from json import JSONEncoder as encoder
书籍类
这个类是其余代码更改出现的地方。下面几节简要描述了每一项更改,并给出了修改后的类的完整列表。请注意,我们做的第一件事是将类名从 Book 更改为 Books,因为我们建模的是书籍的集合,而不是单本书。
Tip
这是使用集合时必须做出的基本“思考”改变之一。虽然您可以对单个文档进行建模,但是大多数人会自然地倾向于对一组文档(事物)进行建模。
类别声明
下面显示了修改后的类声明。请注意,我们重命名了类和注释,以反映对类模型的更改。注意,我们还在构造函数中声明了另外两个类变量;book_schema
和book_col
。类变量book_schema
用于存储模式对象的实例,变量book_col
用于存储集合对象的实例。这些将在connect()
函数中初始化。
#
# Books collection simple abstraction (document store)
#
class Books(object):
"""Books class
This class encapsulates the books collection permitting CRUD operations
on the data.
"""
def __init__(self):
self.session = None
self.book_schema = None
self.book_col = None
创建功能
接下来,我们更改create()
函数,以便更好地处理集合。在这种情况下,我们更改参数列表以包含发布者的三段数据;姓名、城市和网址。我们还将使用一个函数来创建 JSON 复杂文档。新功能被命名为make_book_json()
,将在后面描述。您也可以删除make_authors_json()
功能。最后,我们修改代码,使用book_col
类变量将书添加到集合中,然后查找新书的文档 id。回想一下,文档 id 是由服务器分配的,因此以这种方式检索它允许我们使用它来快速定位文档。清单 9-21 用粗体显示了修改后的 create()语句。
Note
库类中函数的缩进应该缩进 4 个空格。为了可读性,清单中省略了空格。
def create(self, ISBN, Title, Pub_Year, Pub_Name, Pub_City, Pub_URL,
Authors=[], Notes=[], Edition=1, Language="English"):
assert ISBN, "You must supply an ISBN for a new book."
assert Title, "You must supply Title for a new book."
assert Pub_Year, "You must supply a Year for a new book."
assert Pub_Name, "You must supply a Publisher Name for a new book."
assert Authors, "You must supply at least one Author Name for a new book."
last_id = None
try:
book_json = self.make_book_json(ISBN, Title, Pub_Year, Pub_Name,
Pub_City, Pub_URL, Authors, Notes,
Edition, Language)
self.book_col.add(book_json).execute()
last_id = self.book_col.find(
"ISBN = '{0}'".format(ISBN)).execute().fetch_all()[0]["_id"]
except Exception as err:
print("ERROR: Cannot add book: {0}".format(err))
return last_id
Listing 9-21Create Function (Version 3)
读取功能
既然我们使用了集合对象,read()函数就大大简化了。事实上,我们需要做的就是调用 find()函数,传入文档 id,然后获取文档——所有这些都在一行代码中完成!第 2 版中的辅助读取功能被删除,详情请参见后面的“实用功能”部分。
def read(self, bookid=None):
return self.book_col.find("_id = '{0}'".format(bookid)).execute().fetch_one()
更新功能
更新功能是大量代码被更改的地方。这是由于我们如何形成用于更新集合的modify()
子句链。更具体地说,代码已经更改,可以检测数据元素何时更改,如果更改了,就调用modify().set().execute()
链来修改数据。因为我们可能要做不止一组这样的更改,所以我们使用 session 类变量来启动一个事务,然后如果所有语句都成功,就将它提交给集合。如果没有,我们回滚更改。
其他变化与我们如何处理 notes 和 author 数组有关。发布者数据很简单,因为我们将在网页上放置文本框来保存数据。清单 9-22 显示了修改后的create()
功能。这段代码的大部分与版本 2 不同。
def update(self, book_id, book_data, ISBN, Title, Pub_Year, Pub_Name, Pub_City,
Pub_URL, Authors=[], New_Note=None, Edition=1, Language="English"):
assert book_id, "You must supply an book id to update the book."
try:
bkid = "_id = '{0}'".format(book_id)
self.session.start_transaction()
if ISBN != book_data["ISBN"]:
self.book_col.modify(bkid).set("ISBN", ISBN).execute()
if Title != book_data["Title"]:
self.book_col.modify(bkid).set("Title", Title).execute()
if Pub_Year != book_data["Pub_Year"]:
self.book_col.modify(bkid).set("Pub_Year", Pub_Year).execute()
if Pub_Name != book_data["Publisher"]["Name"]:
self.book_col.modify(bkid).set("$.Publisher.Name", Pub_Name).execute()
if Pub_City != book_data["Publisher"]["City"]:
self.book_col.modify(bkid).set("$.Publisher.City", Pub_City).execute()
if Pub_URL != book_data["Publisher"]["URL"]:
self.book_col.modify(bkid).set("$.Publisher.URL", Pub_URL).execute()
if Edition != book_data["Edition"]:
self.book_col.modify(bkid).set("Edition", Edition).execute()
if Language != book_data["Language"]:
self.book_col.modify(bkid).set("Language", Language).execute()
if New_Note:
#
# If this is the first note, we create the array otherwise,
# we append to it.
#
if not "Notes" in book_data.keys():
mod_book = self.book_col.modify(bkid)
mod_book.set("Notes", [{"Text":New_Note}]).execute()
else:
mod_book = self.book_col.modify(bkid)
mod_book.array_append("Notes", {"Text":New_Note}).execute()
if Authors and (Authors != self.make_authors_str(book_data['Authors'])):
authors_json = self.make_authors_dict_list(Authors)
self.book_col.modify(bkid).set("Authors", authors_json).execute()
self.session.commit()
except Exception as err:
print("ERROR: Cannot update book: {0}".format(err))
self.session.rollback()
Listing 9-22Update Function (Version 3)
花些时间通读这段代码,以确保您看到我们是如何从简单地更新整个数据行到检查哪些项目需要更改并设置它们的。X DevAPI 的本质是,它支持(并鼓励)这样的行为,因为我们只想改变已经改变的内容,而不是为了节省处理时间。
删除功能
delete()函数也经历了一些变化,但是比 create()函数少得多。与 read()函数一样,X DevAPI 使我们执行删除操作变得更加容易。我们不需要像在版本 1 和版本 2 中那样执行一系列删除操作,我们只需要使用集合的 remove_one()便利函数,通过文档 id 找到图书并删除它。很好!下面显示了修改后的 delete()函数。
def delete(self, book_id):
assert book_id, "You must supply a book id to delete the book."
try:
self.book_col.remove_one(book_id).execute()
except Exception as err:
print("ERROR: Cannot delete book: {0}".format(err))
self.session.rollback()
效用函数
效用函数也有一些变化。下面总结了所需的更改,后面的段落提供了有关更改的更多详细信息。
- 删除不再需要的功能。
- 将
connect()
和get_books()
功能从旧的Library
类移到Books
类。 - 添加处理 JSON 文档的新函数。
对于一个纯文档存储代码模块来说,有许多额外的函数是不需要的。我们从库类中删除了make_authors_json()
、read_notes()
、add_note()
、read_authors()
、get_session()
、is_connected()
、disconnect()
、make_rows()和sql()
函数,因为我们不再需要它们了。因为作者和出版商数据是文档的一部分,所以我们将集合视为对象,而不是数据库服务器的网关。
connect()
函数需要一些小的改动,以便我们能够使用会话对象。清单 9-23 用粗体显示了修改后的connect()
功能。这个函数被移到了Books
类中。这里,我们尝试使用传递的连接参数获得一个会话(注意没有数据库参数),然后获得library_v3
模式的模式对象,最后获得books
集合的集合对象。
def connect(self, username, passwd, host, port):
config = {
'user': username,
'password': passwd,
'host': host,
'port': port,
}
try:
self.session = mysqlx.get_session(**config)
if self.session.is_open():
self.book_schema = self.session.get_schema("library_v3")
self.book_col = self.book_schema.get_collection("books")
except Exception as err:
print("CONNECTION ERROR:", err)
self.session = None
raise
Listing 9-23Connect( ) Function (Version 3)
get_books()函数在版本 2 中得到了简化,因为我们不用发出 SQL 语句来读取一本书,而是使用集合对象来查找所有的书。我们还使用一个旧函数的重写来返回一个可以在 Python 中使用的文档数组。这个新函数被命名为make_row_array()
,将在接下来的章节中解释。
def get_books(self):
rows = []
try:
book_docs = self.book_col.find().sort("ISBN").execute().fetch_all();
rows = self.make_row_array(book_docs)
except Exception as err:
print("ERROR: {0}".format(err))
raise
return rows
最后,我们需要添加一些新的函数来使 JSON 文档的使用变得更加容易。下面列出并总结了这些新功能。我们把这些代码留到下一节,在那里我们列出了Books
类的完整代码。正如您将看到的,这些函数的编码方式并不令人惊讶。
make_authors_str(<array>)
:给定一个作者数组,返回一个逗号分隔的名和姓的列表。make_authors_dict_list(<string>)
:给定一个逗号分隔的作者姓名列表,返回一个包含作者名和姓的字典列表(数组)。make_book_json(<params>)
:给定 web 页面上字段数据的参数列表,返回一个填充了数据的 JSON 文档。make_row_array(<array or JSON documents>)
:给定一个 JSON 文档数组,返回一个包含 JSON 文档元素子集的字典数组。注意,这是用来显示集合中的图书列表的。
完整代码
因为对这个版本的数据库代码,特别是对Books
类有很多必要的修改,所以完整地查看完整的代码是一个好主意。清单 9-24 显示了Books
类的完整代码。您可以研究这段代码,看看上面的许多更改是如何实现的。
class Books(object):
"""Books class
This class encapsulates the books collection permitting CRUD operations
on the data.
"""
def __init__(self):
self.session = None
self.book_schema = None
self.book_col = None
def create(self, ISBN, Title, Pub_Year, Pub_Name, Pub_City, Pub_URL,
Authors=[], Notes=[], Edition=1, Language="English"):
assert ISBN, "You must supply an ISBN for a new book."
assert Title, "You must supply Title for a new book."
assert Pub_Year, "You must supply a Year for a new book."
assert Pub_Name, "You must supply a Publisher Name for a new book."
assert Authors, "You must supply at least one Author Name for a new book."
last_id = None
try:
book_json = self.make_book_json(ISBN, Title, Pub_Year, Pub_Name,
Pub_City, Pub_URL, Authors, Notes,
Edition, Language)
self.book_col.add(book_json).execute()
last_id = self.book_col.find(
"ISBN = '{0}'".format(ISBN)).execute().fetch_all()[0]["_id"]
except Exception as err:
print("ERROR: Cannot add book: {0}".format(err))
return last_id
def read(self, bookid=None):
return self.book_col.find("_id = '{0}'".format(bookid)).execute().fetch_one()
def update(self, book_id, book_data, ISBN, Title, Pub_Year, Pub_Name, Pub_City,
Pub_URL, Authors=[], New_Note=None, Edition=1, Language="English"):
assert book_id, "You must supply an book id to update the book."
try:
bkid = "_id = '{0}'".format(book_id)
self.session.start_transaction()
if ISBN != book_data["ISBN"]:
self.book_col.modify(bkid).set("ISBN", ISBN).execute()
if Title != book_data["Title"]:
self.book_col.modify(bkid).set("Title", Title).execute()
if Pub_Year != book_data["Pub_Year"]:
self.book_col.modify(bkid).set("Pub_Year", Pub_Year).execute()
if Pub_Name != book_data["Publisher"]["Name"]:
self.book_col.modify(bkid).set("$.Publisher.Name", Pub_Name).execute()
if Pub_City != book_data["Publisher"]["City"]:
self.book_col.modify(bkid).set("$.Publisher.City", Pub_City).execute()
if Pub_URL != book_data["Publisher"]["URL"]:
self.book_col.modify(bkid).set("$.Publisher.URL", Pub_URL).execute()
if Edition != book_data["Edition"]:
self.book_col.modify(bkid).set("Edition", Edition).execute()
if Language != book_data["Language"]:
self.book_col.modify(bkid).set("Language", Language).execute()
if New_Note:
#
# If this is the first note, we create the array otherwise,
# we append to it.
#
if not "Notes" in book_data.keys():
mod_book = self.book_col.modify(bkid)
mod_book.set("Notes", [{"Text":New_Note}]).execute()
else:
mod_book = self.book_col.modify(bkid)
mod_book.array_append("Notes", {"Text":New_Note}).execute()
if Authors and (Authors != self.make_authors_str(book_data['Authors'])):
authors_json = self.make_authors_dict_list(Authors)
self.book_col.modify(bkid).set("Authors", authors_json).execute()
self.session.commit()
except Exception as err:
print("ERROR: Cannot update book: {0}".format(err))
self.session.rollback()
def delete(self, book_id):
assert book_id, "You must supply a book id to delete the book."
try:
self.book_col.remove_one(book_id).execute()
except Exception as err:
print("ERROR: Cannot delete book: {0}".format(err))
self.session.rollback()
#
# Connect to a MySQL server at host, port
#
# Attempts to connect to the server as specified by the connection
# parameters.
#
def connect(self, username, passwd, host, port):
config = {
'user': username,
'password': passwd,
'host': host,
'port': port,
}
try:
self.session = mysqlx.get_session(**config)
if self.session.is_open():
self.book_schema = self.session.get_schema("library_v3")
self.book_col = self.book_schema.get_collection("books")
except Exception as err:
print("CONNECTION ERROR:", err)
self.session = None
raise
def make_authors_str(self, authors):
author_str = ""
num = len(authors)
i = 0
while (i < num):
author_str += "{0} {1}".format(authors[i]["LastName"],
authors[i]["FirstName"])
i += 1
if (i < num):
author_str += ", "
return author_str
def make_authors_dict_list(self, author_list=None):
if not author_list:
return None
author_dict_list = []
authors = author_list.split(",")
for author in authors:
try:
last, first = author.strip(' ').split(' ')
except Exception as err:
last = author.strip(' ')
first = ''
author_dict_list.append({"LastName":last,"FirstName":first})
return author_dict_list
def make_book_json(self, ISBN, Title, Pub_Year, Pub_Name, Pub_City, Pub_URL,
Authors=[], Notes=[], Edition=1, Language="English"):
notes_list = []
for note in Notes:
notes_list.append({"Text":"{0}".format(note)})
book_dict = {
"ISBN": ISBN,
"Title": Title,
"Pub_Year": Pub_Year,
"Edition": Edition,
"Language": Language,
"Authors": self.make_authors_dict_list(Authors),
"Publisher": {
"Name": Pub_Name,
"City": Pub_City,
"URL": Pub_URL,
},
"Notes": notes_list,
}
return encoder().encode(book_dict)
#
# Build row array
#
def make_row_array(self, book_doc_list):
rows = []
for book in book_doc_list:
book_dict = book
# Now, we build the row for the book list
row_item = (
book_dict["_id"],
book_dict["ISBN"],
book_dict["Title"],
book_dict["Publisher"]["Name"],
book_dict["Pub_Year"],
self.make_authors_str(book_dict["Authors"]),
)
rows.append(row_item)
return rows
#
# Get list of books
#
def get_books(self):
rows = []
try:
book_docs = self.book_col.find().sort("ISBN").execute().fetch_all();
rows = self.make_row_array(book_docs)
except Exception as err:
print("ERROR: {0}".format(err))
raise
return rows
Listing 9-24Books Class (Version 3)
哇,变化真大!这个版本显示了处理集合的代码与混合解决方案有多么不同。现在,让我们看看应用代码的变化。
应用代码
这个版本的应用代码的变化没有数据库代码模块的变化长。本质上,我们删除了出版商列表和细节视图,并将图书视图功能转换为 JSON 文档。还有一些其他的小变化。下面总结了这些变化。后面的部分将更详细地描述这些变化。
- 从
library_v3
模块导入Books
类。 - 将
Library
类切换到Books
类,并调用connect()
函数。 - 移除
NewSelectField()
类。 - 移除
PublisherForm()
类。 - 更改
BookForm
类,将发布者数据作为字段列出。 - 更改
BookForm
类,为文档 id 和 JSON 字符串添加隐藏字段。 - 从
ListForm
模板中删除发布者选项。 - 移除
publisher
查看功能。 - 修改
book
视图函数来处理 JSON 文档。
下面几节详细介绍了三个主要变化领域:设置和初始化、表单类和视图函数。要创建文件,只需将文件从version1/database/library_v2.py
复制到version2/database/library_v3.py
。
设置和初始化
对设置和初始化部分的更改很小。我们必须从library_v3
代码模块导入 Books 类,并修改代码以使用Books()
对象,而不是版本 2 中的Library()
对象。下面用粗体显示了这些变化。
from wtforms import (HiddenField, TextField, TextAreaField,
IntegerField, SubmitField)
from wtforms.validators import Required, Length
from database.library_v3 import Books
...
#
# Setup the books document store class
#
books = Books()
# Provide your user credentials here
books.connect(<user>, <password>, 'localhost', 33060)
表单类
表单类的变化也很小。首先,我们删除了NewSelectField()
和PublisherForm()
类,因为我们不再需要它们了。其次,我们必须修改BookForm()
表单类,使用发布者数据的文本字段。回想一下,这是姓名、城市和 URL。我们还想添加两个隐藏字段:一个用于文档 id,另一个用于 JSON 文档。
文档 id 对于方便检索或更新 JSON 文档至关重要,存储在表单中的 JSON 文档将允许我们检测数据何时发生了变化。回想一下关于数据库代码模块中的Books
类的讨论,我们在update()
函数中正是这样做的。像这样使用隐藏字段来包含数据是很常见的,但是应该谨慎使用这种技术,因为隐藏字段中的数据就像任何其他字段一样,您必须确保更新代码中的数据,否则您可能会处理过时的数据。
清单 9-25 显示了更新后的BookForm
类,变化以粗体显示。
class BookForm(FlaskForm):
isbn = TextField('ISBN ', validators=[
Required(message=REQUIRED.format("ISBN")),
Length(min=1, max=32, message=RANGE.format("ISBN", 1, 32))
])
title = TextField('Title ',
validators=[Required(message=REQUIRED.format("Title"))])
year = IntegerField('Year ',
validators=[Required(message=REQUIRED.format("Year"))])
edition = IntegerField('Edition ')
language = TextField('Language ', validators=[
Required(message=REQUIRED.format("Language")),
Length(min=1, max=24, message=RANGE.format("Language", 1, 24))
])
pub_name = TextField('Publisher Name', validators=[
Required(message=REQUIRED.format("Name")),
Length(min=1, max=128, message=RANGE.format("Name", 1, 128))
])
pub_city = TextField('Publisher City', validators=[
Required(message=REQUIRED.format("City")),
Length(min=1, max=32, message=RANGE.format("City", 1, 32))
])
pub_url = TextField('Publisher URL/Website')
authors = TextField('Authors (comma separated by LastName FirstName)',
validators=[Required(message=REQUIRED.format("Author"))])
create_button = SubmitField('Add')
del_button = SubmitField('Delete')
new_note = TextAreaField('Add Note')
# Here, we book id for faster updates
book_id = HiddenField("BookId")
# Here, we store the book data structure (document)
book_dict = HiddenField("BookData")
Listing 9-25Book Form Class (Version 3)
图书视图功能
查看功能区域是对应用进行大多数更改的地方。这是因为我们必须修改函数来使用 JSON 文档(数据)。幸运的是,JSON 对象很好地转换成代码,允许我们以数组和字典键查找的形式使用路径表达式。酷!下面列出了图书视图功能所需的更改。后面的段落更详细地解释了这些变化。
- 将发布者列表更改为字段。
- 删除填充发布者选择字段。
- 将 ISBN 路径的变量更改为文档 id。
- 使用
books()
实例代替book()
。 - 当从集合中检索数据时,直接在 Python 中使用 JSON 文档,通过数组索引和字典键访问数据项。
- 检测可选字段何时缺少数据元素。
- 使用修改后的参数列表调用 CRUD 函数,添加发布者字段。
如前所述,book view 函数需要修改,以包含现在表示为字段的出版商数据,因此我们不再需要填充 select 字段,从而稍微简化了代码。
因为数据现在在 JSON 中,所以我们可以使用文档 id 作为键,从而消除用户更改主键(例如 ISBN)的顾虑。事实上,使用 JSON 文档允许用户更改任何字段(或添加新字段),而不会产生键和索引问题。整洁!
当从图书集合中检索信息时,我们有一个 JSON 文档,我们可以用 Python 访问数据,就像它是一个大字典一样。例如,我们可以通过名称来访问数据项,如data["ISBN"]
,其中 ISBN 是字典中的关键字。很好!我们在if id_selected:
条件之后的部分看到了这些变化。对于那些可选的字段,我们可以检查字典(JSON 对象)来查看该键是否存在,如果存在,就检索数据。
我们还可以看到我们添加了任务,将文档 id 和原始 JSON 文档保存到隐藏字段中。最后,我们还必须对调用 CRUD 函数的方式做一点小小的改变,因为我们有发布者数据的额外参数。清单 9-26 显示了图书视图功能的完整的修改后的代码,修改后的部分以粗体显示。
def book(id_selected=None):
notes = []
form = BookForm()
# Get data from the form if present
bookid = form.book_id.data
isbn = form.isbn.data
title = form.title.data
year = form.year.data
author_list = form.authors.data
pub_name = form.pub_name.data
pub_city = form.pub_city.data
pub_url = form.pub_url.data
edition = form.edition.data
language = form.language.data
new_note = form.new_note.data
# If the route with the variable is called, change the create button to update
# then populate the form with the data from the row in the table. Otherwise,
# remove the delete button because this will be a new data item.
if id_selected:
# Here, we get the data and populate the form
data = books.read(id_selected)
if data == []:
flash("Book not found!")
#
# Here, we populate the data
#
form.book_dict.data = data
form.book_id.data = data["_id"]
form.isbn.data = data["ISBN"]
form.title.data = data["Title"]
form.year.data = data["Pub_Year"]
#
# Since edition is optional, we must check for it first.
#
if "Edition" in data.keys():
form.edition.data = data["Edition"]
else:
form.edition.data = '1'
form.pub_name.data = data["Publisher"]["Name"]
#
# Since publisher city is optional, we must check for it first.
#
if "City" in data["Publisher"].keys():
form.pub_city.data = data["Publisher"]["City"]
else:
form.pub_city = ""
#
# Since publisher URL is optional, we must check for it first.
#
if "URL" in data["Publisher"].keys():
form.pub_url.data = data["Publisher"]["URL"]
else:
form.pub_url.data = ""
#
# Since language is optional, we must check for it first.
#
if "Language" in data.keys():
form.language.data = data["Language"]
else:
form.language.data = "English"
form.authors.data = books.make_authors_str(data["Authors"])
# We also must retrieve the notes for the book.
if "Notes" in data.keys():
all_notes = data["Notes"]
else:
all_notes = []
notes = []
for note in all_notes:
notes.append(note["Text"])
form.create_button.label.text = "Update"
else:
del form.del_button
if request.method == 'POST':
# First, determine if we must create, update, or delete when form posts.
operation = "Create"
if form.create_button.data:
if form.create_button.label.text == "Update":
operation = "Update"
if form.del_button and form.del_button.data:
operation = "Delete"
if form.validate_on_submit():
# Get the data from the form here
if operation == "Create":
try:
books.create(ISBN=isbn, Title=title, Pub_Year=year,
Pub_Name=pub_name, Pub_City=pub_city,
Pub_URL=pub_url, Authors=author_list,
Notes=notes, Edition=edition,
Language=language)
flash("Added.")
return redirect('/list/book')
except Exception as err:
flash(err)
elif operation == "Update":
try:
books.update(id_selected, form.book_dict.data, ISBN=isbn,
Title=title, Pub_Year=year, Pub_Name=pub_name,
Pub_City=pub_city, Pub_URL=pub_url,
Authors=author_list, Edition=edition,
Language=language, New_Note=new_note)
flash("Updated.")
return redirect('/list/book')
except Exception as err:
flash(err)
else:
try:
books.delete(form.book_id.data)
flash("Deleted.")
return redirect('/list/book')
except Exception as err:
flash(err)
else:
flash_errors(form)
return render_template("book.html", form=form, notes=notes,
authors=author_list)
Listing 9-26Book View Function (Version 3)
最后,我们可以删除publisher()
视图函数和simple_list()
视图中的 publisher 部分,因为我们也不需要它们。
模板
对模板文件的更改很小。如果您还没有这样做,请将模板从版本 2 复制到版本 3。例如,将所有文件从version2/templates/*
复制到version3/templates
。一旦复制,你可以删除publisher.html
模板,因为它不再需要。
我们还需要对 base.html 文件做两个小的修改,以更改版本号并从导航栏中删除发行者列表。清单 9-27 显示了 base.html 文件的摘录,更改以粗体显示。
<div class="navbar navbar-inverse" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">MyLibrary v3</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a href="/list/book">Books</a></li>
</ul>
</div>
</div>
</div>
Listing 9-27Base Template (Version 3)
我们还必须对book.html
模板做一些小的修改,为逗号分隔的作者列表显示一个文本字段。清单 9-28 用粗体显示了修改后的模板的摘录。
...
{% block page_content %}
<form method=post> {{ form.csrf_token }}
<fieldset>
<legend>Book - Detail</legend>
{{ form.hidden_tag() }}
<div style=font-size:20pz; font-weight:bold; margin-left:150px;>
{{ form.isbn.label }} <br>
{{ form.isbn(size=32) }} <br>
{{ form.title.label }} <br>
{{ form.title(size=100) }} <br>
{{ form.year.label }} <br>
{{ form.year(size=10) }} <br>
{{ form.edition.label }} <br>
{{ form.edition(size=10) }} <br>
{{ form.language.label }} <br>
{{ form.language(size=34) }} <br>
{{ form.pub_name.label }} <br>
{{ form.pub_name(style="width: 300px;") }} <br>
{{ form.pub_city.label }} <br>
{{ form.pub_city(style="width: 300px;") }} <br>
{{ form.pub_url.label }} <br>
{{ form.pub_url(style="width: 300px;") }} <br><br>
{{ form.authors.label }} <br>
{{ form.authors(size=100) }} <br>
...
Listing 9-28Book Template (Version 3)
好了,改动到此为止;现在让我们来看看运行中的代码。
执行代码
现在我们已经写好了代码,让我们试一试。要执行应用,可以用 Python 解释器指定runserver
命令来启动它。下面显示了一个执行应用的示例。注意,我们使用了 port 选项来指定端口。您应该从version3
文件夹中输入这个命令。
$ cd version3
$ python ./mylibrary_v3.py runserver -p 5003
* Running on http://127.0.0.1:5003/ (Press CTRL+C to quit)
应用将启动并运行,但是数据库中还没有任何数据。你应该从拿几本你最喜欢的书开始,输入图书数据。现在还不要担心音符。一旦你添加了几本书,你应该在默认视图中看到它们(通过点击导航栏中的MyLibrary v3
或Books
)。图 9-7 显示了一个你应该看到的例子。
图 9-7
Library application book list (version 3)
如果这开始看起来有点熟悉,你是对的,它是。这个版本实现了相同的接口,只是没有发布者和作者视图。与其他版本一样,您可以点击任何图书的修改链接,查看图书的详细信息。图 9-8 显示了更新后的图书详细视图。请注意,publisher 条目现在是一组三个文本字段,而不是一个下拉列表。作者列表与版本 2 相比没有变化。
图 9-8
Book detailed view (version 3)
现在,让我们花点时间来讨论一下对这个版本的应用的一些观察。
观察
以下是对该应用版本的一些观察。有些是数据库设计的结果,有些来自代码,还有一些是我们做得更好的。观察结果以无序列表的形式呈现。这个列表比以前的版本更短,因为我们实现了更好的应用!因此,这是一个成功的列表,而不是改进的列表。
- 数据库代码更短:使用 X DevAPI 处理集合和文档时,我们需要的代码更少。
- 更容易理解的数据库代码:使用 JSON 文档是 Python(和其他语言)的自然扩展。
- 应用代码显著缩短:我们需要更少的应用代码,因为我们简化了用户界面。事实上,代码几乎是版本 1 的 50%。
还有一个观察值得讨论。使用这三个版本的应用对用户体验的改变很小。事实上,其中一个目标是将用户界面更改保持在最低限度,以证明从传统的关系数据库模型迁移到混合模型并最终迁移到纯文档存储模型并不意味着必须重新设计整个用户界面!
尽管可能需要进行一些更改来促进数据存储和检索方式的变化,如我们在 authors 和 books_authors 连接表中看到的那样,但这些更改通常有助于解决数据库设计中的问题,或者在这种情况下有助于消除一个错误的前提,即特定作者和出版商的数据在书籍的上下文之外是有意义的。在这个示例应用中,情况并非如此。因此,设计单独的表(或文档)来存储信息是不必要的,并且增加了我们不需要的复杂性。这就是围绕 JSON 文档和 MySQL 文档存储引擎设计数据的挑战和回报。
挑战
这两个版本的应用在功能上都非常简单。如果您发现该应用非常适合进行进一步的试验,甚至是作为另一项工作的基础,那么您可能需要考虑在一些方面改进设计和代码。以下是可以改进的地方的简要列表。
- ISBN 查找服务:添加使用 ISBN 查找服务(如
isbntools
(http://isbntools.readthedocs.io/en/latest/
)或 SearchUPC (http://www.searchupc.com
)检索图书信息的功能。一些服务需要创建帐户,而其他服务可能是收费服务。 - 独立的库模块:针对版本 1 和版本 2,将库文件分成独立的代码模块(图书、作者、出版商、库)。
- 独立的代码模块:较大的 Flask 应用通常将主代码文件中的视图(表单类)分解成独立的代码模块。
- 删除硬编码值:为 MySQL 参数生成用户、密码、主机和端口数据,而不是硬编码值(提示:使用
argparse
)。 - 扩展数据:修改数据库或文档存储来存储其他媒体,如杂志、文章和在线参考资料。
摘要
毫无疑问,新的 MySQL 8 版本将是 MySQL 历史上最大、最重要的版本。JSON 数据类型和 X DevAPI 的加入对于 MySQL 应用来说简直是开创性的。
在这一章中,我们探讨了关系数据库解决方案和增加了 JSON 字段的关系数据库解决方案之间的区别,最后是一个纯文档存储解决方案。正如我们所发现的,新的 X DevAPI 使得开发 MySQL 解决方案比关系数据库解决方案更容易、更快、代码更少。这给了我们很多理由开始采用文档存储。
在第 10 章中,我总结了我对 MySQL 8 文档库的探索,看看如何准备现有和未来的应用计划来整合文档库。这包括关于升级到 MySQL 8 的说明以及如何迁移到文档存储解决方案的提示。
十、MySQL 8 和文档存储的规划
这本书涵盖了很多内容,包括 MySQL 8 的一些最新特性的简要概述。我将重点放在 MySQL 文档存储上,包括它的所有组件:X 协议、X DevAPI、MySQL Shell 以及对带有 MySQL X 插件的服务器的更改。不仅如此,我还演示了如何使用 X DevAPI 开发应用——从基于 SQL 到混合再到 NoSQL 解决方案。这些技术是对服务器特性的极好的补充,与传统的关系数据库应用开发相比,它们有望为您的开发资源带来更多的回报。很明显,MySQL 8 不仅仅是版本编号上的一个新的、活泼的跳跃。
回想一下,我们看到了一些新的高可用性特性,如组复制和 InnoDB 集群。但这并没有结束,不是吗?我们还有新的身份验证机制、新的数据字典和许多小而重要的更新。那么,从哪里开始考虑迁移和升级到 MySQL 8 的影响呢?在这一章中,我将介绍一些迁移到 MySQL 8 的策略,包括迁移应用以使用文档存储的注意事项和最佳实践,以及另一个迁移现有数据库应用的例子。我探索了一些使用 MySQL 8 的技巧和诀窍。
让我们首先简单讨论一下考虑从 MySQL 5.7 和更早版本升级到 MySQL 8 的一些策略。
从 MySQL 5.7 和更早版本升级
虽然这本书不是关于如何升级到 MySQL 8 的教程,但在采用 MySQL 文档存储之前,有一些事情你应该考虑,这将很可能导致升级你现有的 MySQL 服务器。
有几种方法可以学习如何进行升级。最明显和推荐的途径是阅读在线 MySQL 参考手册,其中包含一个关于升级 MySQL 的章节(提供您必须知道的关键信息)。但是,有一些适用于任何形式的升级或迁移的高级或一般实践。本节介绍的升级实践将帮助您避免升级像 MySQL 这样的主要系统时遇到的一些麻烦。
在这一节中,我们将看看 MySQL 的升级类型,然后讨论一些规划和执行升级的一般做法。我们以关于执行升级的原因的简短讨论来结束本节。我们将在最后讨论进行升级的原因,以便您更好地理解其中包含的风险。
让我们先来看看您可能会遇到的升级类型。
升级类型
在线 MySQL 参考手册和类似出版物描述了两种基本的升级方法,即如何进行升级的策略和步骤。下面是方法的总结。
- 就地:MySQL 服务器实例通过使用现有数据字典的二进制文件进行升级。这种方法使用各种实用程序和工具来确保平稳过渡到新版本。
- 逻辑:在安装新版本覆盖旧版本之前备份数据,在升级之后恢复数据。
尽管这些描述了升级 MySQL 的两个通用策略,但它们并没有涵盖所有可能的选项。事实上,我们将在后面的章节中看到另一种方法。毕竟,您的安装可能会略有不同——特别是如果您已经使用 MySQL 很长时间了,或者有许多配置了高可用性的 MySQL 服务器,或者在您自己的应用中使用第三方应用和组件。这些因素会使遵循一个给定的、通用的程序成为问题。
与其尝试扩展升级方法,不如让我们从系统管理员的角度来看一下。特别是我们有 x.y.z 版本,想升级到 a.b.c 怎么办?以下部分描述了基于版本的升级。
Caution
Oracle 仅建议升级 GA 版本。不建议升级其他版本,这可能需要额外的时间进行迁移,并接受潜在的不兼容性。升级非 GA 版本的风险由您自行承担。
MySQL Version Number Terminology
MySQL 使用三位数的版本号,以主要版本号、次要版本号和修订版本号的形式表示(奇怪的是,它在文档中也被称为版本号)。这通常用点符号表示。例如,版本 5.7.20 将主版本定义为 5,次版本定义为 7,修订版定义为 20。通常,版本号后面跟有文本(在文档中称为后缀),表示附加的版本历史、稳定性或一致性,如通用版本(GA)、候选版本(RC)等。关于 MySQL 中版本号的完整解释,参见 https://dev.mysql.com/doc/refman/8.0/en/which-version.html
。
版本升级
最简单的升级形式是在只更改修订版号的情况下进行升级。这通常是指 X.Y.Z 版本号中的 Z,或者简称为“主要.次要版本的版本”例如,5.7.20 版是 5.7 的修订版 20 或版本 20。
在此版本级别升级通常是安全的,尽管不能保证工作完美,但风险很低。但是,您仍然应该在执行升级之前阅读发行说明。如果您正在使用非通用版本(GA ),这一点尤其正确。如果该版本不是 GA 版本,您必须注意在线 MySQL 参考手册中的发行说明和升级部分。虽然这种情况很少发生,但有时您必须计划并克服一些特殊的考虑因素,以实现升级。幸运的是,Oracle 在传达任何必要的步骤和过程方面做得非常好——您只需要阅读文档!
小升级
下一种升级形式是在次要版本号改变时进行升级。这通常是指 X.Y.Z 版本号中的 Y——例如,从 5.6 升级到 5.7。
对于次要版本的个位数增量,升级通常是可接受的并有记录。例如,支持从 5.6 升级到 5.7,但不直接支持从 5.0 升级到 5.7。这是因为版本之间有太多的差异,使得升级不可行(但并非不可能)。
然而,如果您有相应的计划,您可以升级较小的版本变更,风险可控。更多关于管理风险的内容将在后面的章节中介绍。
重大升级
下一种升级形式是在主版本号更改时升级。除了不兼容的版本之外,这个类别是风险最大的类别,也是最有可能需要更多工作的类别。
主版本的版本升级很少发生,只有当 Oracle 发布了对服务器的一组新的、主要的更改(因此得名)时才会发生。MySQL 8 server 在 MySQL 5 的基础上做了许多改进,其中大部分在性能、高级功能和稳定性方面都有显著提高。但是,有一些变化使得旧版本中的一些功能不兼容。
例如,一旦 MySQL 8.0 正式发布,就支持从 MySQL 5.7 升级到 MySQL 8.0,但您可能需要迁移某些功能才能完成升级。
幸运的是,Oracle 已经详细记录了所有问题领域,并提供了如何迁移到新特性的建议。我们甚至看到这种情况扩展到了主要版本之外 MySQL 文档库就是一个很好的例子。
不兼容的升级
正如您可能已经猜到的那样,有些升级不被推荐,要么是因为缺少支持升级的功能,要么是因为主要的不兼容性。比如你不要考虑从 MySQL 5.0 升级到 MySQL 8.0。这仅仅是因为 8.0 不支持一些较老的 5.0 特性。由于这些类型的升级并不常见,我们在下面的列表中总结了一些不兼容的升级。不兼容的原因不是你想升级到的新版本,而是你想升级的旧版本。
- 跳过主要版本:升级主要版本可能会引入不兼容的更改。
- 跳过次要版本:次要版本的某些升级可能会引入不兼容的更改。
- 升级不兼容的硬件:升级一种字节序的硬件可能与另一种不兼容。例如,big-endian 到 little-endian 可能不兼容。
- 更改 InnoDB 格式的版本:在 InnoDB 存储引擎内部发生了一些变化。大多数计划用于兼容的次要版本升级(例如,从 5.7.3 升级到 5.7.12),但有些需要一些额外的步骤来准备数据。
- 新特性:新特性的引入很少会带来不兼容性。例如,添加数据字典来呈现。FRM 元数据已过时。
- 平台变更:包括变更平台在内的一些升级可能需要额外的工作或引入潜在的不兼容性。例如,从文件系统中不支持区分大小写的平台转移到支持区分大小写的平台。
- 升级非正式发布版本:不建议从非正式发布版本升级到正式发布版本,从正式发布版本升级到非正式发布版本,以及在非正式发布版本之间升级。
毫无疑问,不兼容性取决于某些功能、硬件或内部存储机制。在大多数情况下,在线文档概述了您可以做些什么来确保成功。有时这需要遵循特定的升级路径,例如在升级到目标版本之前先升级到一个版本。
What If I Must Upgrade An Incompatible Version?
如果您发现您的升级策略属于本节列出的不兼容升级,不要绝望。您可能仍然能够执行升级,但它可能更昂贵,需要更多的工作。例如,您可以通过使用带有mysqldum
p 或mysqlpump
的 SQL 语句来备份数据,安装新版本,然后使用 SQL 文件来调整它们以消除任何不兼容性,从而执行逻辑升级。尽管这确实带来了相当大的风险,即您仍然可以干净地导入所有数据,但这仍然是可能的。如果您发现自己处于这种情况,一定要花更多的时间使用并行安装和延长测试时间等策略来解决风险。
既然我们已经很好地了解了可能的升级类型,那么让我们来看看执行升级的一些最佳实践。
升级实践
在升级任何系统时,都有一些您应该遵守或至少应该用作指南的常规做法。本节描述了升级 MySQL 服务器时应该考虑的一些基本实践。同样,其中一些可能是熟悉的,一些可能不是您在升级 MySQL 时会考虑使用的。此外,其中一些在在线 MySQL 参考手册中没有列出。
正如您将看到的,这些实践不一定是连续的,甚至不一定是下一个实践的先决条件。例如,计划还应该包括测试时间。因此,这里讨论的实践按照重要性的一般顺序排列,但是不应该按照这个顺序来考虑或实现。
检查先决条件
升级 MySQL 时,您应该做的第一件事是查看文档以了解任何先决条件。有时这只是安全地备份您的数据,但也可以包括诸如您需要使用哪些实用程序和工具来迁移某些功能(或数据)之类的内容。确保在升级之前满足所有先决条件。
升级文档还将包括不兼容问题。大多数情况下,这发生在升级主要版本时,但有时也会发生在次要版本上。幸运的是,在线 MySQL 参考手册中概述了这些内容。检查先决条件还可以帮助您提供可用于规划升级的详细信息。
Caution
当出现问题时,在线 MySQL 参考手册中关于升级的部分应该是你的第一站,而不是最后一站。“预先阅读”意味着被预先警告。
一旦通读了文档,作为先决条件,您需要做的一件事就是使用mysqlcheck
实用程序来检查 MySQL 安装的兼容性。例如,升级到 MySQL 8 的先决条件之一是,根据在线 MySQL 参考手册中题为“MySQL 升级策略”的一节,“不得有使用过时数据类型、过时函数、孤儿的表。frm 文件、使用非本机分区的 InnoDB 表,或者定义器缺失或为空,或者创建上下文无效的触发器。我们可以使用mysqlcheck
实用程序来识别这些情况,如清单 10-1 所示。
$ mysqlcheck -u root -p --all-databases --check-upgrade
Enter password:
library_v1.authors OK
library_v1.books OK
library_v1.books_authors OK
library_v1.notes OK
library_v1.publishers OK
library_v2.books OK
library_v2.notes OK
library_v2.publishers OK
library_v3.books OK
...
mysql.user OK
sys.sys_config OK
Listing 10-1Using mysqlcheck to Identify Upgrade Issues
为了获得最佳效果,您应该使用正在升级的版本中的mysqlcheck
实用程序。这将确保该实用程序是最新的,并应识别更多的升级问题。
规划升级
一旦您制定了所有的先决条件,并确定了需要特殊处理来解决不兼容问题的任何功能,就应该计划升级服务器了。如果您有数千台服务器,这可能是一件显而易见的事情,但对于只有几台(甚至一台)服务器需要升级的人来说,这就不那么明显了。
您应该抵制简单地运行升级而不计划要做什么的诱惑。回想一下,我们希望通过降低(或消除)风险来确保升级顺利进行。这对于生产环境来说更加重要,但是任何潜在的可用性、性能或数据损失都会导致生产效率的损失。
您可以从文档中获得大部分需要规划的内容,但是文档并不特定于您的安装、服务器、平台等等。因此,您必须填写这些空白处,并根据您自己的安装修改文档中建议的过程。然而,通过阅读“MySQL 8.0 中的新特性”一节,您可以学到很多东西,请注意在线 MySQL 参考手册中标有“升级的后果”的任何小节。在那里你会找到一些提示,可以帮助你避免一些复杂的决定,或者更好地避免复杂的维修。
这还包括确保你手头有合适的人员来进行升级,或者在出现问题时做好准备。 1 例如,不要忘记你的开发者、网站管理员和其他关键角色。
计划的形式由你决定;但是,我建议你把你打算做的事情写下来,和别人分享。这样,升级所有权链中的每个人都知道要做什么。你会惊讶地发现,小小的交流可以减少出错的风险。
Caution
如果您正在使用或计划使用支持自动更新的平台,并且这些设施包括监控 MySQL 的存储库,您可能要考虑将 MySQL 排除在自动更新之外。对于生产环境来说尤其如此。您不应该在生产环境中为任何关键任务数据自动更新 MySQL。
考虑并行部署
在升级需要大量工作的系统时,最有帮助的一种做法是与现有版本并行安装新版本。这是软件工程中的一种做法,旨在确保在安装和配置新系统时,现有的数据和应用保持不变。新版本(安装)将被视为一个开发平台,通常在完成足够的迁移测试后投入生产。
尽管这本身并不是一次升级(这是一次新的安装),但是运行一个新的 MySQL 并行版本在如何处理现有数据和应用的迁移方面提供了相当大的自由度。毕竟,如果出现问题,您的数据在旧系统上仍然是可操作的。
这种实践还为您提供了另一个好处:您可以更改平台或其他主要硬件,而不必拿现有数据冒险。因此,如果您现有的服务器有要同时更新的硬件,您可以使用并行安装在新硬件上安装 MySQL,从而隔离新硬件带来的风险。
最后,采用并行安装可以确保现有系统完全能够运行,从而有助于安排和规划您的迁移。更好的是,如果在迁移过程中出现问题,您可以随时回到旧系统。
并行部署通常包括让两个系统都运行一段时间。时间的长短可能取决于您愿意承担的风险,也可能取决于完全切换所有应用所需的时间。
不幸的是,有些人可能没有资源来考虑并行部署。因为同时安装两个 MySQL 可能会给开发者、管理员和支持人员带来更大的负担。考虑到并行开发的好处,在短时间内增加额外的资源或接受一些人员的低生产率可能是值得的。
然而,如果你没有进行足够的测试,即使这个安全网也是脆弱的。
测试测试测试!
这种实践和计划一起,经常被忽视或被轻视。有时,这是由于外部因素造成的,例如没有合适的人员可用,或者计划失败,导致没有时间进行广泛的测试。不管借口是什么,未能充分测试您的升级会增加超出大多数人愿意忍受的风险。
测试应包括确保所有数据都已迁移,所有应用完全正常工作,以及所有访问(用户帐户、权限等。)都是功能性的。然而,不要就此止步。您还应该确保您的所有操作实践都已针对新版本进行了修改。更具体地说,您的维护脚本、过程和工具都可以在新版本中正常工作。
此外,您的测试应该会决定是否接受升级。如果事情不工作或有太多的问题,您可能需要决定保留或拒绝升级。并行安装实践可以以这种方式提供帮助,因为在确定一切正常之前,您不会破坏现有的数据或安装。确保你把这些标准写进你的计划,以确保成功。
Tip
确保测试所有现有的操作程序,作为验收标准的一部分。
生产部署策略
如果您有一个生产和开发(或测试)环境,您还应该考虑如何将开发或测试部署转移到生产环境中。如果您使用并行安装,可能只是简单地切换应用路由和类似的设备和应用。如果您使用就地安装,它可能会更复杂。例如,您可能需要计划一段停机时间来完成迁移。
对于并行安装,计划停机时间可能更精确,并且涉及的时间更短,因为您有更多的时间来测试。但是,对于就地升级,您可能需要留出一段时间来完成迁移。正如所料,您希望通过尽可能多地进行迁移来最大限度地减少停机时间。但在 MySQL 的基地里,这可能无非就是形成一个计划,聚集资源。底线是,不要放弃在你的计划中包括生产部署。
既然我们已经讨论了升级实践,那么让我们花一点时间来讨论一下我们可能想要考虑执行升级的一些原因,这显然是一个非常复杂的过程,具有一定的风险。
升级的原因
如果您像大多数平台或系统的狂热用户一样,每当有新版本发布时,您都会希望升级到最新、最好的版本。精明的管理员和规划者知道,在生产数据库环境中,这种行为几乎没有存在的空间。因此,升级的理由将需要一些真正物有所值的东西。也就是说,它必须值得你去做。升级 MySQL 的主要原因包括以下几点。
- 特性:发布了一个可以改进应用或数据的新特性,例如文档存储、组复制和 InnoDB 集群
- 性能:新版本提高了性能,使您的应用更好。例如,最新的 5.7 版本比以前的版本快很多倍,MySQL 8 承诺会在这方面有所改进。
- 维护:有一些新功能可以帮助您更好地维护系统。示例包括新的数据字典、组复制和 MySQL 企业备份等辅助工具。
- 错误修复:旧版本中可能存在缺陷,需要解决方法或限制。较新的版本可能包含对关键错误的修复,因此您可以删除由缺陷引起的变通办法和限制。
- 合规性:您的平台、标准操作程序或外部实体需要升级以实现合规性。例如,根据合同协议,您可能需要运行特定版本的 MySQL。
底线是你必须回答这个问题,“我为什么要升级?”这个答案一定会给你、你的数据、客户、员工和公司的未来带来一些好处。将资源花费在几乎没有或根本没有好处的升级上是没有意义的,这也是公司经常跳过版本升级的另一个原因。唉,跳过太多升级会让以后的升级更成问题。然而,考虑到 MySQL 8.0 相对于 MySQL 5.7 和更早版本的改进,许多人将会计划升级到 MySQL 8。
Tip
有关迁移到 MySQL 8 的更多详细信息,包括特定平台的步骤,请参见 http://dev.mysql.com/doc/refman/8.0/en/upgrading-from-previous-series.html
。
So, Should I Upgrade To MySQL 8 or Not?
本节的讨论可能会对您是否应该升级到 MySQL 8 产生一些疑问。事实并非如此。事实上,这本书应该说服你尽快升级到 MySQL 8,只要你能以一种安全、无风险的方式升级。因此,在本节中,我建议您需要仔细规划和执行升级,以确保成功。
升级到 MySQL 8 的注意事项
在线 MySQL 参考手册 MySQL 8.0 中发现了几个兼容性问题。以下是您在计划 MySQL 8.0 升级时应该了解的一些情况。
- 数据字典:新的元数据、事务存储机制是架构中的一个主要变化。如果您有处理
.frm
文件和其他元数据的 DevOps,您可能需要进行更改以迁移到使用数据字典。 - 认证插件:默认认证插件已经改变。这可能会导致使用旧身份验证机制的用户出现连接问题。
- 错误代码:一些错误代码已经改变。如果您想让应用使用错误代码,那么您会希望研究这些更改,以避免升级后出现应用错误。
- 分区:默认的分区存储引擎支持已被删除。如果你正在使用一个定制的存储引擎(或者一个旧的),你将需要确保有一个升级的版本用于 MySQL 8。
- INFORMATION_SCHEMA:对视图的微小更改。如果您的应用或开发应用使用这些视图,请务必检查您正在使用的视图是否已被删除或更改。
- SQL 命令:有一些新的和过时的 SQL 命令。请务必检查您的 SQL 语句,看看您是否在使用一些旧的、已删除的命令。
- 默认字符集:默认字符集(charset)已经更改为 utf8mb4。如果您的应用支持字符集,您可能需要使用新的默认值进行测试,以确保兼容性。
同样,请务必阅读在线 MySQL 参考手册部分“验证 MySQL 5.7 安装的升级先决条件”和“影响 MySQL 8.0 升级的更改”部分,了解升级到 MySQL 8.0 所需的这些和其他先决条件和迁移任务的最新信息。
另一个很好的资源是 https://mysqlserverteam.com/
的工程博客。这些博客通常会在新功能正式发布之前对其进行讨论,并且是关于该功能如何工作以及工程团队已经确定或正在努力克服的任何升级问题的知识来源。关注博客将会给变化一个很好的预警。
Tip
请访问 https://mysqlserverteam.com/
的工程博客,了解关于新特性以及如何使用它们的早期公告。
既然我们已经讨论了 MySQL 的总体升级和升级到 MySQL 8.0 的一些细节,那么让我们讨论一个非常重要的开发概念,在完全采用 MySQL 文档存储之前,您应该花一些时间来理解这个概念。
迁移到无模式文档
无模式是什么意思?这仅仅意味着我们不会将数据存储限制为带有给定类型的特定字段的严格格式。采用无模式思维要记住的关键因素是发挥 JSON 文档的优势:只存储所需数据的能力,包括在需要的地方添加文档元素并将所有有意义的数据放在一起的能力。这也被称为灵活性,是设计无模式文档的基础。
例如,如果开发者发现必须添加一个新字段,它可以。或者,如果开发者发现嵌入信息可以产生更快、更简单的代码,那么他们应该权衡应用和用户体验的好处,而不是严格的数据存储规则。
灵活性还有另一个角度。关系数据库通常使用相同的规则和工具集来设计。以至于适用于一个数据库的东西会出现在另一个数据库中。在无模式设计中,将数据放入单独的集合或嵌入很大程度上是基于数据的使用方式,因此会因应用而异。
然而,你不应该由此得出结论,开发者可以免费搭车,可以随心所欲。相反,您应该将灵活性视为一种工具,在经过适当的过程后,您可以使用它来评估变更。
无模式思维的另一个好处是无需改造即可扩展数据。例如,如果我们的应用获得了更多的特性,导致文档中有更多的数据,就没有必要返回并强制将旧文档转换成新的结构。随着应用特性的成熟,我们简单地缩放文档。如果我们确实需要返回并向旧文档中添加数据,这只是一件简单的编码工作。
尽管减少不必要的复杂性和模糊性应该是任何数据存储设计的目标,但无模式思维应该更愿意接受权衡的责任,并努力最小化它们的影响,而不是遵守一套固定的规则。
因此,无模式思维应该是灵活性和可伸缩性的思维,在这种思维中,我们强调这些品质而不是数据结构和一致性。请记住,我们努力将数据保存在一起,以减少检索数据的次数。
以下几节指出了在采用无模式思维时,您可能需要考虑的一些方面
规范化与反规范化
最基本的思维挑战之一是认识到规范化和反规范化之间的区别。从根本上说,规范化是关系数据库设计者努力减少存储的数据量而又不存在重复或歧义的目标。反规范化是文档存储设计者的一个目标,通过最完整地描述一个实体,尽量使数据局部化,减少对重复的关注。
在无模式的世界中,我们使用反规范化来消除对连接的需求,从而可能提高性能。然而,这并没有结束。目标是让数据模型存储作为一个单元使用的数据。换句话说,文档应该包含所有相关的数据。
在某些情况下,这可能导致在文档中嵌入通常存储在单独的集合(或表)中的数据。因此,对数据进行反规范化可能会引入一些重复。例如,我们在上一章中看到了这一点,我们在 book 文档中存储了作者姓名。在这种情况下,作者的名字确实在有相同作者的书中重复了。然而,数据的使用方式——查看书目信息——意味着实际上不需要搜索或查询作者数据。只需简单地列出作者就可以了。因此,检索规范化作者数据的成本是固定规则的人工应用。
在处理规范化或反规范化问题时,不仅要从数据的组织方式的角度,还要从数据的使用方式的角度来分析数据。有时,您可能会发现隔离数据的原因并不重要。例如,如果不需要子表,并且可以接受一些重复,那么可以将信息嵌入到一个或多个 JSON 字段中,从而应用足够的非规范化来获得将数据放在一个地方的好处(一次检索)。
形式规则与启发式规则
另一个需要考虑的领域是如何设计数据存储,或者更恰当地说,使用什么机制来驱动开发。在关系数据库世界中,我们使用一组规则(例如,被称为范式的不妥协且有时不宽容的 2 规则)来指导设计者实现最少冗余、最准确的检索解决方案。在无模式的世界中,我们在设计数据存储时使用启发法或经验法则,以获得描述我们正在建模的事物的最佳存储,并使数据可访问。
区别不在于数据是如何设计的,而在于数据是如何显示的。在关系数据库世界中,数据存储很大程度上是根据如何存储它来设计的,而在无模式世界中,文档是根据如何使用它以及用户如何查看数据来设计的。
因此,我们可以相当好地预测关系数据库将如何被访问,如何形成查询(通常在应用之前),甚至使用正确的工具预测查询将如何执行。然而,在无模式的解决方案中,我们不能通过检查文档来判断它将如何执行。我们必须用应用测试它,以了解如何更有效地访问它。有时,这可能会导致对代码或文档本身进行微小的更改。幸运的是,进行修改的过程比在关系数据库中容易得多。因此,在无模式的世界里,我们必须采取更加友好的态度。
这是将两者区分开来的原因之一。例如,在关系数据库中,当我们需要更改一个表(或一组表)时,我们通常必须提前做好计划,因为更改严格模式通常会迫使应用开发者更改他们的应用。另一方面,修改文档不需要长时间的重组工作。事实上,开发者可以简单地在代码中添加新的数据项,而不会给数据库管理员带来压力。我们来看一个例子。
考虑一个我们存储地址的解决方案。我们都知道它们的样子——街道 1、街道 2、城市、州和邮政编码。但是,如果您必须使您的数据库可用于国际数据,该怎么办呢?现在,我们正在考虑添加一个国家名称的可能性,如果不是为特定国家的地址添加额外的字段的话。毫无疑问,这将需要修改表(以及代码)。现在,考虑一个存储地址的文档。如果我们需要新的字段,我们只需在代码中添加它们,并编写代码来检测新的字段。
将数据视为代码
对于那些使用过关系数据库的人来说,关于无模式设计最难理解的一个概念是,数据(文档)应该(可以)被视为代码。考虑 JSON 结构——它是代码!因此,将数据视为代码的一部分将有助于设计更好的文档。
例如,如果您知道您的文档包含列表元素,并且您需要迭代这些列表中的项目,那么执行此操作的代码通常是某种形式的循环或迭代器机制,例如 for each 或 for X in Y 构造。因此,您可以查看文档中是否有在这些循环结构中使用的数据。是的,这类似于在结果集(行的数组)中思考,但是在这种情况下,我们更接近真正的代码。事实上,由于 JSON 工作方式的独特性,我们可以编写代码来引用文档的元素,而不是我们在结果集处理中看到的抽象层。也就是说,我们通过名称访问字段,而不是要求库给我们“第 n”个字段。这使得代码更易于阅读,并由数据描述(反之亦然)。
认为存储是理所当然的
这听起来可能有点奇怪,但是存储机制——将文档放在文档存储中——很大程度上可以被无模式设计人员和代码开发者忽略。例如,我们不关心表格行、字段等等。模式是灵活的,重点是收集文档。
一般来说,用于文档存储的 API 和 X DevAPI 使得这些操作无处不在,从而使设计人员和开发者能够专注于如何使用文档,而不是如何存储文档。
正如所料,在有些情况下,我们希望确保不会过度反规范化,在这些情况下,我们需要从逻辑上考虑如何组织集合中的数据,但这也是一个更高的层次,不直接喜欢存储机制。
嵌入还是分离?
当你设计更多的无模式文档时,知道何时嵌入数据是你将学到的技能之一。但是,有一些通用规则可以遵循,以帮助回答“嵌入还是不嵌入?”下面列出了一些情况,在这些情况下,您可能需要决定是嵌入还是分离数据。
- 完整性:嵌入数据仅适用于此文档。它不会在其他地方使用,也很少在没有文档的情况下被更改(或查看)。如果它可以在其他文档中使用,并且更改必须应用于所有引用,或者它是一个单独的实体,则不应该嵌入它。
- 有限的增长:嵌入数据的长度不可能增长。例如,如果嵌入的数据是一个数组,数组的大小(项数)将保持较小(或很少)。如果有机会增长超过一个合理的大小,它应该是一个单独的集合。
- Containership:如果存在一种关系,其中一个文档包含另一个文档,并且这些文档只能作为一个集合被访问,那么您可能希望嵌入。但是,如果文档可以单独访问或更改(这样做是有意义的),您可能希望考虑不要嵌入文档。
- 编辑频率:如果文档中的数据很少更改,可以嵌入它。但是,如果数据可能从另一个访问点(视图)或应用中不使用原始文档的机制频繁更改;您可能需要考虑将数据移动到它自己的集合中。
- 链接:如果您想要嵌入的数据只是偶尔从一个文档中引用,您可以考虑嵌入它。但是,如果它被多个文档引用,并且所有引用的数据必须相同,则应将链接的数据放在它自己的集合中。
迁移到文档存储的策略
现在我们对无模式思维有了更好的理解,让我们回顾一下将现有关系数据库迁移到文档存储时可以采用的一些策略。本节使用另一个迁移到文档存储的例子来巩固本书中到目前为止学到的经验。
正如我们在第 9 章中看到的,我们不必一次性迁移所有的数据库和数据——尽管如果您有资源、时间和足够的需求,您可以这样做。然而,大多数人希望缓慢地迁移到文档存储。还有一种情况是,一个纯粹的文档存储可能无法满足您的需求,或者成本太高,这使得迁移成为一个长期目标。
让我们对联系人列表使用一个众所周知的数据库解决方案。在这里,我们存储姓名、地址和电话号码。联系人列表的典型关系数据库解决方案将所有数据分组到一个数据库中,地址、电话号码、电子邮件地址等具有一对多的关系。这是因为我们知道每个联系人都有一个或多个这样的数据项。图 10-1 显示了这种关系数据库设计的实体关系图(ERD)。
图 10-1
Contact list (relational database)
首先,注意这个数据库设计不是完全标准化的。例如,联系人列表可能包括两个或更多具有相同工作或家庭地址以及相同电话号码的人。毫无疑问,我们可以建立多对多的关系,但这种关系过于正常化了。更具体地说,我们不会将地址和电话号码与联系数据分开存储,这样使用数据没有任何意义。
其次,请注意,数据库支持为每个联系人存储多个电子邮件、地址和电话号码。也就是说,contacts
表与email_addresses
、addresses
和phones
表之间存在一对多的关系。我们将使用数据库名称contact_list1
,这样我们可以将它迁移到其他表单进行比较。
在这个例子中,所有表上都有外键约束和主键。清单 10-2 显示了数据库中表的 SQL 语句。
CREATE DATABASE IF NOT EXISTS `contact_list1`;
CREATE TABLE `contact_list1`.`contacts` (
`contact_id` int(11) NOT NULL AUTO_INCREMENT,
`first` char(30) DEFAULT NULL,
`last` char(30) DEFAULT NULL,
PRIMARY KEY (`contact_id`),
KEY `contact_id` (`contact_id`),
CONSTRAINT `email_addresses_ibfk_1` FOREIGN KEY (`contact_id`) REFERENCES `contacts` (`contact_id`)
) ENGINE=InnoDB;
CREATE TABLE `contact_list1`.`addresses` (
`addr_id` int(11) NOT NULL AUTO_INCREMENT,
`contact_id` int(11) NOT NULL,
`address_type` ENUM('work', 'home', 'other') DEFAULT 'home',
`street1` char(100) DEFAULT NULL,
`street2` char(100) DEFAULT NULL,
`city` char(30) DEFAULT NULL,
`state` char(30) DEFAULT NULL,
`zip` char(10) DEFAULT NULL,
PRIMARY KEY (`addr_id`,`contact_id`),
KEY `contact_id` (`contact_id`),
CONSTRAINT `addresses_ibfk_1` FOREIGN KEY (`contact_id`) REFERENCES `contacts` (`contact_id`)
) ENGINE=InnoDB;
CREATE TABLE `contact_list1`.`email_addresses` (
`email_id` int(11) NOT NULL AUTO_INCREMENT,
`contact_id` int(11) NOT NULL,
`email_address` char(64) DEFAULT NULL,
PRIMARY KEY (`email_id`,`contact_id`)
) ENGINE=InnoDB;
CREATE TABLE `contact_list1`.`phones` (
`phone_id` int(11) NOT NULL AUTO_INCREMENT,
`contact_id` int(11) NOT NULL,
`phone` char(30) DEFAULT NULL,
PRIMARY KEY (`phone_id`,`contact_id`),
KEY `contact_id` (`contact_id`),
CONSTRAINT `phones_ibfk_1` FOREIGN KEY (`contact_id`) REFERENCES `contacts` (`contact_id`)
) ENGINE=InnoDB;
Listing 10-2Contact List Relational Database
Note
精明的数据库设计者会注意到电话号码字段被过分非规范化了。你能发现问题吗? 3
现在我们已经看到了数据库设计,让我们考虑一下文档存储设计者会注意到并希望改变的一些事情。也就是说,让我们以更批判的眼光来看待这个设计。请注意,对于我们想要查看的任何联系人,我们可能有多达三个额外的查询来检索所有数据。这是因为我们将电话、电子邮件和地址分别放在不同的表格中。我们可以发出一个连接查询来获取所有数据,但是这会产生额外的数据(除非您使用外部连接或类似的技巧)。
为了简单起见,让我们坚持每个依赖表一个查询。尽管如此,我们总共要执行四个查询来检索给定联系人的所有数据。清单 10-3 显示了我们需要执行的查询,以获取名为“比尔·史密斯”的联系人的数据。
MySQL localhost:33060+ SQL > SELECT * FROM contact_list1.contacts WHERE first = 'Bill' AND last = 'Smith';
+------------+-------+-------+
| contact_id | first | last |
+------------+-------+-------+
| 1 | Bill | Smith |
+------------+-------+-------+
1 row in set (0.00 sec)
MySQL localhost:33060+ SQL > SELECT * FROM contact_list1.addresses WHERE contact_id = 1;
+---------+------------+--------------+-----------------+---------+----------+-------+-------+
| addr_id | contact_id | address_type | street1 | street2 | city | state | zip |
+---------+------------+--------------+-----------------+---------+----------+-------+-------+
| 1 | 1 | home | 123 Main Street | NULL | Anywhere | VT | 12388 |
+---------+------------+--------------+-----------------+---------+----------+-------+-------+
1 row in set (0.00 sec)
MySQL localhost:33060+ SQL > SELECT * FROM contact_list1.email_addresses WHERE contact_id = 1;
+----------+------------+----------------------------+
| email_id | contact_id | email_address |
+----------+------------+----------------------------+
| 1 | 1 | bill@smithmanufacturing.co |
| 2 | 1 | bill.smith@gomail.com |
+----------+------------+----------------------------+
2 rows in set (0.00 sec)
MySQL localhost:33060+ SQL > SELECT * FROM contact_list1.phones WHERE contact_id = 1;
+----------+------------+----------------+
| phone_id | contact_id | phone |
+----------+------------+----------------+
| 1 | 1 | (301) 555-1212 |
+----------+------------+----------------+
1 row in set (0.00 sec)
Listing 10-3Queries to Retrieve a Contact (Relational Database)
如您所见,这涉及到多次访问数据库服务器以获取数据。如果您的应用被设计为使用标签或其他用户界面机制来隐藏电话、地址和电子邮件,直到用户点击显示信息,有四个查询可能是好的,可能会节省您的一些努力。然而,几乎每个联系人列表解决方案都包括姓名、地址和电话号码。因此,从数据库往返行程的一般角度来看,我们并没有节省太多。
还要注意,它要求我们跟踪传递给三个相关查询的contact_id
。一个好的关系数据库设计者会说,“那又怎样?”在这些观察中。然而,无模式思维告诉我们尽量减少连接,并将所有数据放在一起。让我们看看如何应用无模式思维将数据库迁移到混合解决方案,保留基表,但加入 JSON 字段。
Note
我不会深入讨论如何创建、更新和删除操作,因为这些操作在关系数据库系统中非常常见。
迁移到混合解决方案
删除连接是提高数据检索性能的一个好策略。这也符合无模式设计的标准之一;将数据放在一起。这两种策略都属于非规范化数据。上一节中的示例联系人列表数据库被规范化为包含四个表:一个用于联系人姓名,另一个包含所有地址、电话号码和电子邮件地址。
我们的目标并不是让单独的表可以搜索,甚至可以访问,并且可以独立呈现。毕竟,任何人看到一个与所有者没有任何联系的电话号码列表有什么用呢?相反,规范化意味着将相似的数据放在一起并消除重复。
例如,如果你认识几个在同一个地方工作的人,他们的工作地址和电话号码可能是相同的。同样,同一家庭的成员可能有相同的地址和电话号码。 4 然后规范化产生一个主表,其中有三个一对多关系的依赖表。
但是,如果您认为我们很少需要单独查询电话号码、电子邮件地址或地址表,并且这些表中的数据与一个联系人相关联,并且只有将这些数据作为一个集合来查看才有意义,那么我们可以通过将这些数据嵌入到联系人数据中来对其进行反规范化。
我们可以很容易地做到这一点,只需使用 JSON 字段向 contacts 表添加三个字段来嵌入数据并保持结构不变。回想一下,我们可以在代码中使用 JSON 文档,因此可以使用原始表中的所有字段名。当您这样做时,迁移代码会更容易,因为您将引用相同的数据名称。下面显示了使用混合解决方案对数据库的重新设计。
CREATE DATABASE IF NOT EXISTS `contact_list2`;
CREATE TABLE `contact_list2`.`contacts` (
`contact_id` int(11) NOT NULL AUTO_INCREMENT,
`first` char(30) DEFAULT NULL,
`last` char(30) DEFAULT NULL,
`addresses` json DEFAULT NULL,
`email_addresses` json DEFAULT NULL,
`phones` json DEFAULT NULL,
PRIMARY KEY (`contact_id`)
) ENGINE=InnoDB;
这里,我们通过将关系作为 JSON 数组添加来删除这三个表。但是等等,我们如何格式化这些 JSON 文档呢?这难道不是一个问题吗?不,不是真的。要迁移到混合解决方案,您需要将嵌入数据中的字段名作为 JSON 文档中的键。
我们还可以通过添加引用区号、交换机和电话号码的键来改进电话号码的数据。更好的是,如果我们后来发现需要添加国家代码值,我们可以为那些需要它们的联系人添加。请记住,JSON 文档的优点在于它们是可变的,您可以根据需要添加或删除字段。唯一的问题是,您的代码必须编写为预期的遗漏和新字段。因此,没有理由重组您的数据来为新联系人添加国家代码。
下面显示了如何使用上一节中显示的同一个 contact 来完成创建操作。
INSERT INTO contact_list2.contacts VALUES(
NULL, 'Bill', 'Smith',
'{"addresses":[{“address_type”:”home”, "street1":"123 Main Street","street2":"","city":"Anywhere","state":"VT","zip":12388}]}',
'{"email_addresses":["bill@smithmanufacturing.co","bill.smith@gomail.com"]}',
'{"phones":[{"area_code":301,"exchange":555,"number":1212}]}'
);
这导致从一个SELECT
查询返回一个有趣的行。我们在下面看到了结果。
MySQL localhost:33060+ SQL > SELECT * FROM contact_list2.contacts WHERE first = 'Bill' AND last = 'Smith' \G
*************************** 1\. row ***************************
contact_id: 1
first: Bill
last: Smith
addresses: {"addresses": [{"zip": 12388, "city": "Anywhere", "state": "VT", "street1": "123 Main Street", "street2": "", "address_type": "home"}]}
email_addresses: {"email_addresses": ["bill@smithmanufacturing.co", "bill.smith@gomail.com"]}
phones: {"phones": [{"number": 1212, "exchange": 555, "area_code": 301}]}
1 row in set (0.00 sec)
在这里,可以看到我们使用了与原始表中的字段相同的名称。访问数据变得更加容易,因为我们可以将代码从在行对象中查找列迁移到直接在代码中使用字段名。例如,为了显示嵌入的地址、电话和电子邮件列表,我们可以使用循环。清单 10-4 展示了一个完成这项工作的示例脚本。
import mysqlx
from json import JSONDecoder
GET_BILL = """
SELECT * FROM contact_list2.contacts
WHERE last = 'Smith' AND first = 'Bill'
"""
# Connect to database
session = mysqlx.get_session("root:password@localhost:33060")
# Read the row
row = session.sql(GET_BILL).execute().fetch_one()
# Convert JSON strings to Python dictionaries
addresses = JSONDecoder().decode(row["addresses"])["addresses"]
phones = JSONDecoder().decode(row["phones"])["phones"]
email_addresses = JSONDecoder().decode(row["email_addresses"])["email_addresses"]
# Display the data
print("Contact List (Hybrid)")
print("---------------------")
print("Name: {0} {1}".format(row["first"],row["last"]))
print("\nAddresses:")
for address in addresses:
print("\t({0})".format(address["address_type"].upper()))
print("\t{0}".format(address["street1"]))
if address["street2"]:
print("\t{0}".format(address["street2"]))
print("\t{0}, {1} {2}".format(address["city"],
address["state"],
address["zip"]))
print("\nPhones:")
for phone in phones:
print("\t({0}) {1}-{2}".format(phone["area_code"],
phone["exchange"],
phone["number"]))
print("\neMail Addresses:")
for email in email_addresses:
print("\t{0}".format(email))
print("")
Listing 10-4Sample Read Operation for Contact List (Hybrid)
请注意,代码可读性很好,我们可以确切地看到我们在循环中访问的是哪些数据。然而,在将 JSON 字符串转换成 Python 字典时,会有一些重复。这是因为我们在表中有一个字段,在 JSON 字符串中有一个同名的键。例如,有一个addresses
字段,JSON 文档中的键是addresses
。这可能看起来有点奇怪,但这正是您在字段中访问 JSON 文档的方式。有些人可能希望重命名字段或 JSON 键,使其不那么模糊。
下面显示了这段代码的执行过程。请注意,输出确实与您通过阅读代码看到的数据相似。使用制表符(\t
)有助于将字符串打印到控制台。
$ python ./hybrid_read.py
Contact List (Hybrid)
---------------------
Name: Bill Smith
Addresses:
(HOME)
123 Main Street
Anywhere, VT 12388
Phones:
(301) 555-1212
eMail Addresses:
bill@smithmanufacturing.co
bill.smith@gomail.com
这个解决方案更好,确实解决了删除连接和将数据保存在一起的问题,但是如果我们需要存储姓氏、后缀、头衔,或者如果联系人有两个以上的名字,该怎么办呢?同样,如果您发现需要找到在某个地区居住或工作的所有联系人,该怎么办?数据是存在的,但是因为电话号码是单个字符串,所以更难搜索数据(但不是不可能)。
我们可以解决这个问题(以及类似的问题),只需修改表格,将数据分成不同的字段。这是可行的,也是大多数开发者会做的。但是,您如何处理任何现有数据呢?你会用自己创造的特殊工具重新格式化吗?你没有任何选择,如果有大量的数据,转换可能是痛苦和耗时的。
遗憾的是,具有关系部分(表、字段)的混合解决方案仍然是固定的,因此混合解决方案不能避免变化。我们需要的是实现可变性——能够在我们需要的任何时候改变结构,而不必重新调整。如果你在想,“肯定有更好的方法”,你是对的——确实有。让我们看看如何通过将数据库转换为纯文档存储来克服这些问题。
转换到文档存储
也许文档存储解决方案的最佳属性是可变性。这一点,再加上“数据即代码”的概念,使得使用文档存储比使用关系数据库容易得多。尽管我们在联系人数据库的混合解决方案中看到了一些改进,但是我们还没有到可以实现可变性的地步。
具体来说,我们在混合解决方案中仍然有固定的字段。如果这些字段是完整的集合(对于所有时间),我们可能会对混合解决方案感到满意。但是如果你在一个国际化的环境中工作,你会发现存储名字和姓氏太随意了,在某些情况下是不够的。
比如热拉尔多·何塞·米格尔·戈麦斯。你用这样的名字做什么?将名称任意拆分,将部分放在两个字段中?如果这个人用米格尔作为他的名字呢?现在,你的数据库将列出他的名字为“米盖尔”,“热拉尔多·何塞·戈麦斯”,这是不正确的。此外,如果您以这种方式拆分姓名,那么任何关于名字或姓氏的查询都会导致不正确的结果,或者至少会在查询之后进行额外的解析来解决这些异常。
如果我们使用文档存储,我们可以添加任何我们需要的字段。我们只需要保持代码和数据同步。也就是说,当我们添加新字段时,我们也必须在 CRUD 操作中添加代码来进行补偿。例如,向文档中添加昵称字段很容易,但是读取和显示数据的代码必须允许使用昵称。最重要的是,我们可以添加更改,而不必修改任何现有的数据。
这就是我们的目标:让您的数据无模式化,并与代码紧密集成。一旦你接受了这种心态,你会发现正常化的耻辱很容易被抛弃。尽管这并不意味着所有的无模式解决方案都优于它们的关系型解决方案(事实上不太可能),但这确实意味着您可以让数据为您服务,而不是与您作对。开发者尤其会喜欢这种自由。
正如我们在第 9 章和本书前面所学的,我们可以用代码轻松地创建一个集合。回想一下,这包括连接到服务器、获取模式对象实例和创建集合。然后,我们可以创建、阅读、更新或删除集合中的文档。图 10-2 显示了使用 MySQL Shell 为联系人列表创建模式和集合的快照。我们还添加了我们作为示例使用的行。
图 10-2
Creating a document store
在这里,我们看到我们已经将僵化的关系数据库模型迁移到一个可变的 JSON 文档中,其中嵌入了数据,可以将每个联系人的所有数据保存在一起。
现在,让我们看看在文档存储上执行读操作的代码。清单 10-5 显示了从集合中读取文档并将其打印到控制台的代码。
import mysqlx
# Connect to server
session = mysqlx.get_session("root:password@localhost:33060")
# Get the schema
schema = session.get_schema("contact_list3")
# Get the collection
contacts = schema.get_collection("contacts")
# Read the row
row = contacts.find("first = '{0}' and last = '{1}'".format('Bill',
'Smith')).execute()
contact = row.fetch_one()
addresses = contact["addresses"]
phones = contact["phones"]
email_addresses = contact["email_addresses"]
# Display the data
print("Contact List (DocStore)")
print("-----------------------")
suffix = ""
if "suffix" in contact.keys():
suffix = ", {0}".format(contact["suffix"])
print("Name: {0} {1}{2}".format(contact["first"],contact["last"],suffix))
if "title" in contact.keys():
print("Title: {0}".format(contact["title"]))
print("\nAddresses:")
for address in addresses:
print("\t({0})".format(address["address_type"].upper()))
print("\t{0}".format(address["street1"]))
if "street2" in address.keys():
print("\t{0}".format(address["street2"]))
print("\t{0}, {1} {2}".format(address["city"],
address["state"],
address["zip"]))
print("\nPhones:")
for phone in phones:
print("\t({0}) {1}-{2}".format(phone["area_code"],
phone["exchange"],
phone["number"]))
print("\neMail Addresses:")
for email in email_addresses:
print("\t{0}".format(email))
print("")
Listing 10-5Sample Read Operation for Contact List (Document Store)
注意,代码与混合解决方案非常相似。实际上,打印部分是相同的。这种差异在检索数据的早期就能看出来。在这种情况下,我们可以检索文档,然后将地址、电话和电子邮件地址存储为字典,这使得代码更容易阅读。非常好!
下面显示了正在执行的代码。如您所见,输出与混合解决方案相同。
$ python ./docstore_read.py
Contact List (DocStore)
-----------------------
Name: Bill Smith, Jr
Title: Salesman
Addresses:
(HOME)
123 Main Street
Anywhere, VT 12388
Phones:
(301) 555-1212
eMail Addresses:
bill@smithmanufacturing.co
bill.smith@gomail.com
现在我们已经讨论了什么是无模式思维,让我们回顾一下使用 MySQL 文档库的一些技巧和诀窍。
文档存储提示和技巧
下面包含了使用 MySQL Document Store 规划、开发和管理应用的一些最佳实践。有些可能看起来很直观,而其他的可能只是提醒你去做那些我们都知道应该做的事情,但有时为了简洁而走捷径。它们以项目符号列表的形式呈现,旨在成为一种资源,您可以在迁移或开发工作开始时定期参考。
- 最小化连接:连接可能很昂贵。减少需要联接数据的位置有助于加快查询速度。移除连接可能会导致某种程度的反规范化,但可以更快地访问数据。
- 可变性计划:无模式设计关注可变性。构建能够根据需要(并在合理的范围内)修改文档的应用。
- 消除多对多关系:使用嵌入式数组和列表来存储文档之间的关系。这可以像在文档中嵌入数据或在文档中嵌入文档 id 数组一样简单。在第一种情况下,您一阅读文档就可以获得数据,而在第二种情况下,只需要一个额外的步骤来检索数据。在很少读取(使用)关系的情况下,将数据与一个 id 数组链接会更有效(第一遍读取的数据更少)。
- 避免过度反规范化:反规范化可能走得太远。如果以复制为代价,通过在文档中嵌入内容来使数据非规范化,您可能会在某个时候发现需要更改复制的数据。如果发生这种情况,你已经越界了,现在你的数据更新噩梦开始了。因此,无论何时进行反规范化,都要考虑数据将如何(或是否)更新。如果它可以单独更新(比如只对一个或多个文档进行更新),并且这些更改不需要存在于其他文档中,那么您的反规范化应该没问题。但是,如果您认为需要更新所有的嵌入数据,那么您必须考虑将数据移动到另一个集合中,并使用嵌入列表按 id 链接文档。
- 了解您的数据:这听起来可能很明显,但是您必须了解您在设计中使用的数据。不仅仅是它可以(或者必须)包含什么,而且是它将如何被使用。通常,关系数据库设计者只关心检索数据任何部分的能力,而忽略了如何使用数据。因此,在关系数据库世界中,我们经常发现自己在应用和数据设计好之后优化查询。在无模式的世界中,我们必须从一开始就关注如何使用数据,这样我们就可以将相关的数据一起存储在一个单独的或者有时是链接的文档中。了解您的数据将如何在应用中使用会对您形成文档的方式产生影响。它还可以帮助您在开始编写代码之前确定如何编写代码来检索数据。
- 避免大文档:将所有数据存储在一个文档中确实是无模式设计的目标之一,但是这也必须在使用时有一些判断。如果您的文档非常大,那么在尝试检索多个文档(比如一个列表或对一组文档执行操作)时,可能会遇到性能问题。因此,您应该考虑何时使用文档的哪些部分。您可能会发现,您可以将您的文档拆分成几个更小的文档(每个文档位于各自的集合中)。这样,您可以优化大多数操作的检索,只在需要时才检索不常用的数据。
- 使用 JSON 列在表中嵌入数据:如果希望通过减少连接的数量来改进现有的关系数据库,可以使用 JSON 字段来嵌入数据。例如,通过存储指向依赖表的指针(键)数组,使用 JSON 字段折叠多对多连接表。一个明显的候选是具有编码数据的 BLOB 字段的文本。
摘要
MySQL 文档存储和服务器的最新化身 MySQL 8 代表了功能性、可靠性和可用性方面的巨大飞跃。最重要的是,MySQL 8 不会强迫你进入一个新的范式。通过 X DevAPI 的 NoSQL 选项完美地补充了 NDB 集群的高级集群和可用性。但是与 NDB 集群不同,您可以使用现有的 MySQL 服务器。 5
这意味着,您不必学习和完全改造您的基础设施和应用,就可以使用最新的功能。我们在本书和第 9 章的实践中看到,您可以选择使用 MySQL 8 作为传统的关系数据库存储,将您的应用迁移到具有一个或多个 JSON 字段的关系数据的混合体,或者通过迁移到纯文档存储解决方案来彻底重新思考您的数据。
事实上,MySQL 使得应用的迁移变得很容易,因为 X DevAPI 同时支持 SQL 和 NoSQL 接口。因此,第一步是将所有基于 SQL 接口的应用迁移到 X DevAPI,然后您可以选择将这些应用迁移到混合或纯文档存储解决方案。
对于 MySQL 用户来说,这是一个激动人心的时刻。Oracle 继续信守承诺,不仅继续开发 MySQL,还投入资源改进和扩展特性集。请密切关注更多优秀的特性以及进一步的改进和更新。MySQL 8 已经发布,现在是时候加入了。在 MySQL 8 上寻找更多的标题!
Footnotes 1
对于数据库或 web 管理员来说,接到电话(通常在半夜)来解决升级中出现的问题总是令人震惊的——尤其是当他们不知道已经计划了这样的升级时!是的,这确实发生了——太频繁了。
小心正常形态的狂热者!他们的整个世界都围绕着达到第五范式的禅宗般的状态。可悲的是,他们可能会错过反规范化的要点。
提示:如果您需要查找居住在某个区号内的所有联系人,该怎么办?您将如何编写查询?
两代或更多代人(或其中的成员)住在同一个家里并不罕见。随着医疗和长期护理成本的上升,这种情况只会变得更加普遍。
NDB 群集需要几台安装了 NDB 群集服务器的服务器。请参阅在线 MySQL 参考手册中的 NDB 集群部分了解更多详细信息。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战