Cuando cerrar cursores usando MySQLdb

86

Estoy construyendo una aplicación web WSGI y tengo una base de datos MySQL. Estoy usando MySQLdb, que proporciona cursores para ejecutar declaraciones y obtener resultados. ¿Cuál es la práctica estándar para obtener y cerrar cursores? En particular, ¿cuánto deberían durar mis cursores? ¿Debería obtener un nuevo cursor para cada transacción?

Creo que debe cerrar el cursor antes de confirmar la conexión. ¿Existe alguna ventaja significativa en encontrar conjuntos de transacciones que no requieran confirmaciones intermedias para que no tenga que obtener nuevos cursores para cada transacción? ¿Hay muchos gastos generales para obtener nuevos cursores o simplemente no es un gran problema?

jmilloy
fuente

Respuestas:

80

En lugar de preguntar cuál es la práctica estándar, ya que a menudo es poco claro y subjetivo, puede intentar consultar el módulo en sí para obtener orientación. En general, usar la withpalabra clave como sugirió otro usuario es una gran idea, pero en esta circunstancia específica puede que no le brinde la funcionalidad que espera.

A partir de la versión 1.2.5 del módulo, MySQLdb.Connectionimplementa el protocolo del administrador de contexto con el siguiente código ( 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()

withYa hay varias preguntas y respuestas sobre , o puede leer Entender la declaración "con" de Python , pero esencialmente lo que sucede es que se __enter__ejecuta al comienzo del withbloque y se __exit__ejecuta al salir del withbloque. Puede utilizar la sintaxis opcional with EXPR as VARpara vincular el objeto devuelto por __enter__a un nombre si desea hacer referencia a ese objeto más adelante. Entonces, dada la implementación anterior, aquí hay una forma simple de consultar su base de datos:

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,),)"

La pregunta ahora es, ¿cuáles son los estados de la conexión y el cursor después de salir del withbloque? El __exit__método que se muestra arriba solo llama a self.rollback()o self.commit(), y ninguno de esos métodos continúa llamando al close()método. El cursor en sí no tiene un __exit__método definido, y no importaría si lo hiciera, porque withsolo administra la conexión. Por tanto, tanto la conexión como el cursor permanecen abiertos después de salir del withbloque. Esto se confirma fácilmente agregando el siguiente código al ejemplo anterior:

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'

Debería ver la salida "cursor abierto; conexión abierta" impresa en stdout.

Creo que debe cerrar el cursor antes de confirmar la conexión.

¿Por qué? La API de MySQL C , que es la base de MySQLdb, no implementa ningún objeto de cursor, como se implica en la documentación del módulo: "MySQL no admite cursores; sin embargo, los cursores se emulan fácilmente". De hecho, la MySQLdb.cursors.BaseCursorclase hereda directamente objecty no impone tal restricción a los cursores con respecto al compromiso / retroceso. Un desarrollador de Oracle dijo lo siguiente :

cnx.commit () antes de cur.close () me suena más lógico. Tal vez pueda seguir la regla: "Cierre el cursor si ya no lo necesita". Por lo tanto, commit () antes de cerrar el cursor. Al final, para Connector / Python, no hace mucha diferencia, pero para otras bases de datos podría.

Supongo que eso es lo más cercano a la "práctica estándar" sobre este tema.

¿Existe alguna ventaja significativa en encontrar conjuntos de transacciones que no requieran confirmaciones intermedias para que no tenga que obtener nuevos cursores para cada transacción?

Lo dudo mucho, y al intentar hacerlo, puede introducir un error humano adicional. Mejor decidirse por una convención y ceñirse a ella.

¿Hay muchos gastos generales para obtener nuevos cursores o simplemente no es un gran problema?

La sobrecarga es insignificante y no toca el servidor de la base de datos en absoluto; está completamente dentro de la implementación de MySQLdb. Usted puede mirar en BaseCursor.__init__github si tiene mucha curiosidad por saber qué está sucediendo cuando crea un nuevo cursor.

Volviendo a cuando estábamos discutiendo with, tal vez ahora pueda entender por qué la MySQLdb.Connectionclase__enter__ y los __exit__métodos le brindan un nuevo objeto de cursor en cada withbloque y no se moleste en seguirlo o cerrarlo al final del bloque. Es bastante ligero y existe únicamente para su conveniencia.

