MySQL-Connection-Python-揭秘-四-

94 阅读1小时+

MySQL Connection/Python 揭秘(四)

原文:MySQL Connector/Python Revealed

协议:CC BY-NC-SA 4.0

八、SQL 表

到目前为止,X DevAPI 的主要焦点是在 MySQL 文档存储中使用它。然而,正如第六章所解释的,它也支持“传统的”SQL 表。事实上,有两个接口可以处理 SQL 表。第七章的焦点中的创建-读取-更新-删除(CRUD)动作也存在于 SQL 表中,并提供了一个 NoSQL API。此外,X DevAPI 可用于执行任意 SQL 语句。

本章将首先概述使用 SQL 表时的工作流程,然后介绍 X DevAPI 如何通过 NoSQL API 使用 SQL 表。本章的第二部分将介绍执行 SQL 语句的接口。

工作流程

在 NoSQL API 中使用 SQL 表的工作流程与您在上一章中看到的集合非常相似。然而,当使用 SQL 语句时,工作流有些不同。在研究细节之前,有必要先了解一下一般的工作流程。

图 8-1 显示了从模式开始到结果结束的工作流程。主要关注的是获得语句对象之前的步骤。本章将详细介绍如何使用这些语句。红色(深灰色)框是被调用的方法。

img/463459_1_En_8_Fig1_HTML.jpg

图 8-1

使用 SQL 表时的工作流概述

该图从模式对象开始。有四种不同的方式继续;然而,实际上只有两条不同的路径。get_collection_as_table()用存储集合的 SQL 表返回一个 table 对象,由get_view()返回的 view 对象是一个 table 的子类,唯一的区别是用于检查对象是否存在于数据库中的查询。因此,在大多数情况下,集合(如表、视图和表)可以被视为同一事物。

一旦有了 table 对象,就可以用它来执行 insert、select、update 或 delete 语句。除了count()方法(没有包含在图中),没有像您看到的用于集合的其他方法。一旦执行了该语句,select 语句将返回一个行结果,其他方法将返回一个没有行的结果。

工作流中最大的不同是 SQL 语句,它们是直接从 schema 对象创建的。不管查询类型如何,SQL 语句总是返回 SQL 结果。

工作流就绪后,是时候看看用于 SQL 表的 NoSQL API 了。

小费

本章中有许多示例程序。列表中出现的所有示例程序都可以下载。有关使用示例程序的更多信息,参见第一章中对示例程序的讨论。一般来说,更改会被回滚,以允许重新执行示例并得到相同的结果。

SQL 表的 NoSQL API

在前一章中,您使用了文档存储,这意味着纯粹使用 NoSQL API 来管理。然而,也可以继续使用 SQL 表,但避免编写 SQL 语句。NoSQL 表 API 类似于文档存储 API,本节的讨论假设您熟悉第六章中描述的 X DevAPI。

表格 API 是本节的主题。首先,您将获得一个表对象,并获得关于该对象的信息。然后,表对象将用于执行查询。

注意

对表 API 的讨论与用于处理集合的 API 有相当大的重叠。建议在本章之前阅读第六章,以便从这个表 API 讨论中获得最大收益。

表和视图对象

表对象相当于集合对象;不同之处在于,table 对象被设计用于一般的 SQL 表,而集合专用于带有存储文档的 JSON 列的表。事实上,如果您愿意,您可以请求以表的形式检索集合。视图对象与表对象非常相似,但它是针对视图(虚拟表)而不是基表的。除非明确指出,否则表和视图的行为是相同的。

获取表格对象有三种不同的方法。它们在表 8-1 中列出。返回的对象是相对于mysqlx模块的。

表 8-1

模式表方法

|

方法

|

争论

|

描述

| | --- | --- | --- | | get_collection_as_table | Name | 将具有指定名称的集合作为crud.Table对象返回。如果check_existence = True,如果集合不存在,则发生ProgrammingError异常。 | | check_existence=False | | get_table | name | 以一个crud.Table对象的形式返回指定名称的表格。get_table()方法也可以用来打开一个作为crud.Table对象的视图。如果check_existence = True,如果表格不存在,则发生ProgrammingError异常。 | | check_existence=False | | get_ tables |   | 返回架构中表和视图的列表。列表中的每个元素都是crud.Table类的一个实例。 | | get_view | name | 以一个crud.View对象的形式返回具有指定名称的视图。如果check_existence = True,如果视图不存在,将出现ProgrammingError异常。 | | check_existence=False |

get_table()get_tables()方法相当于前一章中的get_collection()get_collections()方法。没有创建表、更改表定义或添加索引的方法。这必须使用 SQL 语句来完成。

有许多方法可以获得关于表对象的信息。这些方法总结在表 8-2 中。返回的对象是相对于mysqlx模块的。这些方法都不需要参数。

表 8-2

表和视图实用程序方法

|

方法

|

返回对象

|

描述

| | --- | --- | --- | | exists_in_ database |   | 根据表格是否存在,返回TrueFalse。 | | get_ connection | connection.Connection | 返回基础连接对象。 | | get_ name |   | 返回表的名称。这与name属性相同。 | | get_ schema | crud.Schema | 返回表所在架构的对象。这与schema属性相同。 | | get_ session | connection.Session | 返回会话的对象。这与会话属性相同。在 8.0.12 版本中添加。 | | is_ view |   | 根据表格对象是否为视图,返回TrueFalse。这是基于数据库中的实际定义,而不是使用了get_table()还是get_view()来获取对象。 |

模式对象、会话对象(8.0.12 和更高版本)和表名也可以分别在schemasessionname属性中找到。到目前为止,除了表不具备集合的所有特性之外,集合之间的差别很小。对于查询,有一些区别,我们来看看。

表查询

使用 table 对象和 NoSQL 方法查询表在某种程度上是使用 SQL 语句和文档存储创建、读取、更新和删除(CRUD)方法的混合。还不支持连接表或更高级的 SQL 特性,如通用表表达式(cte)和窗口函数。可用的方法也属于 CRUD 功能之一。到目前为止,这听起来又像是集合,但是方法名反映了用于操作的 SQL 语句。

注意

使用表对象执行的查询主要是针对使用 UTF-8 字符集的表。还有一些更复杂的结构;例如,转换列的字符集,这是不支持的。在这些情况下,您必须使用直接 SQL 语句,如本章后面所述。

表格 CRUD 方法可在表格 8-3 中找到。除了count()方法之外,所有的方法都返回对应于查询类型的语句对象。语句对象类都在mysqlx.statement模块中。

表 8-3

表和视图对象的 CRUD 方法

|

方法

|

争论

|

返回对象

|

描述

| | --- | --- | --- | --- | | count |   |   | 以整数形式返回表中的行数。 | | delete |   | DeleteStatement | 创建 delete 语句。 | | insert | *字段 | InsertStatement | 创建 insert 语句,在该语句中为指定的字段设置值。 | | select | *字段 | SelectStatement | 创建将检索指定字段的 select 语句。 | | update |   | UpdateStatement | 创建更新语句。 |

虽然对于那些习惯于编写 SQL 语句的人来说,方法名听起来非常熟悉,但是它们的用法需要更多的解释。本节的剩余部分将逐一介绍 CRUD 方法。count()方法将作为insert()select()update()delete()方法的补充。

CRUD:创建

CRUD 的第一部分是创建数据。对于 SQL 表,这是将数据插入到表中,因此创建数据的 table 对象方法就是insert()方法。这是下面讨论的话题。

insert()方法的参数是您将为其提供数据的字段。该方法返回一个mysqlx.statement.InsertStatement类的对象。该对象可用于添加值和执行 insert 语句。表 8-4 中列出了InsertStatement类的重要方法。

表 8-4

insert 语句方法

|

方法

|

争论

|

描述

| | --- | --- | --- | | execute |   | 执行 insert 语句,并将结果作为一个result.Result对象返回。 | | values | *values | 要添加到 insert 语句中的一行的值。values()可以被多次调用来插入几行。每行的值必须与字段的顺序相同。 |

InsertStatement对象有两个属性:

  • 模式:插入语句的模式对象

  • 目标:插入语句的表对象

这些属性的主要用途是当 insert 语句被传递给其他函数或方法时,返回到父对象。

要查看插入数据的工作流程,请参见清单 8-1 。这是一个在world.city表中插入两个城市的例子。返回结果后,将打印插入的行数和第一个生成的自动递增 ID。在 insert 语句前后,表中的行数是用Table. count()方法确定的。

import mysqlx
from config import connect_args

fmt = "{0:28s}: {1:4d}"

db = mysqlx.get_session(**connect_args)

# Get the world.city table
schema = db.get_schema("world")

city = schema.get_table("city")

db.start_transaction()

print(fmt.format(
  "Number of rows before insert",
  city.count()

))

# Define the insert statement

insert = city.insert(

  "Name",
  "CountryCode",
  "District",
  "Population"

)

# Add row using a list
darwin = [
  "Darwin",
  "AUS",
  "Northern Territory",
  145916
]

insert.values(darwin)

# Add row by arguments

insert.values(

  "Sunshine Coast",
  "AUS",
  "Queensland",
  302122

)

# Execute the insert

result = insert.execute()

# Get the auto-increment ID generated
# for the inserted row
print(fmt.format(
  "Number of rows inserted",
  result.get_affected_items_count()))
print(fmt.format(
  "First ID generated",
  result.get_autoincrement_value()))

print(fmt.format(
  "Number of rows after insert",
  city.count()))

# Reset the data
db.rollback()
db.close()

Listing 8-1Inserting Rows into a Table Using the 
insert()

Method

首先,使用get_table()模式方法获得表对象。insert 语句是使用insert()方法创建的,您为其指定值的四个字段作为参数给出。如果在代码流中更好地工作,字段也可以作为列表或元组给出。city表还有一个ID字段,默认情况下,它被赋予一个自动递增的值。在这个例子中,您为ID字段使用默认行为。

使用values()方法将这两行添加到 insert 语句中。Darwin 是通过首先用值创建一个元组来添加的,然后这个元组被传递给values()。阳光海岸是通过将每个字段值作为单独的参数传递给values()来添加的。

