DEBUG.WIKI
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
雨云;给项目一台云服务器
RackNerd;$21.99/年 · 1GB KVM

Python sqlite3 执行多份脚本后 rollback 没有撤销前面的写入

sqlite3 的 legacy 模式会在 executescript 前提交待处理事务。明确 Python 3.12 之后的 autocommit=False,或在一次完整脚本中显式控制事务,并检查失败后的实际数据。

用 Python 的 sqlite3.executescript() 逐个执行 SQL 文件,最后一个文件报错后调用 rollback(),前面的写入却仍然存在。把所有文件拼成一次调用,也不一定能回滚:如果脚本没有真正处于一笔事务中,前面的语句可能早已各自提交。

这里需要同时看 Python 驱动的事务模式和脚本内部的事务语句。Python 3.12 引入了 Connection.autocommit;不能把旧版“executescript 会先提交”的规则无条件推广到所有新模式。

legacy 模式会在脚本前提交待处理事务

在 Python 3.11 的事务控制方式,以及 Python 3.12 及之后设置 autocommit=sqlite3.LEGACY_TRANSACTION_CONTROL 的连接中,executescript() 会先提交已有的待处理事务,再执行脚本。修改 isolation_level 不能取消 legacy 模式的这一步。

因此,在循环外执行一次 BEGIN,随后多次调用 executescript(),不能保证所有文件都属于最初那笔事务。脚本执行本身也没有自动替你包一层完整的事务。

先检查连接创建代码、Python 版本,以及每份脚本是否含 BEGIN、COMMIT、ROLLBACK 或其他事务控制。不要为观察效果在真实业务库中故意制造失败。

Python 3.12 及之后可明确使用 autocommit=False

如果准备由 Python 统一提交或回滚,可在新连接建立时明确使用 autocommit=False。这个模式会维持一个已打开的事务,executescript() 不再触发 legacy 模式的预提交。前提是传入脚本不自行结束或重新开启事务,也不包含不能放在事务内执行的维护语句。

以下完整例子只连接内存数据库,用重复主键制造可控错误,检查前一个脚本的插入是否一起回滚:

import sqlite3

con = sqlite3.connect(":memory:", autocommit=False)
try:
    con.execute("CREATE TABLE demo (id INTEGER PRIMARY KEY, value TEXT)")
    con.commit()

    scripts = [
        "INSERT INTO demo VALUES (1, 'first');",
        "INSERT INTO demo VALUES (1, 'duplicate');",
    ]
    try:
        for script in scripts:
            con.executescript(script)
        con.commit()
    except sqlite3.Error:
        con.rollback()

    assert con.execute("SELECT count(*) FROM demo").fetchone()[0] == 0
finally:
    con.close()

此例要求 Python 3.12 或更新版本,断言是预期验收条件。建表之后先提交,确保验证的是这批数据操作,而不是把测试表也纳入回滚。

真实程序回滚后通常还应把异常重新抛出或记录为失败,不能像测试例子一样只验证行数后继续业务。连接关闭应放在清理路径;关闭连接不等于成功提交。

不要在已有待处理业务事务中随意切换为 autocommit=True。该模式不是“手动控制提交”的同义词,连接的 commit() 和 rollback() 在这个模式下没有作用。

保留 legacy 时,把事务放入一次完整脚本

无法立即调整连接模式时,可以将经过审查的多份脚本合成一次调用,在脚本开头加入 BEGIN;,末尾加入 COMMIT;。使用专门的新连接,避免调用前还有另一笔业务写入被隐式提交:

import sqlite3

con = sqlite3.connect(":memory:", isolation_level=None)
try:
    con.execute("CREATE TABLE demo (id INTEGER PRIMARY KEY)")
    script = """
BEGIN;
INSERT INTO demo VALUES (1);
INSERT INTO demo VALUES (1);
COMMIT;
"""
    try:
        con.executescript(script)
    except sqlite3.Error:
        if con.in_transaction:
            con.execute("ROLLBACK")

    assert con.execute("SELECT count(*) FROM demo").fetchone()[0] == 0
finally:
    con.close()

这段采用 Python 3.11 的连接写法;在 Python 3.12 及之后保留此方案时,还应在 connect() 中显式传入 autocommit=sqlite3.LEGACY_TRANSACTION_CONTROL,不依赖未来版本的默认值。

合并脚本需要保留文件顺序,并确认各文件本身没有事务控制语句。只拼接内容却不加入事务,不能获得整批原子性;在每个文件里分别加入 BEGIN 和 COMMIT,也只是多个独立事务。

若希望逐条 execute(),应由已有 SQL 解析或迁移机制提供完整语句,不要简单按分号切字符串:字符串字面量、注释和触发器体内都可能出现分号。executemany() 用来对同一条参数化语句执行多组参数,不是任意多语句脚本的替代品。

已经提交的内容不能靠补一次 rollback 恢复

修复前保留一致的数据备份和迁移记录,先确认本次失败前哪些语句已经提交。不要直接重跑包含建表、写入或删除的全部文件;部分成功可能使重试产生冲突或重复数据。

在隔离副本中分别验证正常完成和中途失败两条路径,并检查实际业务记录,而不只检查 Python 是否抛出异常。回退连接配置只恢复旧行为,不撤销已提交数据;已落库的修改需按记录和备份另行制定恢复方式。