Manejo de excepciones en procedimientos almacenados llamados usando bloques insert-exec

10

Tengo un procedimiento almacenado que se llama en un bloque insert-exec:

insert into @t
    exec('test')

¿Cómo puedo manejar las excepciones generadas en el procedimiento almacenado y seguir procesando?

El siguiente código ilustra el problema. Lo que quiero hacer es devolver 0 o -1 dependiendo del éxito o el fracaso de la exec()llamada 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;

Mi problema es el return(-1). El camino del éxito está bien.

Si omito el bloque try / catch en el procedimiento almacenado, se genera el error y falla la inserción. Sin embargo, lo que quiero hacer es manejar el error y devolver un buen valor.

El código como está devuelve el mensaje:

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.

Este es quizás el peor mensaje de error que he encontrado. Parece que realmente significa "No manejó un error en una transacción anidada".

Si pongo el if @@TRANCOUNT > 0, entonces recibo el mensaje:

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.

He intentado jugar con las declaraciones de transacción de inicio / confirmación, pero nada parece funcionar.

Entonces, ¿cómo puedo hacer que mi procedimiento almacenado maneje errores sin abortar la transacción general?

Editar en respuesta a Martin:

El código de llamada real es:

        declare @RetvalTable table (retval int);

        set @retval = -1;

        insert into @RetvalTable
            exec('

declarar @retval int; exec @retval = '+ @ query +'; seleccione @retval ');

        select @retval = retval from @RetvalTable;

¿Dónde @queryestá la llamada al procedimiento almacenado? El objetivo es obtener el valor de retorno del procedimiento almacenado. Si esto es posible sin un insert(o, más específicamente, sin iniciar una transacción), sería genial.

No puedo modificar los procedimientos almacenados en general para almacenar el valor en una tabla, porque hay demasiados. Uno de ellos está fallando, y puedo modificar eso. Mi mejor solución actual es 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
fuente
¿Qué intentas insertar en la variable de tabla? El valor de retorno no se inserta allí de todos modos. declare @t table (i int);declare @RC int;exec @RC = test;insert into @t values (@RC);select * from @t;funciona bien.
Martin Smith
@MartinSmith. . . La forma en que realmente funciona el código es más parecida select @retval; return @retvalal final. Si conoce otra forma de obtener el valor de retorno de una llamada de procedimiento almacenado dinámico, me encantaría saberlo.
Gordon Linoff
Bueno, otra forma seríaDECLARE @RC INT;EXEC sp_executesql N'EXEC @RC = test', N'@RC INT OUTPUT', @RC = @RC OUTPUT;insert into @t VALUES (@RC)
Martin Smith,
@MartinSmith. . . Creo que eso funcionará. Pasamos la mitad del día buscando fallas de hardware ("no puede soportar operaciones que escriben en el archivo de registro" suena como una falla de hardware) y las últimas dos horas tratando de obtener el código correcto. La sustitución de variables es una excelente respuesta.
Gordon Linoff

Respuestas:

13

El error en la EXECparte de la INSERT-EXECdeclaración está dejando su transacción en un estado condenado.

Si PRINTsale XACT_STATE()en el CATCHbloque, está configurado en -1.

No todos los errores establecerán el estado en esto. El siguiente error de restricción de verificación pasa al bloque catch y se realiza INSERTcorrectamente.

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; 

Agregando esto al CATCHbloque

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

No ayuda Da el error

No se puede usar la instrucción ROLLBACK dentro de una instrucción INSERT-EXEC.

No creo que haya una forma de recuperarse de tal error una vez que ha sucedido. Sin INSERT ... EXECembargo, para su caso de uso específico, no necesita de todos modos. Puede asignar el valor de retorno a una variable escalar y luego insertarlo en una declaración separada.

DECLARE @RC INT;

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

INSERT INTO @t
VALUES      (@RC) 

O, por supuesto, podría reestructurar el procedimiento almacenado llamado para que no genere ese error en absoluto.

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
fuente