web-dev-qa-db-ja.com

Python sqlite3のトランザクション

Sqliteデータベースを使用するコードをPythonに移植しようとしています。トランザクションを機能させようとしていますが、本当に混乱しています。本当に混乱しています。 ;素晴らしいため、他の言語でsqliteを多く使用しましたが、ここで何が間違っているのかを単純に判断することはできません。

テストデータベースのスキーマは次のとおりです(sqlite3コマンドラインツールに入力します)。

_BEGIN TRANSACTION;
CREATE TABLE test (i integer);
INSERT INTO "test" VALUES(99);
COMMIT;
_

これがテストプログラムです。

_import sqlite3

sql = sqlite3.connect("test.db")
with sql:
    c = sql.cursor()
    c.executescript("""
        update test set i = 1;
        fnord;
        update test set i = 0;
        """)
_

意図的な間違いに気付くかもしれません。これにより、更新が実行された後、2行目でSQLスクリプトが失敗します。

ドキュメントによると、_with sql_ステートメントは、コンテンツの周りに暗黙的なトランザクションを設定することになっています。これは、ブロックが成功した場合にのみコミットされます。ただし、実行すると、予想されるSQLエラーが発生します...しかし、iの値は99から1に設定されます。最初の更新をロールバックする必要があるため、99のままになります。

commit()およびrollback()を明示的に呼び出す別のテストプログラムを次に示します。

_import sqlite3

sql = sqlite3.connect("test.db")
try:
    c = sql.cursor()
    c.executescript("""
        update test set i = 1;
        fnord;
        update test set i = 0;
    """)
    sql.commit()
except sql.Error:
    print("failed!")
    sql.rollback()
_

これはまったく同じように動作します--- iが99から1に変更されます。

今、私は明示的にBEGINとCOMMITを呼び出しています:

_import sqlite3

sql = sqlite3.connect("test.db")
try:
    c = sql.cursor()
    c.execute("begin")
    c.executescript("""
            update test set i = 1;
            fnord;
            update test set i = 0;
    """)
    c.execute("commit")
except sql.Error:
    print("failed!")
    c.execute("rollback")
_

これも失敗しますが、方法は異なります。私はこれを得る:

_sqlite3.OperationalError: cannot rollback - no transaction is active
_

ただし、c.execute()への呼び出しをc.executescript()に置き換えると、works(iは99のままです)!

(また、begincommitexecutescriptへの内部呼び出し内に配置すると、すべての場合に正しく動作することを追加する必要がありますが、残念ながら、それを使用することはできません私のアプリケーションでのアプローチ。さらに、_sql.isolation_level_を変更しても動作に違いはないようです。)

誰かが私にここで何が起こっているのか説明してもらえますか?これを理解する必要があります。データベース内のトランザクションを信頼できない場合、アプリケーションを機能させることができません...

Python 2.7、python-sqlite3 2.6.0、sqlite3 3.7.13、Debian。

32
David Given
12
CL.

Sqlite3 libの欠点に関係なく作業したい人は、次の2つのことを行うと、トランザクションをある程度制御できることがわかりました。

  1. セットする Connection.isolation_level = Nonedocs により、これは自動コミットモードを意味します)
  2. docs によれば、「最初にCOMMITステートメントを発行する」ため、つまりトラブルが発生するため、executescriptを使用しないでください。実際、手動で設定されたトランザクションを妨げることがわかりました

したがって、次のテストの適応が私にとってはうまくいきます:

import sqlite3

sql = sqlite3.connect("/tmp/test.db")
sql.isolation_level = None
c = sql.cursor()
c.execute("begin")
try:
    c.execute("update test set i = 1")
    c.execute("fnord")
    c.execute("update test set i = 0")
    c.execute("commit")
except sql.Error:
    print("failed!")
    c.execute("rollback")
28
yungchin

ドキュメント ごと

接続オブジェクトは、トランザクションを自動的にコミットまたはロールバックするコンテキストマネージャーとして使用できます。例外が発生した場合、トランザクションはロールバックされます。それ以外の場合、トランザクションはコミットされます。

したがって、例外が発生したときにPython with-statementを終了させると、トランザクションはロールバックされます。

import sqlite3

filename = '/tmp/test.db'
with sqlite3.connect(filename) as conn:
    cursor = conn.cursor()
    sqls = [
        'DROP TABLE IF EXISTS test',
        'CREATE TABLE test (i integer)',
        'INSERT INTO "test" VALUES(99)',]
    for sql in sqls:
        cursor.execute(sql)
try:
    with sqlite3.connect(filename) as conn:
        cursor = conn.cursor()
        sqls = [
            'update test set i = 1',
            'fnord',   # <-- trigger error
            'update test set i = 0',]
        for sql in sqls:
            cursor.execute(sql)
except sqlite3.OperationalError as err:
    print(err)
    # near "fnord": syntax error
with sqlite3.connect(filename) as conn:
    cursor = conn.cursor()
    cursor.execute('SELECT * FROM test')
    for row in cursor:
        print(row)
        # (99,)

利回り

(99,)

予想通り。

17
unutbu

Pythonのsqlite3バインディングとSqlite3の公式ドキュメントを読んだことに基づいて、私が考えていることは次のとおりです。簡単な答えは、適切なトランザクションが必要な場合は、このイディオムに固執する必要があるということです。

with connection:
    db.execute("BEGIN")
    # do other things, but do NOT use 'executescript'

私の直感に反して、with connectionは、スコープに入るときにBEGINを呼び出しますnot。実際、それは __enter__ で何もしません。スコープが__exit__の場合のみ有効です。 スコープが正常に終了するか例外で終了するかによって、COMMITまたはROLLBACKのいずれかを選択します

