SQLite SAVEPOINT:局部失败怎样回滚,RELEASE 什么时候才提交

10-01 4阅读

把一批工作划出可撤回的局部

导入一份清单时,前半段已经通过检查,后半段可能包含重复编号。若业务允许跳过这一小段,就需要撤销这段已经成功写入的记录,同时保留前面的工作。只捕获报错不够:报错之前成功执行的语句,仍可能留在当前事务里。先明确失败单元包含哪些操作,再为它放置保存点,才有可核对的恢复边界。

SQLite 官方规则是:ROLLBACK TO 回到对应保存点建立后的状态,保留该保存点;内部 RELEASE 移除保存点,并不单独提交外层事务。只有释放最外层保存点并使事务栈变空时,RELEASE 才等同提交。普通 ROLLBACK 则撤销整个当前事务。下面通过两次不同位置的 RELEASE 检查这些差异。

SQLite SAVEPOINT:局部失败怎样回滚,RELEASE 什么时候才提交

AI生成概念示意图,非真实界面

用临时数据库观察每个边界

运行环境需要 Python 标准库 sqlite3。请在准备好的实验目录运行;示例在当前目录创建临时数据库,结束后自动清理。连接设置 isolation_level=None,让示例自己用 SQL 明确控制事务,避免阅读时把驱动自动开启事务的时机混进来。先建表,再开始导入;这样最终整批回滚后,空表仍存在,方便继续第二个实验。

import sqlite3
from pathlib import Path
from tempfile import TemporaryDirectory

base = Path.cwd()
with TemporaryDirectory(prefix='savepoint-', dir=base) as work:
    path = Path(work) / 'demo.sqlite'
    db = sqlite3.connect(path, isolation_level=None)
    db.execute('CREATE TABLE item(id INTEGER PRIMARY KEY, note TEXT)')
    ids = lambda: [row[0] for row in db.execute('SELECT id FROM item ORDER BY id')]
    db.execute('BEGIN')
    db.execute("INSERT INTO item VALUES(1, 'accepted')")
    db.execute('SAVEPOINT chunk')
    try:
        db.execute("INSERT INTO item VALUES(2, 'temporary')")
        db.execute("INSERT INTO item VALUES(1, 'duplicate')")
    except sqlite3.IntegrityError:
        db.execute('ROLLBACK TO chunk')
    db.execute('RELEASE chunk')
    print('partial:', ids(), db.in_transaction)
    assert ids() == [1] and db.in_transaction

    db.execute('SAVEPOINT checked')
    db.execute("INSERT INTO item VALUES(3, 'reviewed')")
    db.execute('RELEASE checked')
    print('inner release:', ids(), db.in_transaction)
    assert ids() == [1, 3] and db.in_transaction
    db.execute('ROLLBACK')
    print('outer rollback:', ids(), db.in_transaction)
    assert ids() == [] and not db.in_transaction

    db.execute('SAVEPOINT standalone')
    db.execute("INSERT INTO item VALUES(4, 'committed')")
    db.execute('RELEASE standalone')
    assert not db.in_transaction
    db.close()
    other = sqlite3.connect(path)
    actual = other.execute('SELECT id FROM item').fetchall()
    print('reopened:', actual)
    assert actual == [(4,)]
    other.close()

第一笔编号一位于 chunk 之前。编号二先成功插入,随后再次插入编号一触发主键冲突。局部回滚后,partial 应只剩编号一,并显示 True,表示外层事务还开着。如果把这一步的局部回滚删掉,编号二就会留下,这正好说明“失败那条没写进去”和“整段没有副作用”不是同一个验收条件。

已释放的内部保存点仍受外层控制

接着编号三写入另一个保存点并被 RELEASE。此时 inner release 应包含一和三,事务状态仍是 True。随后主动回滚外层,两个编号全部消失。这段实验很适合放进导入流程的审查:某个子函数返回成功,只代表它完成了本地步骤;只要调用者仍管理外层事务,子函数就不该向外部宣告整批数据已经保存。

最后没有先执行 BEGIN,而是直接创建 standalone。释放它后关闭连接,再从新连接读取,应只见编号四。这里检查的是正常关闭与重新打开后的结果,并未模拟断电,也不声称测过存储设备的持久性保证。应用真正使用时,还需把提交失败、磁盘错误和重试规则纳入整体流程,不能用一条成功查询代替全部可靠性验证。

让失败策略和保存点名字一样明确

这个例子选择了“跳过失败小段、继续保留之前内容”,但有些导入要求整份全成或全败,那就应让异常交给外层处理。保存点是实现策略的工具,不会替产品决定哪些记录可以缺席。实际程序还应该记录被跳过的来源行号与原因,防止用户只看到导入成功,却不知道少了几条业务记录。

  • 人工核对四次输出:局部回滚后是一;内部释放后是一和三;外层回滚后为空;重开数据库后是四。

  • 实验故意只捕获主键冲突。不要据此假设任意数据库异常之后都能继续使用同一个事务。

  • 将保存点名字限定在代码里的固定标识,避免把外部文本直接拼进事务控制语句。

参考资料

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。