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


87

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

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

回答:


81

標準的な方法を尋ねる代わりに、それはしばしば不明確で主観的であるため、モジュール自体にガイダンスを求めてみるとよいでしょう。一般に、with別のユーザーが提案したようにキーワードを使用することは素晴らしいアイデアですが、この特定の状況では、期待する機能が十分に得られない場合があります。

モジュールのバージョン1.2.5以降、次のコード(github)を使用してコンテキストマネージャープロトコルMySQLdb.Connection実装します

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」ステートメントについて読むことができますが、基本的に__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に「カーソルが開いています。接続が開いています」という出力が出力されます。

接続を確定する前にカーソルを閉じる必要があると思います。

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

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

これは、このテーマに関する「標準的な慣行」に到達するのと同じくらい近いと思います。

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

私はそれを非常に疑っています、そしてそうしようとすると、あなたは追加のヒューマンエラーを導入するかもしれません。コンベンションを決定し、それに固執する方が良いです。

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

オーバーヘッドはごくわずかであり、データベースサーバーにはまったく影響しません。それは完全にMySQLdbの実装内にあります。あなたは、することができます見てBaseCursor.__init__githubの上のあなたは、あなたが新しいカーソルを作成するときに何が起こっているかを知って、本当に興味があれば。

以前に説明したときに戻るとwithMySQLdb.Connectionクラス__enter____exit__メソッドがすべてのwithブロックでまったく新しいカーソルオブジェクトを提供し、それを追跡したり、ブロックの最後で閉じたりする必要がない理由を理解できたと思います。それはかなり軽量で、純粋にあなたの便宜のために存在します。

カーソルオブジェクトを細かく管理することが本当に重要な場合は、contextlib.closeingを使用して、カーソルオブジェクトに__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ブロック。(単にクラスを定義し、この動作を確認するにはFoo__enter____exit__、およびclose方法は、単純含むprint文を、そしてあなたが行うときに何が起こるかを比較するwith Foo(): passあなたはときに何が起こるかにwith closing(Foo()): pass。)これは、二つの重要な意味を持っています:

