使用Python sqlite3进行交易

时间:2013-04-06 22:58:01

标签: python-2.7 sqlite python-db-api

我正在尝试将一些代码移植到使用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;
        """)

你可能会注意到其中的故意错误。执行更新后,这会导致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和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(),那么的工作(我仍然是99)!

(我还应该补充一点,如果我将begincommit置于executescript的内部调用中,那么它在所有情况下都会正常运行,但不幸的是我不能使用它我的应用程序中的方法。此外,更改sql.isolation_level似乎对行为没有任何影响。)

有人可以向我解释这里发生了什么吗?我需要理解这一点;如果我不能信任数据库中的事务,我就无法使我的应用程序工作......

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

7 个答案:

答案 0 :(得分:29)

对于任何想要使用sqlite3 lib而不管其缺点的人,我发现如果你做这两件事,你可以控制交易:

  1. 设置Connection.isolation_level = None(根据docs,这意味着自动提交模式)
  2. 完全避免使用executescript,因为根据docs,它“首先发出一个COMMIT语句” - 即麻烦。事实上,我发现它会干扰任何手动设置的交易
  3. 那么,以下适合您的测试适用于我:

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

答案 1 :(得分:17)

the docs

  

连接对象可以自动用作上下文管理器   提交或回滚事务。如果发生例外,则   交易回滚;否则,交易已经提交:

因此,如果您在发生异常时让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,)

正如所料。

答案 2 :(得分:13)

Python的DB API试图变得聪明,begins and commits transactions automatically

我建议使用使用Python DB API的数据库驱动程序,例如apsw

答案 3 :(得分:4)

根据我对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时生效。

另一个怪癖是executescriptalways issues a COMMIT before running your script。这很容易弄乱交易,所以你可以选择

  • 在交易中只使用一个executescript而不是其他任何内容,或
  • 完全避免executescript;您可以根据需要多次拨打execute,但需遵守每个execute一个语句的限制。

答案 4 :(得分:2)

您可以将连接用作上下文管理器。然后,它会在发生异常时自动回滚事务或以其他方式提交事务。

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

答案 5 :(得分:2)

正常.execute()正常工作,具有舒适的默认自动提交模式,with conn: ...上下文管理器执行自动提交 OR 回滚 - 除外受保护的读 - 修改 - 写交易,这在本答复的最后解释。

sqlite3模块的非标准conn_or_cursor.executescript()不参与(默认)自动提交模式(因此不能与with conn: ...上下文管理器一起正常工作)但转发脚本而不是生的。因此,它只是在“开始生效”之前提交潜在的待处理自动提交事务开始

这也意味着如果脚本executescript()内部没有“BEGIN”,则无需事务,因此在出错或其他情况下无回滚选项。

因此,对于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,)]
>>> 

脚本未到达“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说“ Out of all关于sqlite / python中的事务写的很多很多东西,这是让我做我想做的唯一事情(对数据库有一个独占的读锁)。“在一个包含一个{的示例的注释中{1}}。虽然sqlite3通常不会进行长阻塞独占读锁定,但实际写回的持续时间除外,但更聪明的5阶段锁定可以实现足够的保护,防止重叠更改。

c.execute("begin")自动提交上下文尚未在5-stage locking scheme of sqlite3中放置或触发足够强的锁以进行受保护的读 - 修改 - 写。只有在发出第一个数据修改命令时才会隐含地进行这种锁定 - 因此为时已晚。 只有明确的with conn:才会触发想要的行为:

  针对数据库的

The first read操作会创建一个SHARED锁   第一个写操作创建一个RESERVED锁。

因此,一般使用编程语言(而不是特殊的原子SQL UPDATE子句)的受保护读 - 修改 - 写事务如下所示:

BEGIN (DEFERRED) (TRANSACTION)

失败时,这样的读 - 修改 - 写事务可能会重试几次。

答案 6 :(得分:0)

这是一个有点老的线程,但如果它有帮助我发现在连接对象上做回滚就可以了。