2009-12-10 10 views
7

Estoy usando un bloque TRY CATCH en un procedimiento almacenado donde tengo dos instrucciones INSERT.SQL Server: excepción de Rethrow con el número de excepción original

Si algo sale mal, el bloque CATCH se encarga de deshacer todos los cambios realizados y funciona bien, excepto una cosa.

La excepción detectada por mi aplicación ASP.NET es una SqlException con el número 50000. ¡Este no es el número original! (el número que esperaba era un 2627)

En la propiedad Mensaje de la excepción, puedo ver el número original de la excepción y el mensaje formateado.

¿Cómo puedo obtener el número de excepción original?

try 
{ 
    // ... code 
} 
catch 
(SqlException sqlException) 
{ 
    switch (sqlException.Number) 
    { 
     // Name already exists 
     case 2627: 
      throw new ItemTypeNameAlreadyExistsException(); 

     // Some other error 
     // As the exception number is 50000 it always ends here!!!!!! 
     default: 
      throw new ItemTypeException(); 
    } 
} 

En este momento, el valor de retorno ya se está utilizando. Supongo que podría usar un parámetro de salida para obtener el número de excepción, pero ¿es una buena idea?

¿Qué puedo hacer para obtener el número de excepción? Gracias

PD: Esto es necesario porque tengo dos instrucciones INSERT.

Respuesta

0

Gracias a todos por sus respuestas. Obtener el error del mensaje de la extensión relanzada fue algo que ya había hecho.

@gbn También me gustó la respuesta de gbn, pero me atendré a esta respuesta ya que es la que funciona mejor y la estoy publicando aquí esperando que también sea útil para otros.

La respuesta es usar transacciones en la aplicación. Si no encuentro la excepción en el procedimiento almacenado, obtendré el número original en el objeto SqlException. Después de agarrar la excepción original en la aplicación, escribo el código siguiente

transaction.Rollback(); 

lo contrario:

transaction.Commit(); 

Es mucho más simple de lo que en primer lugar, que esperaba!

http://msdn.microsoft.com/en-us/library/system.data.sqlclient.sqltransaction.aspx

+1

No creo que necesite transacciones si solo quiere detectar el error original: simplemente no use TRY/CATCH o RAISERROR() en el lado de T-SQL. Además, el enfoque de TransactionScope funciona bien para consultas simples o SP, pero hay casos de esquina con SP más complejos en los que no hace lo correcto. – RickNZ

+0

@RickNZ, olvidé mencionar en la pregunta que estoy usando dos instrucciones INSERT y que es por eso que se necesita la transacción. Buena contribución sin embargo! –

0

uso el siguiente patrón:

CreatePROCEDURE [dbo].[MyProcedureName] 
@SampleParameter Integer, 
[Other Paramaeters here] 
As 
Set NoCount On 
Declare @Err Integer Set @Err = 0 
Declare @ErrMsg VarChar(300) 

    -- ---- Input parameter value validation ------ 
    Set @ErrMsg = ' @SampleParameter ' + 
        'must be either 1 or 2.' 
    If @SampleParameter Not In (1, 2) Goto Errhandler 
    -- ------------------------------------------ 

    Begin Transaction 
    Set @ErrMsg = 'Failed to insert new record into TableName' 
    Insert TableName([ColumnList]) 
    Values [ValueList]) 
    Set @Err = @@Error If @Err <> 0 Goto Errhandler 
    -- ------------------------------------------ 
    Set @ErrMsg = 'Failed to insert new record into Table2Name' 
    Insert TableName2([ColumnList]) 
    Values [ValueList]) 
    Set @Err = @@Error If @Err <> 0 Goto Errhandler 

    -- etc. etc.. 

    Commit Transaction 
    Return 0 

    /* *************************************************/ 
    /* ******* Exception Handler ***********************/ 
    /* *************************************************/ 
    /* *************************************************/ 

    ErrHandler: 
     If @@TranCount > 0 RollBack Transaction 
     -- ------------------------------------ 
     RaisError(@ErrMsg, 16, 1) 
     If @Err = 0 Set @Err = -1 
     Return @Err 
