Rozdział ten w całości będzie
poświęcony operowaniu na relacyjnych danych oraz niebezpieczeństwu jakie
występuje podczas takich operacji. Dowiemy się, jak zapobiegać przypadkom
wycieku danych lub błędom podczas operowania na zasobach repozytorium.
Aby nie opowiadać o abstrakcyjnych procesach, podam kilka przykładowych
operacji, które mogą zaburzyć spójność całego systemu i doprowadzić do jego
błędnego działania. Książkowy przykład zastosowania transakcji to przelew
bankowy. Jeśli chcemy zapłacić za zakupy w sklepie, system informatyczny musi
dokonać następujących czynności:
- sprawdzić, czy stan twojego konta pozwala na dokonanie zakupu
- pobrać odpowiednią kwotę z twojego konta
- przekazać pobraną sumę i przesłać ją na konto sklepu
Najważniejsze w tym wszystkim jest to, że jeśli któraś z powyższych operacji się nie powiedzie musimy powrócić do stanu sprzed rozpoczęcia operacji.
Innym technicznym przykładem
jest proces wstawiania danych. Przypuśćmy, że obsługujemy bazę danych operatora
telefonii komórkowej. Robimy rozliczenie miesięczne i podliczanie kosztów dla
poszczególnego użytkownika z danego miesiąca. Operacja na tak ogromnym zasobie
danych może trwać parę godzin. I w pewnym momencie komputer się zawiesza. Co
się dzieje z danymi? Czy dane przeliczone zostały już wstawione? A jak zostały
wstawione - czy poprawnie? Oba powyższe przykłady powinny mieć status
dwuwartościowy. Albo się w pełni poprawnie wykonały, albo w całości zostają
odwołane. Do tego właśnie służy transakcja. Do odwołania wszystkich operacji,
na które została nałożona.
Teraz, kiedy troszeczkę wyjaśniłem, czym jest transakcja przejdźmy do
formalnego jej zdefiniowania.
Definicja transakcji
Transakcja jest jednostką wykonywania, w której wszystkie polecenia są wykonane poprawnie lub w przeciwnym przypadku nie jest wykonywane żadne z poleceń. Jest to sekwencja operacji wykonywanych jako jedna logiczna jednostka pracy.
Każda z transakcji tworzona jest z zachowaniem pewnych standardowych własności:
Własności transakcji
Atomic – transakcja jest najmniejszą jednostką logiczną. Znaczy to, że jest wykonywana w całości albo w całości jest odwołana
Consistent – transakcja nie zmienia spójności bazy. To znaczy, że jeśli baza była spójna przed wykonaniem transakcji, będzie też spójna po jej ukończeniu.
Isolated – transakcja musi być izolowana, czyli nie może wchodzić w konflikty z innymi transakcjami wykonywanymi na tym samym zbiorze danych
Durable - transakcja jest trwała, jeżeli gwarantowane jest, że wykonane działania pozostaną kompletne bez względu na to, co się stanie z bazą po poprawnym zakończeniu transakcji. Jeżeli wystąpi awaria zasilania i serwer bazy danych ulegnie awarii, istnieje gwarancja, że transakcja będzie kompletna po ponownym uruchomieniu serwera.
Aby zagwarantować spójność bazy i mieć pewność, że wykonywane operacje zakończą się sukcesem lub porażką, stosowany jest mechanizm blokowania. Blokady są gwarancją tego, że podczas operowania na danych przez jedną transakcję dane nie zostaną zmienione bądź usunięte.
Rozpoczęcie transakcji na SQL Server 2005 można dokonać na trzy sposoby:
Explicite – jawne rozpoczęcie transakcji za pomocą polecenia BEGIN TRANSACTION
Autocommit – automatyczne, operacje na serwerze standardowo podlegają transakcjom. Każde z poleceń jest automatycznie zatwierdzane po poprawnym wykonaniu. Nie ma wtedy potrzeby commitowania transakcji.
Implicit – niejawne, wywoływane przez programy użytkowe działające na bazie danych.
Zakończenie transakcji odbywa się za pomocą poleceń COMMIT lub ROLLBACK. Polecenia COMMIT używamy, gdy wszystkie operacje od rozpoczęcia transakcji powiodły się. W takiej sytuacji możemy zatwierdzić wszystkie zmiany, jakie zostały wprowadzone. Polecenie to dodatkowo zdejmuje wszystkie blokady z tabel, które zostały zablokowane na czas trwania transakcji.
@STRONA@Komenda ROLLBACK odwołuje transakcję. Polecenie to odwołuje wszystkie modyfikacje jakie zostały dokonane podczas trwania transakcji poprzez przywrócenie stanu danych sprzed transakcji. Polecenie to, podobnie jak COMMIT, również zdejmuje wszystkie blokady z danych, jakie zostały założone na czas transakcji.
Poniżej przedstawiony jest skrypt zmieniający imiona wszystkich Gregory i John na ich polskie odpowiedniki. Samo zapytanie jest już dla wszystkich oczywiste. Nowością jest oczywiście zabezpieczenie go transakcją.
BEGIN TRANSACTION
UPDATE [AdventureWorks].[Person].[Contact]
SET [FirstName] = 'Grzegorz'
WHERE [FirstName] = 'Gregory'
IF @@ERROR <> 0
BEGIN
RAISERROR
('Błąd, operacja nie udana!', 16, -1)
ROLLBACK
TRANSACTION
END
UPDATE
[AdventureWorks].[Person].[Contact]
SET [FirstName] = 'Jan'
WHERE [FirstName] = 'John'
IF @@ERROR <> 0
BEGIN
RAISERROR ('Błąd, operacja nie
udana!', 16, -1)
ROLLBACK TRANSACTION
END
COMMIT TRANSACTION
Pierwszą operacją, jaką robimy,
jest otworzenie transakcji. W tym celu pojawia się polecenie BEGIN
TRANSACTION. Następnie mamy nasz skrypt przeprowadzający operacje na
danych. Potem następuje blok kodu sprawdzający czy operacja się udała.
Standardowo zmienna systemowa @@ERROR posiada informacje z
numerem ostatniego błędu. Jeśli błąd nie wystąpił, zmienna ta ma wartość zero.
Wyrażenie warunkowe IF @@ERROR <> 0 służy do sprawdzania,
czy wystąpił błąd. Jeśli wystąpił, wchodzimy do skryptu, który go wypisze (RAISERROR)
i odwołujemy całą transakcję. Jeśli wszystko zakończyło się pomyślnie
przechodzimy do kolejnej operacji zmiany imienia i znowu sprawdzamy czy
wystąpił jakiś nieprzewidziany wypadek.
Ważne, żeby uświadomić sobie fakt, iż zwykła pojedyncza operacja
UPDATE,
DELTE czy INSERT, taka jak poniżej:
UPDATE
[AdventureWorks].[Person].[Contact]
SET [FirstName] = 'Jan'
WHERE
[FirstName] = 'John'
nie musi być opatrzona przez
nas transakcją, gdyż jest to standardowa operacja Autocommit,
która z definicji zabezpieczona jest przez transakcje.
Powracając jednak do wcześniejszych przykładów, należy pamiętać, że jeśli
modyfikujemy dane, które są ze sobą wzajemnie powiązane i niepowodzenie przy
zmianie jednego zbioru danych wiąże się z dezaktualizacją zmian na innym zbiorze
danych, musimy użyć transakcji.
Na tym etapie wystarczy używać standardowego bloku kodu, który widzieliśmy
powyżej.
BEGIN TRANSACTION
/*
BLOK KODU ZMIENIAJĄCY
POWIĄZANE ZE SOBĄ DANE
*/
IF @@ERROR <> 0
BEGIN
RAISERROR
('Błąd, operacja nieudana!', 16, -1)
ROLLBACK
TRANSACTION
END
COMMIT TRANSACTION
W najnowszej wersji SQL Server 2005 pojawiła się strukturalna pojawia obsługa nie krytycznych wyjątków. Umożliwia to nieco przyjemniejszą pracę z transakcjami. Kod który funkcjonalnie odpowiada temu z pierwszego przykładu wygląda następująco.
BEGIN TRY
BEGIN TRANSACTION
UPDATE
[AdventureWorks].[Person].[Contact]
SET [FirstName]
= 'Grzegorz'
WHERE
[FirstName] = 'Gregory'
UPDATE
[AdventureWorks].[Person].[Contact]
SET [FirstName]
= 'Jan'
WHERE
[FirstName] = 'John'
COMMIT
END TRY
BEGIN CATCH TRAN_ABORT
ROLLBACK
END CATCH
Szablon bloku try-catch wygląda tak.
BEGIN TRY
BEGIN TRANSACTION
/*
KOD
WYKONYWANY
*/
COMMIT
END TRY
BEGIN CATCH TRAN_ABORT
ROLLBACK
END CATCH
Podsumowanie
Transakcje są jednym z kluczowych elementów zapewniających spójność i integralność systemów bazodanowych. W dużych systemach informatycznych nie ma mowy o niezabezpieczeniu operacji na danych za pomocą transakcji. Niewielkie systemy, gdzie utrata bądź niewielkie przekłamania w danych nie czynią dużych szkód i nie wpływają na straty finansowe, mogą sobie pozwolić taki komfort. Moim jednak zdaniem niewielki wysiłek jaki należy włożyć w odpowiednie zabezpieczanie spójności za pomocą opisanego mechanizmu jest małym kosztem i warto stosować politykę zabezpieczenia danych.