Kapan menutup kursor menggunakan MySQLdb

86

Saya sedang membangun aplikasi web WSGI dan saya memiliki database MySQL. Saya menggunakan MySQLdb, yang menyediakan kursor untuk menjalankan pernyataan dan mendapatkan hasil. Apa praktik standar untuk mendapatkan dan menutup kursor? Secara khusus, berapa lama kursor saya bertahan? Apakah saya harus mendapatkan kursor baru untuk setiap transaksi?

Saya yakin Anda perlu menutup kursor sebelum melakukan koneksi. Adakah keuntungan yang signifikan untuk menemukan rangkaian transaksi yang tidak memerlukan komitmen perantara sehingga Anda tidak perlu mendapatkan kursor baru untuk setiap transaksi? Apakah ada banyak biaya tambahan untuk mendapatkan kursor baru, atau itu bukan masalah besar?

jmilloy.dll
sumber

Jawaban:

80

Daripada menanyakan apa itu praktik standar, karena sering kali tidak jelas dan subjektif, Anda dapat mencoba melihat modul itu sendiri sebagai panduan. Secara umum, menggunakan withkata kunci seperti yang disarankan pengguna lain adalah ide bagus, tetapi dalam keadaan khusus ini mungkin tidak memberikan fungsionalitas yang Anda harapkan.

