web-dev-qa-db-fra.com

Quand fermer les curseurs avec MySQLdb

Je construis une application Web WSGI et j'ai une base de données MySQL. J'utilise MySQLdb, qui fournit des curseurs pour exécuter des instructions et obtenir des résultats. Quelle est la pratique habituelle pour obtenir et fermer des curseurs? En particulier, combien de temps mes curseurs doivent-ils durer? Dois-je obtenir un nouveau curseur pour chaque transaction?

Je crois que vous devez fermer le curseur avant de valider la connexion. Y a-t-il un avantage important à rechercher des ensembles de transactions ne nécessitant pas de validation intermédiaire afin de ne pas avoir à obtenir de nouveaux curseurs pour chaque transaction? Y at-il beaucoup de frais généraux pour obtenir de nouveaux curseurs, ou est-ce que ce n'est pas un gros problème?

75
jmilloy

Au lieu de demander quelle est la pratique habituelle, étant donné que cela est souvent peu clair et subjectif, vous pouvez essayer de consulter le module lui-même pour obtenir des conseils. En général, utiliser le mot clé with comme suggéré par un autre utilisateur est une excellente idée, mais dans ce cas précis, il se peut que cela ne vous donne pas exactement les fonctionnalités que vous attendez.

A partir de la version 1.2.5 du module, MySQLdb.Connection Implémente le protocole du gestionnaire de contexte avec le code suivant ( 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()

Il existe déjà plusieurs questions-réponses sur with ou vous pouvez lire Comprendre la déclaration "with" de Python , mais l'essentiel est que __enter__ Est exécuté au début de la with block et __exit__ s'exécutent en quittant le bloc with. Vous pouvez utiliser la syntaxe facultative with EXPR as VAR Pour lier l'objet renvoyé par __enter__ À un nom si vous souhaitez faire référence à cet objet ultérieurement. Donc, étant donné l'implémentation ci-dessus, voici un moyen simple d'interroger votre base de données:

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 question qui se pose maintenant est la suivante: quels sont les états de la connexion et du curseur après avoir quitté le bloc with? La méthode __exit__ Présentée ci-dessus appelle uniquement self.rollback() ou self.commit(), et aucune de ces méthodes n'appelle la méthode close(). Le curseur lui-même n'a pas de méthode __exit__ Définie - et cela n'aurait pas d'importance si c'était le cas, car with ne fait que gérer la connexion. Par conséquent, la connexion et le curseur restent ouverts après la sortie du bloc with. Ceci est facilement confirmé en ajoutant le code suivant à l'exemple ci-dessus:

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'

La sortie "curseur est ouvert; la connexion est ouverte" devrait apparaître sur la sortie standard.

Je crois que vous devez fermer le curseur avant de valider la connexion.

Pourquoi? La API MySQL C , qui constitue la base de MySQLdb, n’implémente aucun objet curseur, comme l’implique la documentation du module: "MySQL ne supporte pas les curseurs; cependant , les curseurs sont facilement émulés. " En effet, la classe MySQLdb.cursors.BaseCursor hérite directement de object et n’impose aucune telle restriction aux curseurs en ce qui concerne commit/rollback. Un développeur Oracle avait ceci à dire :

cnx.commit () avant cur.close () me semble le plus logique. Vous pouvez peut-être suivre la règle: "Fermez le curseur si vous n'en avez plus besoin." Donc commit () avant de fermer le curseur. Au final, pour Connector/Python, cela ne fait pas grande différence, mais cela pourrait être le cas pour d’autres bases de données.

Je m'attends à ce que ce soit aussi proche que possible de la "pratique courante" à ce sujet.

Y a-t-il un avantage important à rechercher des ensembles de transactions ne nécessitant pas de validation intermédiaire afin de ne pas avoir à obtenir de nouveaux curseurs pour chaque transaction?

J'en doute fort, et en essayant de le faire, vous pouvez introduire une erreur humaine supplémentaire. Mieux vaut choisir une convention et s'y tenir.

Y at-il beaucoup de frais généraux pour obtenir de nouveaux curseurs, ou est-ce que ce n'est pas un gros problème?

La surcharge est négligeable et ne touche pas du tout le serveur de base de données; c'est entièrement dans l'implémentation de MySQLdb. Vous pouvez regardez BaseCursor.__init__ Sur github si vous êtes vraiment curieux de savoir ce qui se passe lorsque vous créez un nouveau curseur.

Revenons à plus tôt lorsque nous discutions de with, vous pouvez peut-être maintenant comprendre pourquoi les méthodes MySQLdb.Connection Class __enter__ Et __exit__ Vous donnent un nouvel objet curseur dans chaque with bloc et ne prenez pas la peine de le suivre ou de le fermer à la fin du bloc. Il est assez léger et existe uniquement pour votre commodité.

S'il est vraiment important pour vous de microgérer l'objet curseur, vous pouvez utiliser contextlib.closing pour compenser le fait que l'objet curseur ne possède pas de méthode __exit__ Définie. D'ailleurs, vous pouvez également l'utiliser pour forcer l'objet de connexion à se fermer lors de la sortie d'un bloc with. La sortie "my_curs est fermé; my_conn est fermé":

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'

Notez que with closing(arg_obj) n'appellera pas les méthodes __enter__ Et __exit__ De l'objet argument; il seulement appellera la méthode close de l'objet argument à la fin du bloc with. (Pour voir cela en action, définissez simplement une classe Foo avec les méthodes __enter__, __exit__ Et close contenant des instructions simples print, et comparez ce qui se passe lorsque vous faites with Foo(): pass à ce qui se produit lorsque vous faites with closing(Foo()): pass.) Ceci a deux implications importantes:

Premièrement, si le mode autocommit est activé, MySQLdb BEGIN une transaction explicite sur le serveur lorsque vous utilisez with connection Et validez ou annulez la transaction à la fin du bloc. Ce sont des comportements par défaut de MySQLdb, destinés à vous protéger du comportement par défaut de MySQL qui consiste à commettre immédiatement toutes les instructions DML. MySQLdb suppose que lorsque vous utilisez un gestionnaire de contexte, vous voulez une transaction, et utilise explicitement BEGIN pour contourner le paramètre autocommit sur le serveur. Si vous avez l'habitude d'utiliser with connection, Vous pouvez penser que la validation automatique est désactivée alors qu'en réalité, elle était uniquement ignorée. Vous pourriez avoir une mauvaise surprise si vous ajoutez closing à votre code et perdez l'intégrité transactionnelle; vous ne pourrez pas annuler les modifications, vous pourriez commencer à voir des bogues d'accès concurrentiel et vous ne comprendrez peut-être pas immédiatement pourquoi.

Deuxièmement, with closing(MySQLdb.connect(user, pass)) as VAR lie l'objet de connexion à VAR, contrairement à with MySQLdb.connect(user, pass) as VAR, qui lie un nouvel objet curseur à VAR. Dans ce dernier cas, vous n'avez aucun accès direct à l'objet de connexion! Au lieu de cela, vous devrez utiliser l'attribut connection du curseur, qui fournit un accès proxy à la connexion d'origine. Lorsque le curseur est fermé, son attribut connection est défini sur None. Cela aboutit à une connexion abandonnée qui restera en place jusqu'à ce que l'une des situations suivantes se produise:

  • Toutes les références au curseur sont supprimées
  • Le curseur sort du cadre
  • La connexion expire
  • La connexion est fermée manuellement via les outils d'administration du serveur.

Vous pouvez tester cela en surveillant les connexions ouvertes (dans Workbench ou par avec SHOW PROCESSLIST ) tout en exécutant les lignes suivantes une par une:

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
68
Air

Il est préférable de le réécrire en utilisant le mot clé 'with'. 'With' s'occupera de fermer le curseur (c'est important parce que c'est une ressource non gérée) automatiquement. L'avantage est qu'il fermera le curseur en cas d'exception aussi.

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()
29
Roman Podlinov

Remarque: cette réponse concerne PyMySQL , qui remplace directement MySQLdb et constitue la dernière version de MySQLdb depuis que MySQLdb n'est plus maintenu. Je crois que tout ici est également vrai de l’ancienne MySQLdb, mais n’a pas été vérifié.

Tout d'abord, quelques faits:

  • La syntaxe with de Python appelle la méthode __enter__ Du gestionnaire de contexte avant d'exécuter le corps du bloc with, puis sa méthode __exit__ .
  • Les connexions ont une méthode __enter__ qui ne fait rien à part créer et renvoyer un curseur, et une méthode __exit__ qui valide ou annule ( selon qu’une exception a été levée). ( ne ferme pas la connexion .
  • Les curseurs dans PyMySQL sont purement une abstraction implémentée en Python; Il n'y a pas de concept équivalent dans MySQL lui-même.1
  • Les curseurs ont une méthode __enter__ qui ne fait rien et une méthode __exit__ qui "ferme" le curseur (ce qui signifie simplement l'annulation) référence du curseur à sa connexion parente et élimination des données stockées sur le curseur).
  • Les curseurs contiennent une référence à la connexion qui les a générés, mais les connexions ne contiennent aucune référence aux curseurs qu'ils ont créés.
  • Les connexions ont une méthode __del__ qui les ferme
  • Par https://docs.python.org/3/reference/datamodel.html , CPython (la valeur par défaut Python)) utilise le comptage de références et supprime automatiquement un objet. une fois le nombre de références atteint zéro.

En réunissant ces éléments, nous voyons qu'un code naïf comme celui-ci est en théorie problématique:

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

# ... happily carry on and do something unrelated

Le problème est que rien n'a fermé la connexion. En effet, si vous collez le code ci-dessus dans un shell Python puis exécutez SHOW FULL PROCESSLIST Sur un shell MySQL, vous pourrez voir la connexion inactive que vous avez créée. Depuis Le nombre de connexions par défaut de MySQL est 151 , ce qui n’est pas énorme , vous pourriez théoriquement commencer à rencontrer des problèmes si vous en aviez beaucoup processus gardant ces connexions ouvertes.

Cependant, dans CPython, il existe un moyen de sauvegarde qui garantit que le code tel que celui présenté ci-dessus ne (probablement) ne vous fera pas laisser beaucoup de connexions ouvertes. Cette grâce est que dès que cursor sort de la portée (par exemple, la fonction dans laquelle elle a été créée se termine ou cursor obtient une autre valeur qui lui est affectée), le nombre de références atteint zéro, ce qui entraîne sa suppression, ramenant le compte de références de la connexion à zéro, provoquant ainsi l’appel de la méthode __del__ de la connexion qui force la fermeture de la connexion. Si vous avez déjà collé le code ci-dessus dans votre Python Shell, vous pouvez maintenant simuler cela en exécutant cursor = 'arbitrary value'; Dès que vous faites cela, la connexion que vous avez ouverte disparaîtra la sortie SHOW PROCESSLIST.

Cependant, s’en remettre à cela est inélégant et pourrait théoriquement échouer dans les implémentations de Python autres que CPython. Cleaner, en théorie, consisterait à explicitement .close()) (à libérer une connexion sur la base de données sans attendre Python pour détruire l'objet). Ce code plus robuste ressemble à ceci:

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

C'est moche, mais ne comptez pas sur Python détruire vos objets pour libérer vos connexions (nombre fini de connexions disponibles) à la base de données.

Notez que la fermeture du curseur , si vous fermez déjà la connexion de manière explicite, est totalement inutile.

Enfin, pour répondre aux questions secondaires ici:

Y at-il beaucoup de frais généraux pour obtenir de nouveaux curseurs, ou est-ce que ce n'est pas un gros problème?

Nope, instancier un curseur ne frappe pas du tout MySQL et ne fait fondamentalement rien .

Y a-t-il un avantage important à rechercher des ensembles de transactions ne nécessitant pas de validation intermédiaire afin de ne pas avoir à obtenir de nouveaux curseurs pour chaque transaction?

C’est une situation et il est difficile de donner une réponse générale à cette question. Comme https://dev.mysql.com/doc/refman/en/optimizing-innodb-transaction-management.html le dit, "une application peut rencontrer problèmes de performances s’il s’engage des milliers de fois par seconde et problèmes de performances différents s’il n’engage que toutes les 2 à 3 heures ". Vous payez un surcoût de performance pour chaque validation, mais en laissant les transactions ouvertes plus longtemps, vous augmentez les chances que d'autres connexions passent du temps en attente de verrous, vous augmentez le risque d'impasses et augmentez éventuellement le coût de certaines recherches effectuées par d'autres connexions. .


1 MySQL a une construction, il appelle un curseur mais ils n'existent que dans les procédures stockées; ils sont complètement différents des curseurs PyMySQL et ne sont pas pertinents ici.

6
Mark Amery

Je pense que vous feriez mieux d'essayer d'utiliser un curseur pour toutes vos exécutions et de le fermer à la fin de votre code. Il est plus facile de travailler avec, et cela pourrait aussi avoir des avantages d'efficacité (ne me citez pas là-dessus).

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

Le fait est que vous pouvez stocker les résultats de l'exécution d'un curseur dans une autre variable, libérant ainsi votre curseur pour effectuer une seconde exécution. Vous rencontrez des problèmes de cette façon uniquement si vous utilisez fetchone (), et vous devez effectuer une seconde exécution du curseur avant que vous n'ayez parcouru tous les résultats de la première requête.

Autrement, je dirais qu'il suffit de fermer vos curseurs dès que vous avez fini de récupérer toutes les données. De cette façon, vous n'aurez plus à craindre de perdre des points dans votre code.

5
nct25