最后,使用execute()方法插入两行。返回的result.Result对象可用于检查插入的行数和生成的第一个要插入的行的自动增量 ID(Darwin)。不可能为后面的行获取自动递增 ID。

运行该示例的输出示例如下

Number of rows before insert: 4079
Number of rows inserted     :    2
First ID generated          : 4111
Number of rows after insert : 4081

生成的第一个 ID 取决于之前如何使用city表,因此通常与示例输出不同。数据插入到表中后,让我们看看如何再次检索它。

读取

read 语句是大多数数据库的主力。对于表和视图对象,select()方法用于从底层数据库对象获取数据。本节将展示如何做到这一点。

select()方法获取应该从表中检索的字段。如果没有指定字段,将包含所有字段;这相当于SELECT * FROM。该方法返回一个可用于细化查询的mysqlx.statement.SelectStatement对象。

可用于进一步定义查询的SelectStatement方法在表 8-5 中列出。它们可用于指定要包含的行必须满足的条件、要返回多少行、分组等。

表 8-5

修改 Select 语句的方法

|

方法

|

争论

|

描述

| | --- | --- | --- | | where | condition | 这是过滤查询结果的条件。 | | group_ by | *fields | 描述对于涉及聚合函数的查询,要对哪些字段进行分组。 | | having | condition | 描述在查询以其他方式得到解决后,根据什么来筛选结果(排序除外)。这对于按聚合函数的值进行过滤非常有用。 | | order_ by | *clauses | 描述结果的排序依据。 | | limit``offset | row_count``offset=0 | 第一个参数设置要返回的最大行数。第二个可选参数定义偏移量。默认偏移量为 0。**注意:**这在版本 8.0.12 中有所改变,其中 offset 已被弃用。请改用offset()方法。设置要返回的行的偏移量。在 8.0.12 版本中添加。 | | offset | | lock_ exclusive |   | 使语句获得独占锁。一次只能有一个语句拥有独占锁。如果稍后将在同一事务中更新行,请使用此选项。 | | lock_ shared |   | 使语句获取共享锁。这可以防止其他语句修改匹配的行,但是它们可以读取这些行。 | | bind | *args | 第一个参数提供要替换的参数的名称。第二个参数提供值。每个参数调用bind()一次。 | | execute |   | 执行 select 语句。 |

一旦语句被完全定义,就可以使用execute()方法来执行。这将返回一个RowResult对象,该对象可用于获取关于结果的信息并获取行。

例如,考虑一个 select 语句,其中找到美国人口超过 1,000,000 的城市,并按州进行分组(District字段)。找到了每个州的城市数量和最大城市的人口。根据城市数量,然后根据人口最多的城市,对结果进行降序排序。这个例子可以在清单 8-2 中看到。

import mysqlx
from config import connect_args

db = mysqlx.get_session(**connect_args)

# Get the world.city table
schema = db.get_schema("world")
city = schema.get_table("city")

db.start_transaction()

statement = city.select(

  "District",
  "COUNT(*) AS NumCities",
  "MAX(Population) AS LargestCityPop")

statement.where(

  "CountryCode = :country"
  + " AND Population > :min_pop")

statement.group_by("District")

statement.order_by(

  "NumCities DESC",
  "LargestCityPop DESC")

statement.bind("country", "USA")

statement.bind("min_pop", 1000000)

print("SQL statement:\n{0}"
  .format(statement.get_sql()))

result = statement.execute()
print("Number of rows in result: {0}\n"
  .format(result.count))

fmt = "{0:12s}   {1:6d}   {2:12d}"
print("{0:12s}   {1:6s}   {2:12s}"
  .format(
    "State",
    "Cities",
    "Largest City"
))
print("-"*37)

for row in result.fetch_all():

  print(fmt.format(
    row["District"],
    row["NumCities"],
    row["LargestCityPop"]
  ))

print("")
print("Number of rows in result: {0}\n"
  .format(result.count))

db.commit()
db.close()

Listing 8-2Example of Using a Select Statement

检索到表对象后,创建语句并定义结果中应包含的字段。然后使用where()方法设置一个过滤器,并设置分组和排序。传递给where()方法的值使用两个命名参数countrymin_pop,以允许稍后使用bind()方法绑定这些值。这确保了报价对于所提供的类型是正确的(但并不确保它是正确的类型!)并允许您在需要再次执行相同的查询(但两个参数的值不同)时重用语句的其余部分。

在执行语句之前,打印生成的 SQL。get_sql()方法是SelectStatement类独有的。该方法采用SelectStatement并构建 SQL 语句,该语句将作为已定义查询的结果来执行。如果您希望通过 MySQL Shell 手动执行查询,或者需要将生成的 SQL 与查询所基于的 SQL 语句进行比较,这将非常有用。

获得结果后,打印结果中的行数(在检索结果后再次打印)。使用fetch_all()方法获得结果,该方法返回一个result.Row对象。在打印结果时,Row对象可以用作字典。输出是

SQL statement:
SELECT District,COUNT(*) AS NumCities,MAX(Population) AS LargestCityPop FROM world.city WHERE CountryCode = :country AND Population > :min_pop GROUP BY District ORDER BY NumCities DESC,LargestCityPop DESC

Number of rows in result: 0

State          Cities   Largest City
------------------------------------
Texas               3        1953631
California          2        3694820
New York            1        8008278
Illinois            1        2896016
Pennsylvania        1        1517550
Arizona             1        1321045

Number of rows in result: 6

输出中最值得注意的是,结果中的行数在检索结果之前报告为 0,而在检索结果之后报告为 6。这在第六章中针对RowResult对象进行了讨论:count 属性是在使用fetch_all()方法读取行时设置的。

现在让我们更新表格中的数据。

CRUD:更新

对表的更新用新值替换一个或多个字段的值。与文档不同,无法添加新字段或删除现有字段;这些操作需要更改表定义。下面的讨论将详细介绍表更新在 MySQL Connector/Python 的 CRUD 世界中是如何工作的。

update()方法本身不接受任何参数。调用它的唯一目的是创建一个可以用来定义更新的UpdateStatement对象。做这件事的方法可以在表 8-6 中看到。

表 8-6

方法来定义更新语句

|

方法

|

争论

|

描述

| | --- | --- | --- | | set | field | 为给定字段设置新值。该值必须是标量。 | | value | | where | condition | 这是筛选应该更新哪些行的条件。 | | order_by | *clauses | 描述行的更新顺序。在 8.0.12 版本中添加。 | | sort | *sort_clauses | 描述行的更新顺序。**注意:**这在 8.0.12 版本中已被弃用;用order_by()代替。 | | limit | row_count | 该参数设置要更新的最大行数。 | | bind | *args | 第一个参数提供要替换的参数的名称。第二个参数提供值。每个参数调用bind()一次。 |

需要注意的一点是,绑定参数不能与set()方法一起使用。如果新值是基于用户输入的,请确保像往常一样验证输入。该语句在执行时返回一个result.Result对象。

警告

在语句中使用用户输入之前,请始终验证用户输入。这不仅适用于 update 语句,甚至适用于 X DevAPI。这应该是处理用户输入的标准部分。

执行该语句时,必须定义一个where条件。否则,将会出现 ProgrammingError 异常:

mysqlx.errors.ProgrammingError: No condition was found for update

这是一种安全预防措施,旨在避免意外更新表中的所有行。如果您真的想更新所有的行,将where()条件设置为True或类似的评估为True的条件:

update.where(True)

该要求不仅意味着您不会因为缺少条件而错误地更新所有行,设置为True的条件还有助于证明您确实想要更新所有行。

清单 8-3 展示了一个城市人口被更新的例子。在更新之前和之后打印填充,以验证 update 语句的效果。

import mysqlx
from config import connect_args

db = mysqlx.get_session(**connect_args)

# Get the world.city table
schema = db.get_schema("world")
city = schema.get_table("city")

db.start_transaction()

# Check the population before the update
select = city.select()
select.where(
  "Name = :city"
  + " AND CountryCode = :country"
)
select.bind("city", "Sydney")
select.bind("country", "AUS")
result = select.execute()
sydney = result.fetch_one()
print("Old population: {0}".format(
  sydney["Population"]))

# Define the update

update = city.update()

update.set("Population", 5000000)

update.where(

  "Name = :city"
  + " AND CountryCode = :country")

update.bind("city", "Sydney")

update.bind("country", "AUS")

result = update.execute()

print("Number of rows updated: {0}"
  .format(
    result.get_affected_items_count())
)

# Check the affect
result = select.execute()
sydney = result.fetch_one()
print("New population: {0}".format(
  sydney["Population"]))

# Reset the data
db.rollback()
db.close()

Listing 8-3Using an Update Statement

定义 update 语句的步骤遵循 X DevAPI CRUD 方法的通常模式。该程序的输出是

Old population: 3276207
Number of rows updated: 1
New population: 5000000

这就剩下了最后一个 CRUD 方法:如何删除行。

CRUD:删除

关于 CRUD 方法,最后要讨论的是如何删除表中的行。这是通过使用带有可选过滤条件、排序和限制的delete()表格方法完成的。

delete()方法采用一个可选参数来定义过滤行的条件,该方法返回一个DeleteStatement对象。可以通过DeleteStatement对象的方法对删除语句进行进一步的细化。这些方法在表 8-7 中列出。

表 8-7

方法来定义 Delete 语句

|

方法

|

争论

|

描述

| | --- | --- | --- | | where | condition | 这是筛选应该删除哪些行的条件。 | | order_by | *clauses | 描述删除行的顺序。在 8.0.12 版本中添加。 | | sort | *sort_clauses | 描述删除行的顺序。**注意:**这在 8.0.12 版本中已被弃用;use order_by()相反。 | | limit | row_count | 该参数设置要删除的最大行数。 | | bind | *args | 第一个参数提供要替换的参数的名称。第二个参数提供值。每个参数调用bind()一次。 | | execute |   | 执行 delete 语句。 |

如果您很好地掌握了其他 CRUD 方法,delete 语句的使用就很简单了。与update()方法一样,必须设置一个条件;否则出现ProgrammingError:

