Kiedy zamykać kursory za pomocą MySQLdb

86

Buduję aplikację internetową WSGI i mam bazę danych MySQL. Używam MySQLdb, która zapewnia kursory do wykonywania instrukcji i uzyskiwania wyników. Jaka jest standardowa praktyka pobierania i zamykania kursorów? W szczególności, jak długo powinny działać moje kursory? Czy powinienem otrzymać nowy kursor dla każdej transakcji?

Uważam, że przed wykonaniem połączenia należy zamknąć kursor. Czy jest jakaś istotna zaleta znajdowania zestawów transakcji, które nie wymagają pośrednich zatwierdzeń, aby nie trzeba było pobierać nowych kursorów dla każdej transakcji? Czy zdobycie nowych kursorów wiąże się z dużymi kosztami, czy to po prostu nic wielkiego?

jmilloy
źródło

Odpowiedzi:

80

Zamiast pytać, jaka jest standardowa praktyka, ponieważ jest to często niejasne i subiektywne, możesz spróbować poszukać wskazówek w samym module. Ogólnie rzecz biorąc, użycie withsłowa kluczowego w sposób sugerowany przez innego użytkownika jest świetnym pomysłem, ale w tych konkretnych okolicznościach może nie zapewnić oczekiwanej funkcjonalności.