Si es realmente tan importante para usted microgestionar el objeto cursor, puede usar contextlib.closing para compensar el hecho de que el objeto cursor no tiene un __exit__método definido . De hecho, también puede usarlo para forzar que el objeto de conexión se cierre al salir de un withbloque. Esto debería mostrar "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'

Tenga en cuenta que with closing(arg_obj)no llamará a los métodos __enter__y del objeto argumento __exit__; será solamente llamar el objeto de discusión closemétodo al final del withbloque. (Para ver esto en acción, sólo tiene que definir una clase Foocon __enter__, __exit__y closemétodos que contiene simples printdeclaraciones, y comparar lo que sucede cuando usted hacewith Foo(): pass a lo que sucede cuando lo hace with closing(Foo()): pass.) Esto tiene dos implicaciones importantes:

Primero, si el modo de confirmación automática está habilitado, MySQLdb realizará BEGINuna transacción explícita en el servidor cuando utilice su código y pierda la integridad transaccional; no podrá deshacer los cambios, es posible que comience a ver errores de simultaneidad y es posible que no sea inmediatamente obvio por qué.with connection y confirme o deshaga la transacción al final del bloque. Estos son comportamientos predeterminados de MySQLdb, destinados a protegerlo del comportamiento predeterminado de MySQL de confirmar inmediatamente todas y cada una de las declaraciones DML. MySQLdb asume que cuando usa un administrador de contexto, desea una transacción y usa el explícito BEGINpara omitir la configuración de confirmación automática en el servidor. Si está acostumbrado a usar with connection, podría pensar que la confirmación automática está deshabilitada cuando en realidad solo se estaba omitiendo. Puede obtener una sorpresa desagradable si agregaclosing

En segundo lugar, with closing(MySQLdb.connect(user, pass)) as VARse une el objeto de conexión para VAR, en contraste con with MySQLdb.connect(user, pass) as VAR, que se une un nuevo objeto cursor a VAR. En el último caso, no tendría acceso directo al objeto de conexión. En su lugar, tendría que utilizar el connectionatributo del cursor , que proporciona acceso de proxy a la conexión original. Cuando el cursor está cerrado, su connectionatributo se establece en None. Esto da como resultado una conexión abandonada que se mantendrá hasta que ocurra una de las siguientes situaciones:

  • Se eliminan todas las referencias al cursor
  • El cursor sale fuera de alcance
  • La conexión se agota
  • La conexión se cierra manualmente mediante las herramientas de administración del servidor.

Puede probar esto monitoreando las conexiones abiertas (en Workbench o usandoSHOW PROCESSLIST ) mientras ejecuta las siguientes líneas una por una:

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
Aire
fuente
14
su publicación fue más exhaustiva, pero incluso después de volver a leerla varias veces, todavía me siento desconcertado con respecto a los cursores de cierre. A juzgar por las numerosas publicaciones sobre el tema, parece ser un punto común de confusión. Mi conclusión es que los cursores aparentemente NO requieren que se llame a .close (), nunca. Entonces, ¿por qué incluso tener un método .close ()?
SMGreenfield
6
La respuesta corta es que cursor.close()es parte de la API de Python DB , que no se escribió específicamente con MySQL en mente.
aire
1
¿Por qué la conexión se cerrará después de del my_curs?
BAE
@ChengchengPei my_curscontiene la última referencia al connectionobjeto. Una vez que esa referencia ya no existe, el connectionobjeto debe ser recolectado como basura.
aire
Esta es una respuesta fantástica, gracias. Excelente explicación de withy MySQLdb.Connection's __enter__y __exit__funciones. Nuevamente, gracias @Air.
Eugene
33

