Wann werden Cursor mit MySQLdb geschlossen?

84

Ich erstelle eine WSGI-Webanwendung und habe eine MySQL-Datenbank. Ich verwende MySQLdb, das Cursor zum Ausführen von Anweisungen und zum Abrufen von Ergebnissen bereitstellt. Was ist die Standardpraxis zum Abrufen und Schließen von Cursorn? Wie lange sollten meine Cursor insbesondere halten? Soll ich für jede Transaktion einen neuen Cursor erhalten?

Ich glaube, Sie müssen den Cursor schließen, bevor Sie die Verbindung herstellen. Gibt es einen signifikanten Vorteil beim Finden von Transaktionssätzen, für die keine Zwischen-Commits erforderlich sind, damit Sie nicht für jede Transaktion neue Cursor erhalten müssen? Gibt es viel Aufwand, um neue Cursor zu bekommen, oder ist es einfach keine große Sache?

jmilloy
quelle

Antworten:

79

Anstatt zu fragen, was Standardpraxis ist, da dies oft unklar und subjektiv ist, können Sie versuchen, das Modul selbst als Anleitung zu betrachten. Im Allgemeinen ist die Verwendung des withSchlüsselworts als Vorschlag eines anderen Benutzers eine gute Idee, aber unter diesen besonderen Umständen erhalten Sie möglicherweise nicht die erwartete Funktionalität.