Od wersji 1.2.5 modułu MySQLdb.Connectionimplementuje protokół zarządzania kontekstami z następującym kodem ( 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()

Istnieje już kilka pytań i odpowiedzi na ten temat withlub możesz przeczytać instrukcję Understanding Python "with" , ale zasadniczo dzieje się tak, że jest __enter__wykonywana na początku withbloku i __exit__wykonywana po opuszczeniu withbloku. Możesz użyć opcjonalnej składni, with EXPR as VARaby powiązać obiekt zwracany przez __enter__z nazwą, jeśli zamierzasz później odwoływać się do tego obiektu. Tak więc, biorąc pod uwagę powyższą implementację, oto prosty sposób przeszukiwania bazy danych:

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

Teraz pytanie brzmi, jakie są stany połączenia i kursora po wyjściu z withbloku? __exit__Sposób pokazany powyżej połączeń tylko self.rollback()czy self.commit(), a żadna z tych metod przejść do wywołania close()metody. Sam kursor nie ma __exit__zdefiniowanej metody - i nie miałoby znaczenia, gdyby tak było, ponieważ withzarządza tylko połączeniem. Dlatego zarówno połączenie, jak i kursor pozostają otwarte po wyjściu z withbloku. Można to łatwo potwierdzić, dodając następujący kod do powyższego przykładu:

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'

Powinieneś zobaczyć wyjście "kursor jest otwarty; połączenie jest otwarte" wypisane na standardowe wyjście.

Uważam, że przed wykonaniem połączenia należy zamknąć kursor.

Czemu? Interfejs MySQL C API , który jest podstawą MySQLdb, nie implementuje żadnego obiektu kursora, jak wynika z dokumentacji modułu: „MySQL nie obsługuje kursorów, jednak kursory są łatwo emulowane”. W rzeczywistości MySQLdb.cursors.BaseCursorklasa dziedziczy bezpośrednio po objectkursorach i nie nakłada na nie takich ograniczeń w odniesieniu do zatwierdzenia / wycofania. Programista Oracle tak powiedział :

cnx.commit () przed cur.close () brzmi dla mnie najbardziej logicznie. Może możesz skorzystać z reguły: „Zamknij kursor, jeśli już go nie potrzebujesz”. Tak więc commit () przed zamknięciem kursora. Ostatecznie w przypadku Connector / Pythona nie ma to większego znaczenia, ale w przypadku innych baz danych może.

Spodziewam się, że jest to tak blisko, jak zbliżasz się do „standardowej praktyki” w tym temacie.

Czy jest jakaś istotna zaleta znajdowania zestawów transakcji, które nie wymagają pośrednich zatwierdzeń, aby nie trzeba było pobierać nowych kursorów dla każdej transakcji?

Bardzo w to wątpię, a próbując to zrobić, możesz wprowadzić dodatkowy błąd ludzki. Lepiej zdecydować się na konwencję i się jej trzymać.

Czy zdobycie nowych kursorów wiąże się z dużymi kosztami, czy to po prostu nic wielkiego?

Narzut jest pomijalny iw ogóle nie dotyka serwera bazy danych; jest całkowicie w ramach implementacji MySQLdb. Możesz spojrzeć BaseCursor.__init__na github, jeśli naprawdę chcesz wiedzieć, co się dzieje, gdy tworzysz nowy kursor.

Wracając do wcześniejszego okresu, kiedy omawialiśmy with, być może teraz możesz zrozumieć, dlaczego MySQLdb.Connectionklasa __enter__i __exit__metody dają ci zupełnie nowy obiekt kursora w każdym withbloku i nie zawracają sobie głowy śledzeniem go lub zamykaniem go na końcu bloku. Jest dość lekki i istnieje wyłącznie dla Twojej wygody.

Jeśli naprawdę ważne jest dla ciebie mikrozarządzanie obiektem kursora, możesz użyć contextlib.closing, aby zrekompensować fakt, że obiekt kursora nie ma zdefiniowanej __exit__metody. W tym przypadku można go również użyć do wymuszenia zamknięcia obiektu połączenia po wyjściu z withbloku. Powinno to spowodować wyświetlenie komunikatu „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'

Zauważ, że with closing(arg_obj)nie wywoła argumentów obiektów __enter__i __exit__metod; wywoła metodę obiektu argumentu tylkoclose na końcu withbloku. (Aby zobaczyć w akcji, wystarczy zdefiniować klasę Fooz __enter__, __exit__i closemetody zawierający proste printwypowiedzi i porównać to, co się dzieje, kiedy zrobić with Foo(): pass, co się dzieje, kiedy zrobić with closing(Foo()): pass). Ma to dwie poważne konsekwencje:

Po pierwsze, jeśli tryb automatycznego zatwierdzania jest włączony, MySQLdb wykona BEGINjawną transakcję na serwerze, gdy użyjesz with connectioni zatwierdzisz lub wycofasz transakcję na końcu bloku. Są to domyślne zachowania MySQLdb, mające na celu ochronę użytkownika przed domyślnym zachowaniem MySQL polegającym na natychmiastowym zatwierdzaniu wszystkich instrukcji DML. MySQLdb zakłada, że ​​kiedy używasz menedżera kontekstu, chcesz transakcji i używa jawnego, BEGINaby ominąć ustawienie automatycznego zatwierdzania na serwerze. Jeśli jesteś przyzwyczajony do używania with connection, możesz pomyśleć, że automatyczne zatwierdzanie jest wyłączone, podczas gdy w rzeczywistości było tylko pomijane. Jeśli dodasz, możesz spotkać się z nieprzyjemną niespodziankąclosingdo swojego kodu i utracić integralność transakcyjną; nie będziesz w stanie cofnąć zmian, możesz zacząć widzieć błędy współbieżności i może nie być od razu oczywiste, dlaczego.

Po drugie, with closing(MySQLdb.connect(user, pass)) as VARwiąże obiekt połączenia do VAR, w przeciwieństwie do with MySQLdb.connect(user, pass) as VAR, który wiąże nowy obiekt kursora do VAR. W tym drugim przypadku nie miałbyś bezpośredniego dostępu do obiektu połączenia! Zamiast tego musiałbyś użyć connectionatrybutu kursora , który zapewnia dostęp proxy do oryginalnego połączenia. Kiedy kursor jest zamknięty, jego connectionatrybut jest ustawiony na None. Powoduje to porzucone połączenie, które będzie się utrzymywać, dopóki nie nastąpi jedna z następujących sytuacji:

  • Wszystkie odniesienia do kursora zostaną usunięte
  • Kursor wychodzi poza zasięg
  • Limit czasu połączenia
  • Połączenie jest zamykane ręcznie za pomocą narzędzi administracyjnych serwera

Możesz to sprawdzić, monitorując otwarte połączenia (w programie Workbench lub używającSHOW PROCESSLIST ) podczas wykonywania następujących wierszy jeden po drugim:

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
Powietrze
źródło
14
Twój post był najbardziej wyczerpujący, ale nawet po kilkukrotnym przeczytaniu go nadal jestem zdziwiony zamykaniem kursorów. Sądząc po licznych postach na ten temat, wydaje się, że jest to powszechny problem. Moim zdaniem jest to, że kursory pozornie NIE wymagają wywołania .close () - nigdy. Więc po co w ogóle mieć metodę .close ()?
SMGreenfield
6
Krótka odpowiedź jest taka, że cursor.close()jest to część API Python DB , która nie została napisana specjalnie z myślą o MySQL.
Air
1
Dlaczego połączenie zostanie zamknięte po usunięciu my_curs?
BAE
@ChengchengPei my_cursprzechowuje ostatnie odniesienie do connectionobiektu. Gdy to odwołanie już nie istnieje, connectionobiekt powinien zostać wyrzucony do pamięci.
Air
To fantastyczna odpowiedź, dzięki. Doskonałe wyjaśnienie withi MySQLdb.Connection„S __enter__i __exit__funkcji. Jeszcze raz dziękuję @Air.
Eugene
33

Lepiej jest przepisać go za pomocą słowa kluczowego „with”. „Z” automatycznie zamknie kursor (jest to ważne, ponieważ jest to niezarządzany zasób). Zaletą jest to, że zamknie kursor również w przypadku wyjątku.

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
źródło
Nie sądzę, aby withbyła to dobra opcja, jeśli chcesz jej używać w Flask lub innym frameworku internetowym. Jeśli http://flask.pocoo.org/docs/patterns/sqlite3/#sqlite3tak się stanie, pojawią się problemy.
James King
@ james-king Nie pracowałem z Flaskiem, ale w twoim przykładzie Flask sam zamknie połączenie db. Właściwie w moim kodzie używam nieco innego podejścia - używam z kursorami bliskimi 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 Tak, jeśli użyjesz go z kursorem, wszystko będzie dobrze.
James King
7

Uwaga: ta odpowiedź dotyczy PyMySQL , który jest bezpośrednim zamiennikiem MySQLdb i faktycznie najnowszą wersją MySQLdb, ponieważ MySQLdb przestał być utrzymywany. Uważam, że wszystko tutaj dotyczy również starszej wersji MySQLdb, ale nie zostało to sprawdzone.

Przede wszystkim kilka faktów:

  • withSkładnia Pythona wywołuje __enter__metodę menedżera kontekstu przed wykonaniem treści withbloku, a następnie jego __exit__metody.
  • Połączenia mają __enter__metodę, która nie robi nic poza tworzeniem i zwracaniem kursora oraz __exit__metodę, która albo zatwierdza, albo wycofuje (w zależności od tego, czy został zgłoszony wyjątek). To nie zamknąć połączenie.
  • Kursory w PyMySQL są czystą abstrakcją zaimplementowaną w Pythonie; w samym MySQL nie ma równoważnej koncepcji. 1
  • Kursory mają __enter__metodę, która nic nie robi i __exit__metodę, która „zamyka” kursor (co oznacza po prostu zerowanie odniesienia kursora do jego połączenia nadrzędnego i wyrzucenie wszelkich danych przechowywanych na kursorze).
  • Kursory zawierają odniesienie do połączenia, które je stworzyło, ale połączenia nie zawierają odniesienia do kursorów, które utworzyły.
  • Połączenia mają __del__metodę, która je zamyka
  • Według https://docs.python.org/3/reference/datamodel.html , CPython (domyślna implementacja Pythona) używa liczenia odwołań i automatycznie usuwa obiekt, gdy liczba odwołań do niego osiągnie zero.

Łącząc te rzeczy razem, widzimy, że taki naiwny kod jest w teorii problematyczny:

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

# ... happily carry on and do something unrelated

Problem w tym, że nic nie zamknęło połączenia. Rzeczywiście, jeśli wkleisz powyższy kod do powłoki Pythona, a następnie uruchomisz SHOW FULL PROCESSLISTw powłoce MySQL, będziesz mógł zobaczyć bezczynne połączenie, które utworzyłeś. Ponieważ domyślna liczba połączeń MySQL to 151 , co nie jest duże , teoretycznie możesz zacząć napotykać problemy, jeśli masz wiele procesów, które utrzymują te połączenia otwarte.

Jednak w CPythonie istnieje możliwość oszczędzania, która zapewnia, że ​​kod taki jak w powyższym przykładzie prawdopodobnie nie spowoduje pozostawienia wielu otwartych połączeń. Ta oszczędność polega na tym, że gdy tylko cursorwyjdzie poza zakres (np. Funkcja, w której została utworzona, kończy się lub cursorotrzymuje inną przypisaną do niej wartość), jej liczba referencji osiąga zero, co powoduje jej usunięcie, upuszczenie liczby referencji połączenia do zera, powodując wywołanie metody połączenia, __del__która wymusza zamknięcie połączenia. Jeśli już wkleiłeś powyższy kod do powłoki Pythona, możesz to teraz zasymulować, uruchamiając cursor = 'arbitrary value'; gdy tylko to zrobisz, otwarte połączenie zniknie z SHOW PROCESSLISTwyjścia.

Jednak poleganie na tym jest nieeleganckie i teoretycznie może zawieść w implementacjach Pythona innych niż CPython. Teoretycznie czystszym rozwiązaniem byłoby jawne otwarcie .close()połączenia (zwolnienie połączenia w bazie danych bez czekania, aż Python zniszczy obiekt). Ten bardziej niezawodny kod wygląda następująco:

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

To jest brzydkie, ale nie polega na tym, że Python niszczy twoje obiekty, aby zwolnić (skończoną liczbę) połączeń z bazą danych.

Zauważ, że zamknięcie kursora , jeśli już zamykasz połączenie w ten sposób, jest całkowicie bezcelowe.

Na koniec, aby odpowiedzieć na pytania drugorzędne:

Czy zdobycie nowych kursorów wiąże się z dużymi kosztami, czy to po prostu nic wielkiego?

Nie, utworzenie instancji kursora w ogóle nie uderza w MySQL i zasadniczo nic nie robi .

Czy jest jakaś istotna zaleta znajdowania zestawów transakcji, które nie wymagają pośrednich zatwierdzeń, aby nie trzeba było pobierać nowych kursorów dla każdej transakcji?

Jest to sytuacja sytuacyjna i trudna do udzielenia ogólnej odpowiedzi. Jak to ujął https://dev.mysql.com/doc/refman/en/optimizing-innodb-transaction-management.html , „aplikacja może napotkać problemy z wydajnością, jeśli wykonuje tysiące razy na sekundę, i inne problemy z wydajnością, jeśli zatwierdza się tylko co 2-3 godziny ” . Płacisz narzut wydajności za każde zatwierdzenie, ale pozostawiając transakcje otwarte na dłużej, zwiększasz szansę, że inne połączenia będą musiały spędzać czas na czekaniu na blokady, zwiększasz ryzyko zakleszczenia i potencjalnie zwiększasz koszt niektórych wyszukiwań wykonywanych przez inne połączenia .


1 MySQL nie posiadają konstrukcję nie wywołuje kursor ale występują tylko wewnątrz procedur przechowywanych; są zupełnie inne niż kursory PyMySQL i nie mają tutaj znaczenia.

Mark Amery
źródło
5

Myślę, że lepiej będzie, jeśli spróbujesz użyć jednego kursora do wszystkich swoich wykonań i zamknąć go na końcu kodu. Łatwiej się z nim pracuje i może również przynieść korzyści w zakresie wydajności (nie cytuj mnie na ten temat).

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

Chodzi o to, że możesz przechowywać wyniki wykonania kursora w innej zmiennej, zwalniając w ten sposób kursor do wykonania drugiego wykonania. W ten sposób napotkasz problemy tylko wtedy, gdy używasz funkcji fetchone () i musisz wykonać drugie wykonanie kursora, zanim przejdziesz przez wszystkie wyniki pierwszego zapytania.

W przeciwnym razie powiedziałbym, że po prostu zamknij kursory, gdy tylko skończysz pobierać z nich wszystkie dane. W ten sposób nie musisz się martwić o zawiązywanie luźnych końców w późniejszym kodzie.

nct25
źródło
Dzięki - biorąc pod uwagę, że musisz zamknąć kursor, aby zatwierdzić aktualizację / wstawienie, myślę, że jednym prostym sposobem na zrobienie tego w przypadku aktualizacji / wstawień byłoby uzyskanie jednego kursora dla każdego demona, zamknięcie kursora, aby zatwierdzić i natychmiastowe uzyskanie nowego kursora więc jesteś gotowy następnym razem. Czy to brzmi rozsądnie?
jmilloy
1
Hej, nie ma problemu. Właściwie nie wiedziałem o zatwierdzaniu aktualizacji / wstawiania przez zamknięcie kursorów, ale szybkie wyszukiwanie online pokazuje to: conn = MySQLdb.connect (arguments_go_here) kursor = MySQLdb.cursor () kursor.execute (mysql_insert_statement_here) try: conn. commit () z wyjątkiem: conn.rollback () # cofa zmiany wprowadzone w przypadku wystąpienia błędu. W ten sposób baza danych sama zatwierdza zmiany, a Ty nie musisz się martwić o same kursory. Wtedy możesz mieć zawsze otwarty 1 kursor. Zajrzyj tutaj: tutorialspoint.com/python/python_database_access.htm
nct25
Tak, jeśli to zadziała, to po prostu się mylę i był jakiś inny powód, który spowodował, że pomyślałem, że muszę zamknąć kursor, aby zatwierdzić połączenie.
jmilloy
Tak, nie wiem, ten link, który zamieściłem, sprawia, że ​​myślę, że to działa. Wydaje mi się, że trochę więcej badań powiedziałoby ci, czy to zdecydowanie działa, czy nie, ale myślę, że prawdopodobnie możesz po prostu to zrobić. Mam nadzieję, że ci pomogłem!
nct25
kursor nie jest bezpieczny dla wątków, jeśli używasz tego samego kursora w wielu różnych wątkach i wszystkie one odpytują z db, funkcja fetchall () poda losowe dane.
ospider
-6

Proponuję zrobić to jak php i mysql. Rozpocznij i na początku swojego kodu przed wydrukowaniem pierwszych danych. Więc jeśli pojawi się błąd połączenia, możesz wyświetlić komunikat o błędzie 50x(Nie pamiętam, co to jest błąd wewnętrzny). I pozostaw ją otwartą przez całą sesję i zamknij, gdy wiesz, że nie będzie już potrzebna.

KilledKenny
źródło
W MySQLdb istnieje różnica między połączeniem a kursorem. Łączę się raz na żądanie (na razie) i mogę wcześnie wykryć błędy połączenia. A co z kursorami?
jmilloy,
IMHO to nie jest dokładna rada. To zależy. Jeśli twój kod będzie utrzymywał połączenie przez długi czas (np. Pobiera trochę danych z DB, a następnie przez 1-5-10 minut robi coś na serwerze i utrzymuje połączenie) i jest to błędna aplikacja wątkowa, wkrótce spowoduje problem ( przekroczy maksymalną dozwoloną liczbę połączeń).
Roman Podlinov