45

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

其行为方式完全相同 --- 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()但是,如果我替换对to的调用c.executescript(),那么它可以工作(我保持在 99)!

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

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

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

4

7 回答 7

46

对于任何想使用 sqlite3 库而不管它的缺点的人,我发现如果你做这两件事,你可以保持对事务的一些控制:

  1. 设置Connection.isolation_level = None(根据文档,这意味着自动提交模式)
  2. 完全避免使用executescript,因为根据文档,它“首先发出 COMMIT 语句” - 即麻烦。事实上,我发现它会干扰任何手动设置的交易

那么,您的测试的以下改编对我有用:

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")
于 2014-05-13T14:58:30.997 回答
19

根据文档

连接对象可以用作自动提交或回滚事务的上下文管理器。如果发生异常,事务回滚;否则,事务被提交:

因此,如果你让 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,)

正如预期的那样。

于 2014-11-01T19:54:33.797 回答
14

Python 的 DB API 试图变得聪明,并自动开始和提交事务

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

于 2013-04-07T09:10:04.927 回答
10

根据我对 Python 的 sqlite3 绑定以及官方 Sqlite3 文档的阅读,这是我认为正在发生的事情。简短的回答是,如果你想要一个正确的交易,你应该坚持这个成语:

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

与我的直觉相反,with connection要求进入范围。事实上,它. 它仅在您选择作用域时才有效,根据作用域是正常退出还是异常退出来选择选择。BEGIN__enter____exit__COMMITROLLBACK

因此,正确的做法是始终with connection使用BEGIN. 这在块内变得isolation_level 无关紧要,因为幸运的是它仅在启用自动提交模式时才有效,并且自动提交模式始终在事务块内被抑制

另一个怪癖是executescript,它总是在运行脚本之前发出 aCOMMIT。这很容易弄乱事务with connection块,所以你的选择是

  • executescript在块内只使用一个,with而不是别的,或者
  • 完全避免executescript;您可以execute根据需要多次调用,但execute仅限于一个语句。
于 2017-06-09T02:42:49.677 回答
2

Normal.execute()使用舒适的默认自动提交模式和with conn: ...执行自动提交回滚的上下文管理器按预期工作 - 除了受保护的 read-modify-write transactions,这在本答案的末尾进行了解释。

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

这也意味着脚本中没有“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,)]
>>> 

脚本没有到达“COMMIT”。因此我们可以查看当前的中间状态并决定回滚(或提交)

因此,一个有效的 try-except-rollback viaexcecutescript()看起来像这样:

>>> 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 阶段锁可以实现对重叠更改的足够保护。

sqlite3 的 5 阶段锁定方案中,with conn:自动提交上下文尚未放置或触发足够强的锁以保护 read-modify-write 。只有在发出第一个数据修改命令时才会隐含地进行这种锁定 - 因此为时已晚。只有一个明确的触发想要的行为:BEGIN (DEFERRED) (TRANSACTION)

对数据库的第一个读操作创建一个共享锁,第一个写操作创建一个保留锁。

因此,以一般方式(而不是特殊的原子 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,))

失败后,此类读取-修改-写入事务可能会重试几次。

于 2017-05-18T12:33:54.900 回答
1

您可以将连接用作上下文管理器。然后它将在发生异常时自动回滚事务,否则将提交它们。

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

于 2017-01-16T02:59:34.647 回答
-1

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

于 2018-04-08T17:03:48.430 回答