何时使用 MySQLdb 关闭游标
Posted
技术标签:
【中文标题】何时使用 MySQLdb 关闭游标【英文标题】:When to close cursors using MySQLdb 【发布时间】:2011-08-05 21:46:26 【问题描述】:我正在构建一个 WSGI Web 应用程序并且我有一个 mysql 数据库。我正在使用 MySQLdb,它提供了用于执行语句和获取结果的游标。 获取和关闭游标的标准做法是什么?特别是,我的游标应该持续多久?我应该为每笔交易获取一个新光标吗?
我相信您需要在提交连接之前关闭光标。查找不需要中间提交的事务集是否有任何显着优势,这样您就不必为每个事务获取新游标?获取新游标是否有很多开销,还是没什么大不了的?
【问题讨论】:
【参考方案1】:不要问什么是标准做法,因为这通常是不清楚和主观的,您可以尝试从模块本身寻求指导。一般来说,按照其他用户的建议使用 with
关键字是个好主意,但在这种特定情况下,它可能无法提供您所期望的功能。
从模块的 1.2.5 版本开始,MySQLdb.Connection
使用以下代码 (github) 实现 context manager protocol:
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
的现有问答,或者您可以阅读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'
您应该会看到输出“光标已打开;连接已打开”打印到标准输出。
我认为您需要在提交连接之前关闭光标。
为什么? MySQL C API 是 MySQLdb
的基础,它没有实现任何游标对象,正如模块文档中所暗示的那样:"MySQL does not support cursors; however, cursors are easily emulated." 实际上,MySQLdb.cursors.BaseCursor
类直接继承自 object
,并且对关于提交/回滚的游标。甲骨文开发者had this to say:
cur.close() 之前的cnx.commit() 对我来说听起来最合乎逻辑。可能是你 可以遵循规则:“如果不再需要,请关闭光标。” 因此在关闭游标之前提交()。最后,对于 连接器/Python,没有太大区别,但或其他 可能的数据库。
我希望这与您在此主题上的“标准实践”一样接近。
查找不需要中间提交的事务集是否有任何显着优势,这样您就不必为每个事务获取新游标?
我非常怀疑这一点,并且在尝试这样做时,您可能会引入额外的人为错误。最好决定一个约定并坚持下去。
获取新游标是否有很多开销,还是没什么大不了的?
开销可以忽略不计,根本不涉及数据库服务器;它完全在 MySQLdb 的实现中。如果您真的很想知道创建新光标时发生了什么,您可以look at BaseCursor.__init__
on github。
回到之前我们讨论with
时,也许现在你可以理解为什么MySQLdb.Connection
类__enter__
和__exit__
方法在每个with
块中给你一个全新的游标对象并且不要'不必费心跟踪它或在块的末尾关闭它。它相当轻巧,纯粹是为了您的方便而存在。
如果对游标对象的微观管理真的很重要,你可以使用contextlib.closing 来弥补游标对象没有定义__exit__
方法的事实。就此而言,您还可以使用它来强制连接对象在退出 with
块时自行关闭。这应该输出“my_curs is closed; my_conn is closed”:
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__
方法;它会仅在with
块的末尾调用参数对象的close
方法。 (要查看实际情况,只需使用包含简单print
语句的__enter__
、__exit__
和close
方法定义一个类Foo
,并将执行with Foo(): pass
时发生的情况与您执行with Foo(): pass
时发生的情况进行比较做with closing(Foo()): pass
。)这有两个重要的含义:
首先,如果启用了自动提交模式,当您使用with connection
并在块末尾提交或回滚事务时,MySQLdb 将在服务器上BEGIN
显式事务。这些是 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
【讨论】:
您的帖子最详尽,但即使重新阅读了几次,我仍然对关闭游标感到困惑。从关于该主题的众多帖子来看,这似乎是一个常见的混淆点。我的收获是游标似乎不需要调用 .close() —— 永远。那么为什么还要有一个 .close() 方法呢? 简短的回答是cursor.close()
是the Python DB API 的一部分,这并不是专门为 MySQL 编写的。
为什么连接会在 del my_curs 之后关闭?
@ChengchengPei my_curs
持有对connection
对象的最后引用。一旦该引用不再存在,connection
对象应该被垃圾回收。
这是一个很棒的答案,谢谢。对with
和MySQLdb.Connection
的__enter__
和__exit__
函数的出色解释。再次感谢@Air。【参考方案2】:
最好用'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()
【讨论】:
如果您想在 Flask 或其他 Web 框架中使用with
,我认为它不是一个好的选择。如果情况是http://flask.pocoo.org/docs/patterns/sqlite3/#sqlite3
,那就有问题了。
@james-king 我没有使用 Flask,但在您的示例中,Flask 将自行关闭数据库连接。实际上,在我的代码中,我使用了稍微不同的方法——我使用 with 来关闭光标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()
@RomanPodlinov 是的,如果你将它与光标一起使用,那就没问题了。【参考方案3】:
注意:此答案适用于PyMySQL,它是 MySQLdb 的直接替代品,并且实际上是 MySQLdb 停止维护后的最新版本。我相信这里的所有内容也适用于旧版 MySQLdb,但尚未检查。
首先,一些事实:
Python 的with
语法在执行with
块的主体之前调用上下文管理器的__enter__
方法,然后调用它的__exit__
方法。
Connections 有一个__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 中,然后在 MySQL shell 中运行SHOW FULL PROCESSLIST
,您将能够看到您创建的空闲连接。由于 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 和 basically does nothing。
查找不需要中间提交的事务集是否有任何显着优势,这样您就不必为每个事务获取新游标?
这是情境性的,很难给出一般性的答案。正如https://dev.mysql.com/doc/refman/en/optimizing-innodb-transaction-management.html 所说,“如果应用程序每秒提交数千次,它可能会遇到性能问题,如果它仅每 2-3 小时提交一次,则可能会遇到不同的性能问题”。您为每次提交付出了性能开销,但是通过让事务打开更长时间,您增加了其他连接必须花费时间等待锁定的机会,增加了死锁的风险,并可能增加其他连接执行的某些查找的成本.
1 MySQL 确实有一个构造它调用cursor,但它们只存在于存储过程中;它们与 PyMySQL 游标完全不同,在这里不相关。
【讨论】:
【参考方案4】:我认为您最好尝试对所有执行使用一个光标,然后在代码末尾关闭它。它更易于使用,并且还可能具有效率优势(请不要引用我的话)。
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() 时才会遇到这种问题,并且需要在遍历第一个查询的所有结果之前执行第二个游标。
否则,我会说在您完成所有数据后立即关闭您的游标。这样您就不必担心以后在代码中打结松散的结尾。
【讨论】:
谢谢 - 考虑到您必须关闭光标才能提交更新/插入,我想更新/插入的一种简单方法是为每个守护进程获取一个光标,关闭光标提交并立即获得一个新光标,以便下次准备好。这听起来合理吗? 嘿,没问题。我实际上并不知道通过关闭游标来提交更新/插入,但在线快速搜索显示:conn = MySQLdb.connect(arguments_go_here) cursor = MySQLdb.cursor() cursor.execute(mysql_insert_statement_here) try: conn. commit() except: conn.rollback() # 如果发生错误,撤消所做的更改。这样,数据库本身就会提交更改,而您不必担心游标本身。然后你可以一直打开 1 个光标。看看这里:tutorialspoint.com/python/python_database_access.htm 是的,如果这样可行,那我就错了,还有其他一些原因让我认为我必须关闭光标才能提交连接。 是的,我不知道,我发布的那个链接让我觉得那行得通。我想更多的研究会告诉你它是否确实有效,但我认为你可能会接受它。希望我对你有帮助! 游标不是线程安全的,如果你在许多不同的线程中使用相同的游标,并且它们都从数据库中查询,fetchall() 将给出随机数据。【参考方案5】:我建议像 php 和 mysql 那样做。在打印第一个数据之前,从代码的开头开始 i 。因此,如果您收到连接错误,您可以显示50x
(不记得内部错误是什么)错误消息。并在整个会话期间保持打开状态,并在您知道不再需要它时关闭它。
【讨论】:
在 MySQLdb 中,连接和游标是有区别的。我每个请求连接一次(现在),并且可以及早检测到连接错误。但是游标呢? 恕我直言,这不是准确的建议。这取决于。如果您的代码将长时间保持连接(例如,它从数据库中获取一些数据,然后在 1-5-10 分钟内它在服务器上执行某些操作并保持连接)并且它是多线程应用程序,它很快就会产生问题(您将超过允许的最大连接数)。以上是关于何时使用 MySQLdb 关闭游标的主要内容,如果未能解决你的问题,请参考以下文章