Python批量执行SQLite脚本,第二条SQL报错,捕获异常后以为已经全部回滚,重新打开数据库却发现第一条还在。先别给rollback再套一层重试:问题可能发生在事务边界。下面区分旧式控制、连接管理和脚本自带事务,用完整文件数据库实验验证哪些数据会留下。
一、先确认自己使用哪种事务控制
本文针对Python标准库sqlite3,不是其他数据库驱动。示例要求Python 3.12及以上,因为connect的autocommit参数从3.12加入。本轮在CPython 3.14.3、SQLite 3.26.0上执行;这是隔离文件实验,不代表真实迁移任务或断电恢复已经验收。
Python 3.14文档中的默认值仍是LEGACY_TRANSACTION_CONTROL。不要从参数名推断默认就是False,也不要把Connection.autocommit和底层SQLite是否正处于事务混为一谈。排查时记录Python版本、sqlite3.sqlite_version、连接的autocommit与in_transaction,并对照创建连接的代码。
二、旧式模式先提交,回滚无法倒带
在LEGACY_TRANSACTION_CONTROL下,如果调用executescript时已有待处理事务,它会先隐式COMMIT,再运行脚本;除此之外,不替脚本补上事务控制。假设先用execute插入0,再执行两条插入1的SQL:唯一约束让第二次插入失败,但0已经在脚本开始前提交了。
如果脚本本身没有BEGIN,且没有别的显式事务包住它,前面成功的语句还可能分别提交。所以失败后留下0和1并不矛盾:错误确实发生了,已经提交的数据也确实不会被后来的rollback撤销。异常只是一张事故通知单,不是时光机。

三、复制这段实验,重新打开文件核对
把下面完整代码保存为demo.py,用符合版本要求的Python执行。临时目录自动清理,每组使用独立数据库;建表先提交,数据用另一条连接重新查询,避免只看原连接中的未提交视图。0代表脚本前的写入,1代表脚本中的写入。
import sqlite3
import tempfile
from pathlib import Path
def 实验(目录, 名称, 模式, 脚本):
路径 = 目录 / f"{名称}.db"
连接 = sqlite3.connect(路径, autocommit=模式)
try:
连接.execute("CREATE TABLE t(n INTEGER UNIQUE)")
连接.commit() # 先把建表单独提交
try:
with 连接:
连接.execute("INSERT INTO t VALUES (?)", (0,))
连接.executescript(脚本)
except sqlite3.IntegrityError:
pass # 仅供实验检查结果;生产代码应报告失败
finally:
连接.close()
检查 = sqlite3.connect(路径, autocommit=True)
try:
return 检查.execute("SELECT n FROM t ORDER BY n").fetchall()
finally:
检查.close()
with tempfile.TemporaryDirectory() as 临时:
路径 = Path(临时)
脚本 = "INSERT INTO t VALUES(1); INSERT INTO t VALUES(1);"
旧式 = sqlite3.LEGACY_TRANSACTION_CONTROL
assert 实验(路径, "旧式", 旧式, 脚本) == [(0,), (1,)]
assert 实验(路径, "连接管理", False, 脚本) == []
assert 实验(路径, "脚本事务", 旧式,
"BEGIN;" + 脚本 + "COMMIT;") == [(0,)]
assert 实验(路径, "成功", False,
"INSERT INTO t VALUES(1);") == [(0,), (1,)]
print("4组文件数据库断言通过")
四组分别应留下:旧式无BEGIN为0和1;连接管理失败为空;旧式脚本自带BEGIN只剩0;连接管理成功为0和1。控制台的通过信息只表示这四组断言成立,不表示读者的真实业务已经完成。代码故意在with外捕获约束异常,以便查看结果;生产入口仍应报告失败或继续抛出。
四、固定脚本与业务写入,别混用控制权
对于这里没有BEGIN、COMMIT或ROLLBACK的固定脚本,autocommit=False让连接保持事务;让异常传播出with连接上下文,退出时回滚当前事务,正常退出则提交。这样脚本前的0与脚本中的1可以属于同一事务。with不负责关闭连接,所以示例仍显式close。
若迁移脚本已经自行管理事务,就不要机械改成False后再执行BEGIN:连接可能早已开启事务,导致嵌套事务错误。另一种方案是旧式模式下让脚本显式BEGIN/COMMIT,但它保护不了executescript调用前已经被提交的写入。选一套边界,把调用前、脚本内和调用后的数据一起纳入设计。
五、SQL失败,通常不是整段事务自动清空
这里的UNIQUE约束使用默认ABORT行为,撤销失败语句,而不是自动撤销整个事务中更早成功的语句。脚本在错误处停止,也不等于前面的语句都没执行。因此不能只看到IntegrityError就写一条“回滚成功”的日志,必须核对事务仍否开启以及独立连接能看到什么。
尤其别在with内部捕获错误后悄悄继续:上下文可能按正常退出提交剩余数据。若要记录错误,通常在外层捕获,或记录后重新抛出。若脚本主动COMMIT,之后再报错,也不能借外层with收回提交。本轮混用还观察到退出with时出现cannot rollback - no transaction is active,原始约束错误保留在异常链中;排查不能只看最外层报错。写文件、发消息等数据库之外的效果更不在这个回滚范围内。
六、按症状排查,不先增加重试次数
| 现象 | 优先检查 | 处理方向 |
|---|---|---|
| 脚本前的数据还在 | LEGACY调用前是否有事务 | 检查预提交边界,不能靠事后回滚 |
| 脚本前半段留下 | 脚本无BEGIN或主动COMMIT | 重新设计整段事务与异常出口 |
| cannot start a transaction | False模式又运行BEGIN | 避免连接与脚本重复管理 |
| rollback方法似乎无效 | autocommit=True或已经提交 | 区分SQL事务与连接方法语义 |
| 异常后竟正常提交 | with内部是否吞掉异常 | 在外层处理,保留失败信号 |
autocommit=True时,连接的commit和rollback方法不发挥事务控制作用;若选择这个模式自行发BEGIN,就也要用明确的SQL控制提交与回滚,不能只换一个开关。排错日志保留模式、异常类型和阶段即可,不要顺手打印含敏感数据的完整业务SQL。
七、动态数据用参数,不往脚本里拼
executescript适合受信任的多语句脚本,不提供execute那样的参数绑定接口。业务数据优先使用execute或executemany加占位符,在同一套事务规则下执行;不要把用户输入格式化进SQL,也不要用split分号自行解析通用SQL,字符串与注释中同样可能出现分号。
验收至少包括成功提交、第二条失败、调用前已有写入、脚本主动提交、内部吞异常和重新打开文件查询。本文随文交付四组最小对照;内部扩展测试不能代替读者自己的触发器、外键、并发、磁盘错误、崩溃恢复和迁移版本验收。
八、最后确认:提交、回滚和业务成功分开
- 锁定Python版本和连接模式,不依赖含糊的“默认事务”。
- 查清脚本是否携带事务语句,明确谁负责开始与结束。
- 错误从正确的上下文出口传播,连接最终关闭。
- 用独立连接核对最终数据,不仅检查日志和退出码。
- 已提交或外部副作用另做恢复设计,不宣称自动回滚。
真正要回答的不是“有没有调用rollback”,而是“失败时哪些改动仍在可回滚的事务里”。边界定清楚,再讨论是否重跑;否则重试可能只是把半份结果又写一遍。批量脚本能跑完与业务迁移正确,也必须分别验收。
参考资料:Python sqlite3事务与executescript文档;SQLite事务控制;SQLite约束冲突处理。
🔕 评论已关闭