Menu

SQLite 保存点:用 ROLLBACK TO 实现嵌套事务

SQLite 保存点的工作原理:在事务中设置带名字的标记,回滚到这里时不必放弃整个事务。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

保存点是事务里带名字的书签

普通事务是全有或全无的:BEGIN 和 COMMIT 之间的一切,要么一起生效,要么一起作废。这通常正是你想要的,但有时你想要更细的粒度:“试试这批修改;如果出错,只撤销这一批,事务的其余部分继续保留。”

这就是保存点。你放下一个带名字的书签,做一些操作,然后要么保留这些操作(RELEASE),要么倒回到书签处(ROLLBACK TO)。

Ada 的扣款和 Boris 的入账都生效了。误改 Nobody 的那次更新被回滚,事务的其余部分没有丢失。

三条命令

整个 API 只有三条语句:

  • SAVEPOINT name:设置一个书签。
  • RELEASE SAVEPOINT name:保留从书签以来做的操作,并移除书签。
  • ROLLBACK TO SAVEPOINT name:撤销从书签以来的所有操作;书签保留在原处,你可以再试一次。

RELEASE 和 ROLLBACK TO 后面的 SAVEPOINT 这个词可以省略:RELEASE risky 和 ROLLBACK TO risky 都可以。

对最终的数据库来说,step 2 attempt 这一行从未存在过。其他所有操作都生效了。

没有外层事务的保存点

这里有个小转折:你可以不先写 BEGIN 就执行 SAVEPOINT。SQLite 会悄悄为你开启一个事务,最外层的保存点就充当事务本身。对这个保存点执行 RELEASE 会提交,执行 ROLLBACK TO 会倒回但不提交。

这就是为什么保存点有时被称为“命名事务”。但在实际代码中混用两种风格很容易让人困惑,选一种就好。大多数人用显式的 BEGIN ... COMMIT 作为外层边界,只把保存点用作内部的局部撤销点。

嵌套保存点

保存点可以叠加。你可以在一个保存点里再设置一个,回滚内层而不影响外层:

最终内容是 a、b、d。回滚到 inner 删除了 c,但在 inner 之前做的操作(插入 b)完好无损,事务也继续进行。

回滚到外层保存点,同样会丢弃内层所做的一切:这个名字之上的整个栈会一次性展开:

b 和 c 都没了。ROLLBACK TO outer 会倒回自 outer 设置以来的所有操作,包括 inner 和插入 c 的那一步。

为什么要用保存点?

经典场景是处理一批数据,其中个别项允许失败,但不能因此作废整批。把每一项包在一个保存点里;如果失败,就回滚到这个保存点,然后继续:

在真实的应用代码中,错误的插入会抛出错误,应用捕获它,执行 ROLLBACK TO,然后继续。两条正确的行写入成功;错误的那一行不会污染整批数据。

ORM 和迁移工具也是用这种方式实现嵌套事务的:它们并没有真正嵌套 BEGIN 块(SQLite 不允许这样做),而是把嵌套调用映射成保存点。

几点需要注意

刚接触保存点的人常在这些细节上栽跟头:

  • COMMIT 总是提交整个事务。 不管你有多少个未关闭的保存点,COMMIT(或它的别名 END)都会关闭整个外层事务。不要把 RELEASE 当成部分提交;在外层事务提交之前,什么都不会被持久保存。
  • ROLLBACK(不带 TO)会中止一切。 它会结束事务并丢弃所有未关闭的保存点。想让事务继续,就用 ROLLBACK TO name。
  • 保存点会一直保持打开,直到被释放或被回滚越过。 忘记 RELEASE 不会丢数据,只是书签会一直留在那里,直到事务结束。
  • 名字不要求唯一。 如果你设置了两次 SAVEPOINT s,ROLLBACK TO s 会找到最近的那一个。这在递归中很有用;如果是无意的,就容易让人困惑。

下一步:视图

保存点让你能更精细地控制写入。下一步是控制读取的方式:把一条查询保存为一个带名字、可复用的对象,像查询表一样对它 SELECT。这就是视图,下一页就讲它。

常见问题

SQLite 中的保存点是什么?

保存点是你在事务内部设置的一个带名字的标记。之后可以用 ROLLBACK TO 回到这个名字,撤销它之后做的所有操作;也可以 RELEASE 它,保留修改并丢弃这个标记。保存点让你可以把事务的各个部分当作更小的、可恢复的单元来处理。

SQLite 中保存点和事务有什么区别?

事务以 BEGIN 开始,以 COMMIT 或 ROLLBACK 结束。保存点是在事务内部用 SAVEPOINT name 设置的,给你一个局部撤销点。回滚到保存点不会结束外层事务,你可以继续操作,稍后再提交。

SQLite 的保存点可以嵌套吗?

可以。你可以叠加多个不同名字的保存点,ROLLBACK TO outer 会撤销到那一层为止的所有操作,包括其中的内层保存点。名字不必唯一:SQLite 会使用最近一个同名的保存点,这是标准的栈行为。

Coddy 编程语言插图

和 Coddy 一起学编程

开始