Quand fermer les curseurs en utilisant MySQLdb

86

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 doivent-ils durer? Dois-je avoir un nouveau curseur pour chaque transaction?

Je pense que vous devez fermer le curseur avant de valider la connexion. Y a-t-il un avantage significatif à trouver des ensembles de transactions qui ne nécessitent pas de validations intermédiaires afin que vous n'ayez pas à obtenir de nouveaux curseurs pour chaque transaction? Y a-t-il beaucoup de frais généraux pour obtenir de nouveaux curseurs, ou n'est-ce pas un problème?

jmilloy
la source

Réponses:

80

Au lieu de demander quelle est la pratique standard, car elle est souvent peu claire et subjective, vous pouvez essayer de consulter le module lui-même pour obtenir des conseils. En général, utiliser le withmot - clé comme un autre utilisateur l'a suggéré est une excellente idée, mais dans ce cas précis, il peut ne pas vous donner tout à fait les fonctionnalités que vous attendez.

Depuis la version 1.2.5 du module, MySQLdb.Connectionimplé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 with, ou vous pouvez lire l'instruction «with» de Python , mais ce qui se passe essentiellement, c'est qu'elle __enter__s'exécute au début du withbloc et __exit__s'exécute en quittant le withbloc. Vous pouvez utiliser la syntaxe facultative with EXPR as VARpour lier l'objet renvoyé par __enter__à un nom si vous prévoyez de référencer 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 est maintenant, quels sont les états de la connexion et du curseur après avoir quitté le withbloc? La __exit__méthode présentée ci-dessus appelle uniquement self.rollback()ou self.commit(), et aucune de ces méthodes n'appelle la close()méthode. Le curseur lui-même n'a pas de __exit__méthode définie - et n'aurait pas d'importance s'il le faisait, car il withne gère que la connexion. Par conséquent, la connexion et le curseur restent ouverts après la sortie du withbloc. 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'

Vous devriez voir la sortie "le curseur est ouvert; la connexion est ouverte" imprimée sur stdout.

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

Pourquoi? L' API C MySQL , qui en est la base MySQLdb, n'implémente aucun objet curseur, comme le suggère la documentation du module: "MySQL ne prend pas en charge les curseurs; cependant, les curseurs sont facilement émulés." En effet, la MySQLdb.cursors.BaseCursorclasse hérite directement des objectcurseurs et n'impose aucune restriction de ce type en ce qui concerne la validation / la restauration. Un développeur Oracle avait ceci à dire :

cnx.commit () avant cur.close () me semble le plus logique. Peut-être pouvez-vous 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 Connector / Python, cela ne fait pas beaucoup de différence, mais ou pour d'autres bases de données, cela pourrait.

Je pense que c'est aussi proche que vous allez arriver à la "pratique standard" sur ce sujet.

Y a-t-il un avantage significatif à trouver des ensembles de transactions qui ne nécessitent pas de validations intermédiaires afin que vous n'ayez pas à obtenir de nouveaux curseurs pour chaque transaction?

J'en doute beaucoup, et en essayant de le faire, vous risquez d'introduire une erreur humaine supplémentaire. Mieux vaut décider d'une convention et s'y tenir.

Y a-t-il beaucoup de frais généraux pour obtenir de nouveaux curseurs, ou n'est-ce pas un 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 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 lorsque nous discutions with, vous pouvez peut-être maintenant comprendre pourquoi la MySQLdb.Connectionclasse __enter__et les __exit__méthodes vous donnent un tout nouvel objet curseur dans chaque withbloc et ne prennent 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 n'a pas de __exit__méthode définie . Pour cette question, vous pouvez également l'utiliser pour forcer l'objet de connexion à se fermer à la sortie d'un withbloc. Cela devrait afficher "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)cela n'appellera pas les méthodes __enter__et de l'objet argument __exit__; il n'appel de l'objet de l' argument de la méthode à la fin du bloc. (Pour voir en action, définir simplement une classe avec , et des méthodes simples contenant des déclarations, et comparer ce qui se passe quand vous faites à ce qui se passe quand vous faites .) Cela a deux conséquences importantes:closewithFoo__enter____exit__closeprintwith Foo(): passwith closing(Foo()): pass

