Języki i Środowiska Programowania Baz Danych | |||||||||||
|
2. Procedury w SBQL Przyjmiemy następującą składnię deklaracji procedury (nie uwzględniającą kontroli typologicznej): procedura ::= procedure nazwaProc {instrukcje}
procedura ::= procedure nazwaProc( ){instrukcje} procedura ::= procedure nazwaProc ( parFormalne){instrukcje} nazwaProc ::= nazwa parFormalne ::= parFormalny | parFormalny; parFormalne parFormalny ::= nazwa |in nazwa | out nazwa instrukcja ::= return [zapytanie]
instrukcja ::= nazwaProc | nazwaProc( ) | nazwaProc ( parAktualne )
zapytanie ::= nazwaProc | nazwaProc( ) | nazwaProc ( parAktualne ) parAktualne ::= parAktualny | parAktualny ; parAktualne parAktualny ::= zapytanie
Przyjęliśmy, że formalne parametry procedury mogą być pozbawione kwalifikatora; wówczas oznacza to ścisłe wołanie przez wartość (strict-call-by-value). Kwalifikator in oznacza zwykłe wołanie przez wartość, zaś kwalifikator out oznacza wołanie przez referencję. Semantyka tych metod transmisji parametrów zostanie objaśniona nieco dalej. Procedura jest bytem ponownego użycia, zatem istotne jest, jak i gdzie będzie zapamiętana. Ta cecha może być elementem języka programowania, ale wtedy odpowiednia instrukcja tworzenia procedury będzie miała jej źródłowy tekst jako parametr. Częstszym rozwiązaniem jest uwzględnienie deklaracji procedury w środowisku rozwoju oprogramowania. Miejsce ulokowania procedury wynika wtedy z innych czynników, np. miejscem jest moduł lub klasa, jeżeli dana procedura jest składową kodu źródłowego tego modułu lub klasy. Ponieważ w przypadku baz danych procedury są bytami pierwszej kategorii programistycznej, mogą być potrzebne specjalne udogodnienia administracyjne dla ich tworzenia, wstawiania w odpowiednie miejsce składu obiektów, kompilowania, usuwania, zabezpieczania, optymalizacji itd. W tym zakresie możliwe jest wiele rozwiązań. W tym wykładzie nie zajmujemy się specyfikacją środowiska rozwoju oprogramowania i środowiska administracyjnego, zatem ten temat jest dla nas uboczny. Nie wydaje się trudny od strony koncepcyjnej. W systemie Loqis przyjęliśmy, że procedury są składowymi modułów źródłowych lub klas, które po kompilacji stają się modułami lub klasami bazy danych. Procedury takie mogą być przenoszone pomiędzy modułami lub klasami poprzez instrukcję insert. Procedurę można zapamiętać w dowolnym środowisku składu obiektów, w szczególności:
|
P14.3. | Procedura BogatyPrac zwraca informacje o pracownikach, który zarabiają brutto co najmniej 3000. Informacja zawiera nazwisko pracownika jako Nazwisko, nazwisko jego szefa jako Szef oraz zarobek netto jako Zarobek. |
Procedura BogatyPrac została użyta w taki sposób, że użytkownik może rozumieć nazwę BogatyPrac jako nazwę obiektów posiadających trzy atrybuty: Nazwisko, Szef i Zarobek. Są to obiekty "wirtualne", istniejące w postaci definicji (i w wyobraźni programisty), ale nieobecne w składzie danych. Procedura BogatyPrac przypomina więc pojęcie z baz danych znane jako perspektywa (view).
W istocie, jeżeli pominąć różnice składniowe, specjalną terminologię, różnorodne ograniczenia i drugorzędne opcje, to wszystkie znane autorowi propozycje dotyczące perspektyw, włączając perspektywy w SQL, są po prostu procedurami funkcyjnymi zapamiętanymi w bazie danych (z zastrzeżeniem dotyczącym aktualizacji perspektyw, o którym dalej). Ten fakt został dawno rozpoznany przez niektórych autorów (m.in. przez M.Atkinsona i P.Bunemana [Atki87]), ale wydaje się, że do dzisiaj nie dotarł do powszechnej świadomości społeczności baz danych.
Przyjęcie założenia, że zdefiniowane przez nas procedury funkcyjne są znanymi z baz danych perspektywami, ma szereg pozytywnych konsekwencji:
Ostatni punkt wymaga dłuższej dyskusji. Uznaliśmy za konieczne wyraźne rozróżnienie pomiędzy procedurą funkcyjną a perspektywą; perspektywom poświęcimy następny rozdział. Zgodnie z tym rozróżnieniem, perspektywy znane z SQL są funkcjami, ale nie są perspektywami, gdyż nie zapewniają przezroczystości operacji aktualizacyjnych. Podobnie z prawie wszystkimi innymi podejściami do perspektyw, w tym perspektyw obiektowych: w istocie, ich autorzy definiują pewną odmianę procedur funkcyjnych raczej niż perspektyw. Nie jest to wyłącznie sprawa terminologii, gdyż wymóg przezroczystości (transparency) jest podstawowy dla pojęcia perspektywy. Oznacza on, że programista aplikacyjny nie jest w stanie odróżnić - w jakikolwiek sposób - czy ma do czynienia z obiektem rzeczywistym, czy też z wirtualnym. Ten wymóg jest m.in. podstawą zarządzania obiektami w standardzie CORBA. Prawie wszystkie podejścia do perspektyw, włączając perspektywy w SQL, tego wymogu nie spełniają w zakresie aktualizacji perspektyw. Istnieje, jak dotąd, tylko jeden zaimplementowany wyjątek: perspektywy definiowane poprzez instead of trigger systemów Oracle i SQL Server.
Dotychczasowy wysiłek badawczy związany z aktualizacją witalnych perspektyw dotyczył dodatkowych ograniczeń na definicje perspektyw i ich aktualizacje, aby zapobiec anomaliom aktualizacyjnym. W przypadku perspektyw obiektowych te ograniczenia są tak silne, że praktycznie nie zezwalają na tworzenie cokolwiek bardziej skomplikowanych perspektyw, co oczywiście silnie zawęża zakres ich stosowalności. Będziemy trzymać się stanowiska, że całość bogatej literatury dotyczącej tego problemu dotyczy rozwiązywania problemu zastępczego (jak ograniczyć aktualizację), a nie problemu zasadniczego (jak zrobić całkowicie przezroczystą perspektywę). Z naszego punktu widzenia, cała ta licząca setki pozycji literatura jest więc praktycznie bezwartościowa.
W tym wykładzie przyjmiemy następującą filozofię. Jeżeli programista lub projektant definiuje funkcję, to powinien być w pełni świadomy, że jest to funkcja, a nie perspektywa. Jeżeli mimo tej świadomości decyduje się na aktualizację poprzez referencje zwracane przez tę funkcję (tak jak w przykładzie P14.2.), to wolno mu to zrobić, ale ponosi za to pełną odpowiedzialność. Wszelkie ograniczanie jego swobody w tym zakresie będziemy uważać za niewłaściwe. Nie dopuszczamy w tym względzie żadnych protez, w rodzaju opcji sprawdzania (check option), niezezwalania na aktualizację po stronie klucza głównego perspektyw powstałych poprzez złączenie lub innych ograniczeń (znanych m.in. z systemu Oracle). Wszystkie te ograniczenia mają przybliżyć pojęcie procedury funkcyjnej do pojęcia perspektywy, ale naszym zdaniem to się nigdy w pełni nie uda, wobec czego na tym nam nie zależy. Zależy nam na prostocie pojęcia funkcji, a wszelkie dodatkowe ograniczenia temu nie służą.
Jeżeli programista lub projektant chce, aby funkcja była rzeczywiście perspektywą o pełnych walorach przezroczystości, wówczas musi stworzyć taką perspektywę explicite, według koncepcji opisanej w następnym rozdziale. Koncepcja ta rozwiązuje problem aktualizacji perspektyw. Perspektywa jest jednak bytem odmiennym i znacznie bardziej wyrafinowanym semantycznie w stosunku do procedury funkcyjnej. Definicja takich perspektyw nie odwołuje się do aktualizacji przez jakiekolwiek efekty uboczne, m.in. przez referencje zwrócone przez procedurę funkcyjną, jak w przykładzie P14.2. Aktualizacja poprzez efekty uboczne jest więc zabroniona, natomiast programista dostaje do ręki inne środki, za pomocą których będzie w stanie precyzyjnie określić semantykę aktualizacji.
Metoda jest procedurą umieszczoną wewnątrz klasy i traktowaną jako inwariant obiektów będących członkami tej klasy. Metoda w SBQL jest zawsze wywoływana w kontekście obiektu, na którym działa; ten kontekst jest określony poprzez umieszczenie binderów do własności obiektu na stosie ENVS i następnie wywołanie metody. Moglibyśmy oczywiście zróżnicować syntaktycznie procedury i metody, ale ze względu na brak istotnych różnic semantycznych w tym wykładzie uznaliśmy, że nie warto tego robić.
P14.4. | Metoda ZarobekNetto umieszczona wewnątrz klasy Pracownik zwraca zarobek netto dla pracownika, obliczając go według pewnej formuły (patrz Rys.55): |
W przykładzie wykorzystaliśmy nazwę self zwracającą referencję do aktualnie przetwarzanego obiektu. Tego rodzaju predefinowana nazwa (self, this, itp.) występuje w większości obiektowych języków programowania. W naszym przypadku nie jest niezbędna. Z technicznego punktu widzenia, wszystkie bindery do wewnętrznych własności przetwarzanego obiektu znajdują się w odpowiedniej sekcji ENVS, zatem poprawna jest również następująca postać powyższej procedury:
P14.5. | procedure ZarNetto { |
Niemniej istnieją powody, dla których taką nazwę warto wprowadzić. Jednym z nich jest modelowanie pojęciowe: nazwa self pozwala programiście widzieć wyraźnie w kodzie metody wszystkie odwołania do przetwarzanego obiektu. Drugim powodem jest to, że w niektórych sytuacjach (np. porównanie referencji) referencja do przetwarzanego obiektu ułatwia napisanie metody.
Z drugiej strony, jak zwykle w przypadku predefiniowanych nazw, w niektórych sytuacjach predefinowana nazwa self może prowadzić do niejednoznaczności i wymagać większej uwagi od programisty, szczególnie w przypadku zmian już napisanego kodu.
Proponowany przez nas sposób wprowadzenia predefinowanej nazwy self jest dość oczywisty i ma tylko pośredni związek z samym tematem metod w SBQL. Taka nazwa mogłaby się przydać również w innych kontekstach. Nazwę self wprowadzimy poprzez poprawienie funkcji nested. Jeżeli argumentem tej funkcji jest pojedyncza referencja r do obiektu, który jest podłączony do klasy, to wynik tej funkcji, oprócz binderów do podobiektów tego obiektu, zawierać będzie binder self(r). Przyjmując to założenie, sytuacja na stosie ENVS w momencie przetwarzania ciała metody ZarNetto dla obiektu Nowaka posiadającego identyfikator i4 jest przedstawiona na Rys.73. Zwrócimy uwagę na pojawienie się w sekcji przetwarzanego obiektu bindera self(i4).
Do rozważenia jest również sytuacja, gdy ciało metody widzi wyłącznie binder self, zaś bindery do wnętrza przetwarzanego obiektu są dla metod przesłonięte. Oznaczałoby to, że dostęp do wszelkich własności przetwarzanego obiektu następowałby wyłącznie poprzez nazwę self.
Zwrócimy uwagę, że w modelu składu M3 dostęp do prywatnych własności danej klasy następuje wyłącznie w momencie wywołania metody tej klasy.
Rys.73. Stan stosu ENVS dla ciała metody ZarNetto z uwzględnieniem bindera self
Jeżeli dana metoda jest procedurą funkcyjną, wówczas pełni rolę "wirtualnego atrybutu", czyli rodzaju perspektywy umieszczonej wewnątrz klasy. Przykładem są metody Wiek i ZarNetto. Taki atrybut będzie zachowywać się przy wyszukiwaniu jak normalny atrybut, ale aktualizacja poprzez taki atrybut jest związana z identycznymi problemami, jak aktualizacja poprzez wynik procedury funkcyjnej (omawianymi poprzednio). Rozwiązanie tego problemu wymaga aktualizowalnych perspektyw będących składnikami klas. Perspektywy omawiane w następnym rozdziale mogą pełnić tę rolę.