2009-12-10 3 views
7

Sto utilizzando un blocco TRY CATCH in una stored procedure in cui sono presenti due istruzioni INSERT.SQL Server: Eliminare l'eccezione con il numero di eccezione originale

Se qualcosa va storto, il blocco CATCH si occupa di ripristinare tutte le modifiche apportate e funziona bene, tranne una cosa!

L'eccezione catturata dalla mia applicazione ASP.NET è una SqlException con numero 50000. Questo non è il numero originale! (il numero che mi aspettavo era un 2627)

Nella proprietà Message dell'eccezione è possibile visualizzare il numero di eccezione originale e il messaggio formattato.

Come posso ottenere il numero di eccezione originale?

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(); 
    } 
} 

In questo momento il valore di ritorno è già in uso. Immagino che potrei usare un parametro di output per ottenere il numero di eccezione, ma è una buona idea?

Cosa posso fare per ottenere il numero di eccezione? Grazie

PS: Questo è necessario perché ho due istruzioni INSERT.

risposta

0

Grazie ragazzi per le vostre risposte. Ottenere l'errore dal messaggio dell'eccesso ridetto era qualcosa che avevo già fatto.

@gbn Mi è piaciuta anche la risposta di gbn, ma seguirò questa risposta in quanto è quella che funziona meglio e la sto postando qui sperando che sia utile anche per gli altri.

La risposta utilizza le transazioni nell'applicazione. Se non rilevo l'eccezione nella stored procedure, otterrò il numero originale nell'oggetto SqlException. Dopo la cattura l'eccezione originale nell'applicazione, scrivo il codice seguente

transaction.Rollback(); 

Altrimenti:

transaction.Commit(); 

E 'molto più semplice di quanto mi aspettassi in primo luogo!

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

+1

Non penso che tu abbia bisogno di transazioni se vuoi solo cogliere l'errore originale: semplicemente non usare TRY/CATCH o RAISERROR() sul lato T-SQL. Inoltre, l'approccio TransactionScope funziona bene per query semplici o SP, ma ci sono casi d'angolo con SP più complessi in cui non fa la cosa giusta. – RickNZ

+0

@RickNZ, ho dimenticato di menzionare nella domanda che sto usando due istruzioni INSERT ed è per questo che è necessaria la transazione. Buon contributo però! –

0

Io uso il seguente schema:

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

Ci dispiace, ma per fare cosa? Questo ancora getta 50000 e non è raccomandato per SQL Server 2005. OP menziona anche usando TRY/CATCH già – gbn

+0

@gbn Avete qualche idea su come possiamo fare questo? –

11

si potrebbe essere in grado di rigenerare in questo modo:

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

Tuttavia, è più probabile perdere un perso di significato a causa del% s ecc. segnaposti nelle righe sys.messages per ERROR_NUMBER()

Si potrebbe fare qualcosa del genere per includere il numero e rilanciare il messaggio originale

.. 
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 

I primi 5 caratteri sono il numero originale.

Ma se si ha codice annidato, si finisce con "0Testo errore".

Personalmente, mi occupo solo di numeri di eccezione SQL per separare i miei errori (50000) dagli errori del motore (ad esempio parametri mancanti) in cui il mio codice non viene eseguito.

Infine, è possibile passare il valore restituito.

ho fatto una domanda su questo: SQL Server error handling: exceptions and the database-client contract

8

Se si utilizza BEGIN TRY/BEGIN CATCH in T-SQL si perde l'eccezione sollevata motore originale. Non si suppone che tu debba innalzare manualmente gli errori di sistema, quindi non puoi controrilanciare il numero di errore originale 2627. La gestione degli errori T-SQL non è simile alla gestione degli errori C#/C++, non ci sono mezzi per rilanciare l'originale eccezione. Ci sono una serie di ragioni per cui esiste questa limitazione, ma è sufficiente dire che è a posto e non puoi ignorarlo.

Tuttavia non ci sono limitazioni per aumentare i propri codici di errore, purché siano superiori all'intervallo 50000.Si registra i propri messaggi utilizzando sp_addmessage, quando è installata l'applicazione:

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

e nella vostra T-SQL si dovrebbe alzare il nuovo errore:

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

Nel codice C# si dovrebbe cercare il numero di errore 50001 invece di 2627:

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

