何时使用MySQLdb关闭游标

时间:2011-04-14 21:23:25

标签: python mysql mysql-python

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

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

5 个答案:

答案 0 :(得分:67)

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

从模块的1.2.5版开始,MySQLdb.Connection使用以下代码(context manager protocol)实现github

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的Q& A,或者您可以阅读Understanding Python's "with" statement,但基本上会发生__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()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'

您应该看到输出"光标已打开;连接是开放的"打印到stdout。

  

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

为什么呢? MySQL C APIMySQLdb的基础,它不实现任何游标对象,如模块文档中所暗示的那样:"MySQL does not support cursors; however, cursors are easily emulated."实际上,MySQLdb.cursors.BaseCursor类直接继承自object {1}}并且在提交/回滚方面对游标没有这样的限制。 Oracle开发人员had this to say

  在cur.close()之前的

cnx.commit()听起来对我来说最合乎逻辑。可能是你   可以遵守规则:"如果您不再需要它,请关闭光标。"   因此在关闭游标之前commit()。最后,为   连接器/ Python,它没有太大的区别,但或其他   它可能是数据库。

我希望你能够接近"标准练习"关于这个问题。

  

找到不需要中间提交的交易集是否有任何显着优势,这样您就不必为每笔交易获得新的游标?

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

  

获取新游标是否有很多开销,或者这不是什么大问题?

开销可以忽略不计,根本不会触及数据库服务器;它完全在MySQLdb的实现中。如果您真的很想知道创建新光标时发生了什么,可以look at BaseCursor.__init__ on github

回到早些时候我们讨论with时,或许现在你可以理解为什么MySQLdb.Connection__enter____exit__方法为你提供了一个全新的游标对象每个with块,并且不要跟踪它或在块的末尾关闭它。它非常轻巧,纯粹为了您的方便而存在。

如果对微观管理游标对象非常重要,可以使用contextlib.closing来弥补游标对象没有定义__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__close方法定义类print,并比较当with Foo(): pass执行with closing(Foo()): pass时会发生什么。)这有两个重要含义:

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

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

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

您可以在逐个执行以下行时监视打开的连接(在Workbench中或通过using 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

答案 1 :(得分:28)

最好使用'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()

答案 2 :(得分: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()时才会遇到问题,并且需要在迭代第一个查询的所有结果之前执行第二个游标执行。

否则,我会说,只要你完成从中获取所有数据就关闭你的游标。这样,您就不必担心以后会在代码中绑定松散的结尾。

答案 3 :(得分:5)

注意:这个答案适用于PyMySQL,它是MySQLdb的替代品,实际上是自MySQLdb停止维护以来最新版本的MySQLdb。我相信这里的所有内容是遗留MySQLdb的真实,但尚未检查。

首先,一些事实:

  • Python的with语法在执行__enter__块的主体之前调用上下文管理器的with方法,之后调用其__exit__方法。
  • Connections有一个__enter__方法,除了创建和返回游标之外什么都不做,还有一个__exit__方法提交或回滚(取决于是否抛出异常)。它没有关闭连接。
  • PyMySQL中的游标纯粹是用Python实现的抽象; MySQL本身没有相同的概念。 1
  • 游标有一个__enter__方法,它不执行任何操作,并且__exit__方法可以“关闭”游标(这意味着将游标对其父连接的引用置零并丢弃任何存储的数据在光标上。)
  • 游标持有对产生它们的连接的引用,但连接不包含对它们已创建的游标的引用。
  • 连接有__del__方法关闭它们
  • Per 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中然后在MySQL shell上运行SHOW FULL PROCESSLIST,您将能够看到您创建的空闲连接。由于MySQL的默认连接数为151,而不是 large ,如果你有许多进程保持这些连接处于打开状态,理论上你可能会遇到问题。

然而,在CPython中,有一个保存优雅,可以确保像上面上面的例子这样的代码不会让你留下大量的开放连接。节省的优点是,只要cursor超出范围(例如,创建它的函数完成,或cursor获得另一个值),其引用计数就会达到零,这会导致要删除它,将连接的引用计数丢弃为零,导致调用连接__del__方法强制关闭连接。如果您已经将上面的代码粘贴到Python shell中,那么现在可以通过运行cursor = 'arbitrary value'来模拟这个代码。一旦这样做,您打开的连接将从SHOW PROCESSLIST输出消失。

然而,依赖于此是不优雅的,理论上可能在CPython以外的Python实现中失败。理论上,Cleaner将明确地.close()连接(释放数据库上的连接而不等待Python破坏对象)。这个更健壮的代码如下所示:

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

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

请注意,关闭游标,如果您已经明确地关闭了连接,则完全没有意义。

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

  

获取新游标是否有很多开销,或者这不是什么大问题?

不,实例化游标根本不会触及MySQL basically does nothing

  

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

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

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

答案 4 :(得分:-4)

我建议像php和mysql一样。在打印第一个数据之前,在代码的开头启动i。因此,如果出现连接错误,您可以显示50x(不记得内部错误)错误消息。并保持整个会话开放,当你知道你不再需要它时关闭它。