Postgres提交是否存在于具有异常阻止的过程中?

问题描述 投票:4回答:2

我很难理解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 procedurecall语句)粘贴到sql文件中,并在Postgres 11.2数据库中运行以进行重新编译。所需的输出将是函数打印A然后B,但相反它打印A然后C以及异常信息。

另请注意,如果您注释掉所有异常处理块,使得该函数根本不捕获异常,那么该函数将输出'A'然后'B'而不会发生异常。这就是为什么我把这个问题命名为“我可以做出什么样的方式'可以在一个有异常阻止的程序中存在一个Postgres提交吗?'

postgresql stored-procedures plpgsql postgresql-11
2个回答
5
投票

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;
$$;

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


2
投票

问题是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")));
© www.soinside.com 2019 - 2024. All rights reserved.