Quando fechar cursores usando MySQLdb

86

Estou construindo um aplicativo da web WSGI e tenho um banco de dados MySQL. Estou usando o MySQLdb, que fornece cursores para executar instruções e obter resultados. Qual é a prática padrão para obter e fechar cursores? Em particular, quanto tempo meus cursores devem durar? Devo obter um novo cursor para cada transação?

Eu acredito que você precisa fechar o cursor antes de confirmar a conexão. Existe alguma vantagem significativa em encontrar conjuntos de transações que não exijam confirmações intermediárias para que você não precise obter novos cursores para cada transação? Há muita sobrecarga para obter novos cursores ou simplesmente não é um grande problema?

Jmilloy
fonte

Respostas:

80

Em vez de perguntar o que é prática padrão, já que isso geralmente é obscuro e subjetivo, você pode tentar consultar o próprio módulo para orientação. Em geral, usar a withpalavra - chave sugerida por outro usuário é uma ótima ideia, mas, nesta circunstância específica, pode não oferecer a funcionalidade que você espera.

A partir da versão 1.2.5 do módulo, MySQLdb.Connectionimplementa o protocolo do gerenciador de contexto com o seguinte código ( 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()

Existem várias perguntas e respostas existentes sobre withjá, ou você pode ler a instrução "with" do Python , mas essencialmente o que acontece é que é __enter__executado no início do withbloco e __exit__ao sair do withbloco. Você pode usar a sintaxe opcional with EXPR as VARpara vincular o objeto retornado por __enter__a um nome se pretende fazer referência a esse objeto posteriormente. Portanto, dada a implementação acima, aqui está uma maneira simples de consultar seu banco de dados:

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

A questão agora é: quais são os estados da conexão e do cursor após sair do withbloco? O __exit__método mostrado acima chama apenas self.rollback()ou self.commit(), e nenhum desses métodos continua para chamar o close()método. O próprio cursor não tem __exit__método definido - e não importaria se tivesse, porque withestá apenas gerenciando a conexão. Portanto, a conexão e o cursor permanecem abertos após a saída do withbloco. Isso é facilmente confirmado adicionando o seguinte código ao exemplo acima:

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'

Você deve ver a saída "cursor is open; connection is open" impressa em stdout.

Eu acredito que você precisa fechar o cursor antes de confirmar a conexão.

Por quê? A API C do MySQL , que é a base para MySQLdb, não implementa nenhum objeto cursor, conforme implícito na documentação do módulo: "O MySQL não oferece suporte a cursores; no entanto, os cursores são facilmente emulados." Na verdade, a MySQLdb.cursors.BaseCursorclasse herda diretamente objecte não impõe tal restrição aos cursores com relação ao commit / rollback. Um desenvolvedor Oracle disse o seguinte :

cnx.commit () antes de cur.close () parece mais lógico para mim. Talvez você possa seguir a regra: "Feche o cursor se não precisar mais dele." Portanto, commit () antes de fechar o cursor. No final, para o Connector / Python, não faz muita diferença, mas para outros bancos de dados pode fazer.

Espero que isso seja o mais próximo que você chegará da "prática padrão" neste assunto.

Existe alguma vantagem significativa em localizar conjuntos de transações que não requerem confirmações intermediárias, de forma que você não precise obter novos cursores para cada transação?

Duvido muito e, ao tentar fazer isso, você pode introduzir um erro humano adicional. Melhor decidir sobre uma convenção e segui-la.

Há muita sobrecarga para obter novos cursores ou simplesmente não é um grande problema?

A sobrecarga é insignificante e não afeta o servidor de banco de dados; está inteiramente dentro da implementação do MySQLdb. Você pode olhar no BaseCursor.__init__github se estiver realmente curioso para saber o que está acontecendo quando você cria um novo cursor.

Voltando a quando estávamos discutindo with, talvez agora você possa entender por que a MySQLdb.Connectionclasse __enter__e os __exit__métodos fornecem um objeto de cursor totalmente novo em cada withbloco e não se preocupe em mantê-lo ou fechá-lo no final do bloco. É bastante leve e existe exclusivamente para sua conveniência.

Se for realmente tão importante para você microgerenciar o objeto cursor, você pode usar contextlib.closing para compensar o fato de que o objeto cursor não tem um __exit__método definido . Além disso, você também pode usá-lo para forçar o objeto de conexão a se fechar ao sair de um withbloco. Isso deve gerar "my_curs is closed; my_conn is closed":

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'

Observe que with closing(arg_obj)não chamará os métodos __enter__e do objeto do argumento __exit__; ele apenas chamará o closemétodo do objeto de argumento no final do withbloco. (Para ver isso em ação, basta definir uma classe Foocom __enter__, __exit__e closemétodos contendo simples printdeclarações, e comparar o que acontece quando você faz with Foo(): passcom o que acontece quando você faz with closing(Foo()): pass.) Isso tem duas implicações importantes:

Primeiro, se o modo autocommit estiver habilitado, o MySQLdb fará BEGINuma transação explícita no servidor quando você usar with connectione confirmar ou reverter a transação no final do bloco. Esses são os comportamentos padrão do MySQLdb, com o objetivo de protegê-lo do comportamento padrão do MySQL de confirmar imediatamente todas e quaisquer instruções DML. O MySQLdb assume que quando você usa um gerenciador de contexto, você quer uma transação, e usa o explícito BEGINpara ignorar a configuração de autocommit no servidor. Se você está acostumado a usar with connection, pode pensar que o autocommit está desabilitado, quando na verdade estava apenas sendo ignorado. Você pode ter uma surpresa desagradável se adicionarclosingao seu código e perder integridade transacional; você não será capaz de reverter as alterações, você pode começar a ver bugs de simultaneidade e pode não ser imediatamente óbvio o porquê.

Em segundo lugar, with closing(MySQLdb.connect(user, pass)) as VARvincula o objeto de conexão a VAR, em contraste com with MySQLdb.connect(user, pass) as VAR, que vincula um novo objeto cursor a VAR. No último caso, você não teria acesso direto ao objeto de conexão! Em vez disso, você teria que usar o connectionatributo do cursor , que fornece acesso de proxy à conexão original. Quando o cursor é fechado, seu connectionatributo é definido como None. Isso resulta em uma conexão abandonada que permanecerá até que uma das seguintes opções aconteça:

  • Todas as referências ao cursor são removidas
  • O cursor sai do escopo
  • A conexão atinge o tempo limite
  • A conexão é fechada manualmente por meio de ferramentas de administração do servidor

Você pode testar isso monitorando conexões abertas (no Workbench ou usandoSHOW PROCESSLIST ) enquanto executa as seguintes linhas uma por uma:

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
Ar
fonte
14
sua postagem foi mais exaustiva, mas mesmo depois de relê-la algumas vezes, ainda fico confuso quanto ao fechamento dos cursores. A julgar pelas inúmeras postagens sobre o assunto, parece ser um ponto comum de confusão. Minha lição é que os cursores aparentemente NÃO requerem .close () para serem chamados - nunca. Então, por que ter um método .close ()?
SMGreenfield
6
A resposta curta é que cursor.close()faz parte da API Python DB , que não foi escrita especificamente com o MySQL em mente.
Air
1
Por que a conexão será encerrada após del my_curs?
BAE
@ChengchengPei my_curscontém a última referência ao connectionobjeto. Quando essa referência não existir mais, o connectionobjeto deve ser coletado como lixo.
ar em
Esta é uma resposta fantástica, obrigado. Excelente explicação de withe MySQLdb.Connections' __enter__e __exit__funções. Mais uma vez, obrigado @Air.
Eugene
33

É melhor reescrevê-lo usando a palavra-chave 'with'. 'Com' cuidará de fechar o cursor (é importante porque é um recurso não gerenciado) automaticamente. A vantagem é que ele também fecha o cursor em caso de exceção.

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
fonte
Não acho que withseja uma boa opção se você quiser usá-lo no Flask ou em outro framework web. Se for a situação, http://flask.pocoo.org/docs/patterns/sqlite3/#sqlite3então haverá problemas.
James King
@ james-king Eu não trabalhei com o Flask, mas no seu exemplo o Flask irá fechar a própria conexão db. Na verdade, em meu código, eu uso uma abordagem um pouco diferente - eu uso com para cursores próximos 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 Sim, se você usá-lo com o cursor, tudo ficará bem.
James King
7

Nota: esta resposta é para PyMySQL , que é um substituto para o MySQLdb e efetivamente a versão mais recente do MySQLdb desde que o MySQLdb parou de ser mantido. Acredito que tudo aqui também se aplica ao MySQLdb legado, mas não verifiquei.

Em primeiro lugar, alguns fatos:

  • A withsintaxe do Python chama o __enter__método do gerenciador de contexto antes de executar o corpo do withbloco e seu __exit__método depois.
  • As conexões têm um __enter__método que não faz nada além de criar e retornar um cursor e um __exit__método que confirma ou reverte (dependendo se uma exceção foi lançada). Ele não fechar a conexão.
  • Cursores em PyMySQL são puramente uma abstração implementada em Python; não há conceito equivalente no próprio MySQL.1
  • Os cursores têm um __enter__método que não faz nada e um __exit__método que "fecha" o cursor (o que significa apenas anular a referência do cursor à sua conexão pai e jogar fora todos os dados armazenados no cursor).
  • Os cursores contêm uma referência à conexão que os gerou, mas as conexões não contêm uma referência aos cursores que criaram.
  • As conexões têm um __del__ método que as fecha
  • Por https://docs.python.org/3/reference/datamodel.html , CPython (a implementação Python padrão) usa contagem de referência e exclui automaticamente um objeto assim que o número de referências a ele atinge zero.

Juntando essas coisas, vemos que um código ingênuo como esse é teoricamente problemático:

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

# ... happily carry on and do something unrelated

O problema é que nada fechou a conexão. Na verdade, se você colar o código acima em um shell Python e, em seguida, executar SHOW FULL PROCESSLISTem um shell MySQL, poderá ver a conexão ociosa que criou. Como o número padrão de conexões do MySQL é 151 , o que não é muito grande , teoricamente você poderia começar a ter problemas se tivesse muitos processos mantendo essas conexões abertas.

No entanto, no CPython, há uma graça salvadora que garante que um código como o meu exemplo acima provavelmente não fará com que você deixe muitas conexões abertas. Essa graça é que, assim que cursorsai do escopo (por exemplo, a função na qual foi criada termina ou cursorrecebe outro valor atribuído a ela), sua contagem de referência atinge zero, o que faz com que seja excluída, descartando a contagem de referência da conexão para zero, fazendo com que o __del__método da conexão seja chamado, o que força o fechamento da conexão. Se você já colou o código acima em seu shell Python, agora você pode simular isso executando cursor = 'arbitrary value'; assim que você fizer isso, a conexão aberta desaparecerá da SHOW PROCESSLISTsaída.

No entanto, confiar nisso é deselegante e, teoricamente, pode falhar em implementações Python diferentes do CPython. Mais limpo, em teoria, seria explicitamente .close()a conexão (para liberar uma conexão no banco de dados sem esperar que o Python destrua o objeto). Este código mais robusto tem a seguinte aparência:

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

Isso é feio, mas não depende do Python destruindo seus objetos para liberar suas (número finito disponível de) conexões de banco de dados.

Observe que fechar o cursor , se já estiver fechando a conexão explicitamente assim, é totalmente inútil.

Finalmente, para responder às perguntas secundárias aqui:

Há muita sobrecarga para obter novos cursores ou simplesmente não é um grande problema?

Não, instanciar um cursor não atinge o MySQL e basicamente não faz nada .

Existe alguma vantagem significativa em localizar conjuntos de transações que não requerem confirmações intermediárias, de forma que você não precise obter novos cursores para cada transação?

Isso é situacional e difícil de dar uma resposta geral. Como https://dev.mysql.com/doc/refman/en/optimizing-innodb-transaction-management.html coloca, "um aplicativo pode encontrar problemas de desempenho se comprometer milhares de vezes por segundo, e diferentes problemas de desempenho se ele comete apenas a cada 2-3 horas " . Você paga uma sobrecarga de desempenho para cada confirmação, mas ao deixar as transações abertas por mais tempo, você aumenta a chance de outras conexões terem que perder tempo esperando por bloqueios, aumenta o risco de deadlocks e, potencialmente, aumenta o custo de algumas pesquisas realizadas por outras conexões .


1 MySQL faz tem uma construção que chama de um cursor , mas eles só existem procedimentos dentro armazenados; eles são completamente diferentes dos cursores PyMySQL e não são relevantes aqui.

Mark Amery
fonte
5

Acho que você ficará melhor tentando usar um cursor para todas as suas execuções e fechá-lo no final do código. É mais fácil de trabalhar e pode ter benefícios de eficiência também (não me diga sobre isso).

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

A questão é que você pode armazenar os resultados da execução de um cursor em outra variável, liberando assim o cursor para fazer uma segunda execução. Você terá problemas dessa forma apenas se estiver usando fetchone () e precisar fazer uma segunda execução do cursor antes de iterar todos os resultados da primeira consulta.

Caso contrário, eu diria apenas fechar seus cursores assim que terminar de obter todos os dados deles. Dessa forma, você não precisa se preocupar em amarrar pontas soltas mais tarde em seu código.

nct25
fonte
Obrigado - Considerando que você tem que fechar o cursor para confirmar uma atualização / inserção, eu acho que uma maneira fácil de fazer isso para atualizações / inserções seria obter um cursor para cada daemon, fechar o cursor para confirmar e imediatamente obter um novo cursor então você está pronto da próxima vez. Isso soa razoável?
jmilloy
1
Ei, sem problemas. Na verdade, eu não sabia sobre como confirmar a atualização / inserção fechando seus cursores, mas uma rápida pesquisa online mostra isso: conn = MySQLdb.connect (arguments_go_here) cursor = MySQLdb.cursor () cursor.execute (mysql_insert_statement_here) try: conn. commit () exceto: conn.rollback () # desfaz as alterações feitas se ocorrer um erro. Dessa forma, o próprio banco de dados confirma as alterações e você não precisa se preocupar com os próprios cursores. Então você pode ter apenas 1 cursor aberto o tempo todo. Dê uma olhada aqui: tutorialspoint.com/python/python_database_access.htm
nct25
Sim, se funcionar, estou simplesmente errado e houve algum outro motivo que me fez pensar que eu tinha que fechar o cursor para confirmar a conexão.
jmilloy 01 de
Sim, eu não sei, esse link que postei me faz pensar que funciona. Acho que um pouco mais de pesquisa diria se isso definitivamente funciona ou não, mas acho que você provavelmente poderia simplesmente continuar. Espero ter ajudado você!
nct25
cursor não é seguro para thread, se você usar o mesmo cursor entre muitos threads diferentes, e todos eles estão consultando db, fetchall () fornecerá dados aleatórios.
ospider
-6

Eu sugiro fazer isso como php e mysql. Comece i no início do seu código antes de imprimir os primeiros dados. Portanto, se você receber um erro de conexão, poderá exibir uma 50xmensagem de erro (Não me lembro qual é o erro interno). E mantenha-o aberto durante toda a sessão e feche-o quando souber que não precisará mais dele.

KilledKenny
fonte
No MySQLdb, há uma diferença entre uma conexão e um cursor. Eu conecto uma vez por solicitação (por enquanto) e posso detectar erros de conexão antecipadamente. Mas e os cursores?
jmilloy
IMHO não é um conselho preciso. Depende. Se o seu código vai manter a conexão por um longo tempo (por exemplo, leva alguns dados do banco de dados e depois por 1-5-10 minutos ele faz algo no servidor e mantém a conexão) e seu aplicativo de thread multy irá criar um problema em breve (você excederá o máximo de conexões permitidas).
Roman Podlinov