+0

Lo sentimos, pero para hacer qué? Esto todavía arroja 50000 y no se recomienda para SQL Server 2005. OP también menciona el uso de TRY/CATCH ya sea – gbn

+0

@gbn. ¿Tiene alguna idea sobre cómo podemos hacer esto? –

11

es posible que pueda volver a generar de esta manera:

.. 
END TRY 
BEGIN CATCH 
    DECLARE @errnum int; 
    SELECT @errnum = ERROR_NUMBER(); 
    RAISERROR (@errnum, 16, 1); 
END CATCH 

Sin embargo, lo más probable perder una pérdida de sentido debido a la% s etc. marcadores de posición en las filas sys.messages para ERROR_NUMBER()

Puede hacer algo como esto para incluir el número y volver a lanzar el mensaje original

.. 
END TRY 
BEGIN CATCH 
    DECLARE @errnum nchar(5), @errmsg nvarchar(2048); 
    SELECT 
     @errnum = RIGHT('00000' + ERROR_NUMBER(), 5), 
     @errmsg = @errnum + ' ' + ERROR_MESSAGE(); 
    RAISERROR (@errmsg, 16, 1); 
END CATCH 

Los primeros 5 caracteres son el número original.

Pero si ha anidado el código, terminará con "0Error text".

Personalmente, solo trato con los números de excepción de SQL para separar mis errores (50000) de los errores del motor (por ejemplo, falta de parámetros) donde no se ejecuta mi código.

Finalmente, puede pasar el valor de retorno.

me hizo una pregunta sobre esto: SQL Server error handling: exceptions and the database-client contract

8

Si utiliza COMENZAR TRY/CATCH COMENZAR en T-SQL de que pierda el motor original excepción planteada. Se supone que no debe generar manualmente errores del sistema, por lo que no puede volver a generar el número de error original 2627. El manejo de errores T-SQL no es similar al manejo de errores C#/C++, no hay forma de volver a lanzar el original excepción. Hay una serie de razones por las cuales existe esta limitación, pero baste decir que está en su lugar y no puede ignorarla.

Sin embargo, no existen limitaciones para generar sus propios códigos de error, siempre que estén por encima del rango 50000.Registrar sus propios mensajes utilizando sp_addmessage, cuando se instala la aplicación:

exec sp_addmessage 50001, 16, N'A primary key constraint failed: %s'; 

y en su T-SQL que elevaría el nuevo error:

