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