mysqlx.errors.ProgrammingError: No condition was found for delete

如果需要删除表中的所有行,将条件设置为True。或者,您可以使用TRUNCATE TABLE SQL 语句重新创建表。

清单 8-4 展示了一个删除所有人口少于 1000 的城市的例子。

import mysqlx
from config import connect_args

fmt = "{0:22s}: {1:4d}"

db = mysqlx.get_session(**connect_args)

# Get the world.city table
schema = db.get_schema("world")
city = schema.get_table("city")

db.start_transaction()

# Check the number of rows before
# deleting rows.
print(fmt.format(
  "Number of rows before",
  city.count()
))

# Define the update

delete = city.delete()

delete.where("Population < :min_pop")

delete.bind("min_pop", 1000)

result = delete.execute()

print(fmt.format(
  "Number of rows deleted",
  result.get_affected_items_count()
))

# Check the affect
print(fmt.format(
  "Number of rows after",
  city.count()
))

# Reset the data
db.rollback()
db.close()

Listing 8-4Deleting Rows from a Table

使用where()方法指定过滤条件。它也可以在第一次创建 delete 语句时指定。输出是

Number of rows before : 4079
Number of rows deleted:   11
Number of rows after  : 4068

关于 SQL 表的 CRUD 方法的讨论到此结束。如上所述,这些方法目前有一些局限性。如果您需要生成不受支持的查询,仍然可以使用 SQL 语句,这将在本章的剩余部分讨论。

SQL 语句

到目前为止,关于 X DevAPI 的讨论都是关于 CRUD 方法,无论是文档集合还是 SQL 表。优秀的旧 SQL 语句哪里去了?他们仍然在这里,这就是本节的内容。

在某些方面,我把最简单的留到了最后,但它也是当前执行最多不同查询的一个。这两件事是相关的,因为 SQL 语句并没有对您在特定 MySQL 服务器版本限制之外的用途施加任何限制。这也意味着不可能在相同的程度上知道每个语句是关于什么的,因此也不可能具体地知道它是如何工作的。这使得它比 CRUD 方法更简单。当然,代价是,它在更大程度上是由开发商来照顾的事情。

小费

X DevAPI 的 SQL 语句功能远不如使用mysql.connector模块时完整。如果您需要的不仅仅是简单的查询,建议使用第 2 到第五章中描述的方法。这包括所有需要参数的情况。

本节将介绍如何使用 X DevAPI 执行 SQL 语句。

执行 SQL 语句

执行 SQL 语句非常简单。使用sql()方法直接从会话中创建 SQL 语句,该方法执行 SQL 语句。返回SqlStatement对象。

SqlStatement类很简单,只有两个方法,总结在表 8-8 中。这两种方法都不带任何参数。

表 8-8

方法来处理 SQL 语句

|

方法

|

返回

|

描述

| | --- | --- | --- | | execute | result.SqlResult对象 | 将查询发送到 MySQL 服务器。返回一个mysqlx.result.SqlResult类的对象。 | | is_doc_ based | Boolean | 该语句是否用于集合。它总是为一个SqlStatement返回False,当一个方法或函数可以处理几种不同类型的语句时最有用。 |

结果对象总是属于SqlResult类,它结合了对修改数据或模式的查询和获取数据的查询有用的信息。

清单 8-5 显示了一个查询德国各州的例子,其中至少有一个城市的人口超过 500,000。

import mysqlx
from config import connect_args

db = mysqlx.get_session(**connect_args)

sql = db.sql("""

SELECT CONVERT(District USING utf8mb4)

         AS District,
       COUNT(*) AS NumCities
  FROM world.city
 WHERE CountryCode = 'DEU'
       AND Population > 500000
 GROUP BY District
 ORDER BY NumCities DESC, District""")

result = sql.execute()

fmt = "{0:19s}   {1:6d}"
print("{0:19s}   {1:6s}".format(
  "State", "Cities"))
print("-"*28)

row = result.fetch_one()

while row:
  print(fmt.format(
    row["District"],
    row["NumCities"]
  ))
  row = result.fetch_one()

db.close()

Listing 8-5Querying Data with a SELECT SQL Statement

首先要注意的是,在查询中,District列被显式转换为utf8mb4。如上所述,这样做的原因是 X DevAPI 只希望返回 UTF-8 数据。如果未完成此转换,将返回一个错误:

UnicodeDecodeError: 'utf-8' codec can't decode byte 0xfc in position 7: invalid start byte

这是另一个更好使用mysql.connector模块中的遗留 API 的例子。如示例所示,解决方法是转换查询中的数据。

结果的处理类似于select() CRUD 方法的结果。可以使用fetch_one()方法一次获取一行,或者使用fetch_all()方法获取所有行。在这种情况下使用前者。输出是

State                 Cities
----------------------------
Nordrhein-Westfalen        5
Baden-Württemberg          1
Baijeri                    1
Berliini                   1
Bremen                     1
Hamburg                    1
Hessen                     1
Niedersachsen              1

SqlStatment类还有一个值得讨论的特性:如何处理返回多个结果集的查询。

具有多个结果集的查询

在第四章中,您看到了在mysql.connector模块中使用遗留 API 时处理来自一个查询的多个结果集。mysqlx模块中的 X DevAPI 也可以处理多个结果集,但是没有额外的功能。

第一个结果集的处理方式与上一个示例相同。不同之处在于,一旦处理了第一个结果,就可以使用SqlResult.next_result()方法重新初始化该结果。这允许您处理下一个结果。根据是否有更多的结果要处理,next_result()方法返回TrueFalse

为了了解这在实践中是如何工作的,考虑清单 8-6 中的world.top_cities存储过程。这类似于第四章中使用的存储过程。

DROP PROCEDURE IF EXISTS world.top_cities;
DELIMITER $$
CREATE PROCEDURE world.top_cities(
    IN in_country char(3)
)
SQL SECURITY INVOKER
BEGIN
  SELECT Name, District, Population
    FROM world.city
   WHERE CountryCode = in_country
         AND Population
   ORDER BY Population ASC
   LIMIT 3;

  SELECT Name, District, Population
    FROM world.city
   WHERE CountryCode = in_country
         AND Population
   ORDER BY Population DESC
   LIMIT 3;
END$$
DELIMITER ;

Listing 8-6The world.top_cities Stored Procedure

该过程返回两个结果集:首先,找到该国人口最少的三个城市,然后找到人口最多的三个城市。清单 8-7 展示了一个处理两个结果集的例子。

import mysqlx
from config import connect_args

db = mysqlx.get_session(**connect_args)

sql = db.sql(

  "CALL world.top_cities('USA')")
result = sql.execute()

fmt = "{0:11s}   {1:14s}   {2:10d}"
print("{0:11s}   {1:14s}   {2:10s}"
  .format(
    "City", "State", "Population"
  )
)

more = True

while more:

  print("-"*41)
  row = result.fetch_one()
  while row:
    print(fmt.format(
      row["Name"],
      row["District"],
      row["Population"]
    ))
    row = result.fetch_one()
  more = result.next_result()

db.close()

Listing 8-7Handling Multiple Result Sets in an X DevAPI SQL Statement

查询照常执行。第一个结果集被正常处理。它被包裹在一个while循环中。处理完第一个结果后,通过调用next_result()来搜索更多的结果。这也将重置结果对象以处理下一个结果。一旦处理完所有结果集,next_result()返回False,循环终止。输出是

City          State            Population
-----------------------------------------
Charleston    South Carolina        89063
Carson        California            89089
Odessa        Texas                 89293
-----------------------------------------
New York      New York            8008278
Los Angeles   California          3694820
Chicago       Illinois            2896016

MySQL 连接器/Python X DevAPI 中关于 SQL 语句的讨论到此结束。

摘要

本章介绍了如何在 X DevAPI 中使用 SQL 表。有两种选择:使用 NoSQL CRUD 方法或执行 SQL 语句。

用于 SQL 表的 NoSQL CRUD 接口非常相似,但比前一章中用于 MySQL 文档存储的接口更简单。CRUD 方法是根据执行该方法底层操作的 SQL 语句来命名的。例如,为了读取数据,使用了select()方法。不支持使用 NoSQL API 更改 SQL 表的模式。

支持使用mysqlx.Session.sql()方法执行任意 SQL 语句。它对于简单的查询很有用;然而,对于更复杂的任务以及向查询中添加用户输入时,建议使用mysql.connector模块的方法。

本章从 MySQL 连接器/Python 的角度完成了 X DevAPI 的演练。还剩下两项非常重要的任务:处理错误和对 MySQL Connector/Python 程序进行故障排除。接下来将讨论这些主题。

九、错误处理

前八章集中于特定的用例:安装、执行查询、处理结果等。在一些地方,提到了错误处理的重要性,但是没有提供太多的细节。这种情况即将改变,因为这一章专门讨论错误处理。

错误处理是所有编程中最重要的主题之一,不仅仅是在使用 MySQL Connector/Python 时。你可以说它和测试应该是你学习的两个首要主题。这种说法有很大程度的真实性;然而,我决定将错误处理放在本书的倒数第二章。不是因为它不重要(短语“最后但并非最不重要”当然适用于本章和下一章关于故障排除的内容),而是因为两个原因:首先,这不是一本像使用 MySQL Connector/Python 那样关于编程的书,所以假设您已经很好地掌握了编程最佳实践。第二,它允许我给例子更多的上下文。

注意

不要将错误处理和测试视为次要任务。确保它们的优先级至少和实现实际功能的优先级一样高。这并不是 MySQL Connector/Python 独有的。

本章将从 MySQL 服务器中的警告、错误和严格模式开始。然后,您将继续讨论 MySQL Connector/Python 本身,在这里,讨论的第一部分将是一般的警告和错误处理、警告配置以及如何获取警告。第二部分将讨论 MySQL 错误号和 SQL 状态。最后,第三部分将概述mysql.connectormysqlx模块的错误类别。

小费

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

MySQL 服务器中的警告、错误和严格模式

