Quand fermer cursors en utilisant 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 standard pour obtenir et fermer les curseurs? en particulier, combien de temps mes curseurs devraient durer? Devrais-je obtenir un nouveau curseur pour chaque transaction?

je crois que vous devez fermer le curseur avant de valider la connexion. Est-il un avantage significatif pour trouver des ensembles de les transactions qui ne nécessitent pas de commit intermédiaire de sorte que vous ne devez pas obtenir de nouveaux curseurs pour chaque transaction? Il y a beaucoup de frais généraux pour l'obtention de nouveaux curseurs, ou est-il tout simplement pas une grosse affaire?

66
demandé sur Air 2011-04-15 01:23:24

5 réponses

au lieu de demander ce qu'est la pratique standard, puisque c'est souvent imprécis et subjectif, vous pourriez essayer de regarder le module lui-même pour des conseils. En général, l'utilisation du mot-clé with comme un autre utilisateur suggéré est une excellente idée, mais dans ce cas précis, il peut ne pas vous donner tout à fait la fonctionnalité que vous attendez.

depuis la version 1.2.5 du module, MySQLdb.Connection implémente le context manager protocol 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 plusieurs questions et Réponses sur with déjà, ou vous pouvez lire comprendre Python's" avec "statement , mais essentiellement ce qui se passe est que __enter__ exécute au début du bloc with , et __exit__ exécute à la sortie du bloc with . Vous pouvez utiliser la syntaxe optionnelle with EXPR as VAR pour lier l'objet retourné par __enter__ à un nom si vous avez l'intention de faire référence à cet objet plus tard. Donc, étant donné l'implémentation ci-dessus, voici une façon 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 est maintenant, quels sont les états de la connexion et du curseur après avoir quitté le bloc with ? La méthode __exit__ montrée ci-dessus appelle seulement 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 __exit__ méthode définie – et ne serait pas important si elle l'était, parce que with est seulement la gestion de la connexion. Par conséquent, la connexion et le curseur restent ouverts après avoir quitté le bloc with . Ceci est facilement confirmé par l'ajout du 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'

vous devriez voir la sortie "le curseur est ouvert; la connexion est ouverte" imprimé à stdout.

je crois que vous devez fermer le curseur avant commettre la connexion.

pourquoi? Le MySQL C API , qui est la base de MySQLdb , n'implémente aucun objet curseur, comme indiqué dans 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 restriction de ce type aux curseurs en ce qui concerne le commit/rollback. Oracle développeur avait ceci à dire :

cnx.commit() avant cur.close() semble très logique pour moi. Peut-être que vous peut suivre la règle: "fermez le curseur si vous n'en avez plus besoin." Ainsi commit () avant de fermer le curseur. En fin de compte, pour Connecteur / Python, il ne fait pas beaucoup de différence, mais ou autre bases de données qu'il peut.

Je m'attends à ce que ce soit aussi proche que vous allez obtenir à" standard la pratique" sur ce sujet.

y a-t-il un avantage significatif à trouver des ensembles de transactions qui ne nécessitent pas de commit intermédiaire de sorte que vous n'ayez pas à obtenir de nouveaux curseurs pour chaque transaction?

j'en doute fort, et en essayant de le faire, vous pouvez introduire une erreur humaine. Mieux vaut décider d'une convention et s'y tenir.

est-il beaucoup de frais généraux pour obtenir de nouveaux curseurs, ou est-ce juste pas une grosse affaire?

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

en revenant à plus tôt quand nous discutions with , peut-être maintenant vous pouvez comprenez pourquoi les méthodes MySQLdb.Connection classe __enter__ et __exit__ vous donnent un tout nouvel objet curseur dans chaque bloc with et ne vous donnez pas la peine de le suivre ou de le fermer à la fin du bloc. Il est assez léger et n'existe que pour votre confort.

