93

我正在构建一个 WSGI Web 应用程序,并且我有一个 MySQL 数据库。我正在使用 MySQLdb,它提供了用于执行语句和获取结果的游标。获取和关闭游标的标准做法是什么?特别是,我的游标应该持续多久?我应该为每笔交易获得一个新的光标吗?

我相信您需要在提交连接之前关闭光标。查找不需要中间提交的事务集是否有任何显着优势,这样您就不必为每个事务获取新游标?获取新游标是否有很多开销,还是没什么大不了的?

4

5 回答 5

86

不要问什么是标准做法,因为这通常是不清楚和主观的,您可以尝试查看模块本身的指导。一般来说,with按照其他用户的建议使用关键字是一个好主意,但在这种特定情况下,它可能无法提供您所期望的功能。

从模块的 1.2.5 版本开始,使用以下代码 ( githubMySQLdb.Connection )实现上下文管理器协议

def __enter__(self):
    if self.get_autocommit():
        self.query("BEGIN")
    return self.cursor()

def __exit__(self, exc, value, tb):
    if exc:
        self.rollback()
    else:
        self.commit()

已经有几个现有的问答with,或者您可以阅读理解 Python 的“with”语句,但本质上发生的是__enter__在块的开头执行with,并__exit__在离开with块时执行。如果您打算稍后引用该对象,则可以使用可选语法with EXPR as VAR将返回的对象绑定 到名称。__enter__因此,鉴于上述实现,这是一种查询数据库的简单方法:

connection = MySQLdb.connect(...)
with connection as cursor:            # connection.__enter__ executes at this line
    cursor.execute('select 1;')
    result = cursor.fetchall()        # connection.__exit__ executes after this line
print result                          # prints "((1L,),)"

现在的问题是,退出with块后连接和光标的状态是什么?上面显示的__exit__方法仅调用self.rollback()or self.commit(),并且这些方法都不会继续调用该close()方法。游标本身没有__exit__定义方法——如果定义了也没关系,因为with它只是管理连接。因此,退出with块后连接和游标都保持打开状态。通过在上面的示例中添加以下代码很容易确认这一点:

try:
    cursor.execute('select 1;')
    print 'cursor is open;',
except MySQLdb.ProgrammingError:
    print 'cursor is closed;',
if connection.open:
    print 'connection is open'
else:
    print 'connection is closed'

您应该看到输出“光标已打开;连接已打开”打印到标准输出。

我相信您需要在提交连接之前关闭光标。

为什么?作为的基础的MySQL C APIMySQLdb没有实现任何游标对象,正如模块文档中所暗示的那样:“MySQL 不支持游标;但是,游标很容易模拟。” 实际上,MySQLdb.cursors.BaseCursor该类直接继承自object游标,并且在提交/回滚方面没有对游标施加此类限制。一位 Oracle 开发人员这样说

cur.close() 之前的 cnx.commit() 对我来说听起来最合乎逻辑。也许您可以按照规则:“如果不再需要光标,请关闭它。” 因此在关闭游标之前提交()。最后,对于连接器/Python,它并没有太大的区别,但或其他数据库可能。

我希望这与您在此主题上的“标准实践”一样接近。

查找不需要中间提交的事务集是否有任何显着优势,这样您就不必为每个事务获取新游标?

我非常怀疑它,并且在尝试这样做时,您可能会引入额外的人为错误。最好决定一个约定并坚持下去。

获取新游标是否有很多开销,还是没什么大不了的?

开销可以忽略不计,根本不涉及数据库服务器;它完全在 MySQLdb 的实现中。如果您真的很想知道创建新游标时发生了什么,您可以BaseCursor.__init__github 上查看。

回到我们之前讨论的时候with,也许现在你可以理解为什么MySQLdb.Connection__enter____exit__方法在每个块中都会给你一个全新的光标对象,with而不必费心跟踪它或在块的末尾关闭它。它相当轻巧,纯粹是为了您的方便而存在。

如果对游标对象进行微观管理真的很重要,则可以使用contextlib.closure来弥补游标对象没有定义__exit__方法的事实。就此而言,您还可以使用它来强制连接对象在退出with块时自行关闭。这应该输出“my_curs 已关闭;my_conn 已关闭”:

from contextlib import closing
import MySQLdb