在讨论 MySQL Connector/Python 中的错误处理时,在连接的 MySQL 服务器端需要考虑一些事情。本节将查看配置设置,以指定注释级消息是否应被视为警告,严格模式如何工作,以及应用如何将消息记录在 MySQL 错误日志中。

将注释级别的消息视为警告

执行语句时发生的事件有三个严重级别。最严重的是一个总是阻止语句完成的错误。下一个级别是警告,允许语句完成,但向用户或应用返回警告,以便决定要做什么。严重性最低的是音符级别,这是本次讨论的主题。

默认情况下,语句的注释级消息(例如,如果数据库存在,并且您试图使用CREATE DATABLASE IF NOT EXISTS创建它)会导致出现警告。例如:

mysql> CREATE SCHEMA IF NOT EXISTS py_test_db;
Query OK, 1 row affected, 1 warning (0.28 sec)

mysql> SHOW WARNINGS\G
*************************** 1\. row ***************************
  Level: Note
   Code: 1007
Message: Can't create database 'py_test_db'; database exists
1 row in set (0.00 sec)

请注意,在SHOW WARNINGS输出中,电平为Note。可以避免产生警告的注释级消息。这是通过改变sql_notes选项的值来实现的。当sql_notes的值为ON(默认值)时,会产生一个警告。如果是OFF,则不产生警告。该选项可以针对会话进行更改,因此,如果您通常希望发出警告,但对于给定的语句,您希望禁用它,则可以禁止显示该消息。要暂时隐藏注释级别的消息,您可以使用以下工作流程:

mysql> SET SESSION sql_notes = OFF;
Query OK, 0 rows affected (0.00 sec)

-- Execute statements

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

因此,如果您知道一个查询将导致一个注释级消息,那么您可以在执行该语句时更改会话的值sql_notes。然而,一般来说,最好是通过改变陈述来避免该消息。

小费

建议启用sql_notes,仅对您知道不需要警告的特定语句禁用它。

严格模式

服务器端配置的第二部分是严格模式。当启用严格模式时,它会告诉 MySQL 将无效数据视为错误而不是警告。在 MySQL 的旧版本中,默认情况下会容忍不适合表的数据,并尽最大努力使其适合。这使得开发应用变得更加容易,但是这样做的一个主要缺点是,它可能会导致数据库以与预期不同的数据结束。

强制数据适应的操作示例有将字符串转换为整数或截断数据。考虑名为table_1的表,其定义如下:

mysql> CREATE SCHEMA db1;
Query OK, 1 row affected (0.41 sec)

mysql> CREATE TABLE db1.table_1 (
          id int unsigned NOT NULL PRIMARY KEY,
          val varchar(5)
       ) ENGINE=InnoDB;
Query OK, 0 rows affected (0.24 sec)

在未启用严格模式的情况下,尝试插入六个字符的值将会导致警告,但仍会插入值被截断为五个字符的行:

mysql> INSERT INTO db1.table_1 VALUES (1, 'abcdef');
Query OK, 1 row affected, 1 warning (0.15 sec)

mysql> SHOW WARNINGS\G
*************************** 1\. row ***************************
  Level: Warning
   Code: 1265
Message: Data truncated for column 'val' at row 1
1 row in set (0.00 sec)

mysql> SELECT * FROM table_1;
+----+-------+
| id | val   |
+----+-------+
|  1 | abcde |
+----+-------+
1 row in set (0.00 sec)

在 MySQL 5.7 和更高版本中,默认情况下为事务表启用严格模式(InnoDBNDBCluster存储引擎)。在这种情况下,如果数据不合适,就会出现错误。防止插入的严格模式的一个例子是

mysql> INSERT INTO db1.table_1 VALUES (2, 'ghijkl');
ERROR 1406 (22001): Data too long for column 'val' at row 1

事务表的严格模式是通过在 SQL 模式列表中包含STRICT_TRANS_TABLES来实现的。有关 SQL 模式的更多信息,请参见 https://dev.mysql.com/doc/refman/en/sql-mode.html 。从 MySQL 连接器/Python 内部,可以使用sql_mode连接选项设置 SQL 模式。

STRICT_TRANS_TABLES SQL 模式相关的是innodb_strict_mode选项。此选项仅适用于 InnoDB 表格。SQL 模式控制数据修改语言(DML)查询,而innodb_strict_mode选项控制数据定义语言(DDL)查询,如CREATE TABLEALTER TABLECREATE INDEX。由innodb_strict_mode选项触发错误的一个最常见的原因是,当创建一个表时,其定义将导致该表的最大可能行大小超过 InnoDB 的限制。

强烈建议同时启用STRICT_TRANS_TABLES SQL 模式和innodb_strict_mode选项。在开始开发应用之前启用它,这样您就可以尽早得到关于不兼容问题的警告。

小费

与完成应用之后相比,在初始开发阶段修复违反严格模式的情况要容易得多。因此,在开始编码之前,启用严格模式;从长远来看,这会节省你的工作。

MySQL 错误日志

在 MySQL 服务器端需要注意的最后一点是,应用执行(或不执行)的操作可能会触发 MySQL 错误日志中的消息。例如,如果应用试图使用无效凭据进行连接,或者它没有正确关闭其连接,则可能会出现类似于以下示例的消息:

2018-03-03T04:10:19.943401Z 52 [Note] [MY-010926] Access denied for user 'pyuser'@'localhost' (using password: YES)

2018-03-03T04:10:28.330173Z 53 [Note] [MY-010914] Aborted connection 53 to db: 'unconnected' user: 'pyuser' host: 'localhost' (Got an error reading communication packets).

第一条消息说来自localhostpyuser用户试图使用密码进行连接,但是密码是错误的(或者该用户不存在)。第二条消息指出,在试图从其中一个连接中读取数据时出现了错误。在这种情况下,是因为连接消失了。

这些消息只有在log_error_verbosity MySQL 服务器选项设置为 3 时才会显示。建议在开发过程中确保这一点,并定期检查错误日志以捕获应用触发的所有消息。这可以通过在 MySQL 配置文件中设置选项来实现。在 MySQL 8.0 中,也可以使用如下的SET PERSIST语句来实现:

mysql> SET PERSIST log_error_verbosity = 3;
Query OK, 0 rows affected (0.04 sec)

这段代码设置当前值,并在 MySQL 重启时保存该值。

关于 MySQL 服务器已经说得够多了。下一个主题是 MySQL 连接器/Python 本身的警告和错误处理。

警告和错误处理

当您在程序中使用 MySQL Connector/Python 时,您会遇到内置 Python 异常和您使用的 MySQL Connector/Python 模块的自定义异常的混合。此外,还有一个子模块将 MySQL 错误代码作为常量。本节将介绍与警告相关的配置以及如何获取警告。MySQL 错误号、SQL 状态和异常类将在接下来的两节中讨论。

配置

当您使用mysql.connector模块中的方法时,可以配置 MySQL 连接器/Python 应该如何处理警告。有两个选项:是否自动获取查询的所有警告,以及是否将警告提升为异常。

MySQL 处理三种不同严重级别的错误消息:

  • 注意:这只是一个关于所发生事情的通知。这通常不是问题的征兆。例如,当您使用CREATE DATABASE IF NOT EXISTS创建一个数据库(模式)并且该数据库确实存在时,就会出现一个注释。在某些情况下,如果注意到了,通常这可能是潜在问题或不良做法的迹象。因此,您不应该自动忽略音符级别的信息。默认情况下,注释级消息被视为警告;这由sql_notes MySQL 服务器选项控制。

  • 警告:这是不妨碍 MySQL 继续下去的事情,但是行为可能不是你所期待的。例如,如果您提供的值不符合列定义,MySQL 会截断或转换所提供的值,就会发生这种情况。如果 MySQL 服务器启用了严格模式,一些警告(如示例中的警告)可能会升级为错误。

  • 错误:这是针对阻止 MySQL 执行查询的情况。他们总是会在 MySQL Connector/Python 中抛出一个异常。一个例子是出现了重复键错误。

一般来说,建议认真对待所有警告和错误。警告通常是事情不正常的迹象,在发展的第一阶段处理警告可以避免以后的重大灾难。

小费

如果您从应用开发的一开始就处理所有警告,您就不会被意外的数据转换或其他问题所困扰。如果警告被忽视,那么一个字符的输入错误在被发现之前可能会导致多年的问题。

当使用mysql.connector模块连接到 MySQL 时,有两个选项控制 MySQL 连接器/Python 如何处理警告。它们在表 9-1 中列出。

表 9-1

警告相关选项

|

名字

|

缺省值

|

描述

| | --- | --- | --- | | get_warnings | False | 当设置为True时,每次查询后自动提取警告。这使得无需手动执行SHOW WARNINGS即可获取警告。 | | raise_on_warnings | False | 当设置为True时,警告会引发异常。设置raise_on_warnings总是将get_warnings设置为相同的值。**注意:**在获取警告之前,不会引发异常。对于有结果的查询,这意味着何时提取行。 |

这两个选项仅在使用游标时适用。在撰写本文时,没有选项可以改变在mysqlx模块中使用 X DevAPI 时的警告行为。

也可以在连接完成后改变get_warningsraise_on_warnings的值。例如,这对于临时启用或禁用设置非常有用,如以下代码片段所示:

import mysql.connector

db = mysql.connector.connect(
  get_warnings=True,
  raise_on_warnings=True,
  option_files="my.ini",
)

cursor = db.cursor()

db.get_warnings = False

db.raise_on_warnings = False

cursor.execute(
  "CREATE SCHEMA IF NOT EXISTS py_test_db")

db.get_warnings = True

db.raise_on_warnings = True

db.close()

建议至少在开发期间启用raise_on_warningsget_warnings。在生产中,也建议至少检查警告。与手动获取警告相比,get_warnings选项没有任何开销,因为只有当查询返回警告时才会执行SHOW WARNINGS语句。当get_warnings启用时,可以使用fetchwarnings()方法检索警告。谈到fetchwarnings(),我们来看看警告是如何获取的。

在 cmd_query()之后提取警告

mysql.connector模块中,获取警告的方式取决于是通过连接对象还是游标执行查询。在这两种情况下,都使用SHOW WARNINGS语句来检索警告,但是游标允许您让它为您处理这个问题。

