Коли закривати курсори за допомогою MySQLdb


86

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

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

Відповіді:


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, або ви можете прочитати Пояснення оператора Python "with" , але, по суті, відбувається те, що __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'

Ви повинні побачити вивід "курсор відкритий; з'єднання відкрито", надрукований на stdout.

Я вважаю, що вам потрібно закрити курсор перед встановленням з'єднання.

Чому? 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

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 сам закриє з'єднання db. На насправді в моєму коді я використовую трохи інший приступає використовувати I з для близьких курсорів 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()
Роман Podlinov

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

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


Дякую - Враховуючи, що вам потрібно закрити курсор, щоб здійснити оновлення / вставку, я думаю, одним простим способом зробити це для оновлення / вставки буде отримання одного курсора для кожного демона, закриття курсору для фіксації та негайне отримання нового курсора так що готовий наступного разу. Це звучить розумно?
jmilloy

1
Гей, не біда. Я насправді не знав про те, щоб здійснити оновлення / вставку, закривши курсори, але швидкий пошук в Інтернеті показує це: conn = MySQLdb.connect (argument_go_here) cursor = MySQLdb.cursor () cursor.execute (mysql_insert_statement_here) try: conn. commit (), крім: conn.rollback () # скасувати зміни, внесені у разі виникнення помилки. Таким чином, сама база даних здійснює зміни, і вам не доведеться турбуватися про самі курсори. Тоді ви можете просто постійно відкривати 1 курсор. Подивіться тут: tutorialspoint.com/python/python_database_access.htm
nct25

Так, якщо це працює, то я просто помиляюся, і була якась інша причина, яка змусила мене думати, що мені потрібно було закрити курсор, щоб зафіксувати з'єднання.
jmilloy

Так, я не знаю, це посилання, яке я розмістив, змушує мене думати, що це працює. Думаю, трохи більше досліджень підкажуть вам, безумовно, це працює чи ні, але я думаю, ви могли б, мабуть, просто піти з цим. Сподіваюся, я вам допомогла!
nct25,

cursor не є безпечним для потоків, якщо ви використовуєте один і той же курсор серед безлічі різних потоків, і всі вони здійснюють запити з db, fetchall () дасть випадкові дані.
ospider

-6

Я пропоную робити це як php та mysql. Почніть i на початку коду перед друком перших даних. Отже, якщо ви отримуєте помилку підключення, ви можете відобразити повідомлення про помилку 50x(Не пам’ятайте, що таке внутрішня помилка). І тримайте його відкритим протягом усього сеансу та закривайте, коли знаєте, що він вам більше не потрібен.


У MySQLdb існує різниця між з'єднанням і курсором. Я підключаюся один раз за запитом (наразі) і можу виявити помилки підключення на ранній стадії. Але як щодо курсорів?
jmilloy

ІМХО це не точна порада. Це залежить. Якщо ваш код буде тримати зв’язок протягом тривалого часу (наприклад, він бере деякі дані з БД, а потім протягом 1-5-10 хвилин робить щось на сервері і зберігає зв’язок), і це додаток із кількома потоками, це досить швидко створить проблему (ви перевищить максимально допустимі з'єднання).
Роман Подлінов
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.