Khi nào đóng con trỏ bằng MySQLdb


86

Tôi đang xây dựng một ứng dụng web WSGI và tôi có một cơ sở dữ liệu MySQL. Tôi đang sử dụng MySQLdb, cung cấp con trỏ để thực thi các câu lệnh và nhận kết quả. Thực hành tiêu chuẩn để lấy và đóng con trỏ là gì? Đặc biệt, con trỏ của tôi sẽ tồn tại trong bao lâu? Tôi có nên nhận con trỏ mới cho mỗi giao dịch không?

Tôi tin rằng bạn cần phải đóng con trỏ trước khi thực hiện kết nối. Có lợi thế đáng kể nào khi tìm các tập hợp giao dịch không yêu cầu cam kết trung gian để bạn không phải nhận con trỏ mới cho mỗi giao dịch không? Có rất nhiều chi phí để có được con trỏ mới hay đó không phải là vấn đề lớn?

Câu trả lời:


80

Thay vì hỏi thực hành tiêu chuẩn là gì, vì điều đó thường không rõ ràng và chủ quan, bạn có thể thử tìm đến chính mô-đun để được hướng dẫn. Nói chung, sử dụng withtừ khóa như một người dùng khác đề xuất là một ý tưởng tuyệt vời, nhưng trong trường hợp cụ thể này, nó có thể không cung cấp cho bạn đầy đủ chức năng như mong đợi.

