跳转到主要内容

47 保存点

使用保存点回滚事务的一部分,并从错误中恢复。

保存点是事务中的一个命名位置。你可以使用 ROLLBACK TO SAVEPOINT name 撤销该位置之后完成的工作,同时保留之前的全部工作。保存点是 PostgreSQL 实现类似嵌套事务语义的方式——当你想尝试一个有风险的步骤、从错误中恢复并继续执行,或者应用程序代码模拟嵌套事务时,它们很有用。

本章要点

  • 使用 SAVEPOINT name 设置保存点
  • 回滚到保存点
  • 释放保存点
  • 从事务中的错误恢复
  • 保存点与 BEGIN/COMMIT 的关系

示例

示例 47.1 保存点
-- Setup
BEGIN;
DROP TABLE IF EXISTS sp_demo;
CREATE TABLE sp_demo (id integer PRIMARY KEY);

INSERT INTO sp_demo VALUES (1);
SAVEPOINT after_one;

INSERT INTO sp_demo VALUES (2);
SAVEPOINT after_two;

INSERT INTO sp_demo VALUES (3);

-- Undo only the last insert
ROLLBACK TO SAVEPOINT after_two;

-- After rolling back to after_two, the table has 1 and 2 (not 3)
SELECT * FROM sp_demo ORDER BY id;

-- Release a savepoint when we are sure we won't need it
RELEASE SAVEPOINT after_one;

-- Recovering from an error mid-transaction
SAVEPOINT before_risky;
-- This insert will fail (duplicate)
DO $$ BEGIN
  INSERT INTO sp_demo VALUES (2);
EXCEPTION WHEN unique_violation THEN
  RAISE NOTICE 'duplicate caught — continuing';
END $$;
-- Or, without the PL/pgSQL: after a failing statement, you can ROLLBACK TO before_risky and keep going.

SELECT * FROM sp_demo ORDER BY id;

COMMIT;

DROP TABLE IF EXISTS sp_demo;

SAVEPOINT name 标记一个位置。ROLLBACK TO SAVEPOINT name 撤销保存点之后直到现在的所有操作,但事务会继续——你可以继续进行工作,最终执行 COMMIT。RELEASE SAVEPOINT name 会忘记该保存点(之后不能再回滚到它),但不会撤销任何工作。

应用框架通过保存点模拟“嵌套事务”:当应用代码已经处于事务中又调用 begin_transaction 时,驱动会设置保存点,而不是启动新的顶层事务。内部代码块中的“回滚”会回到该保存点。

错误恢复是一个重要场景。在事务中,任何错误都会使事务进入“中止”状态,此后每条语句都会失败,直到执行 ROLLBACK。使用保存点时,你可以执行 ROLLBACK TO SAVEPOINT before_risky 并继续——事务会恢复到可用状态。

运行示例

运行方式:

$ psql -f source/savepoints.sql postgres
BEGIN
DROP TABLE
CREATE TABLE
INSERT 0 1
SAVEPOINT
INSERT 0 1
SAVEPOINT
INSERT 0 1
ROLLBACK
 id
----
  1
  2
(2 rows)
RELEASE
 ...
COMMIT

常见陷阱

  • 保存点有开销。包含数千个保存点的事务(常见的 ORM 模式)可能开销很大。
  • 执行 ROLLBACK TO SAVEPOINT s 后,保存点 s 本身仍然存在——你可以再次回滚到它。要移除它,请使用 RELEASE。
  • RELEASE SAVEPOINT s 不会“提交”工作——只有包含它的事务执行 COMMIT 才算提交。RELEASE 只是移除标记。
  • 保存点只存在于显式事务中。在显式事务之外执行 SAVEPOINT s; 会报错。
提示

ORM 经常用保存点实现“事务性”测试:每个测试都在一个保存点中运行,测试结束时回滚到该保存点,从而不改变数据库。可以查阅 pytest-postgresql、RSpec 的事务性测试夹具等。

动手练习

添加第二个保存点,插入更多行,然后 ROLLBACK TO 第一个保存点,看看保存点之后的所有工作是否都被撤销。尝试错误恢复模式:BEGIN; INSERT ... SAVEPOINT s; INSERT (bad row); ROLLBACK TO SAVEPOINT s; INSERT (good row); COMMIT;。

下载

SQL 文件:savepoints.sql · 全部示例文件 · 准备工作