si c'est vraiment important pour vous de gérer l'objet curseur, vous pouvez utiliser contextlib.fermeture pour compenser le fait que l'objet curseur n'a pas méthode définie __exit__ . D'ailleurs, vous pouvez également l'utiliser pour forcer l'objet de connexion à se fermer en sortant d'un bloc with . Cela devrait 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 appellera seulement la méthode close de l'objet argument à la fin du bloc with . (Voir dans cette action, il suffit de définir une classe Foo avec __enter__ , __exit__ , et close méthodes contenant des déclarations simples print , et de comparer ce qui se passe quand vous faites with Foo(): pass à ce qui se passe quand vous faites with closing(Foo()): pass .) Cela a deux conséquences importantes:

tout d'abord, si le mode autocommit est activé, MySQLdb va BEGIN une transaction explicite sur le serveur lorsque vous utilisez with connection et commit ou roll 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 de commettre immédiatement toutes les déclarations DML. MySQLdb suppose que lorsque vous utilisez un gestionnaire de contexte, vous voulez une transaction, et utilise le BEGIN explicite pour contourner le paramètre autocommit sur le serveur. Si vous êtes habitué à utiliser with connection , vous pourriez penser autocommit est désactivé alors qu'en fait il était seulement contourné. Vous pourriez avoir une surprise désagréable si vous ajoutez closing à votre code et perdez l'intégrité transactionnelle; vous ne serez pas en mesure de faire reculer les changements, vous pouvez commencer à voir des bogues de concurrence et il peut ne pas être immédiatement évident pourquoi.

le Second, with closing(MySQLdb.connect(user, pass)) as VAR lie objet de connexion à VAR , contrairement à with MySQLdb.connect(user, pass) as VAR , qui lie un nouveau curseur de l'objet à VAR . Dans ce dernier cas, vous n'auriez pas d'accès direct au objet de connexion! Vous devriez plutôt utiliser l'attribut connection du curseur, qui fournit un accès par procuration à la connexion originale. Lorsque le curseur est fermé, son attribut connection est défini à None . Il en résulte une connexion abandonnée qui restera dans les parages jusqu'à ce que l'un des événements suivants se produise:

  • toutes les références au curseur sont supprimées
  • le curseur sort du champ d'application
  • Le temps de connexion à
  • la connexion se ferme manuellement via les outils d'administration du serveur

vous pouvez le tester en contrôlant les connexions ouvertes (dans Workbench ou par en utilisant 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
57
répondu Air 2016-06-16 01:04:34

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 car c'est une ressource non gérée) automatiquement. L'avantage est qu'il va fermer le curseur en cas d'exception.

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()
27
répondu Roman Podlinov 2014-07-26 18:14:33

je pense que vous serez mieux d'essayer d'utiliser un curseur pour tous vos exécutions et de la fermer à la fin de votre code. Il est plus facile de travailler avec, et il pourrait avoir des avantages d'efficacité (ne pas me citer qu'une).

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 point 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 seulement si vous utilisez fetchone(), et avez besoin de exécutez un deuxième curseur avant d'avoir itéré tous les résultats de la première requête.

sinon, je vous dirais de fermer vos curseurs dès que vous aurez fini d'en extraire toutes les données. De cette façon, vous n'avez pas à vous soucier de régler les derniers détails plus tard dans votre code.

6
répondu nct25 2011-07-30 19:06:12

Note: Cette réponse est pour PyMySQL , qui est un remplacement direct pour MySQLdb et effectivement la dernière version de MySQLdb depuis MySQLdb cessé d'être maintenu. Je crois que tout ici est aussi vrai de L'héritage MySQLdb, mais n'ont pas vérifié.

tout d'Abord, quelques faits:

  • Python with la syntaxe 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 d'autre que de créer et de retourner un curseur, et une méthode __exit__ qui commet ou retourne (selon qu'une exception a été lancée). n'est pas fermer la connexion.
  • les curseurs de 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 __enter__ méthode qui ne fait rien et une __exit__ méthode qui "ferme" le curseur (ce qui signifie juste annuler la référence du curseur à sa connexion parent et jeter toutes les données stockées sur curseur.)
  • Curseurs tiennent une référence à la connexion qui les a engendré, mais les connexions ne tiennent pas une référence aux curseurs qu'ils ont créé.
  • Branchements __del__ méthode de la ferme
  • par https://docs.python.org/3/reference/datamodel.html , CPython (l'implémentation Python par défaut) utilise le comptage de référence et automatiquement supprime un objet lorsque le nombre de références atteint zéro.

