Strukturalna obsługa błędów w ABL cz. I
Zanim przejdziemy do omówienia strukturalnej obsługi błędów w języku ABL, trzeba przypomnieć pewne (oczywiste) fakty dotyczące tego języka.
ABL (wcześniej 4GL) był zawsze proceduralnym językiem bazodanowym o strukturze blokowej. Umożliwia on łączenie logiki biznesowej i interfejsu użytkownika w jednym pliku procedury zawierającym instrukcje, blok główny i bloki zagnieżdżone. Blokowa struktura jest tu niezwykle istotna, ponieważ z nią związane są zakresy transakcji, blokady rekordów a także obsługa błędów.
Od samego początku obsługa błędów (nazywana tradycyjną) była zaimplementowana w języku 4GL. Związane są z nią takie słowa kluczowe jak NO-ERROR, STATUS-ERROR, RETURN ERROR, bloki typu ON ERROR itd. Programiści dobrze wiedzą o co chodzi.
W wersji OpenEdge 10 język 4GL wyewoluował do ABL, zawierając oprócz poprzednich instrukcji także technologię programowania obiektowego i razem z tymi zmianami pojawiła się strukturalna obsługa błędów. Jest to model obsługi błędów spotykany w wielu językach, ale zazwyczaj jest kojarzony z językami obiektowymi i składnią typu: try i catch.
Instrukcja try umożliwia zdefiniowanie bloku kodu, z którym można powiązać zachowanie związane z obsługą błędów. Z kolei instrukcja catch służy do zdefiniowania obsługi błędu i powiązanie jej z typem błędu. Gdy system wykryje błąd w bloku try, wykona kod w bloku catch, który pasuje do błędu. Jeśli takiego bloku nie ma, to obsługa błędu jest przekazywana najczęściej w dół stosu wywołań, dopóki system nie znajdzie odpowiedniego bloku catch.
Strukturalna obsługa błędów posiada następujące cechy:
Jeśli chodzi o implementację tej technologii w języku ABL, to należy przypomnieć, że jest to język o strukturze blokowej, gdzie prawie wszystkie bloki są blokami transakcyjnymi UNDO (poza zwykłym blokiem grupującym DO:…END.) i nie ma potrzeby budowania specjalnych bloków try, co ułatwia programowanie.
OK, napiszmy pierwszy prosty przykład własnej obsługi błędu, korzystając z faktu, że w tablicy Customer nie ma rekordu o polu CustNum = 1000.
DO ON ERROR UNDO, LEAVE: FIND FIRST Customer WHERE CustNum = 1000. MESSAGE Customer.Name VIEW-AS ALERT-BOX. CATCH mySysError AS Progress.Lang.SysError: MESSAGE mySysError:GetMessage(1) VIEW-AS ALERT-BOX ERROR. END CATCH. END.
Wykorzystaliśmy tutaj blok transakcyjny DO ON ERROR UNDO,… W przypadku gdy rekord nie będzie odnaleziony zostanie “wyłapany” błąd systemowy i obsłużony przez nas w bloku CATCH:…END. Blok ten musi być na końcu bloku transakcyjnego. Taki “złapany” błąd przestaje istnieć w sesji ale co jeśli chcemy go przekazać po obsłużeniu do zewnętrznego bloku (np. bloku procedury)? Wystarczy następująca modyfikacja:
DO ON ERROR UNDO, LEAVE: FIND FIRST Customer WHERE CustNum = 1000. MESSAGE Customer.Name VIEW-AS ALERT-BOX. CATCH mySysError AS Progress.Lang.SysError: MESSAGE mySysError:GetMessage(1) VIEW-AS ALERT-BOX ERROR. UNDO, THROW mySysError. END CATCH. END. CATCH mySysError AS Progress.Lang.SysError: MESSAGE mySysError:GetMessage(1) VIEW-AS ALERT-BOX. END CATCH.
Teraz możemy wychwycić go i obsłużyć ponownie.
Należy dodać, że jeśli użyjemy tradycyjnej składni obsługi błędu NO-ERROR, to ma ona pierwszeństwo nad obsługą strukturalną. W poniższym przykładzie blok CATCH nie będzie więc wywołany.
DO ON ERROR UNDO, LEAVE: FIND FIRST Customer WHERE CustNum = 1000 NO-ERROR. IF AVAILABLE (Customer) THEN MESSAGE Customer.Name VIEW-AS ALERT-BOX. CATCH mySysError AS Progress.Lang.SysError: MESSAGE mySysError:GetMessage(1) VIEW-AS ALERT-BOX ERROR. UNDO, THROW mySysError. END CATCH. END.
Poniżej przedstawiłem uproszczony schemat dziedziczenia klas. Progress.Lang.Error służy do wychwycenia dowolnego błędu. Progress.Lang.ProError służy do wychwycenia dowolnego rodzaju błędu Progress: systemu, aplikacji, SOAP. Progress.Lang.SysError służy do wychwycenia błędów języka ABL, np.: niepowodzenie instrukcji FIND, błąd indeksu; wszystko, co obsługuje NO-ERROR. Progress.Lang.AppError służy do wychwycenia błędów aplikacji zgłaszanych (throw) zgodnie z regułami biznesowymi ABL. Z tej klasy może dziedziczyć nasza własna klasa do obsługi błędów.
Zobaczmy teraz jak wychwytywać błędy w procedurach i przesyłać stosowne komunikaty do programu głównego.
W tradycyjnej obsłudze błędów mogliśmy skorzystać ze składni w programie wywołującym: IF ERROR-STATUS:ERROR THEN MESSAGE RETURN-VALUE VIEW-AS ALERT-BOX ERROR.
oraz w wywoływanym RETURN ERROR “Brak rekordu w bazie”.
W obsłudze strukturalnej mamy szersze pole do popisu. Przyjrzyjmy się poniższemu przykładowi.
/* main.p */ VAR INT i = 1. RUN FindCustomer.p (1000). CATCH syserr AS Progress.Lang.SysError: MESSAGE "Customer: " syserr:GetMessage(1) VIEW-AS ALERT-BOX INFO BUTTONS OK. END. CATCH apperr AS Progress.Lang.AppError: DO WHILE i <= apperr:NumMessages: MESSAGE "Country: " apperr:GetMessage(i) apperr:GetMessageNum(i) VIEW-AS ALERT-BOX INFO BUTTONS OK. i = i + 1. END. END. /************************************************************/ /* FindCustomer.p */ BLOCK-LEVEL ON ERROR UNDO, THROW. DEFINE INPUT PARAMETER icustNum AS integer. FIND customer WHERE customer.custnum = icustNum. IF country NE "Poland" THEN UNDO, THROW NEW Progress.Lang.AppError("Niewłaściwy kraj", 555). /* Można dodac więcej komunikatów własnej obsługi błędów */ CATCH e AS Progress.Lang.AppError : e:AddMessage("Klient nie jest z Polski", 777). undo, throw e. END CATCH.
Na początku wywoływanej procedury FindCustomer.p znajduje się instrukcja BLOCK-LEVEL ON ERROR UNDO, THROW. która gwarantuje, że wszystkie nieobsługiwane błędy w blokach transakcyjnych, zostaną propagowane do obiektu wywołującego. Jeśli wywołamy tę procedurę z parametrem 1000 (a więc będzie brak rekordu Customer) błąd zostanie obsłużony w procedurze main.p w bloku CATCH syserr AS Progress.Lang.SysError (ponieważ będzie to błąd systemowy).
Załużmy, że jeśli rekord zostanie znaleziony, będziemy chcieli sprawdzić czy na pewno klient jest z Polski a jeśli nie, to ustawić własny błąd. Definiujemy więc błąd typu AppError: ...THROW NEW Progress.Lang.AppError("Niewłaściwy kraj", 555).
Po pewnym czasie chcielibyśmy dodać jeszcze jeden komunikat dla tego błędu. Możemy to zrobić bez problemu gdyż ta klasa posiada metodę AddMessage jak widać poniżej w Class Browser w Developer's Studio (dla klasy SysError nie ma takiej możliwości).
Możliwości obsługi błędów w aplikacjach ABL jest więc całkiem sporo. Wiem, że niektórzy chcieliby wiedzieć jak poradzić sobie z serwerem aplikacji i czy można pisać własne klasy obsługi, ale o tym następnym razem.