2013-04-06 126 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的限制。