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

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

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

5 ответов


вместо того, чтобы спрашивать, Что такое стандартная практика, так как это часто неясно и субъективно, вы можете попробовать посмотреть на сам модуль для руководства. В общем, используя 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()

Существует несколько Q&о 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 блок? The __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'

вы должны увидеть вывод "курсор открыт; соединение открыто", напечатанный на stdout.

я считаю, что вам нужно закрыть курсор перед фиксацией соединения.

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

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

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

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

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

есть много ресурсов для получения новых курсоров, или это просто не важно?

накладные расходы незначительны и не касаются сервера базы данных вообще; это полностью в рамках реализации MySQLdb. Ты можешь!--103-->посмотреть BaseCursor.__init__ на github если вам действительно интересно знать, что происходит при создании нового курсора.

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

если это действительно так важно для вас, чтобы контролировать объект курсора, вы можете использовать contextlib.закрытие чтобы компенсировать тот факт, что объект курсора не имеет определенного __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 для обхода параметра autocommit на сервере. Если вы привыкли использовать with connection, вы можете подумать, что автокоммит отключен, когда на самом деле его только обходили. Вы можете получить неприятный сюрприз, если вы добавите closing в ваш код и потерять целостность транзакций; вы не сможете откатить изменения, вы можете начать видеть ошибки параллелизма, и это может быть не сразу очевидно, почему.

во-вторых,with closing(MySQLdb.connect(user, pass)) as VAR связывает подключение to VAR в отличие от with MySQLdb.connect(user, pass) as VAR, который обязывает новый объект курсора to VAR. В последнем случае у вас не будет прямого доступа к объекту подключения! Вместо этого вам придется использовать , которым предоставляет прокси-доступ к исходному соединению. Когда курсор закрыт, его 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

лучше переписать его с помощью ключевого слова "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()

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

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 (), и вам нужно сделать второй выполнение курсора перед итерацией всех результатов первого запроса.

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


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

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

  • в Python with синтаксис вызывает __enter__ метод перед выполнением тела из with блок, и его __exit__ потом способ.
  • подключения есть __enter__ метод, который ничего не делает, кроме создания и возврата курсора, и __exit__ метод фиксации или отката (в зависимости от того, произошло исключение). Это не закрыть соединение.
  • курсоры в PyMySQL являются чисто абстракцией, реализованной в Python; в MySQL нет эквивалентной концепции себя.1
  • курсоры есть __enter__ метод, который ничего не делает и __exit__ метод, который "закрывает" курсор (что просто означает обнуление ссылки курсора на его родительское соединение и выбрасывание любых данных, хранящихся на курсоре).
  • курсоры содержат ссылку на соединение, которое их породило, но соединения не содержат ссылки на курсоры, которые они создали.
  • подключения есть __del__ метод, который закрывает их
  • Per 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 и здесь не актуальны.


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