Transakcje Transakcje zwykle składają się z kilku odczytujących i modyfikujących bazę danych instrukcji SQL. Transakcja jest rozpatrywana z punktu widzenia serwera. Określa ona, ile pracy wykona SQL Server, zanim przeprowadzone modyfikacje zostaną zatwierdzone (ang. commit). Transakcja składająca się z wielu poleceń nie spowoduje żadnych trwałych zmian w bazie danych, dopóki nie zostanie wydane polecenie SACTION. Wielopoleceniowa transakcja może cofnąć wykonane operacje za pomocą polecenia ROLLBACK TRANSACTION. Zarówno wsady, jak i transakcje mogą zawierać wiele poleceń. Między tymi dwoma typami modułów istnieją zależności typu wiele-do-wielu". Pojedyncza transakcja może objąć kilka wsadów, (chociaż z punktu widzenia wydajności nie jest to najlepsze rozwiązanie) zaś na pojedynczy wsad może się składać kilka transakcji. Przykład Pierwszy skrypt przedstawia jedną transakcję obejmującą cztery wsady: skrót od słowa TRANSACTION INSERT authors VALUES... UPDATE publishers SET pub_id =... Drugi skrypt jest jednym wsadem z dwoma transakcjami: INSERT authors VALUES. UPDATE publishers SET pub_id. INSERT publishers VALUES. Instrukcje i powodują, że umieszczone pomiędzy nimi polecenia są wykonywane jako całość: INSERT authors VALUES. UPDATE publishers SET pub_id =. Przetwarzanie transakcji w SQL Severze powoduje, że wszystkie polecenia w obrębie jednej transakcji są traktowane jako jednostka robocza - nawet jeżeli wystąpi awaria sprzętu lub ogólna awaria systemu. Tego typu transakcje mają właściwości ACID (Atomicity Consistency, Isolation, Durability) niepodzielność (atomowość) - albo wszystkie akcje wchodzące w skład transakcji są wykonane, albo żadna, spójność po wykonaniu transakcji stan bazy danych powinien być spójny, izolacja (odseparowanie) wynik transakcji powinien być taki sam, jakby od chwili rozpoczęcia transakcji nie działała na wspólnych danych żadna inna transakcja, każdy użytkownik powinien mieć wrażenie, że sam korzysta z bazy danych, trwałość (wytrzymałość) dane zatwierdzone przez transakcję powinny być dostępne nawet w systuacji awarii oprogramowania lub sprzętu. Transakcje jawne i niejawne Domyślnie SQL Server traktuje każdą instrukcję (niezależnie od tego, czy jest przekazywana odrębnie, czy też stanowi część wsadu) jako niezależną i zatwierdza ją natychmiast po wykonaniu. Z logicznego punktu widzenia, jedynym typem instrukcji, która powinna być zatwierdzana, jest instrukcja wprowadzająca jakieś zmiany w bazie danych. Tak, więc mówienie o zatwierdzaniu instrukcji SELECT wydaje się 1
bezsensowne. Jednak, w przypadku niektórych większych transakcji warto umieszczać w nich instrukcję SELECT. Gdy mówimy o transakcjach, z reguły mamy na myśli instrukcje modyfikujące dane (INSERT, UPDATE i DELETE). Tak, więc każda pojedyncza instrukcja modyfikująca dane jest transakcją niejawną (ang. impli-cit transaction) - niezależnie od tego, czy są modyfikowane wszystkie wiersze, czy też nie zmienia się żaden z nich. Jeżeli podczas aktualizacji tabeli z milionem wierszy wystąpi awaria, po ponownym uruchomieniu SQL Servera część aktualizacji zapisana na dysk zostanie wycofana (ang. rollback), a baza danych powróci do stanu sprzed rozpoczęcia modyfikacji. Jeżeli chcemy, aby jedna transakcja składała się z wielu instrukcji, musimy ująć je w bloku instrukcji SACTION i SACTTON lub ROLLBACK TRANSACTION. Transakcje składające się z wielu instrukcji są transakcjami jawnymi (ang. explicit transaction). SQL Server można skonfigurować do niejawnego rozpoczynania transakcji poprzez polecenie SET IMPLICIT_TRANSACTIONS ON lub włączając opcję globalną przy użyciu procedury sp_configure 'user options', 2. Jeżeli włączone są transakcje niejawne, wszystkie instrukcje są traktowane jako część jednej transakcji i SQL Server nie podejmuje żadnych działań modyfikujących, dopóki w sposób jawny nie zostanie wydane polecenie (lub jego synonim COMMIT WORK). Stwierdzenie to jest prawdziwe nawet wówczas, gdy wszystkie instrukcje we wsadzie zostały już wykonane - w kolejnym wsadzie trzeba wydać instrukcję SACTION, aby przeprowadzone wcześniej zmiany zostały utrwalone. Domyślnie opcja IMPLICIT_TRANSACTIONS nie jest włączona i wszystkie transakcje wielopoleceniowe muszą rozpoczynać się od słów. Sprawdzanie błędów w transakcjach Jednym z najczęściej popełnianych przez programistów pracujących w środowisku SQL Servera błędów jest przekonanie, że każda nieprawidłowość w transakcji powinna spowodować jej natychmiastowe wycofanie. Tego typu nieporozumienie jest w pewnym sensie usprawiedliwione, ponieważ o transakcjach mówimy zawsze, że mają charakter niepodzielny. Jeżeli jednak rozważamy awarię, która zmusza do wycofania transakcji, zwykle mamy na myśli jakąś awarię systemu. Gdy cały system przestaje działać, trzeba ponownie uruchomić SQL Servera, a automatyczny proces odzyskiwania spowoduje cofnięcie nie zakończonych transakcji. Jednakże w przypadku wielu błędów występujących w transakcjach przerywana jest tylko bieżąca instrukcja - pozostałe instrukcje w transakcji mogą być kontynuowane, a nawet zatwierdzone. Oto przykład: USE PUBS UPDATE authors SET state = 'FL' WHERE state = 'KS' UPDATE jobs SET min_lvl = min_lvl - 10 WHERE state = 'FL' Powyższa transakcja zawiera dwie instrukcje UPDATE. Pierwsza z nich zmienia wartość pola state na 'FL'. Następna próbuje odjąć 10 od wartości min_lvl z tabeli jobs, jednak w tej kolumnie istnieje ograniczenie CHECK, które stwierdza, że jej wartość musi być większa lub równa 10. Większość wartości w tabeli jobs jest większa niż 20, tak, więc odjęcie 10 nie spowoduje naruszenia ograniczenia. Istnieje jednak jeden wiersz o wartości równej 10, tak więc jego zmniejszenie spowoduje naruszenie ograniczenia. Z powodu tego jednego wiersza nastąpi anulowanie całej instrukcji i żaden wiersz w tabeli jobs nie zostanie zaktualizowany, jednak transakcja nie zostanie wycofana. Instrukcja SELECT po COMMIT dowodzi, że w tej transakcji udało się nam zmienić wartości w polu state tabeli authors. Transakcje składające się z wielu instrukcji powinny sprawdzać błędy poprzez badanie wartości @@ERROR po wykonaniu każdej instrukcji. Jeżeli błąd, który wystąpił, nie jest krytyczny i nie trzeba podejmować w związku z nim jakichś specjalnych działań, rozpoczyna się przetwarzanie następnej instrukcji. Tylko błędy krytyczne powodują automatyczne przerwanie wykonywania wsadu i wycofanie transakcji. Oto jak wyglądałby powyższy przykład po wprowadzeniu w nim kontroli błędów: USE PUBS UPDATE authors SET state = 'FL' WHERE state = 'KS' 2
IF @@ERROR <> 0 BEGIN ROLLBACK TRAN TO ON_ ERROR END UPDATE jobs SET min_lvl = min_lvl - 10 IF @@ERROR <> 0 BEGIN ROLLBACK TRAN TO ONERR0R END ON_ERR0R: WHERE state = 'FL' W powyższym wsadzie sprawdzamy błędy po wykonaniu każdej instrukcji modyfikującej dane. Jednak wycofanie transakcji w przypadku wystąpienia błędu nie wystarcza, ponieważ w większości przypadków instrukcja ROLLBACK TRAN nie zmienia sterowania przebiegiem wykonania instrukcji. ROLLBACK TRAN może cofnąć wszystkie zakończone modyfikacje w bieżącej transakcji i wprowadzić odpowiednie wpisy do dziennika transakcji. Jednak program będzie kontynuowany od następnej linii. W takim przypadku, bez korzystania z instrukcji typu TO, jeżeli pierwsza instrukcja UPDATE spowoduje błąd, nastąpi wykonanie drugiej instrukcji UPDATE. Jeżeli błąd wystąpi również w tej instrukcji, wsad będzie kontynuowany i nastąpi próba zatwierdzenia transakcji (). Ponieważ jednak wykonaliśmy już instrukcję ROLLBACK TRAN, nie istnieje otwarta transakcja, którą można by zatwierdzić, a więc wykonanie instrukcji spowoduje błąd. Wskazówka Kwerenda, która nie znajdzie wierszy spełniających kryteria zadane w klauzuli WHERE, nie powoduje błędu. Nie spowoduje go również instrukcja UPDATE, która nie aktualizuje żadnego wiersza. W obydwu przypadkach funkcja @@ERROR zwróci wartość 0 (brak błędu). Aby stwierdzić, czy wykonywana instrukcja objęła jakieś wiersze, należy sprawdzić funkcję @@ROWCOUNT. ---Błędy składniowe zawsze powodują, że cały wsad jest przerywany jeszcze przed jego rozpoczęciem, natomiast odwołania do nie istniejących obiektów (na przykład instrukcja SELECT dla nie istniejącej tabeli) spowodują przerwanie wykonywania wsadu w miejscu wykrycia błędu. Jednakże z faktu przerwania wsadu nie wynika automatycznie, że transakcja zostanie wycofana. Jak już mówiliśmy, jedna transakcja może obejmować kilka wsadów. Jeżeli wystąpi błąd i nie mamy pewność, czy transakcja została wycofana, możemy sprawdzić jej stan przy użyciu funkcji systemowej @@TRANCOUNT. Jeżeli wartość @@TRANCOUNT jest większa od 0, oznacza to, że istnieje jakaś aktywna transakcja i można wykonać instrukcję ROLLBACK TRAN. Funkcją @@TRANCOUNT zajmiemy się szczegółowo w dalszej części tego podrozdziału. Zwykle błędy składniowe oraz niewłaściwe odwołania do obiektów usuwamy przed wdrożeniem aplikacji, tak więc przedstawiony powyżej problem nie ma dużego znaczenia praktycznego. Jednak błąd składniowy, który wystąpi we wsadzie utworzonym dynamicznie przy użyciu instrukcji EXECUTE('ciqg znaków') jest wykrywany dopiero podczas wykonania. Konstrukcje typu EXECUTE('ciqg znaków') można traktować jako zagnieżdżone wsady. Jeżeli ciąg znaków składa się z wielu instrukqi, wówczas błąd składniowy lub niewłaściwe odwołanie do obiektu spowodują przerwanie tylko tego wsadu. Przetwarzanie będzie kontynuowane od następnej instrukcji zewnętrznego wsadu. Rozważmy na przykład następujący skrypt: DECLARE @tablename sysname SET Otablename - 'authours' EXECUTE ('USE pubs SELECT * FROM + gtablename + PRINT ''Wsad wewnętrzny wykonany' PRINT 'Wsad zewnętrzny wykonany') W czasie analizy składniowej tego wsadu nie zostanie wykryty błąd, a więc SQL Server spróbuje go wykonać. Instrukcje DECLARE i SET zostaną wykonane prawidłowo, jednakże w zagnieżdżonym wsadzie w instrukcji EXECUTE wystąpi problem. W instrukcji SELECT jest błąd, ponieważ w bazie danych pubs nie istnieje tabela authours. Wewnętrzny wsad wygeneruje błąd podczas fazy odwzo-rowywania tej błędnej nazwy i w konsekwencji trzecia instrukcja wewnętrznego wsadu (PRINT) nie zostanie wykonana. Jednak zewnętrzny wsad będzie kontynuowany i uzyskamy komunikat końcowy. Błędy pojawiające się w środowisku aplikacji użytkowych są najczęściej błędami związanymi z 3
zasobami. Prawdopodobnie nie będą występować zbyt często, zwłaszcza jeżeli aplikacja zostanie odpowiednio przetestowana, a ustawienia kon-figuracyjne właściwie zdefiniowane. Błędy związane z brakiem zasobów występują w momencie przepełnienia plików bazy danych, z braku pamięci potrzebnej do wykonania procedury, gdy baza danych jest wyłączona lub niedostępna itp. W przypadku tego typu błędów krytycznych projektowanie działań warunkowych w oparciu o funkcję @@ERROR jest bezcelowe, ponieważ wsad jest automatycznie przerywany. Poniżej przedstawiono listę błędów, którym należy poświęcić największą uwagę: Brak uprawnień do obiektu, Naruszenie ograniczeń, Dublujące się wartości podczas aktualizacji lub wstawiania wierszy, Zakleszczenie z innym użytkownikiem, Naruszenie ograniczeń typu NOT NULL, Niedozwolone wartości dla bieżącego typu danych. W kolejnym przykładzie generowane są trzy tabele, z których dwie tworzą relację klucza podstawowego/obcego. Tabela b zawiera odwołanie klucza obcego do tabeli a; każda instrukcja INSERT w tabeli b powinna zakończyć się błędem, jeżeli wprowadzana wartość nie istnieje już w tabeli a. Następnie odpowiednia procedura próbuje wstawić wiersze do obydwu tabel. Procedura ta została napisana z uwzględnieniem sprawdzania błędów - w przypadku wystąpienia błędu nastąpi wycofanie transakcji. Ponieważ do tabeli a nie wstawiono jeszcze żadnych rekordów, każda próba wstawienia danych do tabeli b zakończy się niepowodzeniem. CREATE TABLE a ( a char(l) primary key) CREATE TABLE b ( b char(l) references a) CREATE TABLE C ( c char(l)) CREATE PROC test as SACTION INSERT c VALUES ('X') IF (@@ERROR <> 0) TO on_error INSERT b VALUES ('X') Nieudane odwołanie IF (@@ERROR O 0) TO on_error SACTION RETURN(O) on_error: ROLLBACK TRANSACHON RETURN(l) Ta prosta procedura ilustruje możliwości języka Transact-SQL. Funkcja systemowa @@ERROR zwraca odpowiednią wartość po wykonaniu każdej instrukcji. Wartość 0 oznacza brak błędu. Podczas wykonywania procedury test instrukcja IN-SERT w tabeli b zakończy się niepowodzeniem z powodu naruszenia klucza obcego. Uzyskamy błąd o numerze 547, opatrzony następującym komunikatem: INSERT statement conflicted with COLUMN FOREIGN KEY constraint 'FK_ b b 723BFC65'. The conflict occurred in database 'pubs', table 'a', column 'a' Wynika z tego, że również funkcja @@ERROR zwróci wartość 547. Z tego względu instrukcja IF(@@ERROR<>0) uzyska wartość TRUE i wykonanie zostanie przekazane do etykiety on_error:, gdzie nastąpi cofnięcie transakcji. Ponadto procedura zwróci kod 1, ponieważ po cofnięciu transakcji zostanie wykonana instrukcja RE-TURN(l). Jeżeli rozgałęzienie onjzrror: nie zostanie wykorzystane, procedura będzie kontynuowana wiersz po wierszu aż do osiągnięcia instrukcji SACTION, po czym zwróci wartość 0, nie dochodząc do sekcji on_error:. Podobnie jak większość innych języków programowania, Transact-SQL może zwrócić kod stanu z procedury, który określa jej pomyślne lub niepomyślne wykonanie (lub jeszcze inne możliwe wyniki). Uzyskane w ten sposób kody stanu są sprawdzane przez podprogramy wywołujące. Trzeba jednak pamiętać o sprawdzaniu stanu po zakończeniu takich procedur. Wykonanie wyłącznie instrukcji 4
EXEC test nie dostarczy bezpośrednich informacji o tym, czy procedura została wykonana zgodnie z oczekiwaniami. Lepszym wyjściem jest zastosowanie zmiennej lokalnej do sprawdzania kodu zwróconego z procedury: DECLARE @retcode int EXEC @retcode-test Po zakończeniu procedury test, zmienna lokalna @retcode będzie miała wartość 0 (jeżeli nie wystąpił żaden błąd) lub 1 (jeżeli nastąpiło wykonanie sekcji on_error:). W SQL Serverze wprowadzono też opcję SET XACT_ABORT, wymuszającą przerwanie wsadu w przypadku wystąpienia każdego błędu, nie tylko krytycznego. Oto jeszcze jeden sposób zapewnienia, że modyfikacje dokonane w transakcji zostaną w całości wycofane, gdy pojawi się jakiś błąd: CREATE PROC test AS SET XACT_ABORT ON SACTION INSERT c VALUES ('X'). INSERT b VALUES ('X') Nieudane odwołanie SACTION EXEC test SELECT * FROM c Po wykonaniu tego wsadu uzyskamy następujący wynik: (0 rows affected) Nazwa opcji XACT_ABORT jest trochę myląca, ponieważ jej ustawienie powoduje w przypadku wystąpienia błędu natychmiastowe przerwanie bieżącego wsadu, a nie tylko transakcji. Z tego faktu wynikają konsekwencje, które trudno od razu dostrzec. Na przykład jeżeli w jednym w wsadzie wykonujemy dwie transakcje, druga z nich nie zostanie wykonana, jeżeli już w pierwszej wystąpi jakiś błąd. Załóżmy, że chcemy stosować się do dobrych zasad programistycznych i sprawdzać stan zwracany przez poprzednią procedurę. (Zauważmy, że chociaż w tej procedurze nie występuje jawna instrukcja RETURN, każda wykonywana procedura zwraca domyślną informację o stanie, którą jest 0 oznaczające pomyślne wykonanie). Spróbujmy wykonać następujący wsad: DECLARE @retcode int EXEC @retcode=test SELECT gretcode Pojawia się tutaj drobny, lecz ważny problem. Jeżeli w procedurze wystąpi błąd, instrukcja SELECT rełcode nigdy nie zostanie wykonana - cały wsad ulegnie przerwaniu z powodu instrukcji SET XACT_ABORT. Wynika z tego, że choć sprawdzanie funkcji @@ERROR jest trochę pracochłonne, zapewnia jednak lepszą kontrolę nad procesem wykonywania procedur. Niestety, obsługa błędów w SQL Serverze 2000 jest trochę niespójna. Na przykład nie można zainstalować podprogramu, który działałby w sposób: Zrób to w przypadku błędu" (za wyjątkiem opcji SET XACT_ABORT, która przerywa działanie, lecz nie pozwala na określenie wykonywanych czynności). Trzeba natomiast stosować rozwiązania podobne do przedstawionego w poprzednim przykładzie, polegające na sprawdzaniu funkcji @@ERROR i wykonywaniu skoku bezwarunkowego TO. Co więcej, nie ma obecnie prostego sposobu, który zawczasu pozwalałby stwierdzić, jakie błędy powinny być traktowane jako krytyczne i powodować przerwanie wsadu, a które należy uznawać za niekrytyczne, zezwalając tym samym na wykonywanie dalszych instrukq"i. W większości przypadków błąd o wadze 16 lub większej jest błędem krytycznym i powoduje przerwanie wsadu. Błąd składniowy polegający na odwołaniu się do nie istniejących funkcji ma wagę 15, lecz również powoduje przerwanie wsadu. Chociaż istnieje funkcja @@ERROR zwracająca określony kod błędu, brak jakiejś funkqi, która pomagałaby określić jego wagę. W takim przypadku trzeba badać zawartość tabeli sysmessages i sprawdzać w niej wagę ostatniego błędu. Aby jeszcze bardziej skomplikować to zagadnienie, dodajmy, że niektóre błędy o wadze 16 nie są krytyczne. Dotyczy to między innymi błędu naruszenia ograniczenia, jaki wystąpił w pierwszym przykładzie z obsługą błędów. Tabela 12-1 przedstawia najczęstsze błędy niekrytyczne o wadze 16. 5