Когда закрывать курсоры с помощью MySQLdb

86

Я создаю веб-приложение WSGI, и у меня есть база данных MySQL. Я использую MySQLdb, который предоставляет курсоры для выполнения операторов и получения результатов. Какова стандартная практика получения и закрытия курсоров? В частности, как долго должны работать мои курсоры? Должен ли я получать новый курсор для каждой транзакции?

Я считаю, что вам нужно закрыть курсор, прежде чем устанавливать соединение. Есть ли какое-либо существенное преимущество в нахождении наборов транзакций, не требующих промежуточных фиксаций, чтобы вам не приходилось получать новые курсоры для каждой транзакции? Есть ли много накладных расходов на получение новых курсоров, или это просто не имеет большого значения?

Jmilloy
источник

Ответы:

80

Вместо того, чтобы спрашивать, что является стандартной практикой, поскольку это часто неясно и субъективно, вы можете попробовать обратиться к самому модулю за руководством. В общем, использование withключевого слова в соответствии с предложением другого пользователя - отличная идея, но в этих конкретных обстоятельствах оно может не дать вам той функциональности, которую вы ожидаете.

Начиная с версии 1.2.5 модуля, MySQLdb.Connectionреализует протокол диспетчера контекста со следующим кодом ( 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()

Уже есть несколько вопросов и ответов with, или вы можете прочитать оператор «with» в Python , но по сути происходит то, что __enter__выполняется в начале withблока и __exit__выполняется после выхода из withблока. Вы можете использовать необязательный синтаксис with EXPR as VARдля привязки объекта, возвращаемого __enter__к имени, если вы намереваетесь ссылаться на этот объект позже. Итак, учитывая приведенную выше реализацию, вот простой способ запроса вашей базы данных:

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

Теперь вопрос в том, каковы состояния соединения и курсора после выхода из withблока? __exit__Способ , показанный выше вызовов только self.rollback()или self.commit(), и ни один из этих методов идти вызвать close()метод. Сам курсор не имеет __exit__определенного метода - и не имело бы значения, если бы он был, потому что withон только управляет соединением. Следовательно, и соединение, и курсор остаются открытыми после выхода из withблока. Это легко подтверждается добавлением следующего кода к приведенному выше примеру:

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'

Вы должны увидеть вывод «курсор открыт; соединение открыто», выводимое на стандартный вывод.

Я считаю, что вам нужно закрыть курсор, прежде чем устанавливать соединение.

Зачем? MySQL C API , который является основой для MySQLdb, не реализует какой - либо объект курсора, как это подразумевается в документации модуля: «MySQL не поддерживает курсоры, однако, курсоры легко эмулируются.» Действительно, MySQLdb.cursors.BaseCursorкласс наследуется напрямую от objectкурсоров и не накладывает на них таких ограничений в отношении фиксации / отката. Разработчик Oracle сказал следующее :

cnx.commit () перед cur.close () мне кажется наиболее логичным. Может быть, вы можете следовать правилу: «Закройте курсор, если он вам больше не нужен». Таким образом, commit () перед закрытием курсора. В конце концов, для Connector / Python это не имеет большого значения, но для других баз данных может.

Я полагаю, что это самое близкое к «стандартной практике» по этому вопросу.

Есть ли какое-либо существенное преимущество в нахождении наборов транзакций, не требующих промежуточных фиксаций, чтобы вам не приходилось получать новые курсоры для каждой транзакции?

Я очень в этом сомневаюсь, и, пытаясь сделать это, вы можете внести дополнительную человеческую ошибку. Лучше определиться с соглашением и придерживаться его.

Есть ли много накладных расходов на получение новых курсоров или это просто не имеет большого значения?

Накладные расходы незначительны и вообще не затрагивают сервер базы данных; это полностью в рамках реализации MySQLdb. Вы можете посмотреть BaseCursor.__init__на github, если вам действительно интересно узнать, что происходит, когда вы создаете новый курсор.

Возвращаясь к тому withмоменту, когда мы обсуждали , возможно, теперь вы можете понять, почему MySQLdb.Connectionкласс __enter__и __exit__методы предоставляют вам новый объект курсора в каждом withблоке и не беспокоятся о его отслеживании или закрытии в конце блока. Он довольно легкий и существует исключительно для вашего удобства.

Если для вас действительно так важно управлять объектом курсора, вы можете использовать contextlib.closing, чтобы компенсировать тот факт, что объект курсора не имеет определенного __exit__метода. В этом отношении вы также можете использовать его для принудительного закрытия объекта соединения при выходе из withблока. Это должно выводить «my_curs закрыто; my_conn закрыто»:

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'

Обратите внимание, что with closing(arg_obj)не будут вызывать методы объекта __enter__и аргумента __exit__; он будет только назвать объект аргумента closeметода в конце withблока. (Чтобы увидеть это в действии, просто определить класс Fooс __enter__, __exit__и closeметоды , содержащие простые printвысказывания, и сравнить то , что происходит , когда вы делаете with Foo(): passто , что происходит , когда вы делаете with closing(Foo()): pass.) Это имеет два существенных последствия:

Во-первых, если включен режим автоматической фиксации, MySQLdb будет выполнять BEGINявную транзакцию на сервере, когда вы используете with connectionи фиксируете или откатываете транзакцию в конце блока. Это поведение MySQLdb по умолчанию, предназначенное для защиты вас от поведения MySQL по умолчанию, заключающегося в немедленной фиксации любых без исключения операторов DML. MySQLdb предполагает, что когда вы используете диспетчер контекста, вам нужна транзакция, и использует явное, BEGINчтобы обойти настройку автоматической фиксации на сервере. Если вы привыкли использовать with connection, вы можете подумать, что автоматическая фиксация отключена, хотя на самом деле она только обходилась. Вы можете получить неприятный сюрприз, если добавитеclosingвашему коду и потеряете транзакционную целостность; вы не сможете откатить изменения, вы можете начать видеть ошибки параллелизма, и может быть не сразу понятно почему.

Во- вторых, with closing(MySQLdb.connect(user, pass)) as VARсвязывает объект подключения к VAR, в отличие от with MySQLdb.connect(user, pass) as VAR, который связывает новый объект курсора к VAR. В последнем случае у вас не будет прямого доступа к объекту подключения! Вместо этого вам придется использовать connectionатрибут курсора , который обеспечивает прокси-доступ к исходному соединению. Когда курсор закрыт, для его connectionатрибута устанавливается значение None. Это приводит к прерванному соединению, которое будет оставаться до тех пор, пока не произойдет одно из следующих событий:

  • Все ссылки на курсор удаляются
  • Курсор выходит за пределы области видимости
  • Время ожидания соединения истекло
  • Соединение закрывается вручную через инструменты администрирования сервера

Вы можете проверить это, отслеживая открытые соединения (в Workbench или используяSHOW PROCESSLIST ), выполняя следующие строки одну за другой:

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
Воздух
источник
14
ваш пост был наиболее исчерпывающим, но даже после того, как я перечитал его несколько раз, я все еще озадачен закрывающими курсорами. Судя по многочисленным сообщениям на эту тему, это обычная путаница. Мой вывод заключается в том, что курсоры, по-видимому, НЕ требуют вызова .close () - никогда. Так зачем вообще нужен метод .close ()?
SMGreenfield 08
6
Короткий ответ: это cursor.close()часть API Python DB , который не был написан специально для MySQL.
Воздух
1
Почему соединение закроется после del my_curs?
BAE
@ChengchengPei my_cursсодержит последнюю ссылку на connectionобъект. Как только эта ссылка больше не существует, connectionобъект следует очистить от мусора.
эфире
Это фантастический ответ, спасибо. Отличное объяснение withи MySQLdb.Connection__enter__и __exit__функций. Еще раз спасибо @Air.
Евгений
33

Лучше его переписать с помощью ключевого слова with. 'With' позаботится о закрытии курсора (это важно, потому что это неуправляемый ресурс) автоматически. Преимущество в том, что он также закроет курсор в случае исключения.

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()
Роман Подлинов
источник
Я не думаю, что withэто хороший вариант, если вы хотите использовать его во Flask или другом веб-фреймворке. Если ситуация http://flask.pocoo.org/docs/patterns/sqlite3/#sqlite3сложится, то будут проблемы.
Джеймс Кинг
@ james-king Я не работал с Flask, но в вашем примере Flask закроет само соединение с БД. На самом деле в моем коде я использую немного другой подход - я использую with для закрытых курсоров 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()
Роман Подлинов
@RomanPodlinov Да, если использовать с курсором, то все будет хорошо.
Джеймс Кинг
7

Примечание: этот ответ предназначен для PyMySQL , который является заменой MySQLdb и, по сути, последней версией MySQLdb, так как MySQLdb перестал поддерживаться. Я считаю , что все , что здесь также относится к унаследованным MySQLdb, но не проверял.

Прежде всего, некоторые факты:

  • withСинтаксис Python вызывает метод диспетчера контекста __enter__перед выполнением тела withблока, а затем его __exit__метод.
  • В соединениях есть __enter__метод, который ничего не делает, кроме создания и возврата курсора, и __exit__метод, который либо фиксирует, либо откатывает (в зависимости от того, было ли создано исключение). Он не закрывает соединение.
  • Курсоры в PyMySQL - это чистая абстракция, реализованная в Python; в самой MySQL нет эквивалентной концепции. 1
  • У курсоров есть __enter__метод, который ничего не делает, и __exit__метод, который «закрывает» курсор (что означает просто обнуление ссылки курсора на его родительское соединение и отбрасывание любых данных, хранящихся в курсоре).
  • Курсоры содержат ссылку на соединение, которое их породило, но соединения не содержат ссылки на курсоры, которые они создали.
  • У подключений есть __del__метод, который их закрывает
  • Согласно https://docs.python.org/3/reference/datamodel.html , CPython (реализация Python по умолчанию) использует подсчет ссылок и автоматически удаляет объект, как только количество ссылок на него достигает нуля.

Собирая все это вместе, мы видим, что такой наивный код теоретически проблематичен:

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

# ... happily carry on and do something unrelated

Проблема в том, что ничто не закрыло соединение. Действительно, если вы вставите приведенный выше код в оболочку Python, а затем запустите ее SHOW FULL PROCESSLISTв оболочке MySQL, вы сможете увидеть созданное вами незанятое соединение. Поскольку количество соединений MySQL по умолчанию составляет 151 , что не очень много , теоретически вы могли бы столкнуться с проблемами, если бы у вас было много процессов, поддерживающих эти соединения открытыми.

Однако в CPython есть льгота экономии, которая гарантирует, что код, подобный моему примеру выше, вероятно , не заставит вас оставить множество открытых соединений. Эта экономия заключается в том, что как только он cursorвыходит за пределы области видимости (например, функция, в которой он был создан, завершается или cursorполучает другое значение, присвоенное ей), его счетчик ссылок достигает нуля, что приводит к его удалению, что приводит к удалению счетчика ссылок соединения. до нуля, вызывая __del__вызов метода соединения, который принудительно закрывает соединение. Если вы уже вставили приведенный выше код в оболочку Python, теперь вы можете смоделировать это, запустив cursor = 'arbitrary value'; как только вы это сделаете, открытое вами соединение исчезнет из SHOW PROCESSLISTвывода.

Однако полагаться на это неуместно и теоретически может привести к сбою в реализациях Python, отличных от CPython. Теоретически более чистым было бы явное .close()соединение (чтобы освободить соединение в базе данных, не дожидаясь, пока Python уничтожит объект). Этот более надежный код выглядит так:

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

Это уродливо, но не полагается на то, что Python разрушает ваши объекты, чтобы освободить ваши (конечное доступное количество) соединений с базой данных.

Обратите внимание, что закрытие курсора , если вы уже закрываете соединение явно таким образом, совершенно бессмысленно.

Наконец, чтобы ответить на второстепенные вопросы здесь:

Есть ли много накладных расходов на получение новых курсоров или это просто не имеет большого значения?

Нет, создание курсора вообще не затрагивает MySQL и в основном ничего не делает .

Есть ли какое-либо существенное преимущество в нахождении наборов транзакций, не требующих промежуточных фиксаций, чтобы вам не приходилось получать новые курсоры для каждой транзакции?

Это ситуационный вопрос, и на него сложно дать общий ответ. Как сказано на https://dev.mysql.com/doc/refman/en/optimizing-innodb-transaction-management.html , «приложение может столкнуться с проблемами производительности, если оно совершает тысячи раз в секунду, и другими проблемами производительности, если он совершает только каждые 2-3 часа » . Вы платите накладные расходы на производительность за каждую фиксацию, но, оставляя транзакции открытыми на более длительный срок, вы увеличиваете вероятность того, что другим соединениям придется тратить время на ожидание блокировок, увеличиваете риск взаимоблокировок и потенциально увеличиваете стоимость некоторых поисков, выполняемых другими соединениями .


1 MySQL делает иметь конструкцию , она называет курсор , но они существуют только внутри хранимых процедур; они полностью отличаются от курсоров PyMySQL и здесь не актуальны.

Марк Эмери
источник
5

Я думаю, вам будет лучше попытаться использовать один курсор для всех ваших выполнений и закрыть его в конце вашего кода. С ним легче работать, и он может иметь преимущества в эффективности (не цитируйте меня по этому поводу).

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

Дело в том, что вы можете сохранить результаты выполнения курсора в другой переменной, тем самым освободив курсор для выполнения второго выполнения. Таким образом, вы столкнетесь с проблемами, только если вы используете fetchone () и вам нужно выполнить второе выполнение курсора, прежде чем вы перебираете все результаты из первого запроса.

В противном случае я бы сказал, просто закройте ваши курсоры, как только вы закончите извлекать из них все данные. Таким образом, вам не придется беспокоиться о том, что в дальнейшем в вашем коде будут лишены дела.

nct25
источник
Спасибо - учитывая, что вам нужно закрыть курсор, чтобы зафиксировать обновление / вставку, я полагаю, что один простой способ сделать это для обновлений / вставок - получить один курсор для каждого демона, закрыть его для фиксации и немедленно получить новый курсор. так что будьте готовы в следующий раз. Звучит разумно?
jmilloy
1
Эй, нет проблем. На самом деле я не знал, как выполнить обновление / вставку путем закрытия курсоров, но быстрый поиск в Интернете показывает это: conn = MySQLdb.connect (arguments_go_here) cursor = MySQLdb.cursor () cursor.execute (mysql_insert_statement_here) try: conn. commit () except: conn.rollback () # отменить изменения, сделанные в случае ошибки. Таким образом, сама база данных фиксирует изменения, и вам не нужно беспокоиться о самих курсорах. Тогда вы можете постоянно держать открытым только 1 курсор. Посмотрите здесь: tutorialspoint.com/python/python_database_access.htm
nct25,
Да, если это сработает, то я просто ошибаюсь, и была другая причина, которая заставила меня подумать, что мне нужно закрыть курсор, чтобы зафиксировать соединение.
jmilloy 01
Да, я не знаю, эта ссылка, которую я опубликовал, заставляет меня думать, что это работает. Думаю, небольшое дополнительное исследование подскажет, работает это определенно или нет, но я думаю, что вы могли бы просто согласиться с этим. Надеюсь, я был вам полезен!
nct25 01
курсор не является потокобезопасным, если вы используете один и тот же курсор среди множества разных потоков, и все они запрашивают из db, fetchall () выдаст случайные данные.
ospider
-6

Предлагаю сделать это как php и mysql. Запустите i в начале вашего кода перед печатью первых данных. Поэтому, если вы получаете сообщение об ошибке подключения, вы можете отобразить сообщение об ошибке 50x(не помню, что такое внутренняя ошибка). И держите его открытым в течение всего сеанса и закройте, когда знаете, что он вам больше не нужен.

Убитый Кенни
источник
В MySQLdb есть разница между соединением и курсором. Я подключаюсь один раз на запрос (пока) и могу рано обнаруживать ошибки подключения. А как же курсоры?
jmilloy
ИМХО это не точный совет. Это зависит. Если ваш код будет поддерживать соединение в течение длительного времени (например, он берет некоторые данные из БД, а затем в течение 1-5-10 минут он что-то делает на сервере и сохраняет соединение), и это многопоточное приложение, оно довольно скоро создаст проблему (вы будет превышать максимально разрешенные соединения).
Роман Подлинов