Kể từ phiên bản 1.2.5 của mô-đun, MySQLdb.Connectiontriển khai giao thức trình quản lý ngữ cảnh với mã sau ( 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()

Có một số câu hỏi và giải đáp hiện có về withđã có, hoặc bạn có thể đọc Hiểu câu lệnh "with" của Python , nhưng về cơ bản những gì xảy ra là __enter__thực thi ở đầu withkhối và __exit__thực thi khi rời khỏi withkhối. Bạn có thể sử dụng cú pháp tùy chọn with EXPR as VARđể liên kết đối tượng được trả về __enter__với một tên nếu bạn định tham chiếu đến đối tượng đó sau này. Vì vậy, với cách triển khai ở trên, đây là một cách đơn giản để truy vấn cơ sở dữ liệu của bạn:

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

Câu hỏi bây giờ là trạng thái của kết nối và con trỏ sau khi thoát ra khỏi withkhối là gì? Các __exit__phương pháp thể hiện cuộc gọi chỉ trên self.rollback()hoặc self.commit(), và không ai trong số những phương pháp tiếp tục gọi close()phương pháp. Bản thân con trỏ không có __exit__phương thức nào được xác định - và sẽ không thành vấn đề nếu nó có làm như vậy, vì withchỉ quản lý kết nối. Do đó, cả kết nối và con trỏ vẫn mở sau khi thoát khỏi withkhối. Điều này dễ dàng được xác nhận bằng cách thêm mã sau vào ví dụ trên:

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'

Bạn sẽ thấy đầu ra "con trỏ đang mở; kết nối đang mở" được in ra stdout.

Tôi tin rằng bạn cần phải đóng con trỏ trước khi thực hiện kết nối.

Tại sao? Các MySQL C API , đó là cơ sở cho MySQLdb, không thực hiện bất kỳ đối tượng con trỏ, như ngụ ý trong tài liệu mô-đun: "MySQL không hỗ trợ con trỏ, tuy nhiên, con trỏ có thể dễ dàng mô phỏng." Thật vậy, MySQLdb.cursors.BaseCursorlớp kế thừa trực tiếp từ objectvà không áp đặt hạn chế như vậy đối với các con trỏ liên quan đến cam kết / khôi phục. Một nhà phát triển Oracle đã nói điều này :

cnx.commit () trước cur.close () nghe hợp lý nhất với tôi. Có thể bạn có thể thực hiện theo quy tắc: "Đóng con trỏ nếu bạn không cần nó nữa." Do đó, commit () trước khi đóng con trỏ. Cuối cùng, đối với Connector / Python, nó không tạo ra nhiều khác biệt, nhưng hoặc các cơ sở dữ liệu khác thì có thể.

Tôi hy vọng điều đó gần như là bạn sẽ đạt được "thực hành tiêu chuẩn" về chủ đề này.

Có lợi thế đáng kể nào khi tìm các tập hợp giao dịch không yêu cầu cam kết trung gian để bạn không phải nhận con trỏ mới cho mỗi giao dịch không?

Tôi rất nghi ngờ điều đó, và khi cố gắng làm như vậy, bạn có thể mắc thêm lỗi của con người. Tốt hơn nên quyết định một quy ước và gắn bó với nó.

Có rất nhiều chi phí để có được con trỏ mới hay đó không phải là vấn đề lớn?

Chi phí là không đáng kể và hoàn toàn không chạm vào máy chủ cơ sở dữ liệu; nó hoàn toàn nằm trong việc triển khai MySQLdb. Bạn có thể xem BaseCursor.__init__trên github nếu thực sự tò mò muốn biết điều gì đang xảy ra khi bạn tạo một con trỏ mới.

Quay trở lại trước đó khi chúng ta thảo luận with, có lẽ bây giờ bạn có thể hiểu tại sao MySQLdb.Connectionlớp __enter____exit__các phương thức cung cấp cho bạn một đối tượng con trỏ hoàn toàn mới trong mỗi withkhối và không bận tâm theo dõi nó hoặc đóng nó ở cuối khối. Nó khá nhẹ và tồn tại hoàn toàn để thuận tiện cho bạn.

Nếu việc quản lý vi mô đối tượng con trỏ thực sự quan trọng đối với bạn, bạn có thể sử dụng contextlib.closing để bù đắp cho thực tế là đối tượng con trỏ không có __exit__phương thức xác định . Đối với vấn đề đó, bạn cũng có thể sử dụng nó để buộc đối tượng kết nối tự đóng khi thoát khỏi một withkhối. Điều này sẽ xuất ra "my_curs is close; my_conn is close":

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'

Lưu ý rằng with closing(arg_obj)sẽ không gọi đối tượng __enter____exit__phương thức của đối số ; nó sẽ chỉ gọi closephương thức của đối tượng đối số ở cuối withkhối. (Để xem điều này trong thực tế, chỉ cần xác định một lớp Foovới __enter__, __exit__closecác phương thức chứa các printcâu lệnh đơn giản và so sánh những gì xảy ra khi bạn làm with Foo(): passvới những gì sẽ xảy ra khi bạn làm with closing(Foo()): pass.)

Đầu tiên, nếu chế độ gửi tự động được bật, MySQLdb sẽ BEGINlà một giao dịch rõ ràng trên máy chủ khi bạn sử dụng with connectionvà cam kết hoặc khôi phục giao dịch ở cuối khối. Đây là các hành vi mặc định của MySQLdb, nhằm bảo vệ bạn khỏi hành vi mặc định của MySQL là thực hiện ngay lập tức bất kỳ và tất cả các câu lệnh DML. MySQLdb giả định rằng khi bạn sử dụng trình quản lý ngữ cảnh, bạn muốn một giao dịch và sử dụng rõ ràng BEGINđể bỏ qua cài đặt tự động gửi trên máy chủ. Nếu bạn đã quen sử dụng with connection, bạn có thể nghĩ rằng tính năng tự động gửi đã bị vô hiệu hóa trong khi thực sự nó chỉ bị bỏ qua. Bạn có thể nhận được một bất ngờ khó chịu nếu bạn thêmclosingmã của bạn và mất tính toàn vẹn trong giao dịch; bạn sẽ không thể khôi phục các thay đổi, bạn có thể bắt đầu thấy các lỗi đồng thời và có thể không rõ ràng ngay lập tức tại sao.

Thứ hai, with closing(MySQLdb.connect(user, pass)) as VARliên kết với các đối tượng kết nối tới VAR, trái ngược với with MySQLdb.connect(user, pass) as VAR, mà liên kết với một đối tượng mới của con trỏ tới VAR. Trong trường hợp sau, bạn sẽ không có quyền truy cập trực tiếp vào đối tượng kết nối! Thay vào đó, bạn sẽ phải sử dụng connectionthuộc tính con trỏ, thuộc tính này cung cấp quyền truy cập proxy vào kết nối ban đầu. Khi đóng con trỏ, connectionthuộc tính của nó được đặt thành None. Điều này dẫn đến một kết nối bị bỏ rơi sẽ tồn tại cho đến khi một trong những điều sau xảy ra:

  • Tất cả các tham chiếu đến con trỏ đều bị xóa
  • Con trỏ đi ra ngoài phạm vi
  • Hết thời gian kết nối
  • Kết nối được đóng theo cách thủ công thông qua các công cụ quản trị máy chủ

Bạn có thể kiểm tra điều này bằng cách theo dõi các kết nối đang mở (trong Workbench hoặc bằng cách sử dụngSHOW PROCESSLIST ) trong khi thực hiện từng dòng sau:

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
bài viết của bạn là đầy đủ nhất, nhưng ngay cả sau khi đọc lại nó một vài lần, tôi thấy mình vẫn còn phân vân về việc đóng con trỏ. Đánh giá từ rất nhiều bài đăng về chủ đề này, có vẻ như đó là một điểm chung của sự nhầm lẫn. Điểm rút ra của tôi là các con trỏ dường như KHÔNG yêu cầu .close () được gọi - luôn luôn. Vậy tại sao lại có cả phương thức .close ()?
SMGreenfield

6
Câu trả lời ngắn gọn là đó cursor.close()là một phần của Python DB API , không được viết riêng cho MySQL.
Air

1
Tại sao kết nối sẽ đóng sau khi xóa my_curs?
BAE

@ChengchengPei my_cursgiữ tham chiếu cuối cùng đến connectionđối tượng. Khi tham chiếu đó không còn tồn tại, connectionđối tượng sẽ được thu thập.
Air

Đây là một câu trả lời tuyệt vời, cảm ơn. Giải thích tuyệt vời về withMySQLdb.Connection's __enter__và các __exit__chức năng. Một lần nữa, cảm ơn bạn @Air.
Eugene

33

Tốt hơn là viết lại nó bằng từ khóa 'with'. 'With' sẽ quan tâm đến việc đóng con trỏ (điều quan trọng vì nó là tài nguyên không được quản lý) tự động. Lợi ích là nó cũng sẽ đóng con trỏ trong trường hợp ngoại lệ.

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

Tôi không nghĩ đây withlà một lựa chọn tốt nếu bạn muốn sử dụng nó trong Flask hoặc các khuôn khổ web khác. Nếu tình hình là như http://flask.pocoo.org/docs/patterns/sqlite3/#sqlite3vậy thì sẽ có vấn đề.
James King

@ james-king Tôi không làm việc với Flask, nhưng trong ví dụ của bạn, Flask sẽ tự đóng kết nối db. Trên thực tế trong mã của tôi, tôi sử dụng hơi khác nhau tôi sử dụng approach- với cho con trỏ gần 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()
La Mã Podlinov

@RomanPodlinov Vâng, Nếu bạn sử dụng nó với con trỏ thì mọi thứ sẽ ổn.
James King,

7

Lưu ý: câu trả lời này dành cho PyMySQL , đây là bản thay thế cho MySQLdb và là phiên bản mới nhất của MySQLdb kể từ khi MySQLdb ngừng được bảo trì. Tôi tin rằng mọi thứ ở đây cũng đúng với MySQLdb kế thừa, nhưng chưa được kiểm tra.

Trước hết, một số sự kiện:

  • withCú pháp của Python gọi __enter__phương thức của trình quản lý ngữ cảnh trước khi thực thi phần thân của withkhối và __exit__sau đó là phương thức của nó .
  • Các kết nối có một __enter__phương thức không làm gì khác ngoài việc tạo và trả về một con trỏ và một __exit__phương thức có thể cam kết hoặc quay trở lại (tùy thuộc vào việc liệu một ngoại lệ có được ném ra hay không). Nó không đóng kết nối.
  • Con trỏ trong PyMySQL hoàn toàn là một sự trừu tượng được triển khai bằng Python; không có khái niệm tương đương trong MySQL. 1
  • Con trỏ có một __enter__phương thức không làm gì cả và một __exit__phương thức "đóng" con trỏ (chỉ có nghĩa là vô hiệu hóa tham chiếu của con trỏ tới kết nối mẹ của nó và loại bỏ bất kỳ dữ liệu nào được lưu trữ trên con trỏ).
  • Con trỏ giữ một tham chiếu đến kết nối đã tạo ra chúng, nhưng các kết nối không giữ một tham chiếu đến con trỏ mà chúng đã tạo.
  • Các kết nối có một __del__phương thức đóng chúng
  • Theo https://docs.python.org/3/reference/datamodel.html , CPython (triển khai Python mặc định) sử dụng tính năng đếm tham chiếu và tự động xóa một đối tượng khi số lượng tham chiếu đến nó bằng không.

Đặt những thứ này lại với nhau, chúng ta thấy rằng đoạn mã ngây thơ như thế này về mặt lý thuyết là có vấn đề:

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

# ... happily carry on and do something unrelated

Vấn đề là không có gì đã đóng kết nối. Thật vậy, nếu bạn dán đoạn mã trên vào một trình bao Python và sau đó chạy SHOW FULL PROCESSLISTtại trình bao MySQL, bạn sẽ có thể thấy kết nối nhàn rỗi mà bạn đã tạo. Vì số lượng kết nối mặc định của MySQL là 151 , con số này không lớn nên về mặt lý thuyết, bạn có thể bắt đầu gặp sự cố nếu bạn có nhiều quy trình giữ các kết nối này mở.

Tuy nhiên, trong CPython, có một cơ hội lưu đảm bảo rằng mã như ví dụ của tôi ở trên có thể sẽ không khiến bạn để lại vô số kết nối đang mở. Cơ hội lưu đó là ngay sau khi cursorvượt ra khỏi phạm vi (ví dụ: hàm mà nó được tạo kết thúc hoặc cursorđược gán giá trị khác cho nó), số lượng tham chiếu của nó chạm đến 0, điều này khiến nó bị xóa, làm giảm số lượng tham chiếu của kết nối về 0, khiến __del__phương thức của kết nối được gọi là lực đóng kết nối. Nếu bạn đã dán đoạn mã trên vào trình bao Python của mình, thì bây giờ bạn có thể mô phỏng điều này bằng cách chạy cursor = 'arbitrary value'; ngay sau khi bạn làm điều này, kết nối bạn đã mở sẽ biến mất khỏi SHOW PROCESSLISTđầu ra.

Tuy nhiên, dựa vào điều này là không phù hợp và về mặt lý thuyết có thể thất bại trong triển khai Python ngoài CPython. Về lý thuyết, Cleaner sẽ rõ ràng .close()là kết nối (để giải phóng kết nối trên cơ sở dữ liệu mà không cần đợi Python phá hủy đối tượng). Mã mạnh mẽ hơn này trông giống như sau:

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

Điều này là xấu, nhưng không dựa vào việc Python phá hủy các đối tượng của bạn để giải phóng (số lượng hữu hạn có sẵn) cơ sở dữ liệu của bạn.

Lưu ý rằng việc đóng con trỏ , nếu bạn đã đóng kết nối một cách rõ ràng như thế này, hoàn toàn vô nghĩa.

Cuối cùng, để trả lời các câu hỏi phụ ở đây:

Có rất nhiều chi phí để có được con trỏ mới hay đó không phải là vấn đề lớn?

Không, việc khởi tạo con trỏ hoàn toàn không ảnh hưởng đến MySQL và về cơ bản không làm gì cả .

Có lợi thế đáng kể nào khi tìm các tập hợp giao dịch không yêu cầu cam kết trung gian để bạn không phải nhận con trỏ mới cho mỗi giao dịch không?

Đây là tình huống và khó đưa ra câu trả lời chung. Như https://dev.mysql.com/doc/refman/en/optimizing-innodb-transaction-management.html đã nói, "một ứng dụng có thể gặp phải các vấn đề về hiệu suất nếu nó thực hiện hàng nghìn lần mỗi giây và các vấn đề hiệu suất khác nhau nếu nó cam kết chỉ 2-3 giờ một lần " . Bạn phải trả chi phí hiệu suất cho mỗi lần cam kết, nhưng bằng cách để các giao dịch mở lâu hơn, bạn sẽ tăng khả năng các kết nối khác phải mất thời gian chờ khóa, tăng nguy cơ tắc nghẽn và có khả năng tăng chi phí của một số tra cứu được thực hiện bởi các kết nối khác .


1 MySQL không có một cấu trúc mà họ gọi là con trỏ nhưng họ chỉ tồn tại các thủ tục lưu trữ bên trong; chúng hoàn toàn khác với con trỏ PyMySQL và không liên quan ở đây.


5

Tôi nghĩ tốt hơn là bạn nên cố gắng sử dụng một con trỏ cho tất cả các lần thực thi của mình và đóng nó ở cuối mã của bạn. Nó dễ làm việc hơn và nó cũng có thể có những lợi ích về hiệu quả (đừng trích dẫn tôi về điều đó).

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

Vấn đề là bạn có thể lưu trữ kết quả thực hiện của con trỏ trong một biến khác, do đó giải phóng con trỏ của bạn để thực hiện lần thực thi thứ hai. Bạn chỉ gặp sự cố theo cách này nếu bạn đang sử dụng fetchone () và cần thực hiện thực thi con trỏ thứ hai trước khi bạn lặp lại tất cả các kết quả từ truy vấn đầu tiên.

Nếu không, tôi muốn nói chỉ đóng các con trỏ của bạn ngay sau khi bạn hoàn thành việc lấy tất cả dữ liệu ra khỏi chúng. Bằng cách đó, bạn không phải lo lắng về việc buộc các đầu lỏng lẻo sau này trong mã của mình.


Cảm ơn - Xem xét rằng bạn phải đóng con trỏ để cam kết cập nhật / chèn, tôi đoán một cách dễ dàng để thực hiện cho các bản cập nhật / chèn sẽ là nhận một con trỏ cho mỗi daemon, đóng con trỏ để cam kết và ngay lập tức nhận được một con trỏ mới vì vậy bạn đã sẵn sàng vào lần sau. Nghe có hợp lý không?
jmilloy 31-07-11

1
Này, không sao. Tôi thực sự không biết về việc thực hiện cập nhật / chèn bằng cách đóng con trỏ của bạn, nhưng tìm kiếm nhanh trên mạng cho thấy điều này: conn = MySQLdb.connect (objects_go_here) cursor = MySQLdb.cursor () cursor.execute (mysql_insert_statement_here) try: conn. commit () ngoại trừ: conn.rollback () # hoàn tác các thay đổi được thực hiện nếu xảy ra lỗi. Bằng cách này, cơ sở dữ liệu tự cam kết các thay đổi và bạn không phải lo lắng về chính các con trỏ. Sau đó, bạn chỉ có thể mở 1 con trỏ vào mọi lúc. Hãy xem tại đây: tutorialspoint.com/python/python_database_access.htm
nct25,

Vâng nếu điều đó hoạt động thì tôi chỉ sai và có một số lý do khác khiến tôi nghĩ rằng tôi phải đóng con trỏ để thực hiện kết nối.
jmilloy

Yea tôi không biết, liên kết tôi đã đăng khiến tôi nghĩ rằng nó hoạt động. Tôi đoán rằng một nghiên cứu nữa sẽ cho bạn biết liệu nó có chắc chắn hoạt động hay không, nhưng tôi nghĩ bạn có thể chỉ cần làm với nó. Hy vọng tôi đã giúp được bạn!
nct25

con trỏ không an toàn cho luồng, nếu bạn sử dụng cùng một con trỏ giữa nhiều luồng khác nhau và chúng đều đang truy vấn từ db, fetchall () sẽ cung cấp dữ liệu ngẫu nhiên.
ospider

-6

Tôi đề nghị làm điều đó như php và mysql. Bắt đầu i ở đầu mã của bạn trước khi in dữ liệu đầu tiên. Vì vậy, nếu bạn gặp lỗi kết nối, bạn có thể hiển thị thông báo lỗi 50x(Không nhớ lỗi nội bộ là gì). Và giữ nó mở trong cả phiên và đóng nó khi bạn biết mình không cần nó nữa.


Trong MySQLdb, có sự khác biệt giữa kết nối và con trỏ. Tôi kết nối một lần cho mỗi yêu cầu (hiện tại) và có thể phát hiện sớm các lỗi kết nối. Nhưng những gì về con trỏ?
jmilloy

IMHO đó không phải là lời khuyên chính xác. Nó phụ thuộc. Nếu mã của bạn sẽ giữ kết nối trong một thời gian dài (ví dụ: nó lấy một số dữ liệu từ DB và sau đó trong 1-5-10 phút, nó thực hiện điều gì đó trên máy chủ và giữ kết nối) và ứng dụng chuỗi khó khăn, nó sẽ sớm tạo ra sự cố (bạn sẽ vượt quá kết nối tối đa được phép).
Roman Podlinov
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.