したがって、正しいことは、BEGINを使用してトランザクションの開始を常に明示的にマークすることです。これは、トランザクション内でisolation_levelrelevantをレンダリングします。これは、ありがたいことに、 自動コミットモードが有効 の間にのみ効果があるためです。 自動コミットモードはトランザクションブロック内で常に抑制されます

別の癖はexecutescriptで、これは スクリプトを実行する前に常にCOMMITを発行する です。これは簡単にトランザクションを台無しにする可能性があるため、選択は次のいずれかです。

  • トランザクション内でexecutescriptを1つだけ使用し、他には何も使用しない、または
  • executescriptを完全に避けてください。 executeの制限に従い、executeを何度でも呼び出すことができます。
5
Rufflewind

通常の.execute()は、快適なデフォルトの自動コミットモードおよび自動コミットを行う_with conn: ..._コンテキストマネージャーで期待どおりに動作します[〜#〜] or [〜# 〜]rollback-保護されたread-modify-writeトランザクションを除きます。これはこの回答の最後で説明されています。

sqlite3モジュールの非標準conn_or_cursor.executescript()は(デフォルト)自動コミットモードに参加しません(したがって、_with conn: ..._コンテキストマネージャーでは正常に動作しません) 。そのため、潜在的にpendingトランザクションを自動コミット開始するだけで、 「生」になる前。

これはまた、スクリプト内に「BEGIN」がなければexecutescript()はトランザクションなしで機能するため、エラー時などのロールバックオプションがありません。

したがって、executescript()では、明示的なBEGINを使用することをお勧めします(最初のスキーマ作成スクリプトが「raw」モードのsqliteコマンドラインツールで行ったように)。そして、この相互作用は、何が起こっているかを段階的に示しています。

_>>> list(conn.execute('SELECT * FROM test'))
[(99,)]
>>> conn.executescript("BEGIN; UPDATE TEST SET i = 1; FNORD; COMMIT""")
Traceback (most recent call last):
  File "<interactive input>", line 1, in <module>
OperationalError: near "FNORD": syntax error
>>> list(conn.execute('SELECT * FROM test'))
[(1,)]
>>> conn.rollback()
>>> list(conn.execute('SELECT * FROM test'))
[(99,)]
>>> 
_

スクリプトは「COMMIT」に到達しませんでした。したがって、現在の中間状態を表示して、ロールバックを決定できます(または、それでもコミットします)。

したがって、excecutescript()を介した有効なtry-except-rollbackは次のようになります。

_>>> list(conn.execute('SELECT * FROM test'))
[(99,)]
>>> try: conn.executescript("BEGIN; UPDATE TEST SET i = 1; FNORD; COMMIT""")
... except Exception as ev: 
...     print("Error in executescript (%s). Rolling back" % ev)
...     conn.executescript('ROLLBACK')
... 
Error in executescript (near "FNORD": syntax error). Rolling back
<sqlite3.Cursor object at 0x011F56E0>
>>> list(conn.execute('SELECT * FROM test'))
[(99,)]
>>> 
_

.execute()がコミット制御を引き継いでいないため、ここでのスクリプトによるロールバックに注意してください)


そして、ここでは、自動コミットモードに関する注意事項と、保護された読み取り-変更-書き込みトランザクションのより難しい問題との組み合わせで、@ Jeremieに言わせました"sqlite/pythonのトランザクションについて書かれた非常に多くのことの中で、これが私が望むことをすることができる唯一のことです(データベースに排他的読み取りロックを持っています)。" c.execute("begin")を含む例についてコメントしてください。 sqlite3は通常、実際の書き戻しの期間を除いて、長いブロッキング排他読み取りロックを行いませんが、重複する変更に対する十分な保護を達成するためのより賢い5段階ロックを行います。

_with conn:_自動コミットコンテキストは、 sqlite3の5段階ロックスキーム で保護されたread-modify-writeに十分な強度のロックを既に設定またはトリガーしていません。このようなロックは、最初のデータ変更コマンドが発行されたときにのみ暗黙的に行われるため、遅すぎます。明示的なBEGIN (DEFERRED) (TRANSACTION)のみが目的の動作をトリガーします。

最初の読み取り データベースに対する操作はSHAREDロックを作成し、最初の書き込み操作はRESERVEDロックを作成します。

したがって、一般的な方法でプログラミング言語を使用する(特別なアトミックSQL UPDATE句ではない)保護された読み取り-変更-書き込みトランザクションは、次のようになります。

_with conn:
    conn.execute('BEGIN TRANSACTION')    # crucial !
    v = conn.execute('SELECT * FROM test').fetchone()[0]
    v = v + 1
    time.sleep(3)  # no read lock in effect, but only one concurrent modify succeeds
    conn.execute('UPDATE test SET i=?', (v,))
_

失敗すると、このような読み取り-変更-書き込みトランザクションは数回再試行される可能性があります。

2
kxr

接続をコンテキストマネージャとして使用できます。その後、例外が発生した場合にトランザクションを自動的にロールバックするか、そうでなければコミットします。

try:
    with con:
        con.execute("insert into person(firstname) values (?)", ("Joe",))

except sqlite3.IntegrityError:
    print("couldn't add Joe twice")

https://docs.python.org/3/library/sqlite3.html#using-the-connection-as-a-context-manager を参照してください

2

これは少し古いスレッドですが、接続オブジェクトでロールバックを行うとうまくいくことがわかりました。

0