Premièrement, si le mode autocommit est activé, MySQLdb effectuera BEGINune transaction explicite sur le serveur lorsque vous utiliserez with connectionet validerez ou annulerez 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 consistant à valider immédiatement toutes les instructions DML. MySQLdb suppose que lorsque vous utilisez un gestionnaire de contexte, vous souhaitez une transaction et utilise l'explicite BEGINpour contourner le paramètre de validation automatique sur le serveur. Si vous avez l'habitude d'utiliser with connection, vous pourriez penser que l'autocommit est désactivé alors qu'en réalité il était seulement contourné. Vous pourriez avoir une mauvaise surprise si vous ajoutezclosingà votre code et perdez l'intégrité transactionnelle; vous ne pourrez pas annuler les modifications, vous pouvez commencer à voir des bogues de concurrence et pourquoi ne pas être immédiatement évident.

Deuxièmement, with closing(MySQLdb.connect(user, pass)) as VARlie 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'auriez pas d'accès direct à l'objet de connexion! Au lieu de cela, vous devrez utiliser l' connectionattribut du curseur , qui fournit un accès proxy à la connexion d'origine. Lorsque le curseur est fermé, son connectionattribut est défini sur None. Cela entraîne une connexion abandonnée qui restera jusqu'à ce que l'un des événements suivants se produise:

  • Toutes les références au curseur sont supprimées
  • Le curseur sort de la portée
  • 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 en utilisantSHOW 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
Air
la source
14
votre message était très complet, mais même après l'avoir relu plusieurs fois, je me trouve toujours perplexe quant à la fermeture des curseurs. À en juger par les nombreux articles sur le sujet, cela semble être un point commun de confusion. Ce que je retiens, c'est que les curseurs n'ont apparemment PAS besoin d'appeler .close () - jamais. Alors pourquoi même avoir une méthode .close ()?
SMGreenfield
6
La réponse courte est que cela cursor.close()fait partie de l'API Python DB , qui n'a pas été écrite spécifiquement avec MySQL à l'esprit.
Air
1
Pourquoi la connexion se fermera-t-elle après la suppression de my_curs?
BAE
@ChengchengPei my_curscontient la dernière référence à l' connectionobjet. Une fois que cette référence n'existe plus, l' connectionobjet doit être récupéré.
Air
C'est une réponse fantastique, merci. Explication excellente withet MySQLdb.Connection« s __enter__et __exit__fonctions. Encore une fois, merci @Air.
Eugene
33

Il est préférable de le réécrire en utilisant le mot clé «avec». 'With' s'occupera de fermer automatiquement le curseur (c'est important car c'est une ressource non gérée). L'avantage est qu'il fermera également 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()
Roman Podlinov
la source
Je ne pense pas que ce withsoit une bonne option si vous souhaitez l'utiliser dans Flask ou dans un autre framework Web. Si tel est le cas, http://flask.pocoo.org/docs/patterns/sqlite3/#sqlite3il y aura des problèmes.
James King
@ james-king Je n'ai pas travaillé avec Flask, mais dans votre exemple, Flask fermera la connexion db elle-même. En fait, dans mon code, j'utilise une approche légèrement différente - j'utilise avec des curseurs proches 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 Ouais, si vous l'utilisez avec le curseur, tout ira bien.
James King
7

Remarque: cette réponse concerne PyMySQL , qui est un remplacement de MySQLdb et effectivement la dernière version de MySQLdb depuis que MySQLdb a cessé d'être maintenu. Je crois que tout ici est également vrai de l'héritage MySQLdb, mais je n'ai pas vérifié.

Tout d'abord, quelques faits:

  • La withsyntaxe de Python appelle la __enter__méthode du gestionnaire de contexte avant d'exécuter le corps du withbloc, et sa __exit__méthode ensuite.
  • Les connexions ont une __enter__méthode qui ne fait rien d'autre que créer et retourner un curseur, et une __exit__méthode qui valide ou annule (selon si une exception a été levée). Cela 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 __enter__méthode qui ne fait rien et une __exit__méthode qui "ferme" le curseur (ce qui signifie simplement annuler la référence du curseur à sa connexion parent et rejeter toutes les données stockées sur le curseur).
  • Les curseurs contiennent une référence à la connexion qui les a engendrés, mais les connexions ne contiennent pas de référence aux curseurs qu'ils ont créés.
  • Les connexions ont une __del__méthode qui les ferme
  • Selon https://docs.python.org/3/reference/datamodel.html , CPython (l'implémentation Python par défaut) utilise le comptage de références et supprime automatiquement un objet une fois que le nombre de références à celui-ci atteint zéro.

En mettant ces choses ensemble, 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 PROCESSLISTsur un shell MySQL, vous pourrez voir la connexion inactive que vous avez créée. Puisque le nombre de connexions par défaut de MySQL est de 151 , ce qui n'est pas énorme , vous pourriez théoriquement commencer à rencontrer des problèmes si vous aviez de nombreux processus gardant ces connexions ouvertes.

