web-dev-qa-db-ja.com

MySQLdbを使用してカーソルを閉じるタイミング

私はWSGI Webアプリを構築しており、MySQLデータベースを持っています。私はMySQLdbを使用しています。これは、ステートメントを実行して結果を取得するためのカーソルを提供します。 カーソルを取得したり閉じたりするための標準的な方法は何ですか?特に、カーソルはどれくらい持続する必要がありますか?トランザクションごとに新しいカーソルを取得する必要がありますか?

接続をコミットする前にカーソルを閉じる必要があると思います。トランザクションごとに新しいカーソルを取得する必要がないように、中間コミットを必要としないトランザクションのセットを見つけることには大きな利点がありますか?新しいカーソルを取得するために多くのオーバーヘッドがありますか、それとも大したことではありませんか?

75
jmilloy

それはしばしば不明確で主観的であるため、標準的な実践とは何かを尋ねる代わりに、モジュール自体にガイダンスを求めてみることができます。一般に、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に関するいくつかの既存のQ&Aがすでにあるか、 Pythonの「with」ステートメントを理解する を読むことができますが、本質的には、withブロックの先頭で__enter__が実行されます。 __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の開発者 言うべきこれを持っていた

cur.close()の前のcnx.commit()は、私にとって最も論理的に聞こえます。 「もう必要ない場合は、カーソルを閉じてください」というルールをたどることができます。したがって、カーソルを閉じる前にcommit()します。最終的に、Connector/Pythonの場合、大きな違いはありませんが、他のデータベースでも違いはありません。

このテーマで「標準的な実践」に到達するのと同じくらい近いと思います。

トランザクションごとに新しいカーソルを取得する必要がないように、中間コミットを必要としないトランザクションのセットを見つけることには大きな利点がありますか?

私はそれを非常に疑っており、そうしようとすると、追加の人為的ミスを導入する可能性があります。コンベンションを決定し、それに固執することをお勧めします。

新しいカーソルを取得するために多くのオーバーヘッドがありますか、それとも大したことではありませんか?

オーバーヘッドは無視でき、データベースサーバーにはまったく影響しません。完全にMySQLdbの実装内にあります。新しいカーソルを作成するときに何が起こっているのか知りたい場合は、 githubのBaseCursor.__init__をご覧ください できます。

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メソッドを呼び出しますonly。 (これを実際に見るには、単純なFooステートメントを含む__enter____exit__、およびcloseメソッドでクラスprintを定義し、with Foo(): passを実行したときの結果を比較します。 with closing(Foo()): passを実行するとどうなりますか。)これには2つの重要な意味があります。

最初に、自動コミットモードが有効になっている場合、with connectionを使用してブロックの最後でトランザクションをコミットまたはロールバックすると、MySQLdbはサーバー上で明示的なトランザクションをBEGINします。これらはMySQLdbのデフォルトの動作であり、すべてのDMLステートメントをすぐにコミットするMySQLのデフォルトの動作から保護することを目的としています。 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に設定されます。これにより、次のいずれかが発生するまで、接続が破棄されたままになります。

  • カーソルへのすべての参照が削除されます
  • カーソルが範囲外になります
  • 接続がタイムアウトする
  • 接続はサーバー管理ツールを介して手動で閉じられます

これをテストするには、次の行を1行ずつ実行しながら、開いている接続を監視します(ワークベンチまたは sing 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
68
Air

「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()
29
Roman Podlinov

注:この回答は PyMySQL に対するものです。これは、MySQLdbのドロップイン置換であり、MySQLdbのメンテナンスが停止されたため、事実上最新バージョンのMySQLdbです。ここにあるものはすべてalsoレガシーMySQLdbにも当てはまると思いますが、チェックしていません。

まず、いくつかの事実:

  • Pythonの with 構文は、withブロックの本体を実行する前にコンテキストマネージャーの___enter___メソッドを呼び出し、その後___exit___メソッドを呼び出します。
  • 接続には、カーソルを作成して返す以外に何もしない ___enter___ メソッドと、コミットまたはロールバックする(依存する ___exit___ メソッド)例外がスローされたかどうか)。 接続を閉じません
  • PyMySQLのカーソルは、純粋にPythonで実装された抽象化です。 MySQL自体には同等の概念はありません。1
  • カーソルには ___enter___ 何もしないメソッドと ___exit___ カーソルを「閉じる」メソッドがあります(つまり、カーソルのNULL親接続への参照と、カーソルに保存されたデータの破棄)。
  • カーソルは、それらを生成した接続への参照を保持しますが、接続は、作成したカーソルへの参照を保持しません。
  • 接続には、閉じるための ___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 Shellに貼り付けてからMySQL Shellで_SHOW FULL PROCESSLIST_を実行すると、作成したアイドル接続を確認できます。MySQLのデフォルトの接続数は 151 であり、これはhugeではありません。これらの接続を開いたままにしておくプロセスが多数ある場合、理論的には問題が発生し始める可能性があります。

ただし、CPythonには、上記の例のようなコード(= /// =)がおそらく開いている接続の負荷を残さないことを保証する猶予があります。この猶予期間は、cursorがスコープから外れるとすぐに(たとえば、作成された関数が終了するか、cursorに別の値が割り当てられる)、参照カウントが0になり、これにより、接続が削除され、接続の参照カウントがゼロになり、接続の___del___メソッドが呼び出され、接続が強制的に閉じられます。既に上記のコードをPython Shellに貼り付けている場合、_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(利用可能な有限数の)データベース接続を解放するためにオブジェクトを破棄することに依存していません。

このように明示的に接続をすでに閉じている場合、cursorを閉じることはまったく意味がありません。

最後に、ここで2番目の質問に答えます。

新しいカーソルを取得するために多くのオーバーヘッドがありますか、それとも大したことではありませんか?

いいえ、カーソルをインスタンス化してもMySQLにはまったくヒットせず、 基本的には何もしません です。

トランザクションごとに新しいカーソルを取得する必要がないように、中間コミットを必要としないトランザクションのセットを見つけることには大きな利点がありますか?

これは状況に応じて、一般的な答えを出すのが困難です。 https://dev.mysql.com/doc/refman/en/optimizing-innodb-transaction-management.html puts、 "アプリケーションでパフォーマンスの問題が発生する可能性があります1秒間に数千回コミットすると、2〜3時間ごとにコミットすると異なるパフォーマンスの問題が発生します」。コミットごとにパフォーマンスのオーバーヘッドが発生しますが、トランザクションを長時間開いたままにすると、他の接続がロックを待機する時間を費やす可能性が高まり、デッドロックのリスクが高まり、他の接続によって実行される一部のルックアップのコストが増加する可能性があります。


1 MySQLdoesには cursor を呼び出す構成がありますが、ストアドプロシージャ内にのみ存在します。それらはPyMySQLカーソルとは完全に異なり、ここでは関係ありません。

6
Mark Amery

すべての実行で1つのカーソルを使用し、コードの最後でカーソルを閉じてみた方が良いと思います。作業は簡単であり、効率性のメリットもあります(そのことについては引用しないでください)。

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

ポイントは、カーソルの実行結果を別の変数に保存できることです。これにより、カーソルを解放して2回目の実行を行うことができます。この方法で問題が発生するのは、fetchone()を使用している場合のみで、最初のクエリのすべての結果を反復処理する前に2番目のカーソルを実行する必要があります。

それ以外の場合は、すべてのデータの取得が完了したらすぐにカーソルを閉じてください。そうすれば、コードの後半でルーズエンドを縛ることを心配する必要がなくなります。

5
nct25