Postgres 提交是否可以存在于具有异常块的过程中?
Can a Postgres Commit Exist in Procedure that has an Exception Block?
我很难理解 Postgres 中的事务。我有一个程序可能会遇到异常。到目前为止,我可能希望在过程的某些部分提交我的工作,以便在出现异常时不会回滚。
我想在过程结束时有一个异常处理块,我在其中捕获异常并将来自异常的信息插入到日志记录中 table。
我已将问题归结为下面的一个简单过程,该过程在 PostgreSQL 11.2 上失败
2D000 cannot commit while a subtransaction is active
PL/pgSQL function x_transaction_try() line 6 at COMMIT
drop procedure if exists x_transaction_try;
create or replace procedure x_transaction_try()
language plpgsql
as $$
declare
begin
raise notice 'A';
-- TODO A: do some insert or update that I want to commit no matter what
commit;
raise notice 'B';
-- TODO B: do something else that might raise an exception, without rolling
-- back the work that we did in "TODO A".
exception when others then
declare
my_ex_state text;
my_ex_message text;
my_ex_detail text;
my_ex_hint text;
my_ex_ctx text;
begin
raise notice 'C';
GET STACKED DIAGNOSTICS
my_ex_state = RETURNED_SQLSTATE,
my_ex_message = MESSAGE_TEXT,
my_ex_detail = PG_EXCEPTION_DETAIL,
my_ex_hint = PG_EXCEPTION_HINT,
my_ex_ctx = PG_EXCEPTION_CONTEXT
;
raise notice '% % % % %', my_ex_state, my_ex_message, my_ex_detail, my_ex_hint, my_ex_ctx;
-- TODO C: insert this exception information in a logging table and commit
end;
end;
$$;
call x_transaction_try();
为什么这个存储过程不起作用?为什么我们从来没有看到 raise notice 'B'
的输出,而是进入了异常块?是否可以使用 Postgres 11 存储过程执行我上面描述的操作?
编辑:这是一个完整的代码示例。将上面的完整代码示例(包括 create procedure
和 call
语句)粘贴到 sql 文件中,然后将其 运行 粘贴到 Postgres 11.2 数据库中进行重现。期望的输出是函数打印 A
然后 B
,但它打印 A
然后 C
以及异常信息。
另请注意,如果您注释掉所有异常处理块,使函数根本不捕获异常,那么函数将输出 'A' 然后 'B' 而不会发生异常。这就是为什么我将问题命名为 'Can a Postgres Commit Exist in Procedure that has an Exception Block?'
问题出在 EXCEPTION
子句上。
这在 PL/pgSQL 中作为 子事务 实现(与 SQL 中的 SAVEPOINT
相同),回滚当到达异常块时。
当子事务处于活动状态时,您不能COMMIT
。
在 src/backend/executor/spi.c
中查看此评论:
/*
* This restriction is required by PLs implemented on top of SPI. They
* use subtransactions to establish exception blocks that are supposed to
* be rolled back together if there is an error. Terminating the
* top-level transaction in such a block violates that idea. A future PL
* implementation might have different ideas about this, in which case
* this restriction would have to be refined or the check possibly be
* moved out of SPI into the PLs.
*/
if (IsSubTransaction())
ereport(ERROR,
(errcode(ERRCODE_INVALID_TRANSACTION_TERMINATION),
errmsg("cannot commit while a subtransaction is active")));
PL/pgSQL 的 error handling 的语义规定:
When an error is caught by an EXCEPTION clause ... all changes to persistent database state within the block are rolled back.
这个是使用子事务实现的,和savepoints基本一样。换句话说,当您 运行 以下 PL/pgSQL 代码时:
BEGIN
PERFORM foo();
EXCEPTION WHEN others THEN
PERFORM handle_error();
END
...实际发生的事情是这样的:
BEGIN
SAVEPOINT a;
PERFORM foo();
RELEASE SAVEPOINT a;
EXCEPTION WHEN others THEN
ROLLBACK TO SAVEPOINT a;
PERFORM handle_error();
END
块中的 COMMIT
将完全打破它;您的更改将成为永久性的,保存点将被丢弃,并且异常处理程序将无法回滚。因此,在此上下文中不允许提交,并且尝试执行 COMMIT
将导致 "cannot commit while a subtransaction is active" 错误。
这就是为什么您看到您的过程跳转到异常处理程序而不是 运行 宁 raise notice 'B'
的原因:当它到达 commit
时,它会抛出错误,并且处理程序会捕获它。
虽然这很容易解决。 BEGIN ... END
块可以嵌套,只有带有 EXCEPTION
子句的块涉及设置保存点,因此您可以将提交前后的命令包装在自己的异常处理程序中:
create or replace procedure x_transaction_try() language plpgsql
as $$
declare
my_ex_state text;
my_ex_message text;
my_ex_detail text;
my_ex_hint text;
my_ex_ctx text;
begin
begin
raise notice 'A';
exception when others then
raise notice 'C';
GET STACKED DIAGNOSTICS
my_ex_state = RETURNED_SQLSTATE,
my_ex_message = MESSAGE_TEXT,
my_ex_detail = PG_EXCEPTION_DETAIL,
my_ex_hint = PG_EXCEPTION_HINT,
my_ex_ctx = PG_EXCEPTION_CONTEXT
;
raise notice '% % % % %', my_ex_state, my_ex_message, my_ex_detail, my_ex_hint, my_ex_ctx;
end;
commit;
begin
raise notice 'B';
exception when others then
raise notice 'C';
GET STACKED DIAGNOSTICS
my_ex_state = RETURNED_SQLSTATE,
my_ex_message = MESSAGE_TEXT,
my_ex_detail = PG_EXCEPTION_DETAIL,
my_ex_hint = PG_EXCEPTION_HINT,
my_ex_ctx = PG_EXCEPTION_CONTEXT
;
raise notice '% % % % %', my_ex_state, my_ex_message, my_ex_detail, my_ex_hint, my_ex_ctx;
end;
end;
$$;
不幸的是,它确实会导致错误处理程序中出现大量重复,但我想不出避免它的好方法。
我很难理解 Postgres 中的事务。我有一个程序可能会遇到异常。到目前为止,我可能希望在过程的某些部分提交我的工作,以便在出现异常时不会回滚。
我想在过程结束时有一个异常处理块,我在其中捕获异常并将来自异常的信息插入到日志记录中 table。
我已将问题归结为下面的一个简单过程,该过程在 PostgreSQL 11.2 上失败
2D000 cannot commit while a subtransaction is active
PL/pgSQL function x_transaction_try() line 6 at COMMIT
drop procedure if exists x_transaction_try;
create or replace procedure x_transaction_try()
language plpgsql
as $$
declare
begin
raise notice 'A';
-- TODO A: do some insert or update that I want to commit no matter what
commit;
raise notice 'B';
-- TODO B: do something else that might raise an exception, without rolling
-- back the work that we did in "TODO A".
exception when others then
declare
my_ex_state text;
my_ex_message text;
my_ex_detail text;
my_ex_hint text;
my_ex_ctx text;
begin
raise notice 'C';
GET STACKED DIAGNOSTICS
my_ex_state = RETURNED_SQLSTATE,
my_ex_message = MESSAGE_TEXT,
my_ex_detail = PG_EXCEPTION_DETAIL,
my_ex_hint = PG_EXCEPTION_HINT,
my_ex_ctx = PG_EXCEPTION_CONTEXT
;
raise notice '% % % % %', my_ex_state, my_ex_message, my_ex_detail, my_ex_hint, my_ex_ctx;
-- TODO C: insert this exception information in a logging table and commit
end;
end;
$$;
call x_transaction_try();
为什么这个存储过程不起作用?为什么我们从来没有看到 raise notice 'B'
的输出,而是进入了异常块?是否可以使用 Postgres 11 存储过程执行我上面描述的操作?
编辑:这是一个完整的代码示例。将上面的完整代码示例(包括 create procedure
和 call
语句)粘贴到 sql 文件中,然后将其 运行 粘贴到 Postgres 11.2 数据库中进行重现。期望的输出是函数打印 A
然后 B
,但它打印 A
然后 C
以及异常信息。
另请注意,如果您注释掉所有异常处理块,使函数根本不捕获异常,那么函数将输出 'A' 然后 'B' 而不会发生异常。这就是为什么我将问题命名为 'Can a Postgres Commit Exist in Procedure that has an Exception Block?'
问题出在 EXCEPTION
子句上。
这在 PL/pgSQL 中作为 子事务 实现(与 SQL 中的 SAVEPOINT
相同),回滚当到达异常块时。
当子事务处于活动状态时,您不能COMMIT
。
在 src/backend/executor/spi.c
中查看此评论:
/*
* This restriction is required by PLs implemented on top of SPI. They
* use subtransactions to establish exception blocks that are supposed to
* be rolled back together if there is an error. Terminating the
* top-level transaction in such a block violates that idea. A future PL
* implementation might have different ideas about this, in which case
* this restriction would have to be refined or the check possibly be
* moved out of SPI into the PLs.
*/
if (IsSubTransaction())
ereport(ERROR,
(errcode(ERRCODE_INVALID_TRANSACTION_TERMINATION),
errmsg("cannot commit while a subtransaction is active")));
PL/pgSQL 的 error handling 的语义规定:
When an error is caught by an EXCEPTION clause ... all changes to persistent database state within the block are rolled back.
这个是使用子事务实现的,和savepoints基本一样。换句话说,当您 运行 以下 PL/pgSQL 代码时:
BEGIN
PERFORM foo();
EXCEPTION WHEN others THEN
PERFORM handle_error();
END
...实际发生的事情是这样的:
BEGIN
SAVEPOINT a;
PERFORM foo();
RELEASE SAVEPOINT a;
EXCEPTION WHEN others THEN
ROLLBACK TO SAVEPOINT a;
PERFORM handle_error();
END
块中的 COMMIT
将完全打破它;您的更改将成为永久性的,保存点将被丢弃,并且异常处理程序将无法回滚。因此,在此上下文中不允许提交,并且尝试执行 COMMIT
将导致 "cannot commit while a subtransaction is active" 错误。
这就是为什么您看到您的过程跳转到异常处理程序而不是 运行 宁 raise notice 'B'
的原因:当它到达 commit
时,它会抛出错误,并且处理程序会捕获它。
虽然这很容易解决。 BEGIN ... END
块可以嵌套,只有带有 EXCEPTION
子句的块涉及设置保存点,因此您可以将提交前后的命令包装在自己的异常处理程序中:
create or replace procedure x_transaction_try() language plpgsql
as $$
declare
my_ex_state text;
my_ex_message text;
my_ex_detail text;
my_ex_hint text;
my_ex_ctx text;
begin
begin
raise notice 'A';
exception when others then
raise notice 'C';
GET STACKED DIAGNOSTICS
my_ex_state = RETURNED_SQLSTATE,
my_ex_message = MESSAGE_TEXT,
my_ex_detail = PG_EXCEPTION_DETAIL,
my_ex_hint = PG_EXCEPTION_HINT,
my_ex_ctx = PG_EXCEPTION_CONTEXT
;
raise notice '% % % % %', my_ex_state, my_ex_message, my_ex_detail, my_ex_hint, my_ex_ctx;
end;
commit;
begin
raise notice 'B';
exception when others then
raise notice 'C';
GET STACKED DIAGNOSTICS
my_ex_state = RETURNED_SQLSTATE,
my_ex_message = MESSAGE_TEXT,
my_ex_detail = PG_EXCEPTION_DETAIL,
my_ex_hint = PG_EXCEPTION_HINT,
my_ex_ctx = PG_EXCEPTION_CONTEXT
;
raise notice '% % % % %', my_ex_state, my_ex_message, my_ex_detail, my_ex_hint, my_ex_ctx;
end;
end;
$$;
不幸的是,它确实会导致错误处理程序中出现大量重复,但我想不出避免它的好方法。