SQL Server 2012 újdonságok – 5. A THROW parancs

Az SQL Server 2005-ben bevezetett TRY/CATCH hibakezelés óriási előrelépést jelentett az átlátható TSQL hibakezelés terén. Más nyelvekhez viszonyítva egy valami azonban hiányzott a képből, a THROW, amivel ki lehet váltani egy kivételt (exceptiont). Volt persze egy hasonló, a RAISERROR, ám ez kicsit más, mint a többi programnyelv throw-ja. A throw ugyanis általában kétféle dolgot tud. Az egyik, hogy feldob egy kivételt, a másik, hogy egy elkapott kivételt továbbdob. Ez utóbbi re-thrownak hívják, és azért nagyon hasznos, mert try-catch blokkban le tudjuk kezelni az eredeti hibát, pl. visszagörgetjük a tranzakciót, naplózunk vagy bármi egyéb akciót hajtunk végre, majd a catch ág végén újra feldobhatjuk az eredeti hibát. Így a kliens alkalmazás vagy SQL hívó pontosan megkapja az eredeti hiba részleteit, például a kivétel sora nem a catch ágba rakott (re)throw-ra mutat, hanem az eredeti hibát kiváltó SQL sorra.
A THROW tehát hasonló helyeken használatos, mint a RAISERROR, de van közöttük pár fontos különbség. A legfontosabbak: a THROW nem tud rendszer kivételeket generálni (SQL Server belső hibaüzeneteket), nem használja a sys.messages táblát (ebben vannak az előre definiált hibaüzenetek) és mindig 16-os severity levelű kivételeket dob.
Az alábbi példában egy update az, ami hibát dobhat, ha beleütközik egy idegen kulcsba. A kivételt elkapjuk, lelogoljuk, majd újradobjuk a kliens részére. A re-throw miatt a hibanaplózó logika nem szól bele a kliens által kapott hibarészletekbe.

BEGIN TRY
	--Ez hibázhat
	update HumanResources.Employee 
	set BusinessEntityID = -1 
	where BusinessEntityID = 2;
END TRY
BEGIN CATCH
	--Naplózzuk a hibát
	INSERT INTO 
	[dbo].[ErrorLog]
    ([ErrorTime]
    ,[UserName]
    ,[ErrorNumber]
    ,[ErrorSeverity]
    ,[ErrorState]
    ,[ErrorProcedure]
    ,[ErrorLine]
    ,[ErrorMessage])
	VALUES(SYSDATETIME(), 
	SUSER_NAME(),
	ERROR_NUMBER(),
	ERROR_SEVERITY(), 
	ERROR_STATE(), 
	ERROR_PROCEDURE(), 
	ERROR_LINE(), 
	ERROR_MESSAGE());
  
  --Újradobjuk a kivételt a kliens részére
  THROW;

END CATCH

A kliens ezt kapja:

Msg 547, Level 16, State 0, Line 3
The UPDATE statement conflicted with the REFERENCE constraint "FK_EmployeeDepartmentHistory_Employee_BusinessEntityID". The conflict occurred in database "AdventureWorks2012", table "HumanResources.EmployeeDepartmentHistory", column 'BusinessEntityID'.

Látható, hogy ez az eredeti hiba, a 3. sorra hivatkozik, nem a 29.-re, ahol valójában a THROW van. Az errorlog táblánkba ugyanez az információ kerül be:

Nyilván nem csak újradobásra jó a THROW, hanem olyan helyeken is, ahol eddig RAISERROR-ral dobtunk fel saját hibát.
Ez:

RAISERROR(N'Az alkalmazott nem törölhető, mert vannak hozzá kapcsolt élő feladatok', 16, 1);

így néz ki THROW-val:

THROW 50000, N'Az alkalmazott nem törölhető, mert vannak hozzá kapcsolt élő feladatok', 1;

A hívó mindkét esetben ezt kapja:

Msg 50000, Level 16, State 1, Line 1
Az alkalmazott nem törölhető, mert vannak hozzá kapcsolt élő feladatok

Összegezve, ha saját hibákat szeretnénk a kliensek felé közvetíteni, illetve, ha meg kell tartani az eredeti hibáról szóló információkat, akkor a THROW-t érdemes használni. Speciális esetekben, pl. nem 16-os szintű hibákhoz, vagy ha a sys.messagesben előre definiált hibaüzenetet kell feldobni, akkor továbbra is elérhető a RAISERROR.
Egyébként az a tény, hogy a THROW nem használja a sys.messages táblát igen fontos lépés, mert így könnyebb költöztetni az adatbázist másik szerverre, nem kell átvinni egyedi hibaüzeneteket a master táblák között. Ez kapcsolódik az itt nem tárgyalt, SQL 2012 újdonság Contained Databases újításhoz is.