Es mejor reescribirlo usando la palabra clave 'con'. 'Con' se ocupará de cerrar el cursor (es importante porque es un recurso no administrado) automáticamente. El beneficio es que también cerrará el cursor en caso de excepción.

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()
Roman Podlinov
fuente
No creo que withsea ​​una buena opción si desea usarlo en Flask u otro marco web. Si la situación es http://flask.pocoo.org/docs/patterns/sqlite3/#sqlite3, habrá problemas.
James King
@ james-king No trabajé con Flask, pero en su ejemplo, Flask cerrará la conexión db. En realidad, en mi código utilizo un enfoque ligeramente diferente: uso con para cursores cercanos 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()
Roman Podlinov
@RomanPodlinov Sí, si lo usas con el cursor, todo irá bien.
James King
7

Nota: esta respuesta es para PyMySQL , que es un reemplazo directo de MySQLdb y efectivamente la última versión de MySQLdb desde que MySQLdb dejó de mantenerse. Creo que todo aquí también es cierto para el MySQLdb heredado, pero no lo he comprobado.

En primer lugar, algunos hechos:

  • La withsintaxis de Python llama al __enter__método del administrador de contexto antes de ejecutar el cuerpo del withbloque y su __exit__método después.
  • Las conexiones tienen un __enter__método que no hace nada más que crear y devolver un cursor, y un __exit__método que confirma o revierte (dependiendo de si se lanzó una excepción). Que no se cierre la conexión.
  • Los cursores en PyMySQL son puramente una abstracción implementada en Python; no existe un concepto equivalente en MySQL. 1
  • Los cursores tienen un __enter__método que no hace nada y un __exit__método que "cierra" el cursor (lo que simplemente significa anular la referencia del cursor a su conexión principal y desechar cualquier dato almacenado en el cursor).
  • Los cursores contienen una referencia a la conexión que los generó, pero las conexiones no tienen una referencia a los cursores que han creado.
  • Las conexiones tienen un __del__método que las cierra
  • Según https://docs.python.org/3/reference/datamodel.html , CPython (la implementación predeterminada de Python) usa el recuento de referencias y elimina automáticamente un objeto una vez que el número de referencias llega a cero.

Juntando estas cosas, vemos que un código ingenuo como este es, en teoría, problemático:

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

# ... happily carry on and do something unrelated

El problema es que nada ha cerrado la conexión. De hecho, si pega el código anterior en un shell de Python y luego lo ejecuta SHOW FULL PROCESSLISTen un shell de MySQL, podrá ver la conexión inactiva que creó. Dado que el número predeterminado de conexiones de MySQL es 151 , que no es enorme , teóricamente podría comenzar a tener problemas si tuviera muchos procesos que mantengan abiertas estas conexiones.

Sin embargo, en CPython, hay una gracia salvadora que garantiza que un código como mi ejemplo anterior probablemente no hará que deje muchas conexiones abiertas. Esa gracia salvadora es que tan pronto como cursorsale del alcance (por ejemplo, la función en la que se creó finaliza, o cursorse le asigna otro valor), su recuento de referencias llega a cero, lo que hace que se elimine, eliminando el recuento de referencias de la conexión. a cero, lo que provoca __del__que se llame al método de la conexión, lo que fuerza el cierre de la conexión. Si ya pegó el código anterior en su shell de Python, ahora puede simularlo ejecutando cursor = 'arbitrary value'; tan pronto como haga esto, la conexión que abrió desaparecerá de la SHOW PROCESSLISTsalida.

Sin embargo, confiar en esto no es elegante y, en teoría, podría fallar en implementaciones de Python distintas de CPython. Más limpio, en teoría, sería explícitamente .close()la conexión (para liberar una conexión en la base de datos sin esperar a que Python destruya el objeto). Este código más robusto se ve así:

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

Esto es feo, pero no depende de que Python destruya sus objetos para liberar su (número finito disponible de) conexiones de base de datos.

Tenga en cuenta que cerrar el cursor , si ya está cerrando la conexión explícitamente así, no tiene sentido.

Finalmente, para responder a las preguntas secundarias aquí:

¿Hay muchos gastos generales para obtener nuevos cursores o simplemente no es un gran problema?

No, crear una instancia de un cursor no afecta a MySQL en absoluto y básicamente no hace nada .

¿Existe alguna ventaja significativa en encontrar conjuntos de transacciones que no requieran confirmaciones intermedias para que no tenga que obtener nuevos cursores para cada transacción?

