Obsługa błędów w SQL i transakcje

Transkrypt

Obsługa błędów w SQL i transakcje
Obsługa błędów w SQL i transakcje
Obsługa błędów w SQL
Zacznijmy od najprostszego przykładu:
CREATE PROCEDURE podziel1
(
@dzielna float,
@dzielnik float
)
AS
BEGIN
SET NOCOUNT ON – – dobry nawyk w przypadku procedur
SELECT @dzielna / @dzielnik
END
Powyższa procedura w większości przypadków zadziała prawidłowo, lecz na przykład poniższe wywołanie:
EXEC podziel1 1, 0
jak można się spodziewać zakończy się błędem. Jakim?
Najprostszy i najłatwiejszy (ale nie zawsze dla programisty) sposób obsługi błędów, to „pilnowanie” aby
te błędy nie wystąpiły już na etapie programowania, na przykład:
CREATE PROCEDURE podziel2
(
@dzielna float,
@dzielnik float
)
AS
BEGIN
SET NOCOUNT ON – – dobry nawyk w przypadku procedur
IF @dzielnik = 0
SELECT ’Dzielenie przez ZERO!’
ELSE
SELECT @dzielna / @dzielnik
END
W przypadku bardziej skomplikowanych programów najwłaściwszym sposobem jest oczywiście przewidzenie wszystkich tych miejsc w kodzie, w których mogą wystąpić problemy, i wykonanie wcześniejszych
walidacji. Niestety nie zawsze jest to możliwe i co wtedy? Zmodyfikujmy więc procedurę podziel2 w
następujący sposób:
CREATE PROCEDURE podziel3
(
@dzielna float,
@dzielnik float
)
AS
BEGIN
SET NOCOUNT ON – – dobry nawyk w przypadku procedur
IF @dzielnik = 0
RAISERROR(’Dzielenie przez ZERO!’,14,1)
ELSE
SELECT @dzielna / @dzielnik
END
Na pierwszy rzut oka procedura ta niewiele się różni od procedury podziel1, zwłaszcza w przypadku
dzielenia przez zero, za wyjątkiem w części zrozumiałego dla polskojęzycznego użytkownika komunikatu.
Polecanie RAISERROR mianowicie powoduje wygenerowanie komunikatu błędu, który na przykład
może być dalej obsłużony w bloku TRY . . . CATCH, na przykład w poniższej procedurze:
CREATE PROCEDURE podziel i pomnoz
(
@a float,
@b float
)
AS
BEGIN
SET NOCOUNT ON – – dobry nawyk w przypadku procedur
BEGIN TRY – – tu może wystąpić błąd
EXEC podziel3 @a, @b – – ale równie dobrze podziel1
END TRY
BEGIN CATCH
PRINT ERROR MESSAGE()
– – ERROR MESSAGE() zwraca komunikat ostatniego błędu
PRINT ’... ale się tym nie przejmujemy i liczymy dalej ...’
END CATCH
BEGIN TRY – – tu może wystąpić błąd
EXEC podziel3 @b, @a – – ale równie dobrze podziel1
END TRY
BEGIN CATCH
PRINT ERROR MESSAGE()
PRINT ’... ale się tym nie przejmujemy i dzielimy dalej ...’
END CATCH
SELECT @a * @b
END
Zadanie 1. Co robi poniższa procedura (przykład z pomocy)? Przy okazji proszę zwrócić uwagę na trzeci
sposób zwracania wyniku przez procedurę!
CREATE PROCEDURE TimeDelay hh mm ss
(
@DelayLength char(8) = ’00:00:00’
)
AS
BEGIN
DECLARE @ReturnInfo varchar(255)
IF ISDATE(’2000-01-01 ’ + @DelayLength + ’.000’) = 0
BEGIN
SELECT @ReturnInfo = ’Invalid time ’ + @DelayLength + ’,hh:mm:ss, submitted.’
– – This PRINT statement is for testing, not use in production.
PRINT @ReturnInfo
RETURN(1)
END
WAITFOR DELAY @DelayLength
SELECT @ReturnInfo = ’A total time of ’ + @DelayLength + ’, hh:mm:ss, has elapsed! ’
+ ’Your time is up.’
– – This PRINT statement is for testing, not use in production.
PRINT @ReturnInfo
END
Poniżej przykład jej wywołania:
EXEC TimeDelay hh mm ss ’00:00:05’
Zadanie 2. Zmodyfikować procedurę z zadania 1 używając blok TRY . . . CATCH tak, aby nie korzystać
z funkcji ISDATE(. . . ).
Transakcje
UWAGA: Kolejne przykłady należy wykonywać w dwóch oknach query. Dobry refleks jest wskazany!
Zanim przystąpimy do doświadczeń zdefiniujmy pomocnicze procedury pisz i czytaj oraz tabelę konta ze
zdefiniowanymi dwoma kontami jak poniżej:
CREATE TABLE konta
CREATE PROCEDURE pisz
(
(
saldo float,
@nazwa varchar(10),
nazwa varchar(10) PRIMARY KEY NOT NULL
@kwota float
)
)
CREATE PROCEDURE czytaj
AS
(
BEGIN
@nazwa varchar(10),
IF EXISTS
@saldo float OUTPUT
(
)
SELECT saldo
AS
FROM konta
BEGIN
WHERE @nazwa = nazwa
SELECT@saldo = saldo
)
FROMkonta
UPDATE konta
WHERE@nazwa = nazwa
SET saldo = @kwota
WHERE @nazwa = nazwa
END
ELSE
INSERT konta(nazwa, saldo)
VALUES(@nazwa, @kwota)
END
Na koniec dodajemy dane kont:
EXEC pisz ’A’, 100
EXEC pisz ’B’, 150
Rozpatrzmy teraz pierwsze dwa zestawy instrukcji:
SET TRANSACTION ISOLATION
LEVEL REPEATABLE READ
SET TRANSACTION ISOLATION
LEVEL REPEATABLE READ
DECLARE @saldoA float
DECLARE @saldoA float
DECLARE @saldoB float
DECLARE @saldoB float
DECLARE @przelew float
DECLARE @przelew float
BEGIN TRAN
BEGIN TRAN
EXEC czytaj ’A’, @saldoA OUTPUT
WAITFOR DELAY ’00:00:03’
SET @przelew = 50
EXEC czytaj ’A’, @saldoA OUTPUT
SET @saldoA = @saldoA - @przelew
SET @przelew = @saldoA * 0.1
WAITFOR DELAY ’00:00:05’
SET @saldoA = @saldoA - @przelew
EXEC pisz ’A’, @saldoA
EXEC pisz ’A’, @saldoA
EXECczytaj ’B’, @saldoB OUTPUT
EXEC czytaj ’B’, @saldoB OUTPUT
SET@saldoB = @saldoB + 50
WAITFOR DELAY ’00:00:05’
EXEC pisz ’B’, @saldoB
SET @saldoB = @saldoB + @przelew
COMMIT TRAN
EXEC pisz ’B’, @saldoB
COMMIT TRAN
Zadanie 3. Z jakimi konfliktami mamy do czynienia w powyższym przykładzie? Co należy zrobić, aby
co najmniej jedna z transakcji zadziałała prawidłowo?
Poniżej kolejny zestaw instrukcji:
SET TRANSACTION ISOLATION
LEVEL READ UNCOMMITTED
SET TRANSACTION ISOLATION
LEVEL READ UNCOMMITTED
DECLARE @saldoA float
DECLARE @saldoA float
DECLARE @saldoB float
DECLARE @saldoB float
DECLARE @przelew float
DECLARE @przelew float
DECLARE @licznik int
DECLARE @licznik int
SET @licznik = 12
SET @licznik = 6
BEGIN TRAN
BEGIN TRAN
WHILE @licznik > 0
BEGIN
WHILE @licznik > 0
BEGIN
EXEC czytaj ’A’, @saldoA OUTPUT
WAITFOR DELAY ’00:00:02’
SELECT @saldoA
EXEC czytaj ’A’, @saldoA OUTPUT
WAITFOR DELAY ’00:00:01’
SET @saldoA = @saldoA - 10
SET @licznik = @licznik - 1
EXEC pisz ’A’, @saldoA
END
COMMIT TRAN
SET @licznik = @licznik - 1
END
COMMIT TRAN
Zadanie 4. Przeprowadzić serię eksperymentów zarówno dla pierwszego jak i drugiego zestawu instrukcji.
Zadanie 5. Zaproponować przykład ilustrujący zakleszczenie.
Wskazówka: Wykorzystać poziom izolacji: READ COMMITTED.
Na zakończenie przykładowa obsługa błędów w transakcji:
BEGIN TRAN
BEGIN TRY
INSERT konta(nazwa, saldo)
VALUES (’A’, 100)
END TRY
BEGIN CATCH
SELECT ERROR NUMBER() AS ErrorNumber,
ERROR SEVERITY() AS ErrorSeverity,
ERROR STATE() AS ErrorState,
ERROR PROCEDURE() AS ErrorProcedure,
ERROR LINE() AS ErrorLine,
ERROR MESSAGE() AS ErrorMessage
IF @@TRANCOUNT > 0
ROLLBACK TRAN
END CATCH
IF @@TRANCOUNT > 0
COMMIT TRAN