Python sqlite3 批量写入:失败时回滚整批数据

举报
yd_254857454 发表于 2026/11/01 11:06:35 2026/11/01
【摘要】 executemany() 能批量执行参数化 SQL,却不自动等于“整批成功或整批失败”。本文用独立内存数据库验证显式事务、约束异常和回滚,说明 Python 3.12 前后的事务接口差异,以及嵌套事务和并发写入的边界。

一、批量接口不等于业务事务

导入物料清单时,一批记录可能包含重复编号。业务要求是“这一批全部写入,或者一条也不写”,但把列表交给 executemany(),并不能单凭这个调用形式得到保证。它会反复执行同一条参数化语句;执行到中间失败时,前面语句的结果是否提交,还取决于连接的事务配置和异常处理。

SQLite 默认的 ABORT 冲突处理会撤销发生约束错误的那条语句,但不会因此自动撤销同一事务里之前成功的语句。在没有显式事务、连接又处于 SQLite 自动提交状态时,先前完成的插入甚至已经提交。捕获异常后返回“导入失败”,也不会把这些记录自动还原。

因此,先确定业务提交边界,再选择批量接口。下面让一个函数完整拥有一次事务:开始、批量写入、提交;中途出现异常则回滚,并继续把异常交给调用方。示例只连接 :memory:,不读取或修改任何已有数据库文件。

二、明确连接配置,再写提交逻辑

Python 的事务控制接口存在版本差异。3.12 之前主要通过 isolation_level 控制模块何时隐式开始事务;设置为 None,模块不再隐式开启事务,可以自行发送 SQL 事务命令。3.12 新增 autocommit,只有它处于 LEGACY_TRANSACTION_CONTROL 模式时,isolation_level 才控制这套旧行为。

下面在提供新参数的版本中显式选择旧事务控制,再关闭隐式开启。这样不依赖新版本的默认值。这个选择服务于演示显式 SQL 边界,并不表示所有应用都应该使用旧模式;如果项目统一采用 autocommit=False,就应围绕该模式另行设计事务,不要直接混用本函数。

import sqlite3


def connect_demo():
    options = {"isolation_level": None}
    if hasattr(sqlite3, "LEGACY_TRANSACTION_CONTROL"):
        options["autocommit"] = sqlite3.LEGACY_TRANSACTION_CONTROL
    return sqlite3.connect(":memory:", **options)


def insert_batch(con, rows):
    if con.in_transaction:
        raise RuntimeError("caller already has an active transaction")
    con.execute("BEGIN")
    try:
        con.executemany(
            "INSERT INTO items(code, quantity) VALUES (?, ?)", rows
        )
        con.execute("COMMIT")
    except BaseException:
        if con.in_transaction:
            con.execute("ROLLBACK")
        raise

BEGIN 放在 try 前面:开始事务本身失败时,函数还没有取得一个应由它回滚的事务。提交则包含在 try 内;提交报错也不会被当成成功返回。异常分支先检查 in_transaction,因为某些 SQLite 错误可能已经使事务自动回滚,再无条件发送 ROLLBACK 反而会产生新的错误。

这里捕获 BaseException 只用于清理并原样继续抛出,也覆盖中断等情况;没有吞掉错误或擅自重试。回滚本身仍可能因底层故障失败,实际项目应同时保留原始异常和回滚异常,停止复用状态不明的连接。这个短示例不能代替完整的存储故障恢复策略。

SQL 中的 ? 用于绑定记录值,不通过字符串拼接生成 SQL。参数绑定处理的是值,不是动态表名或字段名;需要动态结构时,应单独使用可信白名单,而不是把任意标识符塞进占位符。

三、用中间一条失败验证整批回滚

将下面代码接在前一个代码块后运行。先提交一条基准记录,再让新批次的第二条与它冲突。检查的不只是捕获了异常,还包括第一条新记录没有留下、原记录没有改变,以及连接已经退出事务。

con = connect_demo()
try:
    con.execute("""
        CREATE TABLE items(
            code TEXT NOT NULL PRIMARY KEY,
            quantity INTEGER NOT NULL CHECK(quantity >= 0)
        )
    """)
    insert_batch(con, [("seed", 1)])
    try:
        insert_batch(con, [("new-a", 2), ("seed", 9), ("new-b", 3)])
    except sqlite3.IntegrityError:
        pass
    else:
        raise AssertionError("duplicate code should fail")

    assert con.execute("SELECT * FROM items").fetchall() == [("seed", 1)]
    assert not con.in_transaction
    insert_batch(con, [("new-a", 2), ("new-b", 3)])
    assert con.execute("SELECT COUNT(*) FROM items").fetchone()[0] == 3
    print("失败批次已回滚,正确批次已提交")
finally:
    con.close()

本文代码已在 macOS、Python 3.9.6、SQLite 3.51.0 的独立内存数据库中运行。完整验证还覆盖了数量 CHECK 约束失败、输入迭代器在产出一条记录后抛错,以及调用方已开启事务三种情况。前两项没有留下该批次的部分插入;最后一项在进入函数时被拒绝,调用方已有事务保持有效,未被本函数回滚。

3.12 及以上的连接分支依据官方参数语义编写,本次没有在这些版本实际运行,也没有进行断电或磁盘故障测试。约束示例不是完整业务校验:SQLite 普通表的类型行为、编号是否允许空文本、业务数量范围等仍需另行规定。

四、不要让事务边界悄悄扩大

本函数拒绝已经处于事务中的连接,是为了避免替上层提交或回滚不属于这批数据的改动。SQLite 的 BEGIN 事务不能直接嵌套;需要在外层事务中隔离一段操作时,应单独设计 SAVEPOINT 方案,明确谁拥有最终提交权,不能把入口检查简单删掉。

也不要在这段流程中混入 executescript()。在旧事务控制模式下,它可能先提交已有事务,然后执行脚本;脚本若需要原子性,必须按文档明确组织自己的事务。Python 从 3.6 起也不再因执行 DDL 而自动提交既有事务,不能沿用更早版本的印象推断边界。示例在批次开始前创建表,批次里只做参数化插入。

with con: 是另一个常见混淆点:连接上下文管理器用于处理事务的提交或回滚,并不负责关闭连接,也不会替所有配置自动开启事务。本文使用显式 SQL,所以也显式在 finally 关闭连接,不把两种控制方式叠在一起。

并发环境还需要处理锁竞争。SQLite 可以有多个读事务,但同一时刻只有一个写事务;BEGIN IMMEDIATE 可以更早尝试取得写事务,却不能消除锁冲突。本例使用普通 BEGIN,没有设计重试。调用方应先准备和校验输入,缩短持锁时间,再根据错误类型及业务幂等性决定是否重新提交完整批次,不要从失败行之后盲目续写。

对于大批量导入,拆批会改变业务承诺:每批单独提交时,后面失败不会撤销前面已提交的批次。是否允许部分成功,应由接口约定决定。事务能保护的是明确划定的数据库更新边界,外部文件、网络请求和其他系统的副作用不会随这次 SQLite 回滚一起撤销。

参考:Python 标准库 sqlite3 文档的事务控制、executemany()、参数绑定和连接上下文管理器章节;SQLite 文档的 Transaction、ON CONFLICT 与 SAVEPOINT 章节。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。