Esto es situacional y difícil de dar una respuesta general. Como dice https://dev.mysql.com/doc/refman/en/optimizing-innodb-transaction-management.html , "una aplicación puede encontrar problemas de rendimiento si se compromete miles de veces por segundo, y diferentes problemas de rendimiento si se compromete solo cada 2-3 horas " . Paga una sobrecarga de rendimiento por cada confirmación, pero al dejar las transacciones abiertas durante más tiempo, aumenta la posibilidad de que otras conexiones tengan que pasar tiempo esperando bloqueos, aumenta el riesgo de interbloqueos y aumenta potencialmente el costo de algunas búsquedas realizadas por otras conexiones .


1 MySQL hace tener una construcción que llama un cursor pero sólo existe dentro de procedimientos almacenados; son completamente diferentes a los cursores PyMySQL y no son relevantes aquí.

Mark Amery
fuente
5

Creo que será mejor que intente usar un cursor para todas sus ejecuciones y lo cierre al final de su código. Es más fácil trabajar con él y también podría tener beneficios de eficiencia (no me cite en eso).

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()

El punto es que puede almacenar los resultados de la ejecución de un cursor en otra variable, liberando así su cursor para realizar una segunda ejecución. Te encuentras con problemas de esta manera solo si estás usando fetchone () y necesitas realizar una segunda ejecución del cursor antes de haber iterado a través de todos los resultados de la primera consulta.

De lo contrario, diría que cierre los cursores tan pronto como haya terminado de obtener todos los datos de ellos. De esa manera, no tiene que preocuparse por atar cabos sueltos más adelante en su código.

nct25
fuente
Gracias - Teniendo en cuenta que tienes que cerrar el cursor para realizar una actualización / inserción, supongo que una forma fácil de hacerlo para actualizaciones / inserciones sería obtener un cursor para cada demonio, cerrar el cursor para confirmar e inmediatamente obtener un nuevo cursor así que estás listo la próxima vez. ¿Suena razonable?
jmilloy
1
Oye, no hay problema. En realidad, no sabía cómo realizar la actualización / inserción cerrando los cursores, pero una búsqueda rápida en línea muestra esto: conn = MySQLdb.connect (argumentos_go_here) cursor = MySQLdb.cursor () cursor.execute (mysql_insert_statement_here) try: conn. commit () excepto: conn.rollback () # deshacer los cambios realizados si se produce un error. De esta forma, la propia base de datos confirma los cambios y no tiene que preocuparse por los cursores en sí. Entonces solo puede tener 1 cursor abierto en todo momento. Eche un vistazo aquí: tutorialspoint.com/python/python_database_access.htm
nct25
Sí, si eso funciona, entonces estoy equivocado y hubo alguna otra razón que me hizo pensar que tenía que cerrar el cursor para confirmar la conexión.
jmilloy
Sí, no sé, ese enlace que publiqué me hace pensar que eso funciona. Supongo que un poco más de investigación te dirá si definitivamente funciona o no, pero creo que probablemente podrías seguirlo. ¡Espero haberte sido de ayuda!
nct25
el cursor no es seguro para subprocesos, si usa el mismo cursor entre muchos subprocesos diferentes, y todos consultan desde db, fetchall () dará datos aleatorios.
ospider
-6

Sugiero hacerlo como php y mysql. Comience i al comienzo de su código antes de imprimir los primeros datos. Por lo tanto, si recibe un error de conexión, puede mostrar un 50xmensaje de error (No recuerde cuál es el error interno). Manténgalo abierto durante toda la sesión y ciérrelo cuando sepa que ya no lo necesitará.

KilledKenny
fuente
En MySQLdb, existe una diferencia entre una conexión y un cursor. Me conecto una vez por solicitud (por ahora) y puedo detectar errores de conexión temprano. Pero ¿qué pasa con los cursores?
jmilloy
En mi humilde opinión, no es un consejo preciso. Depende. Si su código mantendrá la conexión durante mucho tiempo (por ejemplo, toma algunos datos de la base de datos y luego durante 1-5-10 minutos hace algo en el servidor y mantiene la conexión) y es una aplicación de múltiples subprocesos, creará un problema muy pronto (usted excederá el número máximo de conexiones permitidas).
Roman Podlinov