MySQLdb.ConnectionImplementiert ab Version 1.2.5 des Moduls das Kontextmanagerprotokoll mit dem folgenden Code ( 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()

Es gibt bereits mehrere Fragen und withAntworten, oder Sie können die "with" -Anweisung von Understanding Python lesen. Im Wesentlichen wird diese jedoch __enter__am Anfang des withBlocks und __exit__beim Verlassen des withBlocks ausgeführt. Sie können die optionale Syntax verwenden with EXPR as VAR, um das von zurückgegebene Objekt __enter__an einen Namen zu binden, wenn Sie später auf dieses Objekt verweisen möchten. Angesichts der obigen Implementierung haben Sie hier eine einfache Möglichkeit, Ihre Datenbank abzufragen:

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

Die Frage ist nun, wie sind die Zustände der Verbindung und des Cursors nach dem Verlassen des withBlocks? Die __exit__oben gezeigte Methode ruft nur self.rollback()oder auf self.commit(), und keine dieser Methoden ruft die close()Methode auf. Für den Cursor selbst ist keine __exit__Methode definiert - und es wäre egal, ob dies der Fall ist, da withnur die Verbindung verwaltet wird. Daher bleiben sowohl die Verbindung als auch der Cursor nach dem Verlassen des withBlocks geöffnet . Dies kann leicht bestätigt werden, indem dem obigen Beispiel der folgende Code hinzugefügt wird:

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'

Sie sollten sehen, dass die Ausgabe "Cursor ist offen; Verbindung ist offen" auf stdout gedruckt wird.

Ich glaube, Sie müssen den Cursor schließen, bevor Sie die Verbindung herstellen.

Warum? Die MySQL C-API , auf der die Basis basiert MySQLdb, implementiert kein Cursorobjekt, wie in der Moduldokumentation impliziert: "MySQL unterstützt keine Cursor; Cursor können jedoch leicht emuliert werden." In der Tat MySQLdb.cursors.BaseCursorerbt die Klasse direkt von objectCursorn und legt keine solche Einschränkung in Bezug auf Commit / Rollback fest. Ein Oracle-Entwickler hatte folgendes zu sagen :

cnx.commit () vor cur.close () klingt für mich am logischsten. Vielleicht können Sie sich an die Regel halten: "Schließen Sie den Cursor, wenn Sie ihn nicht mehr benötigen." Legen Sie also commit () fest, bevor Sie den Cursor schließen. Letztendlich macht es für Connector / Python keinen großen Unterschied, aber für andere Datenbanken.

Ich gehe davon aus, dass dies so nah wie möglich an der "Standardpraxis" zu diesem Thema liegt.

Gibt es einen signifikanten Vorteil beim Finden von Transaktionssätzen, für die keine Zwischen-Commits erforderlich sind, damit Sie nicht für jede Transaktion neue Cursor erhalten müssen?

Ich bezweifle es sehr, und wenn Sie dies versuchen, können Sie zusätzliche menschliche Fehler einführen. Entscheiden Sie sich besser für eine Konvention und bleiben Sie dabei.

Gibt es viel Aufwand, um neue Cursor zu bekommen, oder ist es einfach keine große Sache?

Der Overhead ist vernachlässigbar und berührt den Datenbankserver überhaupt nicht. Es liegt vollständig in der Implementierung von MySQLdb. Sie können sich BaseCursor.__init__Github ansehen, wenn Sie wirklich neugierig sind, was passiert, wenn Sie einen neuen Cursor erstellen.

Wenn Sie zu einem früheren Zeitpunkt zurückkehren, als wir darüber gesprochen haben with, können Sie jetzt vielleicht verstehen, warum die MySQLdb.ConnectionKlasse __enter__und die __exit__Methoden Ihnen in jedem withBlock ein brandneues Cursorobjekt geben, und Sie müssen sich nicht die Mühe machen, es zu verfolgen oder am Ende des Blocks zu schließen. Es ist ziemlich leicht und existiert nur für Ihre Bequemlichkeit.

Wenn es für Sie wirklich so wichtig ist, das Cursorobjekt zu verwalten, können Sie contextlib.closing verwenden , um die Tatsache auszugleichen , dass das Cursorobjekt keine definierte __exit__Methode hat. Sie können es auch verwenden, um das Verbindungsobjekt zu zwingen, sich beim Verlassen eines withBlocks selbst zu schließen . Dies sollte "my_curs ist geschlossen; my_conn ist geschlossen" ausgeben:

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'

Beachten Sie, dass with closing(arg_obj)die Argumente __enter__und __exit__Methoden des Argumentobjekts nicht aufgerufen werden. Die Methode des Argumentobjekts wird nurclose am Ende des withBlocks aufgerufen . (Um dies in Aktion zu sehen, definieren Sie einfach eine Klasse Foomit __enter__, __exit__und closeMethoden, die einfache printAnweisungen enthalten, und vergleichen Sie, was passiert, wenn Sie es tun, with Foo(): passmit dem, was passiert, wenn Sie es tun with closing(Foo()): pass.) Dies hat zwei wesentliche Auswirkungen:

Wenn der Autocommit-Modus aktiviert ist, führt MySQLdb zunächst BEGINeine explizite Transaktion auf dem Server durch, wenn Sie with connectiondie Transaktion am Ende des Blocks verwenden und festschreiben oder zurücksetzen . Dies sind Standardverhalten von MySQLdb, das Sie vor dem Standardverhalten von MySQL schützen soll, bei dem alle DML-Anweisungen sofort festgeschrieben werden. MySQLdb geht davon aus, dass Sie bei Verwendung eines Kontextmanagers eine Transaktion wünschen, und verwendet die explizite Option BEGIN, um die Autocommit-Einstellung auf dem Server zu umgehen. Wenn Sie es gewohnt sind with connection, denken Sie möglicherweise, dass Autocommit deaktiviert ist, wenn es tatsächlich nur umgangen wurde. Sie könnten eine unangenehme Überraschung bekommen, wenn Sie hinzufügenclosingzu Ihrem Code und verlieren die Transaktionsintegrität; Sie können Änderungen nicht rückgängig machen, es treten möglicherweise Parallelitätsfehler auf, und es ist möglicherweise nicht sofort ersichtlich, warum.

Zweitens with closing(MySQLdb.connect(user, pass)) as VARbindet das Verbindungsobjekt zu VAR, im Gegensatz zu with MySQLdb.connect(user, pass) as VAR, das bindet ein neues Cursor - Objekt zu VAR. Im letzteren Fall hätten Sie keinen direkten Zugriff auf das Verbindungsobjekt! Stattdessen müssten Sie das connectionAttribut des Cursors verwenden , das den Proxy-Zugriff auf die ursprüngliche Verbindung ermöglicht. Wenn der Cursor geschlossen ist, wird sein connectionAttribut auf gesetzt None. Dies führt zu einer abgebrochenen Verbindung, die so lange bestehen bleibt, bis eine der folgenden Situationen eintritt:

  • Alle Verweise auf den Cursor werden entfernt
  • Der Cursor verlässt den Bereich
  • Die Verbindung läuft ab
  • Die Verbindung wird manuell über Serververwaltungstools geschlossen

Sie können dies testen, indem Sie offene Verbindungen überwachen (in Workbench oder mithilfe vonSHOW PROCESSLIST ), während Sie die folgenden Zeilen einzeln ausführen:

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
Luft
quelle
14
Ihr Beitrag war äußerst ausführlich, aber selbst nachdem ich ihn einige Male erneut gelesen habe, bin ich immer noch verwirrt, was das Schließen von Cursorn angeht. Nach den zahlreichen Beiträgen zu diesem Thema zu urteilen, scheint dies ein häufiger Punkt der Verwirrung zu sein. Mein Tipp ist, dass Cursor anscheinend NICHT erfordern, dass .close () aufgerufen wird - niemals. Warum also überhaupt eine .close () -Methode?
SMGreenfield
6
Die kurze Antwort lautet: Dies cursor.close()ist Teil der Python DB-API , die nicht speziell für MySQL geschrieben wurde.
Air
1
Warum wird die Verbindung nach del my_curs geschlossen?
BAE
@ChengchengPei my_cursenthält den letzten Verweis auf das connectionObjekt. Sobald diese Referenz nicht mehr vorhanden ist , das connectionObjekt soll Müll gesammelt werden.
Air
Dies ist eine fantastische Antwort, danke. Ausgezeichnete Erklärung withund MySQLdb.Connection‚s __enter__und __exit__Funktionen. Nochmals vielen Dank @Air.
Eugene
33

Es ist besser, es mit dem Schlüsselwort 'with' umzuschreiben. 'Mit' sorgt dafür, dass der Cursor automatisch geschlossen wird (wichtig, da es sich um eine nicht verwaltete Ressource handelt). Der Vorteil ist, dass der Cursor auch im Ausnahmefall geschlossen wird.

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
quelle
Ich denke nicht, dass dies witheine gute Option ist, wenn Sie es in Flask oder einem anderen Webframework verwenden möchten. Wenn die Situation ist, http://flask.pocoo.org/docs/patterns/sqlite3/#sqlite3wird es Probleme geben.
James King
@ James-King Ich habe nicht mit Flask gearbeitet, aber in deinem Beispiel wird Flask die Datenbankverbindung selbst schließen. Eigentlich in meinem Code verwende ich etwas anders approach- I Verwendung mit einer engen Cursor 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 Ja, wenn Sie es mit dem Cursor verwenden, dann wäre alles in Ordnung.
James King
7

Hinweis: Diese Antwort bezieht sich auf PyMySQL , einen Ersatz für MySQLdb und effektiv die neueste Version von MySQLdb, da MySQLdb nicht mehr gewartet wird. Ich glaube, dass hier alles auch für die alte MySQLdb gilt, habe es aber nicht überprüft.

Zunächst einige Fakten:

  • Die Python- withSyntax ruft die __enter__Methode des Kontextmanagers auf , bevor der Hauptteil des withBlocks ausgeführt wird, und anschließend die __exit__Methode.
  • Verbindungen haben eine __enter__Methode, die nichts anderes tut, als einen Cursor zu erstellen und zurückzugeben, und eine __exit__Methode, die entweder festschreibt oder zurücksetzt (abhängig davon, ob eine Ausnahme ausgelöst wurde). Die Verbindung wird nicht geschlossen.
  • Cursor in PyMySQL sind eine reine Abstraktion, die in Python implementiert ist. In MySQL selbst gibt es kein gleichwertiges Konzept. 1
  • Cursor haben eine __enter__Methode, die nichts tut, und eine __exit__Methode, die den Cursor "schließt" (was nur bedeutet, den Verweis des Cursors auf seine übergeordnete Verbindung auf Null zu setzen und alle auf dem Cursor gespeicherten Daten wegzuwerfen).
  • Cursor enthalten einen Verweis auf die Verbindung, aus der sie hervorgegangen sind, aber Verbindungen enthalten keinen Verweis auf die von ihnen erstellten Cursor.
  • Verbindungen haben eine __del__Methode, die sie schließt
  • Gemäß https://docs.python.org/3/reference/datamodel.html verwendet CPython (die Standard-Python-Implementierung) die Referenzzählung und löscht ein Objekt automatisch, sobald die Anzahl der Verweise darauf Null erreicht.

Wenn wir diese Dinge zusammenfassen, sehen wir, dass naiver Code wie dieser theoretisch problematisch ist:

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

# ... happily carry on and do something unrelated

Das Problem ist, dass nichts die Verbindung geschlossen hat. Wenn Sie den obigen Code in eine Python-Shell einfügen und dann SHOW FULL PROCESSLISTin einer MySQL-Shell ausführen , können Sie die von Ihnen erstellte Leerlaufverbindung sehen. Da die Standardanzahl von MySQL-Verbindungen 151 ist , was nicht sehr groß ist , könnten theoretisch Probleme auftreten, wenn Sie viele Prozesse hätten, die diese Verbindungen offen halten.

In CPython gibt es jedoch eine Speichergnade, die sicherstellt, dass Code wie in meinem obigen Beispiel wahrscheinlich nicht dazu führt, dass Sie viele offene Verbindungen verlassen. Diese Rettung besteht darin, dass der Referenzzähler , sobald cursorer den Gültigkeitsbereich verlässt (z. B. die Funktion, in der er erstellt wurde, beendet wird oder cursorihm ein anderer Wert zugewiesen wird), Null erreicht, wodurch er gelöscht wird und der Referenzzähler der Verbindung gelöscht wird auf Null, wodurch die Verbindungsmethode __del__aufgerufen wird, die die Verbindung zwangsweise schließt. Wenn Sie den obigen Code bereits in Ihre Python-Shell eingefügt haben, können Sie dies jetzt durch Ausführen simulieren cursor = 'arbitrary value'. Sobald Sie dies tun, verschwindet die Verbindung, die Sie geöffnet haben, aus der SHOW PROCESSLISTAusgabe.

Sich darauf zu verlassen ist jedoch unelegant und kann theoretisch in anderen Python-Implementierungen als CPython fehlschlagen. Cleaner wäre theoretisch, .close()die Verbindung explizit zu machen (um eine Verbindung in der Datenbank freizugeben, ohne darauf zu warten, dass Python das Objekt zerstört). Dieser robustere Code sieht folgendermaßen aus:

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

Dies ist hässlich, hängt jedoch nicht davon ab, dass Python Ihre Objekte zerstört, um Ihre (endlich verfügbare Anzahl) Datenbankverbindungen freizugeben.

Beachten Sie, dass das Schließen des Cursors völlig sinnlos ist, wenn Sie die Verbindung bereits explizit auf diese Weise schließen.

Um die sekundären Fragen hier zu beantworten:

Gibt es viel Aufwand, um neue Cursor zu bekommen, oder ist es einfach keine große Sache?

Nein, das Instanziieren eines Cursors trifft MySQL überhaupt nicht und tut im Grunde nichts .

Gibt es einen signifikanten Vorteil beim Finden von Transaktionssätzen, für die keine Zwischen-Commits erforderlich sind, damit Sie nicht für jede Transaktion neue Cursor erhalten müssen?

Dies ist situativ und schwer zu beantworten. Wie https://dev.mysql.com/doc/refman/en/optimizing-innodb-transaction-management.html es ausdrückt, „könnte eine Anwendung Begegnung Performance - Probleme , wenn es tausende Male pro Sekunde und andere Performance - Probleme verpflichtet , wenn es legt nur alle 2-3 Stunden fest " . Sie zahlen einen Leistungsaufwand für jedes Commit. Wenn Sie jedoch Transaktionen länger offen lassen, erhöhen Sie die Wahrscheinlichkeit, dass andere Verbindungen Zeit auf Sperren warten müssen, erhöhen das Risiko von Deadlocks und erhöhen möglicherweise die Kosten einiger von anderen Verbindungen durchgeführter Suchvorgänge .


1 MySQL hat ein Konstrukt, das einen Cursor aufruft, aber nur in gespeicherten Prozeduren vorhanden ist. Sie unterscheiden sich grundlegend von PyMySQL-Cursorn und sind hier nicht relevant.

Mark Amery
quelle
5

Ich denke, Sie sollten besser versuchen, einen Cursor für alle Ihre Ausführungen zu verwenden und ihn am Ende Ihres Codes zu schließen. Es ist einfacher, damit zu arbeiten, und es könnte auch Effizienzvorteile haben (zitieren Sie mich nicht dazu).

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

Der Punkt ist, dass Sie die Ergebnisse der Ausführung eines Cursors in einer anderen Variablen speichern können, wodurch Ihr Cursor für eine zweite Ausführung freigegeben wird. Auf diese Weise treten nur dann Probleme auf, wenn Sie fetchone () verwenden und eine zweite Cursorausführung durchführen müssen, bevor Sie alle Ergebnisse der ersten Abfrage durchlaufen haben.

Andernfalls würde ich sagen, schließen Sie einfach Ihre Cursor, sobald Sie alle Daten aus ihnen herausgeholt haben. Auf diese Weise müssen Sie sich später in Ihrem Code keine Gedanken mehr darüber machen, lose Enden zu binden.

nct25
quelle
Danke - Wenn man bedenkt, dass man den Cursor schließen muss, um ein Update / Insert festzuschreiben, besteht eine einfache Möglichkeit, dies für Updates / Einfügungen zu tun, darin, einen Cursor für jeden Daemon zu erhalten, den Cursor zum Festschreiben zu schließen und sofort einen neuen Cursor zu erhalten Also bist du das nächste Mal bereit. Klingt das vernünftig?
Jmilloy
1
Hey, kein Problem. Ich wusste eigentlich nicht, wie man das Update / Insert durch Schließen der Cursor festschreibt, aber eine schnelle Online-Suche zeigt dies: conn = MySQLdb.connect (Argumente_go_here) cursor = MySQLdb.cursor () cursor.execute (mysql_insert_statement_here) try: conn. commit () außer: conn.rollback () # macht rückgängig gemachte Änderungen, wenn ein Fehler auftritt. Auf diese Weise schreibt die Datenbank selbst die Änderungen fest, und Sie müssen sich nicht um die Cursor selbst kümmern. Dann kann immer nur 1 Cursor geöffnet sein. Schauen Sie hier: tutorialspoint.com/python/python_database_access.htm
nct25
Ja, wenn das funktioniert, irre ich mich einfach und es gab einen anderen Grund, der mich zu der Annahme veranlasste, dass ich den Cursor schließen musste, um die Verbindung herzustellen.
Jmilloy
Ja, ich weiß nicht, dieser Link, den ich gepostet habe, lässt mich denken, dass das funktioniert. Ich denke, ein bisschen mehr Forschung würde Ihnen sagen, ob es definitiv funktioniert oder nicht, aber ich denke, Sie könnten wahrscheinlich einfach damit anfangen. Hoffe ich habe dir geholfen!
nct25
Der Cursor ist nicht threadsicher. Wenn Sie denselben Cursor unter vielen verschiedenen Threads verwenden und alle von db abfragen, gibt fetchall () zufällige Daten aus.
ospider
-6

Ich schlage vor, es wie PHP und MySQL zu tun. Beginnen Sie i am Anfang Ihres Codes, bevor Sie die ersten Daten drucken. Wenn Sie also einen Verbindungsfehler erhalten, können Sie eine Fehlermeldung 50x(Ich erinnere mich nicht, was ein interner Fehler ist) anzeigen . Halten Sie es für die gesamte Sitzung offen und schließen Sie es, wenn Sie wissen, dass Sie es nicht mehr benötigen.

KilledKenny
quelle
In MySQLdb gibt es einen Unterschied zwischen einer Verbindung und einem Cursor. Ich verbinde mich (vorerst) einmal pro Anfrage und kann Verbindungsfehler frühzeitig erkennen. Aber was ist mit Cursorn?
Jmilloy
IMHO ist es kein genauer Rat. Es kommt darauf an. Wenn Ihr Code die Verbindung für lange Zeit aufrechterhält (z. B. einige Daten aus der Datenbank benötigt und dann für 1-5-10 Minuten etwas auf dem Server tut und die Verbindung aufrechterhält) und es sich um eine Multy-Thread-Anwendung handelt, wird es ziemlich bald zu einem Problem kommen (Sie überschreitet die maximal zulässigen Verbindungen).
Roman Podlinov