何时使用MySQLdb关闭游标


85

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

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

Answers:


79

既然不知道标准做法是什么,因为它通常不清楚并且很主观,所以您可以尝试向模块本身寻求指导。通常,使用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()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开发人员曾这样说

cnx.commit()在cur.close()之前听起来对我来说最合逻辑。也许您可以遵循以下规则:“如果不再需要,请关闭光标。” 因此,在关闭游标之前使用commit()。最后,对于Connector / Python而言,它并没有多大区别,但是对于其他数据库而言,则可能没有什么不同。

我希望这与您达到该主题的“标准实践”一样近。

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

我对此非常怀疑,在尝试这样做时,您可能会引入其他人为错误。最好决定约定并坚持执行。

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

开销可以忽略不计,完全不涉及数据库服务器;它完全在MySQLdb的实现中。如果您真的想知道创建新游标时发生了什么,可以BaseCursor.__init__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__方法。它只会closewith块的末尾调用参数对象的方法。(要查看实际情况,只需Foo使用__enter____exit__close包含简单print语句的方法定义一个类,然后将执行时的操作with Foo(): pass与执行时的操作进行比较with closing(Foo()): pass。)这有两个重要的含义:

首先,如果启用了自动提交模式,则BEGIN当您with connection在块末尾使用并提交或回滚事务时,MySQLdb将在服务器上进行显式事务。这些是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中或使用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

14
您的帖子内容最为详尽,但是即使重新阅读了几次,我仍然对关闭游标感到困惑。从关于该主题的众多文章来看,这似乎是一个常见的混淆点。我的要点是,游标似乎永远不需要调用.close()。那么,为什么还要使用.close()方法呢?
SMGreenfield

6
简短的答案是这cursor.close()Python DB API的一部分,该API并不是专门为MySQL编写的。
航空

1
为什么在del my_curs之后连接会关闭?
BAE 2015年

@ChengchengPeimy_curs保留对该connection对象的最后引用。一旦该引用不再存在,就应该对该connection对象进行垃圾收集。
航空

谢谢,这是一个很棒的答案。优秀的解释withMySQLdb.Connection__enter____exit__功能。再次感谢@Air。
尤金(Eugene)

33

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

我认为,with如果要在Flask或其他Web框架中使用它,则不是一个好的选择。如果情况http://flask.pocoo.org/docs/patterns/sqlite3/#sqlite3如此,那将会有问题。
詹姆斯·金

@ james-king我没有使用Flask,但是在您的示例中Flask会关闭数据库连接本身。其实在我的代码我使用略有不同的approach-我使用用于关闭游标 with closing(self.db.cursor()) as cur: cur.execute("UPDATE table1 SET status = %s WHERE id = %s",(self.INTEGR_STATUS_PROCESSING, id)) self.db.commit()
罗马Podlinov

@RomanPodlinov是的,如果您将它与光标一起使用,那么一切都会很好。
詹姆斯·金

7

注意:此答案适用于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破坏对象来释放数据库连接(数量有限)。

注意关闭游标,如果已经像这样显式关闭了连接,则完全没有意义。

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

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

不,实例化一个游标根本不会影响MySQL,并且基本上什么也没做

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

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


1 MySQL确实具有调用游标的构造,但它们仅存在于存储过程中;它们与PyMySQL游标完全不同,因此与此处无关。


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()时,您才会遇到这种问题,并且在遍历第一个查询的所有结果之前需要执行第二次游标。

否则,我要说的是,一旦完成从光标中取出所有数据的操作,请立即关闭光标。这样,您不必担心稍后在代码中捆绑松散的末端。


谢谢-考虑到您必须关闭游标才能提交更新/插入,所以我想一种进行更新/插入的简单方法是为每个守护程序获取一个游标,关闭游标以提交并立即获取一个新游标所以您下次就准备好了。听起来合理吗?
jmilloy

1
嘿,没问题。我实际上不知道通过关闭游标来提交更新/插入,但是在线快速搜索显示了这一点:conn = MySQLdb.connect(arguments_go_here)cursor = MySQLdb.cursor()cursor.execute(mysql_insert_statement_here)try:conn。 commit()除外:conn.rollback()#撤消发生错误时所做的更改。这样,数据库本身就会提交更改,而您不必担心游标本身。然后,您可以随时打开1个游标。在这里看看: tutorialspoint.com/python/python_database_access.htm
nct25 2011年

是的,如果那行得通,那么我就错了,还有其他一些原因使我认为必须关闭游标才能提交连接。
jmilloy 2011年

是的,我不知道,我发布的链接使我认为这可行。我猜想还会有更多的研究告诉您它是否确实有效,但是我认为您可能会继续使用它。希望我能对您有所帮助!
2011年

cursor不是线程安全的,如果在许多不同的线程中使用相同的光标,并且它们都是从db查询的,则fetchall()将提供随机数据。
ospider

-6

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


在MySQLdb中,连接和游标之间有区别。我为每个请求连接一次(目前),并且可以及早发现连接错误。但是游标呢?
jmilloy 2011年

恕我直言,这不是准确的建议。这取决于。如果您的代码将保持长时间连接(例如,它从数据库中获取一些数据,然后在1-5-10分钟内在服务器上执行某些操作并保持连接),并且它是多线程应用程序,那么它将很快产生一个问题(您将超过允许的最大连接数)。
罗曼·波德利诺夫
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.