I whish c'era una risposta semplice, ma purtroppo questo è il modo in cui stanno le cose. La gestione delle eccezioni T-SQL non si integra perfettamente nella gestione delle eccezioni CLR.

+1

Da MSDN .... "I blocchi CATCH possono utilizzare RAISERROR per rilanciare l'errore che ha richiamato il blocco CATCH utilizzando le funzioni di sistema come ERROR_NUMBER e ERROR_MESSAGE per recuperare le informazioni sull'errore originale. @@ ERROR è impostato su 0 per impostazione predefinita per i messaggi con una gravità da 1 a 10. " Quindi rethrowing è valido per il 2005 + http://msdn.microsoft.com/en-us/library/ms178592.aspx – maguy

+0

È consentito solo creare messaggi utente> 50000 MA uno può anche generare messaggi di errore da 13000 a 49999. –

1

Ecco il codice che uso per risolvere questo problema (chiamato da CATCH). Esso incorpora il numero errore originale nel testo del messaggio:

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 

Quindi è possibile verificare la presenza di SqlException.Message.Contains("2627:"), per esempio.

+0

Sarebbe anche fattibile, ma in qualche modo recuperare un numero da una stringa con altre cose non mi sembra una buona pratica. –

1

ho pensato su questo argomento per un po 'e si avvicinò con una soluzione molto semplice che non ho visto prima così ho voluto condividere questo:

Come è impossibile rigenerare il stesso errore, si deve lanciare un errore che è molto facile da mappare all'errore originale, ad esempio aggiungendo un numero fisso come 100000 ad ogni errore di sistema.

Dopo che i messaggi appena mappate sono aggiunti al database, è possibile gettare qualsiasi errore di sistema con un offset fisso di 100000

Ecco il codice per la creazione dei messaggi mappate (questo deve essere fatto solo una . tempo per l'intera istanza di SQL Server evitare conflitti con gli altri messaggi definiti dall'utente con l'aggiunta di un offset come 100000 in questo caso appropriata):

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 

E questo è il codice di alzare i codici di errore di nuova creazione che hanno una correzione offset rispetto al codice originale:

C'è un piccolo avvertimento: in questo caso non è possibile fornire un messaggio.Ma questo può essere aggirato aggiungendo un% s aggiuntivo nella chiamata sp_addmessage o cambiando tutti i messaggi mappati sul proprio modello e fornendo i parametri corretti nella chiamata al rialzo. La cosa migliore è impostare tutti i messaggi sullo stesso modello come '% s (riga:% d procedura:% s)% s', quindi puoi fornire il messaggio originale come primo parametro e aggiungere la vera procedura e linea e il tuo messaggio come gli altri parametri.

Nel client è ora possibile eseguire tutte le normali operazioni di gestione delle eccezioni come se fossero stati lanciati i messaggi originali, è sufficiente ricordare di aggiungere l'offset della correzione. È anche possibile gestire le eccezioni originali e rilanciati con lo stesso codice come questo:

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

Quindi non hanno nemmeno bisogno di sapere se si tratta di un rilanciati o l'errore originale, è sempre giusta, anche se si è dimenticato di gestisci i tuoi errori e l'errore originale è sfuggito.

Ci sono alcuni miglioramenti menzionati altrove riguardo l'aumento di messaggi che non è possibile aumentare o gli stati che non è possibile utilizzare. Quelli sono lasciati qui per mostrare solo il nucleo dell'idea.

1

Poiché SQL Server < 2010 non ha la possibilità di eseguire il re-lancio, il modo corretto per eseguire questa operazione è utilizzare una transazione e verificare esplicitamente lo stato di errore (si pensi più "C" che "C++/C#") .

E.g. corpo della SP sarebbe simile:

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; 

Si tratta di una soluzione che funziona in un ambiente ospitato (in cui probabilmente non sarà in grado di creare i propri messaggi di errore).

Pur non essendo conveniente quanto l'utilizzo di eccezioni, questo approccio preserverà i codici di errore del sistema. Ha anche (a) vantaggio di poter restituire più errori per esecuzione.

Se si desidera solo per bombardare-fuori sul primo errore, o inserire le dichiarazioni di ritorno o se vi sentite coraggiosi, un GOTO un blocco di errore (ricordate: Go To Statement Considered Harmful), ad esempio:

(elementi di questa tratto da ASP.NET conto gestione)

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;