with closing(MySQLdb.connect(...)) as my_conn:
    with closing(my_conn.cursor()) as my_curs:
        my_curs.execute('select 1;')
        result = my_curs.fetchall()
try:
    my_curs.execute('select 1;')
    print 'my_curs is open;',
except MySQLdb.ProgrammingError:
    print 'my_curs is closed;',
if my_conn.open:
    print 'my_conn is open'
else:
    print 'my_conn is closed'

注意with closing(arg_obj)不会调用参数对象的__enter____exit__方法;它只会close在块的末尾调用参数对象的方法with。(要查看实际情况,只需使用 、 和包含简单语句的方法定义一个类Foo,并将执行时发生的情况与执行时发生__enter__的情况进行比较。)这​​有两个重要含义:__exit__closeprintwith Foo(): passwith closing(Foo()): pass

首先,如果启用了自动提交模式,MySQLdb 将BEGIN在您使用时在服务器上进行显式事务,with connection并在块结束时提交或回滚事务。这些是 MySQLdb 的默认行为,旨在保护您免受 MySQL 立即提交任何和所有 DML 语句的默认行为。MySQLdb 假定当您使用上下文管理器时,您需要一个事务,并使用显式BEGIN绕过服务器上的自动提交设置。如果您习惯使用with connection,您可能会认为 autocommit 被禁用,而实际上它只是被绕过。如果你添加,你可能会得到一个不愉快的惊喜closing到您的代码并失去交易完整性;您将无法回滚更改,您可能会开始看到并发错误,而且原因可能不是很明显。

其次,将连接对象with closing(MySQLdb.connect(user, pass)) as VAR绑定到,相比之下,将新的游标对象绑定到。在后一种情况下,您将无法直接访问连接对象!相反,您必须使用游标的属性,该属性提供对原始连接的代理访问。当游标关闭时,其属性设置为。这会导致放弃连接,直到发生以下情况之一:VARwith MySQLdb.connect(user, pass) as VARVARconnectionconnectionNone

  • 删除所有对光标的引用
  • 光标超出范围
  • 连接超时
  • 通过服务器管理工​​具手动关闭连接

您可以通过监视打开的连接(在 Workbench 中或使用SHOW PROCESSLIST)来测试这一点,同时一一执行以下行:

with MySQLdb.connect(...) as my_curs:
    pass
my_curs.close()
my_curs.connection          # None
my_curs.connection.close()  # throws AttributeError, but connection still open
del my_curs                 # connection will close here
于 2014-03-24T19:26:39.473 回答
34

最好使用'with'关键字重写它。'With' 会自动关闭游标(这很重要,因为它是非托管资源)。好处是它也会在出现异常时关闭光标。

from contextlib import closing
import MySQLdb

''' At the beginning you open a DB connection. Particular moment when
  you open connection depends from your approach:
  - it can be inside the same function where you work with cursors
  - in the class constructor
  - etc
'''
db = MySQLdb.connect("host", "user", "pass", "database")
with closing(db.cursor()) as cur:
    cur.execute("somestuff")
    results = cur.fetchall()
    # do stuff with results

    cur.execute("insert operation")
    # call commit if you do INSERT, UPDATE or DELETE operations
    db.commit()

    cur.execute("someotherstuff")
    results2 = cur.fetchone()
    # do stuff with results2

# at some point when you decided that you do not need
# the open connection anymore you close it
db.close()
于 2013-05-23T15:59:30.970 回答
9

注意:此答案适用于PyMySQL,它是 MySQLdb 的直接替代品,并且实际上是 MySQLdb 的最新版本,因为 MySQLdb 停止维护。我相信这里的一切适用于遗留 MySQLdb,但尚未检查。

首先,一些事实:

  • Python 的with语法__enter__在执行with块体之前调用上下文管理器的方法,之后调用它的__exit__方法。
  • 连接有一个__enter__除了创建和返回游标之外什么都不做的方法,以及一个__exit__提交或回滚的方法(取决于是否抛出异常)。它不会关闭连接。
  • PyMySQL 中的游标纯粹是用 Python 实现的抽象;MySQL 本身没有等效的概念。1
  • 游标有一个__enter__不做任何事情的__exit__方法和一个“关闭”游标的方法(这只是意味着将游标对其父连接的引用置空并丢弃存储在游标上的任何数据)。
  • 游标包含对生成它们的连接的引用,但连接不包含对它们创建的游标的引用。
  • 连接有一个__del__关闭它们的方法
  • 根据https://docs.python.org/3/reference/datamodel.html,CPython(默认 Python 实现)使用引用计数,并在对对象的引用数达到零时自动删除对象。