@error_message = ERROR_MESSAGE(); 
raiserror(50001, 16, 1, @error_message; 

En el código C# que se busca la número de error 50001 en lugar de 2627:

foreach(SqlError error in sqlException.Errors) 
{ 
switch (error.Number) 
{ 
case 50001: 
    // handle PK violation 
case 50002: 
    // 
} 
} 

que whish hubo una respuesta simple, pero por desgracia esto es como son las cosas. El manejo de excepciones de T-SQL no se integra adecuadamente en el manejo de excepciones de CLR.

+1

Desde MSDN .... "Los bloques CATCH pueden usar RAISERROR para volver a generar el error que invoca el bloque CATCH utilizando funciones del sistema como ERROR_NUMBER y ERROR_MESSAGE para recuperar la información de error original. @ ERROR está configurado en 0 de forma predeterminada para los mensajes con una severidad de 1 a 10. " Por lo tanto, volver a lanzar es válido para 2005+ http://msdn.microsoft.com/en-us/library/ms178592.aspx – maguy

+0

Solo se permite crear mensajes de usuario> 50000 PERO uno también puede generar mensajes de error de 13000 a 49999. –

1

Aquí está el código que utilizo para tratar este problema (llamado de CATCH). Se incrusta el número de error original en el texto del mensaje:

CREATE PROCEDURE [dbo].[ErrorRaise] 
AS 
BEGIN 
    DECLARE @ErrorMessage NVARCHAR(4000) 
    DECLARE @ErrorSeverity INT 
    SET @ErrorMessage = CONVERT(VARCHAR(10), ERROR_NUMBER()) + ':' + 
     ERROR_MESSAGE() 
    SET @ErrorSeverity = ERROR_SEVERITY() 
    RAISERROR (@ErrorMessage, @ErrorSeverity, 1) 
END 

entonces usted puede comprobar para SqlException.Message.Contains("2627:"), por ejemplo.

+0

Eso también sería factible, pero de alguna manera recuperar un número de una cadena con otras cosas no me parece una buena práctica. –

1

pensé acerca de este tema por un tiempo y se le ocurrió una solución muy sencilla que no he visto antes, así que quería compartir esto:

Como es imposible volver a generar la mismo error, uno tiene que arrojar un error que es muy fácil de asignar al error original, por ejemplo, agregando un número fijo como 100000 a cada error del sistema.

Después de los mensajes recién asignados se añaden a la base de datos, es posible lanzar cualquier error del sistema, con un desplazamiento fijo de 100000

Este es el código para la creación de los mensajes asignados (esto tiene que ser hecho sólo una . tiempo para toda la instancia de SQL Server evitar conflictos con otros mensajes definidos por el usuario mediante la adición de un desplazamiento como 100.000 en este caso apropiado):

DECLARE messageCursor CURSOR 
READ_ONLY 
FOR select 
    message_id + 100000 as message_id, language_id, severity, is_event_logged, [text] 
from 
    sys.messages 
where 
    language_id = 1033 
    and 
    message_id < 50000 
    and 
    severity > 0 

DECLARE 
    @id int, 
    @severity int, 
    @lang int, 
    @msgText nvarchar(1000), 
    @withLog bit, 
    @withLogString nvarchar(100) 

OPEN messageCursor 

FETCH NEXT FROM messageCursor INTO @id, @lang, @severity, @withLog, @msgText 
WHILE (@@fetch_status <> -1) 
BEGIN 
    IF (@@fetch_status <> -2) 
    BEGIN 

     set @withLogString = case @withLog when 0 then 'false' else 'true' end  

     exec sp_addmessage @id, @severity, @msgText, 'us_english', @withLogString, 'replace' 
    END 
    FETCH NEXT FROM messageCursor INTO @id, @lang, @severity, @withLog, @msgText 
END 

CLOSE messageCursor 
DEALLOCATE messageCursor 

Y este es el código para elevar los códigos de error de nueva creación que tienen un arreglo desplazamiento del código original:

SELECT 
     @ErrorNumber = ERROR_NUMBER(), 
     @ErrorSeverity = ERROR_SEVERITY(), 
     @ErrorState = ERROR_STATE() 

    set @MappedNumber = @ErrorNumber + 100000; 

    RAISERROR 
     (
     @MappedNumber, 
     @ErrorSeverity, 
     1    
     ); 

Hay una pequeña advertencia: en este caso, no puede proporcionar un mensaje por su cuenta.Pero esto se puede eludir agregando un% s adicional en la llamada sp_addmessage o cambiando todos los mensajes asignados a su propio patrón y suministrando los parámetros correctos en la llamada de raise raise. Lo mejor es configurar todos los mensajes con el mismo patrón, como '% s (línea:% d procedimiento:% s)% s', para que pueda suministrar el mensaje original como primer parámetro y anexar el procedimiento real y la línea y su propio mensaje como los otros parámetros.

En el cliente ahora puede hacer todo el manejo de excepciones ordinarias como si se hubieran lanzado los mensajes originales, solo tiene que acordarse de agregar el desplazamiento de la corrección. Incluso puede manejar excepciones originales y relanza con el mismo código como este:

switch(errorNumber) 
{ 
    case 8134: 
    case 108134: 
    { 
    } 
} 

por lo que ni siquiera tiene que saber si se trata de una relanza o el error original, siempre es correcto, incluso si se olvidó de maneja tus errores y el error original se deslizó.

Hay alguna mejora mencionada en otra parte sobre la generación de mensajes que no puede generar o que no puede usar. Esos quedan aquí para mostrar solo el núcleo de la idea.

1

Como SQL Server < 2010 no tiene la capacidad de volver a lanzar, la forma correcta de hacerlo es usar una transacción y comprobar explícitamente el estado de error (piense más 'C' que 'C++/C#') .

E.g. cuerpo del SP sería algo como:

CREATE PROCEDURE [MyProcedure] 
    @argument1 int, 
    @argument2 int, 
    @argument3 int 
AS BEGIN 
    DECLARE @return_code int; 

    IF (@argument1 < 0) BEGIN 
     RAISERROR ("@argument1 invalid", 16, 1); 
    END; 

    /* Do extra checks here... */ 

    /* Now do what we came to do. */ 

    IF (@@ERROR = 0) BEGIN 
     BEGIN TRANSACTION; 

     INSERT INTO [Table1](column1, column2) 
     VALUES (@argument1, @argument2); 

     IF (@@ERROR = 0) BEGIN 
      INSERT INTO [Table2](column1, column2) 
      VALUES (@argument1, @argument3); 
     END; 

     IF (@@ERROR = 0) BEGIN 
      COMMIT TRANSACTION; 
      SET @return_code = 0; 
     END 
     ELSE BEGIN 
      ROLLBACK TRANSACTION; 
      SET @return_code = -1; /* Or something more meaningful... */ 
     END; 
    END 
    ELSE BEGIN 
     SET @return_code = -1; 
    END; 

    RETURN @return_code; 
END; 

Ésta es una solución que funcione en un entorno alojado (en la que probablemente no será capaz de crear sus propios mensajes de error).

Aunque no es tan conveniente como usar excepciones, este enfoque preservará los códigos de error del sistema. También tiene la (des) ventaja de poder devolver múltiples errores por ejecución.

Si lo que desea es bombardear de salida en el primer error, o bien insertar sentencias de retorno o si te sientes valiente, un GOTO un bloque de error (recuerda: Go To Statement Considered Harmful), por ejemplo:

(elementos de esta tomado de la administración de cuentas ASP.NET)

CREATE PROCEDURE [MyProcedure] 
    @argument1 int, 
    @argument2 int, 
    @argument3 int 
AS BEGIN 
    DECLARE @return_code int = 0; 
    DECLARE @tranaction_started bit = 0; /* Did we start a transaction? */ 

    IF (@argument1 < 0) BEGIN 
     RAISERROR ("@argument1 invalid", 16, 1); 
     RETURN -1; /* Or something more specific... */ 
     /* Alternatively one could: 
     SET @return_code = -1; 
     GOTO ErrorCleanup; 
     */   
    END; 

    /* Do extra checks here... */ 

    /* Now do what we came to do. */ 

    /* If no transaction exists, start one. 
    * This approach makes it safe to nest this SP inside a 
    * transaction, e.g. in another SP. 
    */ 
    IF (@@TRANCOUNT = 0) BEGIN 
     BEGIN TRANSACTION; 
     SET @transaction_started = 1; 
    END; 

    INSERT INTO [Table1](column1, column2) 
    VALUES (@argument1, @argument2); 

    IF (@@ERROR <> 0) BEGIN 
     SET @return_code = -1; /* Or something more specific... */ 
     GOTO ErrorCleanup; 
    END; 

    INSERT INTO [Table2](column1, column2) 
    VALUES (@argument1, @argument3); 

    IF (@@ERROR <> 0) BEGIN 
     SET @return_code = -1; /* Or something more specific... */ 
     GOTO ErrorCleanup; 
    END; 

    IF (@transaction_started = 1) BEGIN 
     /* ONLY commit the transaction if we started it! */ 
     SET @transaction_started = 0; 
     COMMIT TRANSACTION; 
    END; 

    RETURN @return_code; 

ErrorCleanup: 
    IF (@transaction_started = 1) BEGIN 
     /* We started the transaction, so roll it back */ 
     ROLLBACK TRANSACTION; 
    END; 
    RETURN @return_code; 
END; 
Cuestiones relacionadas