Pagine

venerdì 26 ottobre 2012

Begin Try, Transazioni, Xact_Abort e Uncommittable transaction

Il costrutto BEGIN TRY, introdotto sin dal'edizione sql 2005, implementa una gestione degli errori simile a quella di altri linguaggi (C#, C++...).
Tenta di eseguire gli statement contenuti nel blocco try, in caso d’errore esegue gli statement del blocco catch.
E' estremamente utile ma presenta alcune restrizioni di cui è bene tenere conto.

Per esempio non intercetta :
1. Errori di sintassi
2. Oggetti non trovati
3. Errori con livello di gravità maggiore o uguali 20 che determinano l'arresto
dell'elaborazione per la sessione.

Inoltre non implementa rollback automatici delle transazioni create dall'utente, nè supporta il costrutto finally.

Il codice T-Sql mette a disposizione il comando SET XACT_ABORT { ON | OFF }
Se impostato a ON specifica che SQL Server deve eseguire automaticamente il rollback della transazione corrente quando un'istruzione Transact-SQL genera un errore di run-time.Il valore di default è OFF.
Se lo attiviamo e nel nostro statement è usato anche il begin try ed una transazione potremmo trovarci con la transazione in stato uncommittable.

Vediamo come si comportano...

Creo 2 tabelle, T1 e T2 che referenzia T1.

use tempdb
Go

Create table T1
(id int primary key clustered)
Go

Create table T2
(id int primary key clustered references T1(id))
Go

--Inserisco dati validi
Insert into T1 values(1)
Insert into T2 values(1)
Go


Proviamo cosa accade alla transazione con l'impostazione di default (Off) di XACT_ABORT.

USE TempDb
Go

SET XACT_ABORT OFF;
BEGIN TRY
    
    BEGIN TRANSACTION;
  
 INSERT INTO T1 VALUES(2)
 INSERT INTO T2 VALUES(2)

        -- La delete fallisce per Violazione di Foreign Key
        DELETE FROM T1 WHERE ID = 1;

    -- Se Delete Ok allora faccio la commit
    COMMIT TRANSACTION;

END TRY
BEGIN CATCH
     SELECT 
   ERROR_NUMBER() AS ErrorNumber
   ,ERROR_SEVERITY() AS ErrorSeverity
   ,ERROR_STATE() AS ErrorState
   ,ERROR_LINE () AS ErrorLine
   ,ERROR_PROCEDURE() AS ErrorProcedure
   ,ERROR_MESSAGE() AS ErrorMessage;
END CATCH;

--Transazioni attive = 1
SELECT @@TRANCOUNT [Transazioni attive Prima della fine del batch]
GO

--All'esterno del batch transazioni attive = 1, SQL NON effettua un rollback automatico
SELECT @@TRANCOUNT [Transazioni attive Dopo della fine del batch]
GO


Come potete vedere la transazione è attiva prima della fine del batch perchè Begin try non implementa il rollback automatico delle transazioni.
Il problema è che la transazione rimane attiva anche dopo che il batch è terminato.
Attenzione inoltre che per l'applicazione chiamante (management studio nel nostro caso) il batch si è concluso con successo perchè l'errore è stato "gestito", si fa per dire, dal blocco catch !!!!

Se tentiamo di accedere alla tabella T1 o T2 da un'altra connessione cosa accade ?

use tempdb
Go

Select * from T1
Go



La Select resta bloccata dalla presenza di un lock di tipo exclusive sulla risorsa che si vuole accedere perchè è attiva una transazione.
Per risolvere questo problema è indispensabile chiudere la transazione con un rollback dato che si è verificato un errore.
Ritorniamo quindi sulla connessione che ha attivato la transazione e implementiamo il Rollback.

ROLLBACK TRANSACTION
GO

A questo punto la seconda connessione può accedere alle righe della tabella.
Ma che cosa contiene T1?
Ovviamente solo le righe presenti prima della transazione, tutte le operazioni sono state annullate.


Proviamo ora ad attivare la Xact_abort e osserviamo il comportamento di SQL Server

SET XACT_ABORT ON;
BEGIN TRY
    
     BEGIN TRANSACTION;
  
 INSERT INTO T1 VALUES(2)
 INSERT INTO T2 VALUES(2)

        -- La delete fallisce per Violazione di Foreign Key
        DELETE FROM T1 WHERE ID = 1;

    -- Se Delete Ok allora faccio la commit
    COMMIT TRANSACTION;