当您直接通过连接对象执行查询时,您必须自己获取警告。此外,您必须注意在获取警告之前获取所有的行,因为否则您将得到一个错误,即您有未读的行。如果您启用了consume_results,则应格外小心,因为在这种情况下获取警告将导致原始结果被取消。

警告

如果您已经启用了consume_results,那么执行SHOW WARNINGS来获得查询的警告将会消除任何未完成的行。

根据使用 C 扩展还是纯 Python 实现,在使用cmd_query()时处理警告的方式有所不同。因此,这两种情况都值得一看。

清单 9-1 展示了一个使用 C 扩展实现的例子,警告是在一个CREATE TABLE语句和一个SELECT语句之后获取的。

import mysql.connector

def print_warnings(warnings):

  if mysql.connector.__version_info__[0:3] > (8, 0, 11):
    (warnings, eof) = warnings

  for warning in warnings:
    print("Level  : {0}".format(
      warning[0]))
    print("Errno  : {0}".format(
      warning[1]))
    print("Message: {0}".format(
      warning[2]))

db = mysql.connector.connect(
  option_files="my.ini", use_pure=False)

# This example only works with the C
# Extension installed. Exit if that is
# not the case.
is_cext = isinstance(
  db,
  mysql.connector.connection_cext.CMySQLConnection
)
if not is_cext:
  print("The example requires the C "
    + "Extension implementation to be "
    + "installed")
  exit()

print("Using the C Extension implementation\n")

# Ensure the DDL statement will cause
# a warnings by executing the same
# CREATE SCHEMA IF NOT EXISTS statement
# twice.
db.cmd_query(
  "CREATE SCHEMA IF NOT EXISTS py_test_db")

# For a DDL statement
result = db.cmd_query(
  "CREATE SCHEMA IF NOT EXISTS py_test_db")

print("Warnings for CREATE SCHEMA:")
print("---------------------------")

print("DDL: Number of warnings: {0}"

  .format(result["warning_count"]))

# Get the warnings

db.cmd_query("SHOW WARNINGS")

warnings = db.get_rows()

print_warnings(warnings)
db.free_result()
print("")

# Try a SELECT statement
result = db.cmd_query("SELECT 1/0")
rows = db.get_rows()
db.free_result()

print("Warnings for SELECT:")
print("--------------------")

print("SELECT: Number of warnings: {0}"

  .format(db.warning_count))

# Get the warnings
db.cmd_query("SHOW WARNINGS")
warnings = db.get_rows()
print_warnings(warnings)

db.close()

Listing 9-1Checking Warnings with the C Extension Implementation and cmd_query()

警告打印在print_warnings()功能中。在 8.0.12 版和更高版本中使用 C 扩展时,由于有一个变化,所以eof包也包括在内,因此有必要拥有版本相关的代码。__version_info__房产用于此。

对于CREATE TABLE语句,cmd_query()返回的结果直接将警告数作为warning_count元素。对于SELECT的说法,稍微复杂一点。首先需要使用结果,然后可以在连接对象的warning_count属性中找到警告的数量。

警告本身是使用SHOW WARNINGS语句获取的,这些语句像任何其他语句一样执行。输出是

Using the C Extension implementation

Warnings for CREATE SCHEMA:
---------------------------
DDL: Number of warnings: 1
Level  : Note
Errno  : 1007
Message: Can't create database 'py_test_db'; database exists

Warnings for SELECT:
--------------------
SELECT: Number of warnings: 1
Level  : Warning
Errno  : 1365
Message: Division by 0

每个 warning()有三个元素:严重性(注意、警告或错误)、错误号(将在本章后面讨论)和描述错误的错误消息。如果 C 扩展名不可用,程序将退出并显示错误:

The example requires the C Extension implementation to be installed

如果使用纯 Python 实现,会有一些不同。首先,SELECT语句的警告计数可以在所有版本的get_row()get_rows()返回的eof部分中找到。另一件事是SHOW WARNINGS语句()的结果在 MySQL Connector/Python 8.0.11 中是以字节数组的形式返回的,所以必须解码。清单 9-2 展示了 8.0.12 及更高版本的示例的纯 Python 等价物。代码示例包括文件 Chapter _ 09/listing _ 9 _ 2 _ version _ 8 _ 0 _ 11 . py 中的 8.0.11 和更早版本。

import mysql.connector

def print_warnings(warnings):
  for warning in warnings:
    print("Level  : {0}".format(
      warning[0]))
    print("Errno  : {0}".format(
      warning[1]))
    print("Message: {0}".format(
      warning[2]))

db = mysql.connector.connect(
  option_files="my.ini", use_pure=True)

print("Using the pure Python implementation\n")

# Ensure the DDL statement will cause
# a warnings by executing the same
# CREATE SCHEMA IF NOT EXISTS statement
# twice.
db.cmd_query(
  "CREATE SCHEMA IF NOT EXISTS py_test_db")

# For a DDL statement
result = db.cmd_query(
  "CREATE SCHEMA IF NOT EXISTS py_test_db")

print("Warnings for CREATE SCHEMA:")
print("---------------------------")
print("DDL: Number of warnings: {0}"
  .format(result["warning_count"]))

# Get the warnings

db.cmd_query("SHOW WARNINGS")

(warnings, eof) = db.get_rows()

print_warnings(warnings)
print("")

# Try a SELECT statement
result = db.cmd_query("SELECT 1/0")
(rows, eof) = db.get_rows()

print("Warnings for SELECT:")
print("--------------------")
print("SELECT: Number of warnings: {0}"
  .format(eof["warning_count"]))

# Get the warnings

db.cmd_query("SHOW WARNINGS")

(warnings, eof) = db.get_rows()

print_warnings(warnings)

db.close()

Listing 9-2Checking Warnings with the Pure Python Implementation and cmd_query()

该示例经历了与前面相同的步骤,但是这一次在获取行时从eof部分检索到了SELECT语句的警告数。和以前一样,警告计数只有在获取了所有行()后才可用。除了头之外,该示例的输出与之前相同:

Using the pure Python implementation

Warnings for CREATE SCHEMA:
---------------------------
DDL: Number of warnings: 1
Level  : Note
Errno  : 1007
Message: Can't create database 'py_test_db'; database exists

Warnings for SELECT:
--------------------
SELECT: Number of warnings: 1
Level  : Warning
Errno  : 1365
Message: Division by 0

如果使用游标,事情通常会简单一些。让我们看看光标和警告是如何工作的。

用游标提取警告

用光标获取警告时所做的工作原则上与使用cmd_query()方法获取警告时相同。然而,许多工作是由光标在后台处理的,这总体上使它更容易使用。

清单 9-3 显示了清单 9-1 和清单 9-2 中检查的等价示例,只是这次使用了一个光标,并启用了get_warnings

import mysql.connector

def print_warnings(warnings):
  for warning in warnings:
    print("Level  : {0}".format(
      warning[0]))
    print("Errno  : {0}".format(
      warning[1]))
    print("Message: {0}".format(
      warning[2]))

print("Using cursors\n")

db = mysql.connector.connect(
  option_files="my.ini")

cursor = db.cursor()

# Ensure the DDL statement will cause
# a warnings by executing the same
# CREATE SCHEMA IF NOT EXISTS statement
# twice.
cursor.execute(
  "CREATE SCHEMA IF NOT EXISTS py_test_db")

# Enable retriaval of warnings

db.get_warnings = True

# For a DDL statement
cursor.execute(
  "CREATE SCHEMA IF NOT EXISTS py_test_db")

# Get the warnings

warnings = cursor.fetchwarnings()

print("Warnings for CREATE SCHEMA:")
print("---------------------------")
print("DDL: Number of warnings: {0}"
  .format(len(warnings)))
print_warnings(warnings)
print("")

# Try a SELECT statement
cursor.execute("SELECT 1/0")

rows = cursor.fetchall()

# Get the warnings
warnings = cursor.fetchwarnings()

print("Warnings for SELECT:")
print("--------------------")
print("SELECT: Number of warnings: {0}"
  .format(len(warnings)))
print_warnings(warnings)

db.close()

Listing 9-3Fetching Warnings Using a Cursor with get_warnings Enabled

在执行任何查询之前,get_warnings选项被启用。这也可以在选项文件中完成,或者作为mysql.connector.connect()函数的一个单独的参数。

启用get_warnings后,DDL 和SELECT语句获得警告的工作流是相同的。这是这种方法的主要优点。使用光标的fetchwarnings()方法获取警告。这将以与上一示例相同的方式返回警告列表。发现警告的数量是列表的长度。对于SELECT语句,您必须在获取警告之前检索结果集中的所有行。输出与清单 9-1 和清单 9-2 相同:

Using cursors

Warnings for CREATE SCHEMA:
---------------------------
DDL: Number of warnings: 1
Level  : Note
Errno  : 1007
Message: Can't create database 'py_test_db'; database exists

Warnings for SELECT:
--------------------
SELECT: Number of warnings: 1
Level  : Warning
Errno  : 1365
Message: Division by 0

用 X DevAPI 获取警告

使用 X DevAPI 时对警告的处理类似于它对游标的工作方式。最大的区别是警告是结果对象的一部分。这确保了不管使用 X DevAPI 的哪一部分和执行的查询类型如何,都有一个统一的处理警告的方法。

无论返回哪种结果对象,警告的处理都使用相同的两种方法。这两种方法是

  • get_warnings():返回查询生成的警告元组列表

  • get_warnings_count():返回一个带有警告数的整数

不需要在查询前启用警告。警告总是可用的。

作为一个例子,让我们重复一下用于cmd_query()和光标的例子,看看在使用 X DevAPI 的程序中如何处理警告。结果代码可以在清单 9-4 中看到。

import mysqlx
from config import connect_args

db = mysqlx.get_session(**connect_args)

# Ensure the DDL statement will cause
# a warnings by executing the same
# CREATE SCHEMA IF NOT EXISTS statement
# twice.
sql = db.sql(
  "CREATE SCHEMA IF NOT EXISTS py_test_db")
sql.execute()

