Manipulando exceções em procedimentos armazenados chamados usando blocos insert-exec

10

Eu tenho um procedimento armazenado chamado em um bloco insert-exec:

insert into @t
    exec('test')

Como posso lidar com exceções geradas no procedimento armazenado e ainda continuar o processamento?

O código a seguir ilustra o problema. O que eu quero fazer é retornar 0 ou -1, dependendo do sucesso ou falha da exec()chamada interna :

alter procedure test -- or create
as
begin try
    declare @retval int;
    -- This code assumes that PrintMax exists already so this generates an error
    exec('create procedure PrintMax as begin print ''hello world'' end;')
    set @retval = 0;
    select @retval;
    return(@retval);
end try
begin catch
    -- if @@TRANCOUNT > 0 commit;
    print ERROR_MESSAGE();
    set @retval = -1;
    select @retval;
    return(@retval);
end catch;
go

declare @t table (i int);

insert into @t
    exec('test');

select *
from @t;

Meu problema é o return(-1). O caminho do sucesso está bom.

Se eu deixar de fora o bloco try / catch no procedimento armazenado, o erro será gerado e a inserção falhará. No entanto, o que eu quero fazer é lidar com o erro e retornar um bom valor.

O código como está retorna a mensagem:

Msg 3930, Level 16, State 1, Line 6
The current transaction cannot be committed and cannot support operations that write to the log file. Roll back the transaction.

Esta é talvez a pior mensagem de erro que encontrei. Parece realmente significar "Você não tratou de um erro em uma transação aninhada".

Se eu inserir if @@TRANCOUNT > 0, recebo a mensagem:

Msg 3916, Level 16, State 0, Procedure gordontest, Line 7
Cannot use the COMMIT statement within an INSERT-EXEC statement unless BEGIN TRANSACTION is used first.

Eu tentei brincar com instruções de transação de início / confirmação, mas nada parece funcionar.

Então, como posso meu procedimento armazenado manipular erros sem interromper a transação geral?

Edite em resposta a Martin:

O código de chamada real é:

        declare @RetvalTable table (retval int);

        set @retval = -1;

        insert into @RetvalTable
            exec('

declare @retval int; exec @retval = '+ @ consulta +'; selecione @retval ');

        select @retval = retval from @RetvalTable;

Onde @queryé a chamada do procedimento armazenado. O objetivo é obter o valor de retorno do procedimento armazenado. Se isso for possível sem uma insert(ou, mais especificamente, sem iniciar uma transação), isso seria ótimo.

Não posso modificar os procedimentos armazenados em geral para armazenar o valor em uma tabela, porque existem muitos deles. Um deles está falhando, e eu posso modificar isso. Minha melhor solução atual é algo como:

if (@StoredProcedure = 'sp_rep__post') -- causing me a problem
begin
    exec @retval = sp_rep__post;
end;
else
begin
    -- the code I'm using now
end;
Gordon Linoff
fonte
O que você está tentando inserir na variável da tabela? O valor de retorno não é inserido lá de qualquer maneira. declare @t table (i int);declare @RC int;exec @RC = test;insert into @t values (@RC);select * from @t;funciona bem.
Martin Smith
@MartinSmith. . . A maneira como o código realmente funciona é mais parecido select @retval; return @retvalcom o final. Se você conhece outra maneira de obter o valor de retorno de uma chamada de procedimento armazenado dinâmico, eu adoraria saber.
Gordon Linoff
Bem, outra maneira seriaDECLARE @RC INT;EXEC sp_executesql N'EXEC @RC = test', N'@RC INT OUTPUT', @RC = @RC OUTPUT;insert into @t VALUES (@RC)
Martin Smith
@MartinSmith. . . Eu acho que vai funcionar. Passamos metade do dia procurando por falha de hardware ("não pode suportar operações que gravam no arquivo de log" parece uma falha de hardware) e nas últimas duas horas tentando acertar o código. A substituição variável é uma excelente resposta.
Gordon Linoff

Respostas:

13

O erro na EXECparte da INSERT-EXECinstrução está deixando sua transação em um estado condenado.

Se você PRINTsair XACT_STATE()do CATCHbloco, está definido como -1.

Nem todos os erros definirão o estado para isso. O seguinte erro de restrição de verificação passa para o bloco catch e INSERTé bem - sucedido.

ALTER PROCEDURE test -- or create
AS
  BEGIN try
      DECLARE @retval INT;

      DECLARE @t TABLE(x INT CHECK (x = 0))

      INSERT INTO @t
      VALUES      (1)

      SET @retval = 0;

      SELECT @retval;

      RETURN( @retval );
  END try

  BEGIN catch
      PRINT XACT_STATE()

      PRINT ERROR_MESSAGE();

      SET @retval = -1;

      SELECT @retval;

      RETURN( @retval );
  END catch; 

Adicionando isso ao CATCHbloco

 IF (XACT_STATE()) = -1
BEGIN
    ROLLBACK TRANSACTION;
END;

Não ajuda Dá o erro

Não é possível usar a instrução ROLLBACK em uma instrução INSERT-EXEC.

Eu não acho que exista uma maneira de recuperar-se de um erro assim que ele aconteceu. Para o seu caso de uso específico, você não precisa de INSERT ... EXECqualquer maneira. Você pode atribuir o valor de retorno a uma variável escalar e depois inseri-lo em uma instrução separada.

DECLARE @RC INT;

EXEC sp_executesql
  N'EXEC @RC = test',
  N'@RC INT OUTPUT',
  @RC = @RC OUTPUT;

INSERT INTO @t
VALUES      (@RC) 

Ou, é claro, você pode reestruturar o procedimento armazenado chamado para que ele não apresente nenhum erro.

DECLARE @RetVal INT = -1

IF OBJECT_ID('PrintMax', 'P') IS NULL
  BEGIN
      EXEC('create procedure PrintMax as begin print ''hello world'' end;')

      SET @RetVal = 0
  END

SELECT @RetVal;

RETURN( @RetVal ); 
Martin Smith
fonte