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?
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:
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
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()
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:
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__
.__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 .__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).__del__
qui les fermeEn 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.
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.