まず、自動コミットモードが有効になっBEGINている場合with connection、ブロックの最後でトランザクションを使用してコミットまたはロールバックすると、MySQLdbはサーバー上で明示的なトランザクションを実行します。これらは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つずつ実行しながら、開いている接続を(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

15
あなたの投稿は最も網羅的でしたが、何度か読み直した後でも、カーソルを閉じることに戸惑いました。このテーマに関する多数の投稿から判断すると、それは一般的な混乱のポイントのようです。私の要点は、カーソルは.close()を呼び出す必要がないように見えるということです。では、なぜ.close()メソッドがあるのでしょうか。
SMGreenfield 2015

6
簡単に言うと、これはPython DB APIのcursor.close()一部であり MySQLを念頭に置いて特別に作成されたものではありません。
エア・

1
del my_cursの後に接続が閉じるのはなぜですか?
BAE

@ChengchengPeimy_cursは、connectionオブジェクトへの最後の参照を保持します。その参照が存在しなくなったら、connectionオブジェクトガベージコレクションする必要があります。
エア・

これは素晴らしい答えです、ありがとう。優秀な説明withMySQLdb.Connection__enter____exit__機能。繰り返しになりますが、@ Airに感謝します。
ユージーン

33

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

withFlaskや他のWebフレームワークで使用したい場合、これは良いオプションではないと思います。その場合、http://flask.pocoo.org/docs/patterns/sqlite3/#sqlite3問題が発生します。
ジェームズキング

@ james-king私はFlaskを使用していませんでしたが、あなたの例では、Flaskはdb接続自体を閉じます。実際、私のコードでは、わずかに異なるアプローチを使用ています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 2014年

@RomanPodlinovええ、カーソルで使用すれば問題ありません。
ジェームズキング

7

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

まず第一に、いくつかの事実:

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

しかし、CPythonの中で、救いは、性を保証することは上記の私の例のようなコードがあることということがあり、おそらくあなたが開いている接続の負荷を周りに残すことはありません。その節約の恩恵はcursor、スコープ外になるとすぐに(たとえば、作成された関数が終了するかcursor、別の値が割り当てられると)、参照カウントがゼロになり、削除されて、接続の参照カウントが削除されることです。ゼロにすると、接続の__del__メソッドが呼び出され、接続が強制的に閉じられます。上記のコードをPythonシェルに既に貼り付けている場合は、cursor = 'arbitrary value';を実行してこれをシミュレートできます。これを行うとすぐに、開いた接続はSHOW PROCESSLIST出力から消えます。

ただし、これに依存することはエレガントではなく、理論的にはCPython以外のPython実装では失敗する可能性があります。よりクリーンなのは、理論的に.close()は、接続を明示的に行うことです(Pythonがオブジェクトを破棄するのを待たずに、データベース上の接続を解放するため)。このより堅牢なコードは次のようになります。

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

これは醜いですが、Pythonがオブジェクトを破壊して(利用可能な有限数の)データベース接続を解放することに依存していません。

このように明示的に接続をすでに閉じている場合は、カーソルを閉じることはまったく無意味であることに注意してください。

最後に、ここで二次的な質問に答えるには:

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

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

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

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


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


5

すべての実行に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回目のカーソル実行を行う必要があります。

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


ありがとう-更新/挿入をコミットするためにカーソルを閉じる必要があることを考えると、更新/挿入のためにそれを行う簡単な方法の1つは、デーモンごとに1つのカーソルを取得し、コミットするためにカーソルを閉じて、すぐに新しいカーソルを取得することだと思います次回は準備ができています。それは合理的に聞こえますか?
jmilloy 2011

1
ねえ、問題ありません。カーソルを閉じて更新/挿入をコミットすることについて実際には知りませんでしたが、オンラインですばやく検索すると、次のように表示されます。conn= MySQLdb.connect(arguments_go_here)cursor = MySQLdb.cursor()cursor.execute(mysql_insert_statement_here)try:conn。 commit()を除く:conn.rollback()#エラーが発生した場合に行われた変更を元に戻します。このように、データベース自体が変更をコミットし、カーソル自体について心配する必要はありません。そうすれば、常に1つのカーソルを開くことができます。こちらをご覧ください: tutorialspoint.com/python/python_database_access.htm
nct25 2011

ええ、それがうまくいくなら、私は間違っているだけで、接続をコミットするためにカーソルを閉じる必要があると私に思わせる他の理由がありました。
jmilloy 2011

うん、わからない、私が投稿したそのリンクは私にそれがうまくいくと思わせる。もう少し研究すれば、それが確実に機能するかどうかがわかると思いますが、おそらくそれでうまくいくと思います。私があなたの助けになったといいのですが!
nct25 2011

カーソルはスレッドセーフではありません。多くの異なるスレッド間で同じカーソルを使用し、それらがすべてdbからクエリを実行している場合、fetchall()はランダムなデータを提供します。
ospider

-6

phpやmysqlのようにすることをお勧めします。最初のデータを出力する前に、コードの最初からiを開始します。したがって、接続エラーが発生した場合は、50x(内部エラーが何であるかを覚えていない)エラーメッセージを表示できます。また、セッション全体を通して開いたままにし、不要になったことがわかったら閉じます。


MySQLdbでは、接続とカーソルに違いがあります。リクエストごとに1回接続し(今のところ)、接続エラーを早期に検出できます。しかし、カーソルはどうですか?
jmilloy 2011

私見それは正確なアドバイスではありません。状況によります。コードが長時間接続を維持し(たとえば、DBからデータを取得し、1〜5〜10分間サーバー上で何かを実行して接続を維持する)、マルチスレッドアプリケーションの場合、すぐに問題が発生します(最大許容接続数を超えます)。
ローマンポドリノフ2013年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.