Cependant, dans CPython, il existe une grâce salvatrice qui garantit que le code comme mon exemple ci-dessus ne vous fera probablement pas laisser de nombreuses connexions ouvertes. Cette grâce salvatrice est que dès qu'elle cursorest hors de portée (par exemple, la fonction dans laquelle elle a été créée se termine ou cursorreçoit une autre valeur qui lui est assignée), son compteur de références atteint zéro, ce qui entraîne sa suppression, ce qui supprime le nombre de références de la connexion. à zéro, ce qui entraîne l' __del__appel de la méthode de connexion qui force la fermeture de la connexion. Si vous avez déjà collé le code ci-dessus dans votre shell Python, 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 de la SHOW PROCESSLISTsortie.

Cependant, s'en remettre à cela n'est pas élégant et pourrait théoriquement échouer dans les implémentations Python autres que CPython. Plus propre, en théorie, serait d'expliciter .close()la connexion (pour libérer une connexion sur la base de données sans attendre que Python détruise 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 cela ne dépend pas de Python qui détruit vos objets pour libérer vos connexions de base de données (nombre fini disponible de).

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

Enfin, pour répondre aux questions secondaires ici:

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

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

Y a-t-il un avantage significatif à trouver des ensembles de transactions qui ne nécessitent pas de validations intermédiaires afin que vous n'ayez pas à obtenir de nouveaux curseurs pour chaque transaction?

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


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

Mark Amery
la source
5

Je pense que vous ferez 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 également avoir des avantages en termes 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 deuxième exécution. Vous ne rencontrez des problèmes de cette façon que si vous utilisez fetchone () et que vous devez effectuer une deuxième exécution du curseur avant de parcourir tous les résultats de la première requête.

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

nct25
la source
Merci - Étant donné que vous devez fermer le curseur pour valider une mise à jour / insertion, je suppose qu'un moyen simple de le faire pour les mises à jour / insertions serait d'obtenir un curseur pour chaque démon, de fermer le curseur pour valider et d'obtenir immédiatement un nouveau curseur donc vous êtes prêt la prochaine fois. Cela semble-t-il raisonnable?
jmilloy
1
Hé, pas de problème. Je ne savais pas vraiment comment valider la mise à jour / l'insertion en fermant vos curseurs, mais une recherche rapide en ligne montre ceci: conn = MySQLdb.connect (arguments_go_here) cursor = MySQLdb.cursor () cursor.execute (mysql_insert_statement_here) essayez: conn. commit () sauf: conn.rollback () # annuler les modifications apportées si une erreur se produit. De cette façon, la base de données elle-même valide les modifications et vous n'avez pas à vous soucier des curseurs eux-mêmes. Ensuite, vous pouvez simplement ouvrir 1 curseur à tout moment. Regardez ici: tutorialspoint.com/python/python_database_access.htm
nct25
Oui, si cela fonctionne, je me trompe et il y a une autre raison qui m'a fait penser que je devais fermer le curseur pour valider la connexion.
jmilloy
Oui, je ne sais pas, ce lien que j'ai posté me fait penser que cela fonctionne. Je suppose qu'un peu plus de recherche vous dirait si cela fonctionne vraiment ou non, mais je pense que vous pourriez probablement y aller. J'espère que je t'ai aidé!
nct25
Le curseur n'est pas thread-safe, si vous utilisez le même curseur parmi de nombreux threads différents, et qu'ils interrogent tous depuis db, fetchall () donnera des données aléatoires.
ospider
-6

Je suggère de le faire comme php et mysql. Commencez i au début de votre code avant d'imprimer les premières données. Donc, si vous obtenez une erreur de connexion, vous pouvez afficher un 50xmessage d'erreur (Je ne me souviens pas de l'erreur interne). Et gardez-le ouvert pendant toute la session et fermez-le lorsque vous savez que vous n'en aurez plus besoin.

TuéKenny
la source
Dans MySQLdb, il existe une différence entre une connexion et un curseur. Je me connecte une fois par demande (pour le moment) et je peux détecter les erreurs de connexion tôt. Mais qu'en est-il des curseurs?
jmilloy
IMHO ce n'est pas un conseil précis. Cela dépend. Si votre code garde la connexion pendant longtemps (par exemple, il prend des données de la base de données, puis pendant 1-5-10 minutes, il fait quelque chose sur le serveur et maintient la connexion) et que c'est une application multithread, cela créera un problème très bientôt (vous dépassera le nombre maximal de connexions autorisées).
Roman Podlinov