SQLite 的 with 块为何没有撤回写入:executescript 的提交边界
批量导入时,常见需求是“全部成功才保存”。如果两次写入之间换用了执行脚本的接口,即使缩进没有改变,事务也可能已经结束。排查这类问题,需要看实际提交发生在哪里。
同一段缩进,不代表同一个事务
本次使用 Python 3.12.14、SQLite 3.53.1,为每组实验新建一个数据库,先建表并提交,再进入被测批次。前三组显式设置 autocommit=sqlite3.LEGACY_TRANSACTION_CONTROL,固定该版本的默认兼容模式,避免把结论依赖于未来默认值。
每组都先在 with con: 内插入标签 before,再执行第二次写入,最后主动抛出异常。异常穿出 with 块后才被捕获。关闭写入连接,再用新只读连接查询,结果如下:
| 第二次操作 | 事务模式 | 异常后留下的标签 |
|---|---|---|
| execute 插入 | LEGACY | 无 |
| executescript 插入 | LEGACY | before、in_script |
| 脚本先 BEGIN 再插入 | LEGACY | before |
| executescript 插入 | autocommit=False | 无 |
连接的上下文管理器会在退出时处理待提交事务;进入 with 本身不会自动开启事务,也不会把此前或期间已经提交的修改重新纳入回滚。
给脚本加 BEGIN,为什么还留下一条
第二组 SQL 跟踪依次出现 BEGIN、第一次 INSERT、COMMIT、脚本内 INSERT。兼容模式下,executescript() 发现已有事务,会先提交它,然后执行脚本;它没有自动把整段脚本包装成一个新事务。本例脚本只有一条 INSERT,执行完就已保存。
第三组把脚本改为 BEGIN; INSERT ...;,异常后脚本内写入被回滚,但 before 仍在。原因是脚本里的 BEGIN 发生得太晚:接口先提交了前一段,新的事务只能覆盖后面的语句。不能靠在脚本文本开头补一个 BEGIN,追回此前的提交。
第四组改用 autocommit=False,同样不含事务控制语句的脚本参与当前事务,两条都被回滚。这个参数从 Python 3.12 起提供,应在代码中明确选择模式;也不能由本例推断任意脚本都能整批撤回,脚本自己包含 COMMIT 时还会改变边界。
批量数据写入,保持接口与异常边界清楚
下面的独立示例采用参数化 executemany(),用重复值触发唯一约束。它只针对重复执行同一条 INSERT 的批量写入,不执行任意 SQL 脚本,也不是 executescript() 的通用替代:
import sqlite3
from contextlib import closing
with closing(sqlite3.connect(":memory:", autocommit=False)) as con:
con.execute("CREATE TABLE task(name TEXT UNIQUE)")
con.commit()
try:
with con:
con.executemany("INSERT INTO task VALUES (?)",
[("A",), ("A",)])
except sqlite3.IntegrityError:
print("拒绝重复,整批回滚")
print(con.execute("SELECT count(*) FROM task").fetchone()[0])
本次输出为“拒绝重复,整批回滚”和 0。捕获异常放在事务 with 外面,让管理器先看到失败并回滚;若在块内吞掉异常再正常退出,前面成功的写入可能被提交。参数占位符用于传值,不能拿它替换表名,也不应把外部数据拼进 SQL 脚本。
外层 closing() 负责关闭连接,因为 with con: 本身不负责关闭。这个连接专用于示例批次,建表后已经提交;复用连接时,要先确认是否存在其他待处理写入,避免一并提交或回滚。
本地观察的范围
2026年11月5日,四组观察与文中代码均实际执行,两个子进程退出0,stderr为空,原始输出、退出码、SQL 跟踪和文件指纹已保存。autocommit=False 在回滚后会重新开启事务,因此本次 in_transaction 仍为真;它不表示被回滚的数据还存在。
全部数据库和内容均为有限合成输入,实验没有接入云服务,也未测并发、锁等待、断电恢复或生产导入。验证支持的是这些模式和语句下的提交结果;完整导入还需要另外约定字段校验、重复处理和失败重试。
- 点赞
- 收藏
- 关注作者
评论(0)