私はWSGI Webアプリを構築しており、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()
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
「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()
注:この回答は PyMySQL に対するものです。これは、MySQLdbのドロップイン置換であり、MySQLdbのメンテナンスが停止されたため、事実上最新バージョンのMySQLdbです。ここにあるものはすべてalsoレガシーMySQLdbにも当てはまると思いますが、チェックしていません。
まず、いくつかの事実:
with
構文は、with
ブロックの本体を実行する前にコンテキストマネージャーの___enter__
_メソッドを呼び出し、その後___exit__
_メソッドを呼び出します。__enter__
_ メソッドと、コミットまたはロールバックする(依存する ___exit__
_ メソッド)例外がスローされたかどうか)。 接続を閉じません。__enter__
_ 何もしないメソッドと ___exit__
_ カーソルを「閉じる」メソッドがあります(つまり、カーソルのNULL親接続への参照と、カーソルに保存されたデータの破棄)。__del__
_ メソッドがありますこれらをまとめると、このような素朴なコードは理論上に問題があることがわかります:
_# 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カーソルとは完全に異なり、ここでは関係ありません。
すべての実行で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番目のカーソルを実行する必要があります。
それ以外の場合は、すべてのデータの取得が完了したらすぐにカーソルを閉じてください。そうすれば、コードの後半でルーズエンドを縛ることを心配する必要がなくなります。