# For a DDL statement
sql = db.sql(
  "CREATE SCHEMA IF NOT EXISTS py_test_db")
result = sql.execute()

# Get the warnings
print("Warnings for CREATE SCHEMA:")
print("---------------------------")
print("DDL: Number of warnings: {0}"
  .format(result.get_warnings_count()))

print(result.get_warnings())

print("")

# Try a SELECT statement
sql = db.sql("SELECT 1/0")
result = sql.execute()

row = result.fetch_all()

# Get the warnings
print("Warnings for SELECT:")
print("--------------------")
print("SELECT: Number of warnings: {0}"
  .format(result.get_warnings_count()))
print(result.get_warnings())

db.close()

Listing 9-4Handling Warnings with the X DevAPI

该示例类似于游标示例,只是可以使用get_warnings_count()方法而不是使用警告列表的长度来找到警告计数。对于作为结果的一部分返回行或文档的查询,在检索警告之前必须获取结果。输出是

Warnings for CREATE SCHEMA:
---------------------------
DDL: Number of warnings: 1
[{'level': 1, 'code': 1007, 'msg': "Can't create database 'py_test_db'; database exists"}]

Warnings for SELECT:
--------------------
SELECT: Number of warnings: 1
[{'level': 2, 'code': 1365, 'msg': 'Division by 0'}]

输出显示警告作为字典列表返回。与其他示例相比,输出中有一个主要区别:严重性级别是一个整数,而不是一个字符串。可以返回的级别是 1 和 2,其含义如下:

  • 1 :这是一个音符级别的信息。

  • 2 :这是一个警告级别的消息。

警告字典的代码元素是 MySQL 错误号,但是 1007 是什么意思呢?我们来看看。

MySQL 错误号和 SQL 状态

MySQL 使用错误号来指定发生了哪个注释、警告或错误事件。您在示例中看到了警告是如何包含错误号的。对于下一节讨论的异常,错误号也起着重要作用。所以在继续之前,让我们暂停一下,更详细地考虑一下错误号。

错误号是四到五位数,用于唯一标识遇到的警告或错误。这些数字是 MySQL 特有的,因此不能与其他数据库系统中的错误进行比较。如果错误号不再相关,可以不再使用,但不会重复使用。这意味着检查是否遇到了给定的错误号并据此采取行动是安全的。

除了错误号,还有 SQL 状态,这意味着可以跨 SQL 数据库移植。这种可移植性的代价是无法对错误进行详细说明。然而,SQL 状态很适合用来表示一组错误。SQL 状态仅作为错误异常的一部分返回。

本节的其余部分将查看错误号和 SQL 状态。

MySQL 错误号

每个已知错误的错误号和 SQL 状态可以在 https://dev.mysql.com/doc/refman/en/error-handling.html 中找到。这些错误分为服务器端错误和客户端错误。客户端错误的数字都在 2000 到 2999 之间。服务器端错误使用范围 1000-1999 和 3000 以上。正如这些范围所表明的,有成千上万的错误号,并且这个数字随着每个 MySQL 版本的增加而增加。

幸运的是,MySQL Connector/Python 有一个映射到常量的错误号列表。如果您需要检查是否遇到了给定的错误,这允许您使用应用中的常量。当您在编写代码几年后再阅读代码时,使用常量可以更容易地发现错误所在。

对于mysql.connectormysqlx模块,错误代码常量在errorcode子模块中定义,用法相同。清单 9-5 显示了一个检查当试图创建一个数据库时返回的警告是否是该数据库已经存在的例子;在这种情况下,忽略该警告是安全的,因为您已经知道该数据库可能存在。

import mysqlx

from mysqlx.errorcode import *

from config import connect_args

db = mysqlx.get_session(**connect_args)

# Ensure the DDL statement will cause
# a warnings by executing the same
# CREATE SCHEMA IF NOT EXISTS statement
# twice.
sql = db.sql(
  "CREATE SCHEMA IF NOT EXISTS py_test_db")
sql.execute()

# For a DDL statement
sql = db.sql(
  "CREATE SCHEMA IF NOT EXISTS py_test_db")
result = sql.execute()

# Get the warnings
for warning in result.get_warnings():
  if warning["code"] == ER_DB_CREATE_EXISTS:
    print("Ignoring the warning")
  else:
    raise mysqlx.errors.DatabaseError(
      warning["msg"], warning["code"])

db.close()

Listing 9-5Comparing an Error Code Against a Constant from the errorcode Module

这个例子从mysqlx.errorcode模块导入所有的错误代码。这允许您检查警告的错误号是否为ER_DB_CREATE_EXISTS (1007)。如果是,警告将被忽略;否则,该警告将用于引发异常。

MySQL 连接器/Python 错误异常还包括 SQL 状态,所以在讨论异常类之前,让我们先来看看 SQL 状态。

SQL 状态

与 MySQL 错误号不同,SQL 状态在所有 SQL 数据库之间共享。如果您编写的代码支持不同的底层数据库存储,那么最好尽可能多地使用 SQL 状态,因为这样更有可能保持应用的可移植性。SQL 状态也可以用来确定错误的类别。在 MySQL Connector/Python 中,SQL 状态仅用于错误异常。

SQL 状态由五个字母和数字组成。前两个字符定义类别,其余三个字符提供详细信息。在表 9-2 中可以看到一些常见的错误类别。Exception列是用于该 SQL 状态类的异常类。

表 9-2

常见 SQL 状态类

|

班级

|

描述

|

例外

|

评论

| | --- | --- | --- | --- | | 00 | 成功 |   | 查询已成功执行。这绝不会导致异常。 | | 01 | 警告 |   | 该查询会导致警告。只有启用了raise_on_warnings选项,才会导致异常。 | | 02 | 尚无数据 | DataError | 当查询不再有数据时,这可能源于存储的程序。 | | 08 | 连接异常 | OperationalError | 这涵盖了创建连接时的各种问题,如连接过多,MySQL Connector/Python 不支持服务器请求的认证协议等。对于此 SQL 状态类,错误必须发生在服务器端。 | | Twenty-one | 数错了 | DataError | 例如,在给定的值数目与指定的字段数目不匹配的插入中会发生这种情况。 | | Twenty-two | 数据不符合 | DataError | 例如,如果字符串对于列来说太长,或者数值超出范围,就会出现这种情况。 | | Twenty-three | 违反约束 | IntegrityError | 当唯一键约束失败、违反外键或试图将NULL指定为NOT NULL列的值时发生。 | | Twenty-five | 无效的事务状态 | ProgrammingError | 如果您试图执行当前事务状态不允许的操作,则会发生这种情况。例如,如果您试图在只读事务中插入数据。 | | Twenty-eight | 未授权 | ProgrammingError | 由于使用了错误的凭据,连接失败。 | | 三维(three dimension 的缩写) | 没有模式 | ProgrammingError | 在没有默认数据库(架构)的情况下执行查询,并且查询中没有显式设置数据库时发生。 | | Forty | 交易错误 | InternalError | 例如,这可能是由于死锁而发生的。另一个原因是使用 MySQL 组复制,事务在提交期间回滚,因为它不能应用于所有节点。 | | forty-two | 语法错误或无法访问 | ProgrammingError | 当 SQL 语句无效或您无权访问所请求的数据时,会出现此错误。 | | 【男子名】亨利 | 其他错误 | DatabaseError | 对于没有定义更具体的 SQL 状态的错误。例如,这包括 InnoDB 的锁等待超时。MySQL 错误号 1210 和 1243 是使用DatabaseError异常类的例外;这两个错误反而引发了一个ProgrammingError异常。 | | 辅助放大器(auxiliary amplifier 的缩写) | XA 交易 | IntegrityError | 用于与 XA 事务相关的所有错误。 |

还有更多 SQL 状态类,但是表 9-2 中列出的是 MySQL Connector/Python 中最常见的。每个 SQL 状态类都映射到一个异常类。所以,让我们看看异常类是如何工作的。

警告

语法错误(SQL 状态类 42)可能是 SQL 注入尝试的迹象。确保给予这些错误高优先级。

异常类

MySQL Connector/Python 使用异常来报告在连接器内部处理命令时遇到的错误,或者在执行查询时出现错误。例外可以是三个类别之一,如本节所述。

可能的异常类别包括从标准 Python 异常到开发人员创建的自定义异常。这三个类别是

  • 标准 Python 异常:用于与 MySQL 无关的错误,不再赘述。

  • MySQL Connector/Python 内置异常:遇到 MySQL 相关错误时会遇到的异常,除非该异常已被自定义异常覆盖。

  • 自定义异常:可以定义自己的异常,并为给定的 MySQL 错误号注册它。

本节的其余部分将讨论内置的 MySQL 连接器/Python 异常和自定义异常。

内置类

根据错误的类型,在 MySQL Connector/Python 中预定义了许多异常类。让我们来探索它们。

无论是使用mysql.connector还是mysqlx模块,预定义的类都是相同的,我们将一起讨论它们。这些职业都使用errors.Error职业作为他们的基础,除了Warning职业。所有基于errors.Error的类都有相同的可用属性。

表 9-3 总结了 MySQL 连接器/Python 中使用的异常类以及它们在哪个(哪些)模块中可用。所有的类都存在于errors子模块中(即mysql.connector.errorsmysqlx.errors,取决于所使用的模块)。

表 9-3

MySQL 连接器/Python 异常类

|

异常类

|

模块

|

描述

| | --- | --- | --- | | DatabaseError | mysql.connector``mysqlx | 对于一般数据库错误。除了以 HY 开头的 SQL 状态之外,这个类不常被直接使用。 | | DataError | mysql.connector``mysqlx | 与非约束错误的数据相关的错误。示例包括数据类型错误或不适合字段,或者提供了错误数量的值。 | | Error | mysql.connector``mysqlx | 这是基本异常类。不直接使用。 | | IntegrityError | mysql.connector``mysqlx | 约束错误或 XA 事务错误。 | | InterfaceError | mysql.connector``mysqlx | 用于与连接相关的错误。 | | InternalError | mysql.connector``mysqlx | 内部数据库错误,如死锁和未处理的结果。 | | NotSupportedError | mysql.connector``mysqlx | 当使用尚未实现的功能时发生。这通常与在错误的上下文中使用特性有关,例如在存储函数中返回结果集。当 4.1.1 (MySQL Server 版本)之前的身份验证协议不可用时,也可以使用该协议进行连接。 | | OperationalError | mysql.connector``mysqlx | 与数据库操作相关的错误。这是进行连接时最常遇到的情况。 | | PoolError | mysql.connector``mysqlx | 有关连接池的错误。 | | ProgrammingError | mysql.connector``mysqlx | 广义上与应用相关的错误。包括语法错误和试图访问不存在或用户无权访问的数据库对象。 | | Warning | mysql.connector | 用于重要警告。 |

