SQLite OR FAIL 与 ABORT:一条插入报错,前面写进去的行为什么还在
一次多行插入遇到重复编号,应用收到 IntegrityError,便认为这条语句一行也没写入。使用默认的 ABORT 时这个判断通常符合本例,但如果 SQL 明确写了 OR FAIL,失败点之前的成功修改会留下来。只捕获异常而不检查数据状态,可能让重试过程重复处理已经完成的一部分。
下面分别建立两个内存数据库,在同一事务中先插入编号九,再运行包含重复编号的多行语句。保存为 demo.py,运行 python demo.py;实验结束主动回滚并关闭连接,不接触任何已有数据库。
AI生成概念插图:遇到停止标记后,一条路径保留已完成行,另一条路径撤回当前语句的修改;不是数据库界面截图。
import sqlite3
def run_case(mode):
assert mode in ("ABORT", "FAIL")
db = sqlite3.connect(":memory:", isolation_level=None)
try:
db.execute("CREATE TABLE item(id INTEGER PRIMARY KEY)")
db.execute("BEGIN")
db.execute("INSERT INTO item VALUES (9)")
try:
db.execute(
f"INSERT OR {mode} INTO item VALUES (1), (2), (2), (3)"
)
except sqlite3.IntegrityError:
pass
else:
raise AssertionError("duplicate unexpectedly accepted")
rows = [r[0] for r in db.execute("SELECT id FROM item ORDER BY id")]
expected = [9] if mode == "ABORT" else [1, 2, 9]
assert rows == expected
assert db.in_transaction
print(mode, "after error:", rows, "active:", db.in_transaction)
db.execute("ROLLBACK")
assert list(db.execute("SELECT id FROM item")) == []
print(mode, "after rollback: []")
finally:
db.close()
run_case("ABORT")
run_case("FAIL")异常相同,留下的状态不同
ABORT 分支输出 [9]:编号一和二属于失败的当前语句,因此被撤回;编号九来自同一事务中的上一条成功语句,所以仍然存在。FAIL 分支输出 [1, 2, 9]:冲突发生时停止继续插入,但不撤回该语句先前成功的两行。冲突之后的编号三也没有被处理。
这里主动使用 ORDER BY 只为稳定显示结果,不依赖数据库碰巧返回的顺序。两组输入使用完全相同的主键约束,差别仅在固定的冲突策略。代码中的字符串插值只接受断言白名单中的两个策略名;真实应用不能把未经约束的用户输入直接拼进 SQL。
语句结束,不表示事务结束
两次异常后 in_transaction 都为 True。ABORT 撤回当前语句,FAIL 留下已执行部分,但它们在本例中都没有替应用决定整个事务的最终去向。随后显式执行 ROLLBACK,编号九也一起消失,因为它和后续插入仍属于同一笔尚未提交的事务。
如果业务要求这一批导入全部成功或全部取消,应在事务边界明确处理失败,并核验回滚后的状态。不要在捕获 IntegrityError 后直接继续 COMMIT,然后向调用方只返回一个笼统的失败提示;这样保存的数据和对方理解的结果可能不同。需要允许部分成功时,则应记录哪些行真正保留。
这份实验刻意使用单条 INSERT 的多个 VALUES。把它换成 Python 的 executemany,会变成多次执行语句,错误边界也随之改变,不能继续套用“当前语句里的前两行”解释。排查前先确认底层发送了几条 SQL,以及事务从哪一行开始。
本文验证的是主键唯一性冲突。外键错误有不同限制,不能假设 OR FAIL 会保留同样的部分结果。也不要把 FAIL 理解成 IGNORE:FAIL 会报错并停止这条语句,IGNORE 对适用的约束可能跳过冲突行并继续。选择策略应依据可接受的数据状态,而不只是希望错误消息消失。
资料核对日期:2026年10月2日。代码在本地 Python 3.12.14、SQLite 3.53.1 实际运行并通过断言;结果只对应文中给定输入。


