我很难理解Postgres中的交易。我的程序可能会遇到异常。在此过程的某些部分中,我可能想要到目前为止进行工作,以便在发生异常时不会回滚它。
我希望在过程结束时有一个异常处理块,在该块中捕获异常,并将异常中的信息插入到日志表中。
我将问题归结为一个简单的过程,下面的过程在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'而不会发生异常。这就是为什么我将问题命名为“具有异常块的过程中的Postgres提交可以存在吗?”的方式的原因。
答案 0 :(得分:4)
PL / pgSQL的error handling的语义规定:
当EXCEPTION子句捕获到错误时...将回滚对该块内的持久数据库状态的所有更改。
这是使用子交易实现的,该子交易与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
将导致“子事务处于活动状态时无法提交”错误。
这就是为什么您看到过程跳到异常处理程序而不是运行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;
$$;
不幸的是,它确实导致了错误处理程序中的许多重复,但是我想不出一种避免这种情况的好方法。
答案 1 :(得分:2)
问题是EXCEPTION
子句。
这在PL / pgSQL中作为 subtransaction 实现(与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")));