下面的类都是DatabaseError类的子类:InternalErrorOperationalErrorProgrammingErrorIntegrityErrorDataErrorNotSupportedError

所有类的特征都是在基类Error中定义的,所以它们对于除了Warning类之外的所有异常类都是一样的。除了所有异常所具有的特性之外,Warning类没有什么特别的特性。为了帮助讨论错误异常的特征,考虑以下未捕获的异常:

mysql.connector.errors.ProgrammingError: 1046 (3D000): No database selected

错误类有三个公共属性,可以在处理异常时使用:

  • msg :这是描述错误的字符串。在本例中,它是“没有选择数据库”

  • errno:MySQL 错误号。在示例中,它是 1046。

  • SQL state:SQL 状态。在示例中,它是 3D000。

清单 9-6 显示了一个触发与刚才讨论的异常相同的异常的例子。异常被捕获,每个属性都被打印出来。最后,将错误号与来自errorcode子模块的常数进行比较。

import mysql.connector
from mysql.connector import errors
from mysql.connector.errorcode import *

db = mysql.connector.connect(
  option_files="my.ini")

cursor = db.cursor()
try:
  cursor.execute("SELECT * FROM city")

except errors.ProgrammingError as e:

  print("Msg .........: {0}"
    .format(e.msg))
  print("Errno .......: {0}"
    .format(e.errno))
  print("SQL State ...: {0}"
    .format(e.sqlstate))
  print("")
  if e.errno == ER_NO_DB_ERROR:
    print("Errno is ER_NO_DB_ERROR")

db.close()

Listing 9-6Example of Handling an Exception

在 Python 中,异常像往常一样被捕获,属性的用法很简单。正如您之前看到的,错误号可以与errorcode子模块中的常数进行比较,以便更容易地看到异常与哪个错误进行比较。该示例的输出是

Msg .........: No database selected
Errno .......: 1046
SQL State ...: 3D000

Errno is ER_NO_DB_ERROR

MySQL 连接器/Python 如何决定应该使用哪个类?这在上一节讨论 SQL 状态时已经部分回答了,但是让我们更详细地看一下这个主题。

将错误映射到异常类

当错误发生时,MySQL Connector/Python 使用错误号和 SQL 状态来确定使用哪个异常类。在大多数情况下,您不需要担心这一点,但是在某些情况下,您可能需要修改所使用的类(目前只有mysql.connector模块支持这一点),并且在所有情况下,理解底层过程都是有用的。

使用以下步骤确定异常类别:

  1. 如果已经为 MySQL 错误号定义了一个自定义异常,就使用它。自定义异常仅适用于mysql.connector模块,将在这些步骤后讨论。

  2. 如果 MySQL 错误号是在errors._ERROR_EXCEPTIONS列表中定义的,那么使用为该错误定义的类。

  3. 如果没有为错误定义 SQL 状态,则使用DatabaseError类。对于作为错误引发的警告,会发生这种情况。

  4. 根据 SQL 状态在errors._SQLSTATE_CLASS_EXCEPTION列表中找到类。

  5. 使用DatabaseError类。

如果您需要一个错误来触发不同的异常,当然可以修改_ERROR_EXCEPTIONS_SQLSTATE_CLASS_EXCEPTION列表。但是,不建议这样做,因为它们应该是私有的(因此名称开头有下划线)。在mysql.connector模块中,有一种更好的方法:自定义异常。

自定义例外

在某些情况下,使用自定义异常来处理特定错误会很有用。当错误发生时,您可能希望触发一个特殊的工作流,例如将一条消息记录到应用日志中。目前只有mysql.connector模块支持自定义异常。

使用errors. custom_error_exception()功能登记自定义异常。您需要提供将使用异常的 MySQL 错误号和异常本身。建议自定义异常类继承error .Error类来包含基本特性。

清单 9-7 显示了一个例子,其中MyError类被用于ER_NO_DB_ERROR错误。与普通类相比,唯一的区别是它向stderr输出一条包含错误信息的消息。如果你使用的是 Python 2.7,你需要添加"from __future__ import print_function"作为代码的第一行。

import mysql.connector
from mysql.connector import errors
from mysql.connector.errorcode \
  import ER_NO_DB_ERROR

# Define the custom exception class

class MyError(errors.Error):

  def __init__(
    self, msg=None, errno=None,
    values=None, sqlstate=None):

    import sys
    super(MyError, self).__init__(
      msg, errno, values, sqlstate)
    print("MyError: {0} ({1}): {2}"
      .format(self.errno,
              self.sqlstate,
              self.msg
      ), file=sys.stderr)

# Register the class

errors.custom_error_exception(

  ER_NO_DB_ERROR,
  MyError

)

# Now cause the exception to be raised
db = mysql.connector.connect(
  option_files="my.ini")

cursor = db.cursor()
try:
  cursor.execute("SELECT * FROM city")

except MyError as e:

  print("Msg .........: {0}"
    .format(e.msg))
  print("Errno .......: {0}"
    .format(e.errno))
  print("SQL State ...: {0}"
    .format(e.sqlstate))

db.close()

Listing 9-7Using a Custom Exception

首先,定义了MyError类。它调用自己的超类的__init__方法来设置所有的标准属性。然后错误信息被打印到stderr。这也可以使用日志服务或其他逻辑。其次,MyError类被注册为错误的异常类,MySQL 错误号被设置为ER_NO_DB_ERROR

程序的其余部分与之前相同,只是您现在捕获的是MyError异常,而不是ProgrammingError异常。执行程序时的输出是

MyError: 1046 (3D000): No database selected
Msg .........: No database selected
Errno .......: 1046
SQL State ...: 3D000

这里假设stderrstdout都被打印到控制台。作为练习,先将stderr重定向,然后将stdout重定向到其他地方,看看这会如何改变输出。

还有一些问题不一定会返回警告或错误。一组可能返回错误也可能不返回错误的问题是锁定问题。因为锁定问题是关于使用数据库的,所以您应该检查它们。

锁定问题

当两个或多个事务(可以是单个查询)试图以不兼容的方式访问或更新相同的数据时,就会发生锁定问题。数据库中的锁这个主题既庞大又复杂,但也很有趣。关于锁定的细节已经超出了本书的范围,但是本节将提供一个简要的概述。

注意

简化了锁的讨论;例如,只提到了行(记录)锁。其他一些锁是间隙锁、表锁、元数据锁和全局读锁。还有不同的锁类型,如意向锁。事务隔离级别也对锁起作用。MySQL 参考手册有好几页是关于 InnoDB 锁的。起点是 https://dev.mysql.com/doc/refman/en/innodb-locking-transaction-model.html

锁定的原因是允许对数据的并发访问,同时仍然确保一致的结果。如果一个事务更新给定的行,然后另一个事务尝试更新同一行,则第二个事务必须等待第一个事务完成(提交或回滚)才能访问该行。如果不是这样,最终结果将是不确定的。

MySQL 中的两个事务存储引擎 InnoDB 和 NDBCluster 都使用行级锁。这意味着只有查询读取或更改的行被锁定。因为直到访问行时才知道是否需要它,所以查询乐观地执行,假设有可能获得所需的锁。

乐观方法在大多数情况下都很有效,但这也意味着有时查询必须等待锁。甚至可能是因为等待时间太长而导致超时。

另一种可能性是,当两个事务都在等待对方的锁时,就会发生冲突。这种情况永远不会自行解决,被称为死锁。死锁这个名字听起来很吓人,但它只是一种情况的名称,在这种情况下,数据库必须进行干预以解决锁问题。InnoDB 选择完成工作最少的事务并回滚它。在这种情况下会返回一个死锁错误,这样应用就知道了事务失败的原因。清单 9-8 显示了一个简单的例子,其中两个连接以死锁结束。

Connection 1> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

Connection 1> UPDATE world.city
                 SET Population = Population + 100
               WHERE Name = 'San Francisco' AND CountryCode = 'USA';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

Connection 2> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

Connection 2> UPDATE world.city
                 SET Population = Population + 200
               WHERE Name = 'Sydney' AND CountryCode = 'AUS';
Query OK, 1 row affected (0.04 sec)
Rows matched: 1  Changed: 1  Warnings: 0

Connection 1> UPDATE world.city
                 SET Population = Population + 100
               WHERE Name = 'Sydney' AND CountryCode = 'AUS';
-- Connection 1 blocks until the deadlock occurs for Connection 2.

Connection 2> UPDATE world.city
                 SET Population = Population + 200
               WHERE Name = 'San Francisco' AND CountryCode = 'USA';

ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting

transaction

Listing 9-8Example of Two Transactions Causing a Deadlock

这两项交易都试图增加悉尼和旧金山的人口;连接 1 有 100 人的那个,连接 2 有 200 人的那个。但是,它们以相反的顺序更新这两个城市,并且是交错的。因此,最后,连接 1 等待悉尼的锁被释放,连接 2 等待旧金山的锁被释放。这是永远不会发生的,所以这是一个僵局。

使用细粒度锁时,锁等待和死锁是不可避免的。记住这一点并确保您的应用能够处理锁问题是很重要的。如果锁等待超时或死锁很少发生,重试事务通常就足够了。如果问题发生得如此频繁,以至于影响到性能,那么您需要努力减少锁争用。下一章将简要讨论锁的故障排除。

小费

确保您的应用能够处理锁等待和死锁。第一种方法是重试事务,可能会有一点延迟,以便给另一个事务一个完成的机会。应该进一步调查频繁出现的锁定问题。

