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

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

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

Ответ 1

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

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

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

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

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

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

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

Если вам действительно важно, чтобы микромеханировать объект курсора, вы можете использовать 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, к тому, что происходит, когда вы do with closing(Foo()): pass.) Это имеет два существенных значения:

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

Во-вторых, 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

Ответ 2

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

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

Ответ 3

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

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

  • Python with синтаксисом вызывает метод __enter__ менеджера контекста перед выполнением тела блока with, а затем его __exit__.
  • Соединения имеют метод __enter__, который ничего не делает, кроме создания и возврата курсора, и __exit__ который либо фиксирует, либо выполняет откат (в зависимости от того, было ли выброшено исключение). Это не закрывает соединение.
  • Курсоры в PyMySQL - это просто абстракция, реализованная в Python; в самом MySQL нет эквивалентной концепции. 1
  • У __enter__ метод __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__ метод соединения __del__ который принудительно закрывает соединение. Если вы уже вставили приведенный выше код в оболочку Python, то теперь вы можете смоделировать это, выполнив cursor = 'arbitrary value'; как только вы это сделаете, открытое вами соединение исчезнет из вывода SHOW PROCESSLIST.

Однако полагаться на это нелегко, и теоретически может произойти сбой в реализациях Python, отличных от CPython. В теории, более .close() было бы явное .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 и здесь не актуальны.

Ответ 4

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

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

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

Ответ 5

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