2013-04-06 162 views
26

我想將一些代碼移植到使用sqlite數據庫的Python,並且我試圖讓事務工作,並且我變得非常困惑。我真的很困惑,我在其他語言中使用過很多sqlite,因爲它很棒,但我根本無法弄清楚這裏有什麼問題。與Python的交易sqlite3

這裏是我的測試數據庫(被饋送到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; 
     """) 

您可能會注意到它的故意錯誤。這會導致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() 

這表現在完全相同的方式---我會從99變到1

現在我打電話BEGIN和明確承諾:

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(),那麼它工作(我仍爲99)!

(我還要補充一點,如果我把begincommit內調用內部executescript那麼它在所有情況下都正確的行爲,但遺憾的是我不能用我的應用程序的方法。此外,改變sql.isolation_level出現對行爲沒有影響。)

有人可以向我解釋發生了什麼嗎?我需要明白這一點;如果我不能在數據庫中信任的交易,我不能讓我的應用程序的工作...

的Python 2.7,蟒蛇-sqlite3的2.6.0,sqlite3的3.7.13,Debian的。

回答

12

Python的DB API嘗試變得聰明,並且begins and commits transactions automatically

我會推薦使用數據庫驅動程序,它不會而不是使用Python DB API,如apsw

+1

謝謝你,apsw正是我要找的。儘管如此,我仍然感到困惑。如果python-sqlite3的事務處理被破壞,爲什麼沒有人注意到並修復它,因爲它似乎是Python的默認Sqlite綁定?交易是否是任何SQL庫的核心競爭力? – 2013-04-09 11:04:01

+0

它是破解的* Python * API;並且出於向後兼容性的原因它不能輕易改變。 – 2013-04-09 16:27:23

+0

那麼,我的問題仍然存在---爲什麼Python API會這樣做? – 2013-04-09 22:39:24

21

對於任何想用sqlite3的LIB工作,無論它的缺點是誰,我發現,你可以保持交易的一些控制,如果你做到以下兩點:

  1. 設置Connection.isolation_level = None(按照docs,這意味着自動提交模式)
  2. 完全避免使用executescript,因爲根據docs它「首先發出COMMIT語句」 - 即麻煩。事實上,我發現它與任何手動設置交易

那麼接下來干擾,測試以下改編爲我工作:

import sqlite3 

sql = sqlite3.connect("/tmp/test.db") 
sql.isolation_level = None 
try: 
    c = sql.cursor() 
    c.execute("begin") 
    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") 
+0

我想這不是線程安全嗎? – hayavuk 2014-11-28 14:39:58

+0

我認爲它應該是,因爲你會在fnord失敗,然後運行回滾。 – rsaxvc 2015-01-25 22:06:30

+0

謝謝。除了在sqlite/python中編寫的關於事務的很多很多事情之外,這是唯一讓我做我想做的事情(對數據庫有獨佔讀鎖)。 – 2015-09-21 03:56:13

15

the docs,可用於

連接對象作爲上下文管理器自動提交或回滾事務。如果發生異常,則回滾事務 ;否則,交易提交:

因此,如果讓Python在發生異常時退出with-語句,則事務將被回滾。

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

如預期。

+0

+1,除了不管由'執行腳本'執行的任何內容,即使它在'with-statement'塊中都不會被回滾,因爲'performcript'首先發出一個COMMIT。 – MLister 2015-05-07 03:43:55

1

正常.execute()的工作與舒適的默認自動提交模式和with conn: ...上下文管理做預期自動提交回滾 - 除了保護的讀 - 修改 - 寫交易,這是在這個答案的最後解釋。

sqlite3模塊的非標準conn_or_cursor.executescript()不參與(默認)自動提交模式(因此不會與with conn: ...上下文管理器正常工作),但轉發腳本相當原始。爲此,它只是犯了潛在未決自動提交在啓動交易「走出原始」之前。

這也意味着,如果沒有一個「BEGIN」無交易executescript()作品的腳本中,從而出現錯誤或以其他方式沒有回退選項。

所以用executescript()我們最好使用一個明確的BEGIN(就像您的原始模式創建腳本爲「原始」模式的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,)] 
>>> 

腳本沒有達到「提交」。因而我們可以認爲目前的中間狀態,並決定回滾(或仍然提交)

這樣一個工作嘗試 - 除了回滾通過excecutescript()看起來是這樣的:

>>> 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()接手提交控制)


這裏了一份關於自動提交模式結合一個保護的讀 - 修改 - 寫TRAN更困難的問題saction - 這使得@Jeremie說「在sqlite/python中編寫的關於事務的所有很多很多事情中,這是唯一讓我做我想做的事情(對數據庫有獨佔讀鎖)。「在一個包含c.execute("begin")的例子中發表評論雖然sqlite3通常不會產生一個長的阻塞排它讀鎖,除了實際回寫的持續時間,但更聰明的5階段鎖實現足夠的保護以防止重疊更改。

with conn:自動提交背景已經不把或引發足夠的鎖強在5-stage locking scheme of sqlite3保護的讀 - 修改 - 寫這樣的鎖隱含提出,只有當第一個數據修改命令發佈 - 因此太 只有明確的BEGIN (DEFERRED) (TRANSACTION)觸發想要的行爲:

The first read對數據庫的操作會創建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,)) 

失敗時這種讀 - 修改寫入事務可能會重試幾次。

1

下面是我認爲基於我閱讀Python的sqlite3綁定以及官方Sqlite3文檔發生的事情。簡短的回答是,如果你想有一個合適的交易,你應該堅持這個成語:

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

出乎我的直覺,with connection在進入範圍不通話BEGIN。其實它doesn't do anything at all in __enter__。只有當你的__exit__範圍choosing either COMMIT or ROLLBACK depending on whether the scope is exiting normally or with an exception時纔有效果。

因此,正確的做法是始終使用BEGIN明確標記您的交易的開始。這使isolation_level在交易中不相關,因爲幸運的是它只有一個效果,而autocommit mode is enabledautocommit mode is always suppressed within transaction blocks

另一個怪癖是executescript,其中always issues a COMMIT before running your script。這很容易搞亂了交易,所以你的選擇是要麼

  • 使用只有一個executescript交易,沒有別的內,或
  • 避免executescript完全;您可以根據需要多次撥打execute,但須遵守單聲明每個execute的限制。