【问题标题】:Transactions with Python sqlite3使用 Python sqlite3 进行交易
【发布时间】:2013-03-29 05:29: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() 的调用替换为 c.executescript(),那么它有效(我仍然保持在 99)!

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

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

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

【问题讨论】:

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


    【解决方案1】:

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

    1. 设置Connection.isolation_level = None(根据docs,这意味着自动提交模式)
    2. 完全避免使用executescript,因为根据docs,它“首先发出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")
    

    【讨论】:

    • “自动提交”模式是什么意思?根据它的名字,我猜任何类型的 MDL 语句都会自动提交。但根据你的解释,我的猜测似乎是错误的。谁能帮帮我?我在谷歌搜索它,也找不到任何有用的答案。
    • ↑ 我可以回答这个的自动提交部分。 isolation_level = None 禁用 Python 包装器为您发出 BEGIN 等的自动处理。剩下的是底层 C 库,默认情况下它会执行“自动提交”。但是,当您执行BEGIN(b/c 您正在使用该语句发出事务信号)时,该自动提交被禁用,这就是上述工作方式的原因。 SQLite's Docs on this
    • 我摆弄了 2 行,因为如果他们扔了你就不想做回滚 - 但一个有用的答案!
    • 我会补充一点,您也可以设置事务级别。例如c.execute("begin exclusive transaction")
    • 我很困惑。 c.execute("commit")sql.commit() 有什么区别?
    【解决方案2】:

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

    正如预期的那样。

    【讨论】:

    • +1,除了executescript 执行的任何内容都不会回滚,即使它在'with-statement' 块中也不会回滚,因为executescript 首先发出一个COMMIT。
    • -1 为每个事务打开一个新连接是非常低效且完全没有必要的。您也没有说事务何时开始(例如,您是否知道您的第一个块,删除并创建表,实际上有三个单独的事务?)
    【解决方案3】:

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

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

    【讨论】:

    • 谢谢,apsw 正是我要找的。不过,我仍然很困惑;如果 python-sqlite3 的事务处理被破坏了,为什么没有人注意到并修复它,因为它似乎是 Python 的默认 Sqlite 绑定?事务肯定是任何 SQL 库的核心能力吗?
    • 其他一些数据库使用它们的 Python 驱动程序是这样工作的。 Python DBAPI 规范试图让它看起来所有数据库功能相同,语义相同,这就是 pysqlite 以这种方式工作的原因。当然,SQLite 非常不同,这就是我首先编写 APSW 的原因。见apidoc.apsw.googlecode.com/hg/pysqlite.html
    • 那么在sqlite3 库中设置isolation_level = None(根据@yungchin 的回答)可以解决这个问题吗?还是使用APSW更好? (如果重要的话,python 3.5。)
    • 好吧,问题是相反的:executescript() 实际上是以原始方式工作的,所以你需要在脚本中“像往常一样”在脚本中 BEGIN ... COMMIT 以在 except 中显式 ROLLBACK 选项。使用智能.execute() 和其他功能以及with conn: 通常也没有问题——它更舒适、安全和一致。仅对于受保护的读-修改-写事务,显式 BEGIN 是必要的。在我的另一篇文章中查看详细信息。 isolation_level = None 已经是默认值了。
    • 从 python3.6 开始,文档如下: >3.6 版更改:sqlite3 用于在 DDL 语句之前隐式提交打开的事务。现在已经不是这种情况了。
    【解决方案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 明确标记事务性with connection 块的开头。这会在块内渲染isolation_level 不相关,因为幸运的是它只在autocommit mode is enabledautocommit mode is always suppressed within transaction blocks 时有效。

    另一个怪癖是executescript,即always issues a COMMIT before running your script。这很容易弄乱事务性with connection 块,所以你的选择是要么

    • with 块内只使用一个executescript,仅此而已,或者
    • 完全避免executescript;您可以根据需要多次调用execute,但需遵守每个execute 一个语句的限制。

    【讨论】:

    • "在一个事务中只使用一个executescript" 这是不对的,因为正如你已经说过的,executescript 事先发出一个提交,所以把它放在 within交易是不可能的。我认为您的意思是相反的:将交易放在 executescript.
    • 我认为当我写“交易”时,我的意思是“单个with connection: 块”。
    • 良好的研究以获得简洁的行为:“因此,正确的做法是始终使用 BEGIN 使用连接块明确标记事务的开始”
    【解决方案5】:

    正常 .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() 进行的有效尝试除外回滚看起来像这样:

    >>> 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 中写了很多关于事务的东西,这是唯一能让我做我想做的事(在数据库上有一个独占读锁)。”在对包含 @ 的示例的评论中987654333@。虽然 sqlite3 通常不会在实际回写期间进行长阻塞排他读锁,但更聪明的 5 阶段锁可以实现对重叠更改的足够保护。

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

    The first read 对数据库的操作会创建一个共享锁和 第一个写操作会创建一个保留锁。

    所以一个受保护的读-修改-写事务以一般方式使用编程语言(而不是特殊的原子 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 模块自动启动事务的做法。但是这个短语意味着一些非常具体和非常不同的东西(几乎相反!):底层的 sqlite 库本身会自动在每个单独的语句周围放置一个小型事务,而不是在事务中。因此,每当 Python 模块自动启动事务时,它实际上使数据库退出自动提交模式!但是您是对的,SELECT 语句不会自动启动事务。
    【解决方案6】:

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

    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

    【讨论】:

    • 问题主要是关于交易何时开始。事实上,您正在回答的问题在他们的第一个示例中已经使用连接作为上下文管理器。
    • @Gabriel Saca 不起作用。
    【解决方案7】:

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-05-06
      • 1970-01-01
      • 2011-04-07
      • 2016-09-10
      • 2012-05-08
      • 1970-01-01
      相关资源
      最近更新 更多