1. ホーム
  2. python

[解決済み] MySQLdbでカーソルを閉じるタイミング

2023-03-06 19:19:52

質問

WSGI ウェブアプリを構築しており、MySQL データベースを使用しています。ステートメントを実行し、結果を得るためのカーソルを提供する MySQLdb を使用しています。 カーソルの取得と終了のための標準的なプラクティスは何ですか? 特に、カーソルはどのくらいの期間持続させるべきですか?各トランザクションのために新しいカーソルを取得すべきですか?

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

どのように解決するのですか?

標準的な方法は何かと尋ねる代わりに、それはしばしば不明瞭で主観的なので、ガイダンスのためにモジュール自体に目を向けてみるのもよいでしょう。一般的には 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()

について、いくつかの既存の Q&A があります。 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に"cursor is open; connection is open"という出力が出力されるのが確認できるはずです。

接続をコミットする前にカーソルをクローズする必要があると思います。

なぜですか?それは MySQL C API の基礎である MySQLdb の基礎である MySQL C API は、モジュール ドキュメントで暗示されているように、カーソル オブジェクトを実装していません。 MySQL はカーソルをサポートしませんが、カーソルは簡単にエミュレートできます。 実際 MySQLdb.cursors.BaseCursor クラスを直接継承しています。 object を継承し、コミット/ロールバックに関してカーソルにそのような制限を課していません。ある Oracle 開発者は は次のように言っています。 :

<ブロッククオート

cur.close()の前にcnx.commit()を実行するのが最も論理的に聞こえますが、どうでしょうか? もしかしたら カーソルが不要になったら、カーソルを閉じる。 したがって、カーソルを閉じる前にcommit()します。結局のところ Connector/Pythonでは、それは大きな違いはありませんが、他のデータベースでは、それは可能かもしれません。 データベースではそうかもしれません。

この件に関しては、quot;標準的な実践に近いと期待しています。

<ブロッククオート

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

そうすることで、さらなるヒューマンエラーが発生する可能性があります。そうしようとすると、さらなるヒューマンエラーが発生する可能性があります。

新しいカーソルを取得するために多くのオーバーヘッドがあるのでしょうか。

オーバーヘッドはごくわずかで、データベースサーバには全く触れていません。これは完全に MySQLdb の実装の範囲内です。あなたは を見る BaseCursor.__init__ github をご覧ください。新しいカーソルを作成するときに何が起こっているのか、本当に知りたければ。

以前、私たちが with について説明したときに戻って、おそらく今なら、なぜ MySQLdb.Connection クラス __enter__ そして __exit__ メソッドを使用すると、すべての with ブロックごとに全く新しいカーソルオブジェクトを与え、それを追跡したりブロックの最後で閉じたりする手間を省くことができます。これはかなり軽量で、純粋にあなたの便宜のために存在します。

カーソルオブジェクトを細かく管理することが本当に重要なのであれば contextlib.closing を使用して、カーソルオブジェクトに定義されていない __exit__ メソッドが定義されていないことを補うために使用できます。を終了する際に、接続オブジェクトを強制的に閉じるために使用することもできます。 with ブロックの終了時に接続オブジェクトを強制的に閉じるために使用することもできます。これは "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'

なお with closing(arg_obj) は、引数オブジェクトの __enter____exit__ メソッドを使用します。 のみ は引数オブジェクトの close メソッドの末尾にある with メソッドを追加します。(これを実際に見るには、単にクラス Foo__enter__ , __exit__ そして close メソッドには、単純な print ステートメントを含むメソッドを作成し、そのメソッドを使用したときに何が起こるかを比較します。 with Foo(): pass を実行したときと with closing(Foo()): pass .) これには2つの重要な意味があります。

まず、自動コミットモードが有効な場合、MySQLdb は BEGIN を使用すると、サーバ上で明示的なトランザクションが発生します。 with connection を使用し、ブロックの最後でトランザクションをコミットまたはロールバックします。これらは 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 . この結果、放棄された接続は以下のいずれかが発生するまで、その場に留まることになります。

  • カーソルへのすべての参照が削除される
  • カーソルがスコープ外になる
  • 接続がタイムアウトしました。
  • サーバー管理ツールで接続を手動で閉じる

開いている接続を監視することでテストできます (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