Jak wspomniałem na końcu poprzedniego wpisu, dzisiejszy tekst będzie traktował o obsłudze wyjątków w PL/SQL. Jednakże – co również wówczas sygnalizowałem – jest to dość złożone zagadnienie, które – w mojej opinii – dalece przekracza materiał, jaki powinien opanować początkujący adept programowania bazy danych “made in Oracle” (oczywiście mam na myśli “koszerny” produkt, a nie MySQL-a). Dlatego też od razu podkreślam, że dzisiejszy wpis będzie raczej rodzajem wprowadzenia do tematyki obsługi wyjątków, w którym to tekście tylko zasygnalizuję pewne kwestie.
O co chodzi z tymi wyjątkami? Otóż nie jest to rzecz specyficzna dla PL/SQL, ponieważ konstrukcje do ich obsługi są dostępne chyba w każdym popularnym języku programowania. Chodzi mianowicie o sposoby radzenia sobie z nietypowymi sytuacjami, które mogą się pojawić w trakcie wykonywania kodu dla naszego programu. Sytuacje te często są spowodowane problemami z wprowadzaniem danych przez użytkownika, choć rzecz jasna nie tylko. Na przykład, jeśli silnik PL/SQL otrzyma instrukcję dzielenia dowolnej liczby przez 0 (zero), to próbę takiego działania potraktuje jako błąd i zwróci go jako wyjątek właśnie. Oczywiście wiele tych problemów można rozwiązać za pomocą odpowiedniej instrukcji warunkowej, która sprawdzi – zanim przystąpimy do dzielenia – czy przekazana wartość dla naszego dzielnika jest na pewno różna od zera i w przeciwnym razie zamiast wykonania planowanej operacji arytmetycznej wyświetli użytkownikowi informację o stosownym błędzie.
Oczywiście to tylko przykład, z resztą bardzo często spotykany przy omawianiu zagadnienia wyjątków. W przypadku PL/SQL – jak w każdym innym języku – te ostatnie mogą mieć różną genezę, chociażby takie jak błędy w logice biznesowej (np. wspomniane dopuszczenie do sytuacji, w której dzielimy przez zero z powodu braku walidacji danych wejściowych), ale również może być pochodną awarii sprzętu. Tym niemniej niezależnie od konkretnej przyczyny jest to zawsze sytuacja niepożądana, ponieważ wystąpienie wyjątku zatrzymuje dalsze wykonywanie programu, chyba że uda się go przechwycić i obsłużyć. O tym ostatnim będzie traktował dzisiejszy wpis.
W przypadku innych języków programowania do tego celu wykorzystywana zazwyczaj jest konstrukcja try…catch, którą opakowuje się jakiś blok kodu. W PL/SQL służy do tego dedykowana (choć opcjonalna) sekcja, o której była mowa w pierwszym wpisie tego cyklu. Tym samym gdy w sekcji wykonawczej zostanie zgłoszony wyjątek, dalsze działanie kodu w tym bloku zostanie zatrzymane (czytaj: żadna z pozostałych instrukcji w tej sekcji nie będzie już wykonana). Zamiast tego silnik bazy danych przeskoczy do sekcji wyjątków, o ile takowy istnieje.
W przypadku PL/SQL można wyróżnić dwa rodzaje wyjątków:
- wstępnie zdefiniowane (predefiniowane),
- zdefiniowane przez użytkownika.
W dalszej części tekstu zostaną omówione oba typy, choć więcej uwagi poświęcę temu pierwszemu.
Predefiniowane wyjątki
W przypadku bazy Oracle mam do dyspozycji całkiem spory zestaw predefiniowanych wyjątków. Gdy wystąpi błąd, który odpowiada któremuś z tych wbudowanych, mówi się często, że w tej sytuacji wyjątek został zgłoszony niejawnie (implicite). Domyślam się, że na ten moment ostatnie zdanie może być niezrozumiałe, ale podejrzewam, że stanie się to jaśniejsze po omówieniu zagadnienia wyjątków definiowanych przez użytkownika. Na ten moment wystarczy napisać: jeśli nasz program narusza którąś z wielu domyślnych reguł ustanowionych dla silnika Oracle, kontrola programu przechodzi automatycznie do sekcji bloku obsługującej wyjątki (oczywiście o ile takowy istnieje).
Po wykonaniu instrukcji umieszczonych w sekcji obsługi wyjątków, wykonywanie całego bloku się kończy, czyli sterowanie programu nie wraca już do jego sekcji wykonawczej. O ile nie ma żadnego zewnętrznego bloku wobec tego, w którym wystąpił wyjątek, działanie programu się kończy.
Oto podstawowa składnia sekcji obsługi wyjątków:
BEGIN
— lista instrukcji w sekcji wykonawczej
…
— początek sekcji obsługi wyjątków
EXCEPTION
WHEN wyjątek 1 THEN
— obsługa wyjątku 1
WHEN wyjątek 2 THEN
— obsługa wyjątku 2
WHEN OTHERS THEN
— obsługa pozostałych wyjątków
END;
W powyższej składni blok obsługi wyjątków, który zaczyna się od słowa kluczowego EXCEPTION, zawiera serię warunków ze słowem WHEN, zaś po każdym z nich występuje nazwa wyjątku, który może zostać zgłoszony w czasie realizacji kodu zapisanego w sekcji wykonawczej. Jeśli w trakcie działania naszego programu jakiś wyjątek faktycznie zostanie zgłoszony, silnik PL/SQL będzie sprawdzał czy dla tego konkretnego wyjątku w sekcji obsługującej wyjątki programista przewidział jakieś rozwiązanie. Takie sprawdzanie rozpoczyna się od pierwszej klauzuli WHEN i będzie przeskakiwać do kolejnych tego typu klauzul aż do momentu dopasowania. Wówczas wykona konkretną część kodu przewidzianego w tym “ustępie”.
Oczywiście może się zdarzyć, że żadna z klauzul WHEN nie pasuje do wyjątku, który w rzeczywistości wystąpił. Na szczęście na taką okoliczność mamy “koło ratunkowe” w postaci frazy WHEN OTHERS, która spełnia funkcję analogiczną jak ELSE w przypadku instrukcji warunkowych. Użycie tej konstrukcji jest opcjonalne, tym niemniej należy pamiętać, że powinna się ona znajdować (tak jak ELSE) na ostatniej pozycji.
Pora wreszcie przejść do przykładu wykorzystania mechanizmu obsługi wyjątków. Do tego celu użyjemy konstrukcję CASE. Jeśli ktoś czytał mój tekst poświęcony instrukcjom warunkowym, ten być może pamięta, że wspomniałem wówczas o pewnej różnicy tego rozwiązania w stosunku do “normalnej” instrukcji warunkowej IF…THEN. Dla przypomnienia: jeśli użyjemy tej konstrukcji i w wyniku jej działania testowana wartość nie zostanie dopasowana do któregokolwiek z warunków, wówczas program zamiast iść dalej – tak jak ma to miejsce w przypadku IF…THEN – zwróci błąd (czy jak teraz wiemy: wyjątek). Oczywiście można temu zapobiec poprzez dodanie klauzuli ELSE, ale z jakiegoś powodu możemy chcieć, by w sytuacji braku dopasowanie został jednak zgłoszony wyjątek (chociażby po to, by zakończyć działanie bloku wykonawczego). Na tę okoliczność mamy dostępny predefiniowany wyjątek CASE_NOT_FOUND, którego użyłem w poniższym kodzie.
DECLARE
v_nazwa VARCHAR2(30) := 'borciugner';
BEGIN
CASE v_nazwa
WHEN 'BORCIUGNER'
THEN
dbms_output.put_line('Witaj BORCIUGNER!');
WHEN 'Borciugner'
THEN
dbms_output.put_line('Witaj Borciugner!');
END CASE;
EXCEPTION
WHEN CASE_NOT_FOUND
THEN dbms_output.put_line('Coś poszło nie tak');
END;
Oczywiście to tylko jeden z wielu takich “gotowców”. Większość predefiniowanych wyjątków – o ile nie wszystkie – są dostępne w pakiecie “STANDARD”, do którego można zajrzeć przy pomocy polecenia
select
text
from
dba_source
where
name = 'STANDARD'
Jak widać niżej – w przypadku mojej wersji Oracle DB – od wiersza 163 zaczyna się sekcja, w której znajduje się lista predefiniowanych wyjątków.
Wyjątki zdefiniowane przez użytkownika
W Oracle, oprócz predefiniowanych wyjątków, programista może rzecz jasna stworzyć własne. Można je powołać do życia bezpośrednio na poziomie podprogramu, o czym za chwilę słów parę. W tym momencie istotne jest to, że takie wyjątki są widoczne tylko w tym konkretnym podprogramie. Ponadto można też taki wyjątek zdefiniować w specyfikacji jakiegoś pakietu i wówczas staje się on (wyjątek, nie pakiet) widoczny wszędzie tam, gdzie pakiet jest dostępny. Tym ostatnim zagadnieniem zajmować się jednak nie będziemy.
Jak już wspomniałem wcześniej, wszystkie predefiniowane wyjątki są zgłaszane niejawnie za każdym razem, gdy w trakcie działania programu wystąpi ten konkretny błąd. Stety lub nie z wyjątkami zdefiniowanymi przez użytkownika – przynajmniej tymi utworzonymi na poziomie podprogramu – nie ma tak dobrze, ponieważ muszą zostać zgłoszone jawnie w sekcji deklaracji oraz bezpośrednio w kodzie dla części wykonawczej przy pomocy słówka RAISE.
Jak to w praktyce wygląda, można zobaczyć na przykładzie zamieszczonym niżej, gdzie próbujemy dzielić liczbę 100 przez 0. Od razu dodam, że stosowny wyjątek można znaleźć wśród tych wprzódy zdefiniowanych (ZERO_DIVIDE).
DECLARE
v_dzielna NUMBER;
v_dzielnik NUMBER;
v_iloraz NUMBER;
ex_dzielenie_przez_zero EXCEPTION;
BEGIN
v_dzielna := 100;
v_dzielnik := 0;
IF
v_dzielnik = 0
THEN
RAISE ex_dzielenie_przez_zero;
ELSE
v_iloraz := v_dzielna/v_dzielnik;
dbms_output.put_line('Wynik dzielenia to: ' || v_iloraz);
END IF;
EXCEPTION
WHEN ex_dzielenie_przez_zero
THEN dbms_output.put_line('NIE WOLNO DZIELIĆ PRZEZ ZERO!!!');
END;
Wydaje mi się, że kod jest na tyle zrozumiały, że nie ma potrzeby dodatkowe komentarza z mojej strony. Tym bardziej, że – jak wspominałem wcześniej – kwestii wyjątków definiowanych przez użytkownika – z różnych względów – nie zamierzałem poświęcać w tym tekście zbyt wiele uwagi i tym samym miejsca. Przy pomocy tego przykładu chciałem po prostu zobrazować jak należy rozumieć twierdzenie o tym, że wyjątki predefiniowane są zgłaszane niejawnie w kontrze do tych, które tworzy programista. W przypadku tych ostatnich musimy najpierw wprost powołać je do życia w sekcji deklaracji przy pomocy słowa kluczowego EXCEPTION, a następnie opisać jego warunki w sekcji wykonawczej przy pomocy słowa kluczowego RISE.
Co dalej?
Kolejny wpis będzie poświęcony zagadnieniu kursorów w PL/SQL i wówczas można będzie przyjąć, że najbardziej elementarne składowe programowania w PL/SQL są już za nami. Natomiast będzie to pierwszy tekst, gdzie będziemy operować na danych zapisanych w tabelach bazy danych, dlatego został on zamieszczony na końcu, choć pewnie – o czym niechybnie się przekonamy – najlepiej byłoby go omówić zaraz po wprowadzeniu zagadnienia pętli w PL/SQL..