最后要讨论的是当出现警告、错误或其他问题时该怎么办。

出问题时该怎么办

到目前为止,关于警告和错误的讨论一直集中在警告和错误如何与 MySQL Connector/Python 一起工作。然而,一个相当重要的问题仍然存在:当您遇到警告、错误和其他问题时,您会怎么做?

这个问题的简短答案是“视情况而定”不仅要看问题,还要看情况。以下是一些需要考虑的事项:

  • 严重性:问题有多严重?

  • 影响:问题影响了多少人和谁?

  • 频率:问题出现的频率?

  • 可重试:是否值得重试导致错误的操作?

  • 努力:要避免这个问题需要做多少工作?

另一个考虑是如何报告失败。如果它涉及到生产环境,相对简短的消息是最好的。如果可能,提供如何避免问题和/或如何获得帮助的信息。

包含完整的堆栈跟踪和确切的异常似乎是个好主意;但是,最终用户无法使用这些信息。事实上,在某些情况下,向前端返回如此多的细节看起来是不专业的,甚至可能会泄露终端用户不应该知道的应用的细节。

错误的全部细节,包括跟踪,当然是开发人员非常感兴趣的。具体如何记录取决于应用,但是可以将其写入应用日志或单独的错误日志。开发环境中的另一个选项是支持“调试”模式,该模式将完整的细节输出到前端,以便在测试期间更容易获得信息。

最后,您希望如何呈现错误取决于您的特定需求、目标用户等等。在某些情况下,甚至有可能避免最终用户受到所遇到问题的影响。这使我们回到了本节开头列出的五个项目;它们将在下面的小节中讨论。

严重

问题的严重性在于它对应用的其余部分和用户有多重要。如果一个错误导致应用根本无法工作,那么处理这个错误显然比导致响应稍慢的错误更重要。

严重程度高的问题需要快速处理。如果是安全问题,或者网站不可用,延迟的解决方案会给公司造成损失。另一方面,如果一百万个请求中有一个比正常情况下多花了 5%的时间处理,这可能仍然是一件令人烦恼的事情,但并不值得你放弃正在做的一切。除了严重性之外,影响是决定问题紧迫性的另一个主要因素。

影响

无论是在面向客户的生产环境、内部非关键应用还是开发系统中遇到问题,都有很大的不同。受影响的用户数量越多,公司对应用的依赖程度越高,解决问题就越紧迫。

在给定的环境中,也可能存在差异。考虑一个开发环境。如果您是唯一受影响的人,并且该问题不影响您当前正在进行的工作,您可以推迟解决方案的工作。

然而,如果 100 个其他开发人员无所事事,或者他们的工作受到影响,那么解决这个问题就变得更加紧迫。问题出现的频率显然也会对影响产生影响。

频率

一个问题的频繁程度会影响需要付出多少努力。如果您很少遇到死锁或锁等待超时的情况,只需重试查询就可以了(参见下一条)。如果相同的锁定问题在一分钟内发生多次,就有必要研究如何避免该问题。

问题发生频率的限制取决于问题的性质,这将频率与严重性和影响联系起来。如果客户遇到应用崩溃,这很快就会成为必须立即处理的问题。同样,一个小时后失败的报告作业必须从头重新启动。

另一方面,如果同样一个小时的报告任务在每次出现问题时都被延迟了几秒钟,那么它就不太可能是优先任务。

可重试

您在 MySQL Connector/Python 中遇到的错误可以分为两类:一类是无论您尝试多少次都会失败的错误,另一类是如果您重试就可能成功的错误。后者更值得关注,因为您可以添加对自动处理它们的支持。

可重试错误通常是由锁争用或资源耗尽引起的。我已经讨论过锁,所以让我们从 MySQL 的角度来看一下是什么导致了资源耗尽。

在连接的生命周期中,有几个地方需要资源。当第一次创建连接时,MySQL 服务器中必须有更多可用的连接,并且操作系统必须允许创建新的线程(默认情况下,MySQL 为每个连接创建一个操作系统线程)。当执行一个查询时,它将需要内存来执行不同的部分,例如对结果进行排序。如果您插入或更改数据,也可能导致表格增长,这需要额外的磁盘空间。如果这些资源耗尽,查询将会失败。

如果您重试查询,并非所有这些错误都有可能消失。例如,如果磁盘已满,可能需要数据库管理员和/或系统管理员进行干预,然后才能再次插入数据。另一方面,如果您有一个锁定问题,并且您的事务都是短期的,那么重试失败的事务可能会成功。

表 9-4 显示了一些可以选择重试的典型错误号。

表 9-4

可能失效的 mysql 错误号

|

错误号

|

常数

|

SQL 状态

|

描述

| | --- | --- | --- | --- | | 1028 | ER_FILSORT_ABORT | HY000 | 排序操作已中止。 | | 1038 | ER_OUT_OF_SORTMEMORY | HY001 | 由于内存不足,排序操作中止。可能需要增加 MySQL 会话变量sort_buffer_size。 | | 1040 | ER_CON_COUNT_ERROR | 08004 | MySQL 连接器/Python 无法连接到 MySQL 服务器,因为所有分配的连接(max_connections)都已被使用。 | | 1041 | ER_OUT_OF_RESOURCES | HY000 | MySQL 服务器内存不足,但也许可以继续。 | | 1043 | ER_HANDSHAKE_ERROR | 08S01 | 如果存在网络问题,在创建连接时会发生这种情况。 | | 1114 | ER_RECORD_FILE_FULL | HY000 | 桌子满了。 | | 1135 | ER_CANT_CREATE_THREAD | HY000 | 不可能为新连接创建线程。这可能是由于内存、文件描述符或允许的进程数耗尽。 | | 1180 | ER_ERROR_DURING_COMMIT | HY000 | 提交事务时出错。 | | 1181 | ER_ERROR_DURING_ROLLBACK | HY000 | 回滚事务时出错。 | | 1203 | ER_TOO_MANY_USER_CONNECTIONS | 42000 | 用户有太多连接。 | | 1205 | ER_LOCK_WAIT_TIMEOUT | HY000 | 事务等待锁的时间超过了超时时间(InnoDB 默认为 50 秒)。 | | 1206 | ER_LOCK_TABLE_FULL | HY000 | 如果与 InnoDB 缓冲池的大小相比锁太多,InnoDB 就会发生这种情况。只有当不是事务本身导致大量锁时,才值得重试。 | | 1213 | ER_LOCK_DEADLOCK | 40001 | 出现死锁,该事务被选作牺牲品。 | | 1226 | ER_USER_LIMIT_REACHED | 42000 | 超过了用户资源限制。 | | 1613 | ER_XA_RBTIMEOUT | XA106 | XA 事务已回滚,因为花费的时间太长。 | | 1614 | ER_XA_RBDEADLOCK | XA102 | 由于死锁,XA 事务已回滚。 | | 1615 | ER_NEED_REPREPARE | HY000 | 准备好的语句需要重新准备。 |

错误列表并不是详尽无遗的,重试成功的机会各不相同。

请注意,您可能能够针对某些错误编写解决方案。例如,如果超过了max_sp_recursion_depth变量允许的递归深度,就会出现错误号 1456 ( ER_SP_RECURSION_LIMIT,SQL 状态 HY000)。如果您已将此选项设置为相对较低的值,但在某些情况下接受增加该值,则可以增加会话的值并重试。显然,对于这种特定情况,如果在第一次尝试之前增加该值会更好,但可能会有一些特殊的考虑因素阻止这样做。

如果决定重试一个事务,还需要决定重试最近的语句是否足够,或者是否必须重试整个事务。通常是后者,并且在所有情况下,它是最安全的。

警告

可能很想重试事务中的最后一条语句,但是要小心,因为前面的语句可能已经回滚了。

自动重试由于连接丢失而失败的查询是很诱人的。但是,在这种情况下要小心,要确保查询依赖的所有内容,比如同一事务中的早期查询,也会被重新执行。

努力

最后要注意的是解决问题的努力。在一个理想的世界里,所有的 bug 都会被修复,但是在现实中,资源是有限的,所以经常需要进行优先级排序。这个想法把所有先前的考虑联系在一起。

软件项目越大,确定哪些问题应该按照什么顺序修复就变得越复杂。可能存在一些利益冲突,例如两个客户受到不同问题的影响。可能还需要新功能的开发保持在正轨上。在这种情况下,有必要让多方参与讨论,以确定所需工作的优先顺序。

MySQL Connector/Python 中警告和错误处理的讨论到此结束。一个相关的主题是如何解决你遇到的错误,这将在下一章讨论。

摘要

本章探讨了警告和错误在 MySQL Server 和 MySQL Connector/Python 中是如何工作的。警告和错误有三种严重级别:注释、警告和错误。

您首先看到了如何使用 MySQL Server 中的sql_notes选项来改变注释级消息是否被视为警告。还可以配置 MySQL 服务器是否应该在严格模式下运行。最后,您看到了应该监视 MySQL 错误日志,以检查应用是否记录了任何消息。

在 MySQL Connector/Python 中,您应该检查警告,并验证它们是否是更严重问题的迹象。至少在开发过程中,让 MySQL Connector/Python 抛出警告作为异常是有用的;然而,这仅在对mysql.connector模块使用游标时才可用,并且仍然需要您获取警告。

错误消息由错误号、SQL 状态和文本消息组成。在mysql.connectormysqlxerrorcode子模块中,错误号也是常量。当您回到代码的这一部分,并且不再记得例如错误号 1046 的含义时,使用常量可以更容易地理解正在使用的错误。

SQL 状态可用于确定错误的总体类别。它们还与错误号一起用于决定使用哪个异常类。非 MySQL 错误通常使用一个标准的 Python 异常类,而 MySQL 错误使用几个特定于 MySQL Connector/Python 的类中的一个。当您使用mysql.connector模块时,也可以为给定的 MySQL 错误号注册您自己的定制异常类。

本章的最后一部分研究了什么是锁定问题,以及遇到问题时应该怎么做。最后,错误可能需要排除,这是下一章的主题。