END TRY
BEGIN CATCH
     SELECT 
   ERROR_NUMBER() AS ErrorNumber
   ,ERROR_SEVERITY() AS ErrorSeverity
   ,ERROR_STATE() AS ErrorState
   ,ERROR_LINE () AS ErrorLine
   ,ERROR_PROCEDURE() AS ErrorProcedure
   ,ERROR_MESSAGE() AS ErrorMessage;
END CATCH;

--Transazioni attive = 1
SELECT @@TRANCOUNT [Transazioni attive Prima della fine del batch]
GO

--All'esterno del batch transazioni attive = 0, SQL effettua un rollback automatico
SELECT @@TRANCOUNT [Transazioni attive Dopo della fine del batch]
GO


SET XACT_ABORT ON impone che venga implementato il rollback automatico in caso d'errore ma non viene implementato nel blocco catch, bensì al termine del batch.
Questa volta vediamo che la transazione risulta attiva prima della fine del batch ma inattiva dopo la conclusione dello stesso.
Come controprova possiamo rilanciare dalla seconda connessione la select su T1 come nell'esempio precedente e vedremo che funziona istantaneamente, restituendo solo le righe presenti nella tabella prima della nostra transazione.

Attenzione che in questo caso l'applicazione riceve anche un errore alla chiusura del batch.

Quale errore ?


Msg 3998, Level 16, State 1, Line 1
Uncommittable transaction is detected at the end of the batch. The transaction is rolled back

Una transazione in questo stato non consente ulteriori operazioni che prevedano la scrittura dei dati, eventuali operazioni di log applicativo sulla stessa connessione non potranno essere implementate.

Come possiamo scoprire lo stato di una transazione ed evitare quindi l'errore di cui sopra ?

Usando la funzione XACT_STATE() e testandone il risultato :
Se 1, la transazione è committable.
Se -1, la transazione è uncommittable ed è necessario implementare il rollback manuale.
Se 0 non sono attive transazioni.

SET XACT_ABORT ON;
BEGIN TRY
    
 BEGIN TRANSACTION;
  
  INSERT INTO T1 VALUES(2)
  INSERT INTO T2 VALUES(2)

        -- La delete fallisce per Violazione di Foreign Key
        DELETE FROM T1 WHERE ID = 1;

    -- Se Delete Ok allora faccio la commit
    COMMIT TRANSACTION;

END TRY
BEGIN CATCH
     SELECT 
   ERROR_NUMBER() AS ErrorNumber
   ,ERROR_SEVERITY() AS ErrorSeverity
   ,ERROR_STATE() AS ErrorState
   ,ERROR_LINE () AS ErrorLine
   ,ERROR_PROCEDURE() AS ErrorProcedure
   ,ERROR_MESSAGE() AS ErrorMessage;

    -- Test XACT_STATE:
        -- Se 1, La transazione è committable.
        -- Se -1, La transazione uncommittable ed è necessario implementare il rollback.
        -- XACT_STATE = 0  No transazioni.

    -- Test della transazione se Uncommittable.
    IF (XACT_STATE()) = -1 --Uncommittable
 BEGIN
        Select 'La transazione è Uncommittable.' as [Stato della transazione]
  ROLLBACK TRANSACTION
 END

 IF (XACT_STATE()) = 1 --Committable
    BEGIN
  Select 'La transazione è Committable.' as [Stato della transazione]
        COMMIT TRANSACTION;   
    END;

END CATCH;

--Transazioni attive = 1
SELECT @@TRANCOUNT [Transazioni attive Prima della fine del batch]
GO

--All'esterno del batch transazioni attive = 1, SQL NON effettua un rollback automatico
SELECT @@TRANCOUNT [Transazioni attive Dopo della fine del batch]
GO


Nel batch abbiamo il costrutto Catch che gestisce l'errore, testa lo stato della transazione e ne effettua la rollback quindi non abbiamo transazioni attive all'uscita del batch. I lock vengono rilasciati ripristinando la normale accessibilità delle risorse del Db e all'applicazione non giungono errori "strani".


Evitiamo di usare XACT_ABORT ON con il costrutto BEGIN TRY, eviteremo errori "strani", problemi di inaccessibilità alle risorse del Db e comportamenti applicativi imprevedibili.
Se proprio dovessimo utilizzarli insieme verifichiamo lo stato della transazione con la funzione XACT_STATE() prima di implementare ogni operazione di scrittura ed effettuiamo il rollback manuale della transazione in caso d'errore.

Sarebbe anche utile verificare sempre, a prescindere da Xact_abort e blocco try-catch, lo stato delle transazioni prima di implementare ulteriori scritture nel batch.

Ciao

Luca

Nessun commento:

Posta un commento