en mettant ces choses ensemble, nous voyons que le 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 lancez SHOW FULL PROCESSLIST dans un shell MySQL, vous pourrez voir la connexion inutilisée que vous avez créée. Depuis Le nombre de connexions par défaut de MySQL est 151 , qui n'est pas énorme , vous pourriez théoriquement commencer à courir dans des problèmes si vous aviez beaucoup de processus garder ces connexions ouvertes.

cependant, dans CPython, il y a une grâce d'économie qui assure que le code comme mon exemple au-dessus de probablement ne vous fera pas quitter autour des charges de connexions ouvertes. Cette grâce salvatrice est que dès que cursor sort du champ d'application (par exemple la fonction dans laquelle il a été créé se termine, ou cursor obtient une autre valeur qui lui est assignée), son nombre de référence atteint zéro, ce qui le fait supprimer, ce qui fait tomber le nombre de référence de la connexion à zéro, ce qui fait que la méthode __del__ est appelée qui force-ferme la connexion. Si vous avez déjà collé le code ci-dessus dans votre shell Python, alors vous pouvez maintenant simuler ceci en exécutant cursor = 'arbitrary value' ; dès que vous faites ceci, la connexion que vous ouvert disparaîtra de la sortie SHOW PROCESSLIST .

cependant, se fier à cela est inélégant, et théoriquement pourrait échouer dans des implémentations Python autres que CPython. Nettoyant, en théorie, serait explicitement .close() la connexion (pour 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 laid, mais ne dépend pas de la destruction de Python vos objets pour libérer votre (finis disponibles nombre de connexions de base de données.

notez que fermer le curseur 1519230920" , si vous fermez déjà la connexion explicitement comme ceci, est totalement inutile.

enfin, pour répondre aux questions secondaires ici:

est - ce qu'il y a beaucoup de frais généraux pour obtenir de nouveaux curseurs, ou est-ce juste pas une grosse affaire?

Non, l'instanciation d'un curseur ne frappe pas MySQL et ne fait rien .

y a-t-il un avantage significatif à trouver des ensembles de transactions qui ne nécessitent pas de commit intermédiaire de sorte que vous n'ayez pas à obtenir de nouveaux curseurs pour chaque transaction?

c'est situationnel et difficile à répondre de façon générale. Comme https://dev.mysql.com/doc/refman/en/optimizing-innodb-transaction-management.html le met, " une application pourrait rencontrer des problèmes de performance si elle engage des milliers de fois par seconde, et des problèmes de performance différents si elle engage seulement toutes les 2-3 heures " . Vous payez un frais généraux de performance pour chaque commit, mais en laissant les transactions ouvertes plus longtemps, vous augmentez la chance d'autres connexions ayant à passer du temps à attendre la serrure, augmenter votre risque de les blocages, et potentiellement augmenter le coût de certaines recherches effectuées par d'autres connexions.


1 MySQL ne ont une construction qu'il appelle une curseur mais ils n'existent qu'à l'intérieur de procédures stockées; ils sont complètement différents de PyMySQL curseurs et ne sont pas pertinentes ici.

2
répondu Mark Amery 2017-03-23 00:19:32

je suggère de le faire comme php et mysql. Démarrez i Au début de votre code avant l'impression des premières données. Ainsi, si vous obtenez une erreur de connexion, vous pouvez afficher un message d'erreur 50x (ne vous rappelez pas ce qu'est une erreur interne). Et gardez-le ouvert pendant toute la session et fermez-le quand vous savez que vous n'en aurez plus besoin.

-4
répondu KilledKenny 2011-04-14 21:40:40