保存点是事务里带名字的书签
普通事务是全有或全无的: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 会使用最近一个同名的保存点,这是标准的栈行为。