把这些东西放在一起,我们看到像这样的幼稚代码在理论上是有问题的:

# Problematic code, at least in theory!
import pymysql
with pymysql.connect() as cursor:
    cursor.execute('SELECT 1')

# ... happily carry on and do something unrelated

问题是没有任何东西关闭连接。实际上,如果您将上面的代码粘贴到 Python shell 中,然后SHOW FULL PROCESSLIST在 MySQL shell 中运行,您将能够看到您创建的空闲连接。由于 MySQL 的默认连接数是151,这并不,理论上如果您有许多进程保持这些连接打开,您可能会开始遇到问题。

但是,在 CPython 中,有一个可取之处可以确保像我上面的示例这样的代码可能不会导致您离开大量打开的连接。这种可取之处在于,一旦cursor超出范围(例如,创建它的函数完成,或cursor获得分配给它的另一个值),它的引用计数就会达到零,这会导致它被删除,从而丢弃连接的引用计数为零,导致__del__调用连接的方法,强制关闭连接。如果您已经将上面的代码粘贴到您的 Python shell 中,那么您现在可以通过运行来模拟它cursor = 'arbitrary value';执行此操作后,您打开的连接将从SHOW PROCESSLIST输出中消失。

然而,依赖它是不优雅的,理论上可能在 CPython 以外的 Python 实现中失败。理论上,更清洁的是显式.close()连接(释放数据库上的连接而不等待 Python 销毁对象)。这个更健壮的代码如下所示:

import contextlib
import pymysql
with contextlib.closing(pymysql.connect()) as conn:
    with conn as cursor:
        cursor.execute('SELECT 1')

这很丑陋,但不依赖 Python 破坏你的对象来释放你的(有限数量的)数据库连接。

请注意,如果您已经像这样明确地关闭连接,则关闭cursor是完全没有意义的。

最后,在这里回答次要问题:

获取新游标是否有很多开销,还是没什么大不了的?

不,实例化游标根本不会命中 MySQL,而且基本上什么都不做

查找不需要中间提交的事务集是否有任何显着优势,这样您就不必为每个事务获取新游标?

这是情境性的,很难给出一般性的答案。正如https://dev.mysql.com/doc/refman/en/optimizing-innodb-transaction-management.html所说,“如果应用程序每秒提交数千次,它可能会遇到性能问题,如果它仅每 2-3 小时提交一次”。您为每次提交付出了性能开销,但是通过让事务打开更长时间,您增加了其他连接不得不花时间等待锁定的机会,增加了死锁的风险,并可能增加其他连接执行的某些查找的成本.


1 MySQL确实有一个调用游标的结构,但它们只存在于存储过程中;它们与 PyMySQL 游标完全不同,在这里不相关。

于 2017-03-23T00:19:32.000 回答
5

我认为您最好尝试对所有执行使用一个光标,并在代码末尾关闭它。它更容易使用,并且它也可能具有效率优势(不要引用我的话)。

conn = MySQLdb.connect("host","user","pass","database")
cursor = conn.cursor()
cursor.execute("somestuff")
results = cursor.fetchall()
..do stuff with results
cursor.execute("someotherstuff")
results2 = cursor.fetchall()
..do stuff with results2
cursor.close()

关键是您可以将游标的执行结果存储在另一个变量中,从而释放游标以进行第二次执行。只有在使用 fetchone() 时才会以这种方式遇到问题,并且需要在遍历第一个查询的所有结果之前进行第二次游标执行。

否则,我会说在您完成所有数据后立即关闭您的游标。这样您就不必担心稍后在代码中打结松散的结尾。

于 2011-07-30T19:06:12.787 回答
-6

我建议像 php 和 mysql 那样做。在打印第一个数据之前,从代码的开头开始 i 。因此,如果您收到连接错误,您可以显示50x(不记得内部错误是什么)错误消息。并在整个会话期间保持打开状态,并在您知道不再需要它时关闭它。

于 2011-04-14T21:40:40.297 回答