Pada versi 1.2.5 modul, MySQLdb.Connectionmengimplementasikan protokol manajer konteks dengan kode berikut ( 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()

Ada beberapa Tanya Jawab yang withsudah ada, atau Anda dapat membaca Memahami pernyataan "dengan" Python , tetapi pada dasarnya apa yang terjadi adalah yang __enter__mengeksekusi di awal withblok, dan __exit__mengeksekusi setelah meninggalkan withblok. Anda dapat menggunakan sintaks opsional with EXPR as VARuntuk mengikat objek yang dikembalikan oleh __enter__nama jika Anda bermaksud untuk mereferensikan objek itu nanti. Jadi, dengan penerapan di atas, berikut adalah cara sederhana untuk membuat kueri database Anda:

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

Pertanyaannya sekarang adalah, apa status koneksi dan kursor setelah keluar dari withblok? The __exit__Metode yang ditunjukkan di atas panggilan hanya self.rollback()atau self.commit(), dan tak satu pun dari metode tersebut pergi untuk memanggil close()metode. Kursor itu sendiri tidak memiliki __exit__metode yang ditentukan - dan tidak masalah jika demikian, karena withhanya mengelola koneksi. Oleh karena itu, koneksi dan kursor tetap terbuka setelah keluar dari withblokir. Ini mudah dikonfirmasi dengan menambahkan kode berikut ke contoh di atas:

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'

Anda akan melihat output "kursor terbuka; koneksi terbuka" dicetak ke stdout.

Saya yakin Anda perlu menutup kursor sebelum melakukan koneksi.

Mengapa? The MySQL C API , yang merupakan dasar untuk MySQLdb, tidak mengimplementasikan objek kursor, seperti yang tersirat dalam dokumentasi modul: "MySQL tidak mendukung kursor, namun kursor yang mudah ditiru." Memang, MySQLdb.cursors.BaseCursorkelas tersebut mewarisi langsung dari objectdan tidak memberlakukan pembatasan seperti itu pada kursor terkait dengan commit / rollback. Pengembang Oracle mengatakan ini :

cnx.commit () sebelum cur.close () terdengar paling logis bagi saya. Mungkin Anda dapat mengikuti aturan: "Tutup kursor jika Anda tidak membutuhkannya lagi." Jadi komit () sebelum menutup kursor. Pada akhirnya, untuk Connector / Python, itu tidak membuat banyak perbedaan, tetapi mungkin untuk database lain.

Saya berharap itu sedekat Anda dengan "praktik standar" tentang subjek ini.

Adakah keuntungan yang signifikan untuk menemukan rangkaian transaksi yang tidak memerlukan komitmen perantara sehingga Anda tidak perlu mendapatkan kursor baru untuk setiap transaksi?

Saya sangat meragukannya, dan saat mencoba melakukannya, Anda mungkin menyebabkan kesalahan manusia tambahan. Lebih baik memutuskan konvensi dan mematuhinya.

Apakah ada banyak biaya tambahan untuk mendapatkan kursor baru, atau itu bukan masalah besar?

Overhead dapat diabaikan, dan tidak menyentuh server database sama sekali; itu sepenuhnya dalam implementasi MySQLdb. Anda dapat melihat di BaseCursor.__init__github jika Anda benar-benar ingin tahu apa yang terjadi saat Anda membuat kursor baru.

Kembali ke awal ketika kita berdiskusi with, mungkin sekarang Anda dapat memahami mengapa MySQLdb.Connectionkelas __enter__dan __exit__metode memberi Anda objek kursor baru di setiap withblok dan tidak repot-repot melacak atau menutupnya di akhir blok. Ini cukup ringan dan hanya ada untuk kenyamanan Anda.

Jika benar-benar penting bagi Anda untuk mengatur mikro objek kursor, Anda dapat menggunakan contextlib.closing untuk menggantikan fakta bahwa objek kursor tidak memiliki __exit__metode yang ditentukan . Dalam hal ini, Anda juga dapat menggunakannya untuk memaksa objek koneksi menutup sendiri saat keluar dari withblok. Ini harus menghasilkan "my_curs ditutup; my_conn ditutup":

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'

Perhatikan bahwa with closing(arg_obj)tidak akan memanggil objek argumen __enter__dan __exit__metode; itu hanya akan memanggil metode objek argumen closedi akhir withblok. (Untuk melihat ini beraksi, cukup tentukan kelas Foodengan __enter__,, __exit__dan closemetode yang berisi printpernyataan sederhana , dan bandingkan apa yang terjadi ketika Anda melakukan with Foo(): passapa yang terjadi ketika Anda melakukannya with closing(Foo()): pass.) Ini memiliki dua implikasi signifikan:

Pertama, jika mode autocommit diaktifkan, MySQLdb akan BEGINmelakukan transaksi eksplisit di server saat Anda menggunakan with connectiondan melakukan atau mengembalikan transaksi di akhir blok. Ini adalah perilaku default MySQLdb, yang dimaksudkan untuk melindungi Anda dari perilaku default MySQL yang segera melakukan setiap dan semua pernyataan DML. MySQLdb mengasumsikan bahwa ketika Anda menggunakan manajer konteks, Anda menginginkan transaksi, dan menggunakan eksplisit BEGINuntuk melewati pengaturan autocommit di server. Jika Anda terbiasa menggunakan with connection, Anda mungkin mengira autocommit dinonaktifkan padahal sebenarnya hanya dilewati. Anda mungkin mendapatkan kejutan yang tidak menyenangkan jika menambahkanclosingke kode Anda dan kehilangan integritas transaksional; Anda tidak akan dapat membatalkan perubahan, Anda mungkin mulai melihat bug konkurensi dan alasannya mungkin tidak langsung diketahui.

Kedua, with closing(MySQLdb.connect(user, pass)) as VARmengikat objek koneksi ke VAR, berbeda dengan with MySQLdb.connect(user, pass) as VAR, yang mengikat objek kursor baru ke VAR. Dalam kasus terakhir, Anda tidak akan memiliki akses langsung ke objek koneksi! Sebagai gantinya, Anda harus menggunakan connectionatribut kursor , yang menyediakan akses proxy ke koneksi asli. Saat kursor ditutup, connectionatributnya disetel ke None. Ini menghasilkan koneksi yang ditinggalkan yang akan bertahan sampai salah satu hal berikut terjadi:

  • Semua referensi ke kursor dihapus
  • Kursor keluar dari ruang lingkup
  • Waktu koneksi habis
  • Koneksi ditutup secara manual melalui alat administrasi server

Anda dapat mengujinya dengan memantau koneksi terbuka (di Workbench atau dengan menggunakanSHOW PROCESSLIST ) saat menjalankan baris berikut satu per satu:

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
Udara
sumber
15
posting Anda paling lengkap, tetapi bahkan setelah membacanya kembali beberapa kali, saya menemukan diri saya masih bingung tentang menutup kursor. Dilihat dari banyak posting tentang masalah ini, tampaknya itu menjadi titik kebingungan yang umum. Pengambilan saya adalah bahwa kursor tampaknya TIDAK memerlukan .close () untuk dipanggil - pernah. Jadi, mengapa bahkan memiliki metode .close ()?
SMGreenfield
6
Jawaban singkatnya adalah itu cursor.close()adalah bagian dari Python DB API , yang tidak ditulis secara khusus dengan MySQL.
Air
1
Mengapa koneksi akan ditutup setelah del my_curs?
BAE
@ChengchengPei my_cursmemegang referensi terakhir ke connectionobjek tersebut. Setelah referensi tersebut tidak ada lagi, connectionobjek tersebut harus dikumpulkan sampahnya.
Air
Ini jawaban yang luar biasa, terima kasih. Penjelasan yang sangat baik dari withdan MySQLdb.Connection's __enter__dan __exit__fungsi. Sekali lagi, terima kasih @Air.
Eugene
33

Lebih baik menulis ulang menggunakan kata kunci 'dengan'. 'Dengan' akan menangani penutupan kursor (penting karena sumber daya tidak terkelola) secara otomatis. Manfaatnya adalah akan menutup kursor jika ada pengecualian juga.

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
sumber
Saya rasa withini bukan pilihan yang baik jika Anda ingin menggunakannya di Flask atau kerangka web lain. Jika situasinya demikian http://flask.pocoo.org/docs/patterns/sqlite3/#sqlite3maka akan ada masalah.
James King
@ james-king Saya tidak bekerja dengan Flask, tetapi dalam contoh Anda Flask akan menutup koneksi db itu sendiri. Sebenarnya dalam kode saya saya menggunakan saya menggunakan pendekatan-sedikit berbeda dengan untuk kursor dekat 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 Ya, Jika Anda menggunakannya dengan kursor maka semuanya akan baik-baik saja.
James King
7

Catatan: jawaban ini untuk PyMySQL , yang merupakan pengganti MySQLdb dan secara efektif merupakan versi terbaru MySQLdb karena MySQLdb tidak lagi dipertahankan. Saya percaya semua yang ada di sini juga berlaku untuk MySQLdb lama, tetapi belum diperiksa.

Pertama-tama, beberapa fakta:

  • withSintaks Python memanggil metode pengelola konteks __enter__sebelum mengeksekusi badan withblok, dan __exit__metode sesudahnya.
  • Koneksi memiliki __enter__metode yang tidak melakukan apa pun selain membuat dan mengembalikan kursor, dan __exit__metode yang melakukan atau memutar balik (tergantung pada apakah pengecualian dilemparkan). Itu tidak menutup koneksi.
  • Kursor di PyMySQL murni abstraksi yang diimplementasikan dengan Python; tidak ada konsep yang setara di MySQL itu sendiri. 1
  • Cursor memiliki __enter__metode yang tidak melakukan apa pun dan __exit__metode yang "menutup" kursor (yang berarti membatalkan referensi kursor ke koneksi induknya dan membuang semua data yang disimpan di kursor).
  • Kursor menyimpan referensi ke koneksi yang melahirkannya, tetapi koneksi tidak memiliki referensi ke kursor yang mereka buat.
  • Koneksi memiliki __del__metode untuk menutupnya
  • Sesuai https://docs.python.org/3/reference/datamodel.html , CPython (implementasi Python default) menggunakan penghitungan referensi dan secara otomatis menghapus sebuah objek setelah jumlah referensi ke sana mencapai nol.

Menyatukan hal-hal ini, kami melihat bahwa kode naif seperti ini secara teori bermasalah:

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

# ... happily carry on and do something unrelated

Masalahnya adalah tidak ada yang menutup koneksi. Memang, jika Anda menempelkan kode di atas ke shell Python dan kemudian menjalankannya SHOW FULL PROCESSLISTdi shell MySQL, Anda akan dapat melihat koneksi idle yang Anda buat. Karena jumlah koneksi default MySQL adalah 151 , yang tidak besar , Anda secara teoritis dapat mulai mengalami masalah jika Anda memiliki banyak proses yang menjaga koneksi ini tetap terbuka.

Namun, di CPython, ada anugrah yang memastikan bahwa kode seperti contoh saya di atas mungkin tidak akan menyebabkan Anda meninggalkan banyak koneksi terbuka. Yang menyelamatkan adalah bahwa segera setelah cursorkeluar dari ruang lingkup (misalnya fungsi di mana itu dibuat selesai, atau cursormendapatkan nilai lain yang ditetapkan untuk itu), jumlah referensinya mencapai nol, yang menyebabkannya dihapus, menghilangkan jumlah referensi koneksi ke nol, menyebabkan metode koneksi __del__dipanggil yang memaksa menutup koneksi. Jika Anda sudah menempelkan kode di atas ke dalam shell Python Anda, maka Anda sekarang dapat mensimulasikannya dengan menjalankan cursor = 'arbitrary value'; segera setelah Anda melakukan ini, koneksi yang Anda buka akan hilang dari SHOW PROCESSLISToutput.

Namun, mengandalkan ini tidak elegan, dan secara teoritis mungkin gagal dalam implementasi Python selain CPython. Cleaner, dalam teori, akan secara eksplisit .close()menghubungkan (untuk membebaskan koneksi pada database tanpa menunggu Python menghancurkan objek). Kode yang lebih kuat ini terlihat seperti ini:

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

Ini jelek, tetapi tidak bergantung pada Python yang merusak objek Anda untuk membebaskan (jumlah terbatas) koneksi database Anda.

Perhatikan bahwa menutup kursor , jika Anda sudah menutup koneksi secara eksplisit seperti ini, sama sekali tidak ada gunanya.

Terakhir, untuk menjawab pertanyaan sekunder di sini:

Apakah ada banyak biaya tambahan untuk mendapatkan kursor baru, atau itu bukan masalah besar?

Tidak, membuat kursor tidak mengenai MySQL sama sekali dan pada dasarnya tidak melakukan apa-apa .

Adakah keuntungan yang signifikan untuk menemukan rangkaian transaksi yang tidak memerlukan komitmen perantara sehingga Anda tidak perlu mendapatkan kursor baru untuk setiap transaksi?

Ini situasional dan sulit untuk diberikan jawaban umum. Seperti yang dikatakan https://dev.mysql.com/doc/refman/en/optimizing-innodb-transaction-management.html , "aplikasi mungkin mengalami masalah kinerja jika melakukan ribuan kali per detik, dan masalah kinerja yang berbeda jika itu hanya dilakukan setiap 2-3 jam " . Anda membayar overhead kinerja untuk setiap komit, tetapi dengan membiarkan transaksi terbuka lebih lama, Anda meningkatkan kemungkinan koneksi lain harus menghabiskan waktu menunggu kunci, meningkatkan risiko kebuntuan, dan berpotensi meningkatkan biaya beberapa pencarian yang dilakukan oleh koneksi lain .


1 MySQL memang memiliki konstruksi yang disebut kursor tetapi mereka hanya ada di dalam prosedur tersimpan; mereka sangat berbeda dengan kursor PyMySQL dan tidak relevan di sini.

Mark Amery
sumber
5

Saya pikir Anda akan lebih baik mencoba menggunakan satu kursor untuk semua eksekusi Anda, dan menutupnya di akhir kode Anda. Lebih mudah untuk bekerja dengannya, dan mungkin juga memiliki manfaat efisiensi (jangan mengutip saya untuk yang itu).

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

Intinya adalah Anda dapat menyimpan hasil eksekusi kursor di variabel lain, sehingga Anda dapat membebaskan kursor untuk melakukan eksekusi kedua. Anda mengalami masalah dengan cara ini hanya jika Anda menggunakan fetchone (), dan perlu melakukan eksekusi kursor kedua sebelum Anda mengulang semua hasil dari kueri pertama.

Jika tidak, saya akan mengatakan tutup kursor Anda segera setelah Anda selesai mendapatkan semua data dari mereka. Dengan begitu, Anda tidak perlu khawatir tentang menyelesaikan masalah nanti dalam kode Anda.

nct25
sumber
Terima kasih - Mempertimbangkan bahwa Anda harus menutup kursor untuk melakukan pembaruan / penyisipan, saya kira salah satu cara mudah untuk melakukannya untuk pembaruan / penyisipan adalah dengan mendapatkan satu kursor untuk setiap daemon, tutup kursor untuk berkomitmen dan segera mendapatkan kursor baru jadi Anda siap di lain waktu. Apakah itu masuk akal?
jmilloy
1
Hei, tidak masalah. Saya sebenarnya tidak tahu tentang melakukan pembaruan / penyisipan dengan menutup kursor Anda, tetapi pencarian cepat online menunjukkan ini: conn = MySQLdb.connect (arguments_go_here) cursor = MySQLdb.cursor () cursor.execute (mysql_insert_statement_here) coba: conn. commit () kecuali: conn.rollback () # batalkan perubahan yang dibuat jika terjadi kesalahan. Dengan cara ini, database itu sendiri melakukan perubahan, dan Anda tidak perlu khawatir tentang kursor itu sendiri. Kemudian Anda hanya dapat membuka 1 kursor setiap saat. Silahkan lihat di sini: tutorialspoint.com/python/python_database_access.htm
nct25
Ya, jika berhasil maka saya salah dan ada beberapa alasan lain yang menyebabkan saya berpikir saya harus menutup kursor untuk melakukan koneksi.
jmilloy
Ya, saya tidak tahu, tautan yang saya posting itu membuat saya berpikir itu berfungsi. Saya kira sedikit lebih banyak penelitian akan memberi tahu Anda apakah itu pasti berhasil atau tidak, tetapi saya pikir Anda mungkin bisa melakukannya begitu saja. Semoga saya bisa membantu Anda!
nct25
kursor tidak aman untuk thread, jika Anda menggunakan kursor yang sama di antara banyak thread berbeda, dan semuanya meminta dari db, fetchall () akan memberikan data acak.
ospider
-6

Saya menyarankan untuk melakukannya seperti php dan mysql. Mulai i di awal kode Anda sebelum mencetak data pertama. Jadi jika Anda mendapatkan kesalahan koneksi, Anda dapat menampilkan 50xpesan kesalahan (Tidak ingat apa itu kesalahan internal). Dan tetap buka untuk seluruh sesi dan tutup saat Anda tahu Anda tidak akan membutuhkannya lagi.

KilledKenny
sumber
Di MySQLdb, ada perbedaan antara koneksi dan kursor. Saya terhubung sekali per permintaan (untuk saat ini) dan dapat mendeteksi kesalahan koneksi lebih awal. Tapi bagaimana dengan kursor?
jmilloy
IMHO itu bukan nasihat yang akurat. Itu tergantung. Jika kode Anda akan menjaga koneksi untuk waktu yang lama (misalnya, ia mengambil beberapa data dari DB dan kemudian selama 1-5-10 menit itu melakukan sesuatu di server dan menjaga koneksi) dan itu aplikasi multy thread itu akan membuat masalah segera (Anda akan melebihi koneksi maksimum yang diizinkan).
Roman Podlinov