13

我很难理解 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 procedureandcall语句)粘贴到 sql 文件中,并在 Postgres 11.2 数据库中运行以进行重现。所需的输出将是函数打印Athen B,但它会打印AthenC以及异常信息。

另请注意,如果您注释掉所有异常处理块,使得该函数根本不捕获异常,则该函数将输出“A”然后“B”而不会发生异常。这就是为什么我将这个问题命名为“在具有异常块的过程中是否存在 Postgres 提交?”

4

2 回答 2

18

PL/pgSQL错误处理的语义规定:

当一个错误被 EXCEPTION 子句捕获时……所有对块内持久数据库状态的更改都将回滚。

这是使用子事务实现的,它与保存点基本相同。换句话说,当您运行以下 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

块内的ACOMMIT将完全打破这一点;您的更改将被永久化,保存点将被丢弃,并且异常处理程序将无法回滚。因此,在此上下文中不允许提交,并且尝试执行 aCOMMIT将导致“子事务处于活动状态时无法提交”错误。

这就是为什么您看到您的过程跳转到异常处理程序而不是运行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;
$$;

不幸的是,它确实导致了错误处理程序中的大量重复,但我想不出一种避免它的好方法。

于 2019-03-29T19:46:42.690 回答
3

问题是EXCEPTION条款。

这在 PL/pgSQL 中实现为子事务(与 SQL 中的 a 相同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")));
于 2019-03-29T08:18:57.507 回答