Języki i Środowiska Programowania Baz Danych | |||||||||||||||
|
4. Kolekcje Kolekcje są nazwanymi zestawami danych o podobnej strukturze. Przyjmuje się, że rozmiaru kolekcji nie można przewidzieć ani ograniczyć. Do kolekcji zaliczane są zbiory, relacje, bagi, sekwencje, listy, drzewa itp. Kolekcje nie występują w wielu popularnych językach programowania, np. w C. W języku Pascal są one zredukowane do jednego pojęcia pliku (file), dodatkowo obciążonego ograniczeniami implementacyjnymi. Brak kolekcji oznacza, że programista musi dość często używać pojęcia sterty (heap) do implementacji różnych zadań, co związane jest z uciążliwymi operacjami oraz znacznie zwiększoną skłonnością do błędów (m.in. wskutek operowania na pointerach, brakiem kontroli typologicznej i możliwością "wyciekania pamięci" (memory leak)). Popularne języki obiektowe (Smalltalk, C++, Java) nie wprowadzają pojęcia kolekcji lub wprowadzają ograniczenia na rodzaj, typ lub uniwersalność tego pojęcia. Najpoważniejsze niewygody związane z posługiwaniem się tym pojęciem dotyczą środków definiowania kolekcji (szczególnie zagnieżdżonych i powiązanych związkami referencyjnymi) środków dostępu do elementów kolekcji i ich użycia w programie. Kolekcje są wprowadzane implicite w popularnych metodykach i notacjach obiektowych takich jak OMT lub UML. Nie jest jednak jasne, czy oznaczenie klasy zawsze implikuje kolekcję jej obiektów, czy w tych notacjach można używać atrybutów będących zbiorami wartości, czy oznaczenia liczności asocjacji implikują kolekcje odpowiednich powiązań itd. Z drugiej strony, bazy danych są przede wszystkim nastawione na przetwarzanie kolekcji. W szczególności, kolekcją jest relacja lub tablica w systemach relacyjnych. Występowanie kolekcji w bazach danych jest bezpośrednią przyczyną pojawienia się języków zapytań. Brak (lub ograniczenia) kolekcji w językach programowania jest główną przyczyną niekorzystnego efektu określanego jako niezgodność impedancji (impedance mismatch). Obiektowe bazy danych, w założeniu, miały uniknąć tego efektu poprzez odpowiednie potraktowanie typów zarówno w bazach danych, jak i w językach programowania. Niestety, w obecnych systemach obiektowych baz danych (w szczególności bazujących na standardzie ODMG) to początkowe założenie pozostało w sferze pobożnych życzeń, głównie z powodu oparcia ich interfejsów na językach C++, Smalltalk i Java.
|
P4.16. | for each ( Pracownik where Stan = "analityk" ) as p do { |
W niektórych opracowaniach, np. w standardzie ODMG, operatory służące do przetwarzania kolekcji są zgrupowane w formie zestawu interfejsów, które niczym nie różnią się od interfejsów do normalnych obiektów, np.
P4.17. | interface Collection: Object{ |
P4.18. | interface Set : Collection{ |
Tego rodzaju podejście do kolekcji jest obarczone błędem wynikającym z faktu, że kolekcje (i interfejsy do kolekcji) są parametryzowane typem elementu tych kolekcji. Stąd, podane wyżej interfejsy są koncepcyjnie różnymi bytami od normalnych interfejsów i bardziej odpowiadają temu, co w C++ nosi nazwę "szablonu" (template). Twórcy standardu ODMG próbują ten fakt zignorować, co w opinii niektórych specjalistów podważa sens tego standardu [Alag97].
Pojęcie kolekcji jest również nieco kontrowersyjne, gdyż prowadzi do sprzeczności z podstawowymi założeniami obiektowości. Jeżeli przyjmiemy następujące założenia:
Pierwsze założenie implikuje, że kolekcje Osoby i Studenci są rozłączne. Drugie założenie implikuje, że wewnątrz klasy Osoby muszą być przetwarzane obiekty Student pochodzące z kolekcji Studenci. Jeżeli w trakcie działania systemu dołożymy nową kolekcję Pracownicy, której obiekty są na mocy drugiego założenia także Osobami, wówczas jest konieczne poprawienie klasy Osoby, co łamie trzecie założenie. Podobny problem powstaje z heterogenicznymi kolekcjami, tj. takimi, które mogą posiadać obiekty różnych typów.
Jak się okazuje, pojęcie kolekcji nie jest niezbędne, a wobec podanych wyżej problemów lepiej go w ogóle nie wprowadzać. W XML pojęcie kolekcji nie występuje; zamiast tego można wprowadzić obiekty z tą samą nazwą na tym samym poziomie hierarchii. Przykładowo, dla obiektu Książka może być wiele atrybutów o tej samej nazwie Autor. Podobnie, na Rys.23 i Rys.24 atrybut Lokacja jest kolekcją, która w modelu składu została przedstawiona jako wiele atrybutów z tą samą nazwą Lokacja. Ten zabieg eliminuję potrzebę wprowadzania pojęcia kolekcji, przez co wyżej wymieniona niespójność nie występuje. W połączeniu z dynamicznymi rolami zabieg ten umożliwia również spójne potraktowanie heterogenicznych kolekcji.
Wartości zerowe (null values) stanowią istotny temat w bazach danych, który zaowocował setkami artykułów oraz wieloma praktycznymi rozwiązaniami. Zwykle są oznaczane jako NULL lub NIL. Istnieje wiele przyczyn powstawania wartości zerowych, np.:
Większość przyczyn powstawania wartości zerowych można określić jako skutek nieregularnych w danych, które nie chcą się zmieścić w zadanym z góry regularnym formacie, który w przypadku relacyjnego modelu danych jest mocnym założeniem ideologicznym i technicznym. Przykładowo, jeżeli w relacyjnej bazie danych o pracownikach występuje atrybut NazwiskoPanieńskie, to dla mężczyzn i kobiet niezamężnych pojawienie się w tej kolumnie wartości NULL jest konsekwencją tego, że projektant bazy danych przyjął jednolity tablicowy format danych dla wszystkich pracowników, podczas gdy powinien w zasadzie stworzyć odrębną tabelę opisującą kobiety zamężne. Gdyby jednak projektant dla każdego przypadku, kiedy spodziewa się wystąpienia wartości zerowej, tworzył odrębny format danych, skutkiem byłaby monstrualna eksplozja liczby formatów (np. tabel w relacyjnej bazie danych) i związane z tym ogromne problemy z utrzymaniem bazy danych oraz tworzeniem i pielęgnacją oprogramowania.
Szczególną uwagę poświęca się wartościom zerowym w modelu i systemach relacyjnych, które są oparte na sztywnych formatach danych. Wartości zerowe leżą u podstaw definicji wielu pojęć modelu relacyjnego, takich jak zewnętrzne złączenie (outer join). Okazuje się, że wartości zerowe zachowują się inaczej niż inne wartości, wobec czego wiele operacji w bazie danych musi je uwzględnić w postaci wydzielonej składni i/lub reguły semantycznej.
Wartości zerowe były przedmiotem licznych prac teoretycznych; nie znalazły one jednak istotnych zastosowań. Większość tych prac przyjmowała założenie, że wartość zerowa pojawia się wskutek niekompletnej informacji, co dla przeważającej ilości rzeczywistych przypadków było założeniem fałszywym. Przyczyn powstawania wartości zerowych jest więcej. Na domiar złego przyjmowano, że wartości zerowe będą traktowane wyłącznie przez bardzo ograniczony język zapytań (np. przez tzw. podzbiór SPJ (select-project-join) algebry relacji, co było niezgodne z rzeczywistym zakresem oddziaływania wartości zerowych. Wartości zerowe, o ile zostały wprowadzone jako pojęcie do bazy danych, muszą być uwzględnione we wszystkich zaawansowanych konstrukcjach języka zapytań (takich jak grupowanie, sortowanie, funkcje agregowane, kwantyfikatory itd.), we wszystkich konstrukcjach imperatywnych danego języka (create, update, insert, delete itd.), w interfejsach wiążących język zapytań z językiem programowania, abstrakcjach dotyczących danych (takich jak perspektywy, metody, procedury bazy danych), systemie kontroli typologicznej itd. Na dodatek, próby ustalenia formalnej semantyki dla wartości zerowych okazały się nieudane, gdyż w większości przypadków użytkownik lub programista jest jedynym autorytetem, który potrafi je poprawnie zinterpretować (w sensie ich zewnętrznej "biznesowej" ontologii). Z tego powodu wartości zerowe zyskały sobie sławę "diabełka", który o ile zostanie wprowadzony do systemu zarządzania bazą danych, potrafi rozprzestrzenić się na całe środowisko tworzenia aplikacji, psując przy tym skutecznie spójność, prostotę i efektywność wielu własności tego środowiska.
Doświadczenie pokazuje jednak, że wartości zerowych nie da się uniknąć w żadnym rzeczywistym systemie bazy danych, zaś zastępowanie ich wartościami domyślnymi (default values), jak proponuje Ch. Date, np. 0, spacje lub pusty ciąg, zwiększa skłonność do błędów. Próby uniknięcia wartości zerowych poprzez specjalizację klas lub tabel (np. utworzenie specjalnej tabeli dla kobiet zamężnych, jeżeli chcemy uniknąć wartości zerowej NazwiskoPanieńskie), co jest postulowane przez niektórych ideologów obiektowości, prowadzą do nienaturalnego, nieczytelnego i niepotrzebnie skomplikowanego schematu bazy danych.
W języku SQL podczas definiowania tablic można ustalić, że pewne ich kolumny mogą zawierać wartości zerowe. Takie kolumny należy rozumieć jako kolumny podwójne: w pierwszej kolumnie są przechowywane normalne wartości, zaś w drugiej informacja (boolowska) wskazująca miejsca, w których są wartości zerowe. To rozdwojenie jest przenoszone dalej na wszystkie interfejsy programistyczne: jeżeli taka wartość ma być podstawiona na zmienną, to należy zadeklarować dwie zmienne, jedną dla wartości, drugą (zwaną indicator variable) dla informacji o wartości zerowej. Język SQL jest wyposażony w szereg cech umożliwiających przetwarzanie tak rozumianych wartości zerowych, m.in. w specjalny predykat (is_null) testujący wystąpienie wartości zerowej, specjalną funkcję (if_null) pozwalającą zastąpić wartość zerową poprzez dowolną inną wartość i specjalne traktowanie wartości zerowych w argumentach funkcji zagregowanych. SQL nie zakłada jakiejkolwiek formalnej semantyki wartości zerowych; są one raczej traktowane jako pewien trik techniczny, który projektant bazy danych lub programista może wykorzystać zgodnie z aktualną potrzebą i pomysłem.
Rozwiązania dotyczące wartości zerowych w SQL są przedmiotem ostrej krytyki (C.J. Date). Chodzi w niej o to, że jak dotąd nie udało się zintegrować wartości zerowych z całością interfejsu programowania baz danych w spójny sposób. Zasada korespondencji, wprowadzona przez autorytety języków programowania, żąda jasnej odpowiedzi na każde pytanie, jak nowe pojęcie wprowadzone do środowiska języka programowania współgra z każdym dotychczas istniejącym pojęciem tego środowiska. Zasada ta jest powszechnie łamana przez twórców systemów relacyjnych, co owocuje wieloma niekonsekwencjami i rafami semantycznymi wytworzonymi dla programistów. SQL dostarcza wręcz kuriozalnych przykładów łamania zasady korespondencji, niekonsekwencji i braku elementarnej logiki. Np. w SQL przyjmuje się, ze wszystkie wartości NULL są różne, zatem predykat X=Y, gdzie X oraz Y mają wartość NULL, zwraca UNKNOWN (nieznana wartość); ale operatory group by, distinct oraz order by traktują te wartości jako identyczne, zaś operatory sum, avg, min, max ignorują je tak, jakby były wyłącznie komentarzem. Predykat zdania select po słowie kluczowym where może wprawdzie zwrócić NULL (lub UNKNOWN), ale w takiej sytuacji jest on traktowany jako FALSE - czyli informacja nieznana jest traktowana jako znana. Uderzający jest przykład podany przez Date ([Date86]): jeżeli atrybuty A i B relacji R mogą posiadać wartości zerowe, to zapytania:
P4.19. | select sum(A) + sum(B) from R |
mogą zwrócić różne wyniki. Można sobie wyobrazić, ilu programistów będzie w podobnych sytuacjach daremnie szukać błędu w swoich programach.
Ktoś mógłby powiedzieć, że jest to wyłącznie kwestia błędów projektowych popełnionych przez twórców SQL, które można będzie wyeliminować w następnej wersji tego języka. Okazuje się jednak, że to się dotąd nie udało, mimo kilku kolejnych wersji. Wręcz odwrotnie, w najnowszym standardzie SQL-99 wprowadzono więcej rodzajów wartości zerowych o różnej nieco semantyce oraz więcej konstrukcji semantycznych związanych m.in. z klauzulą group by, co w kombinacji z ogromną liczbą konstrukcji tego języka czyni z niego twór nieprzewidywalny w implementacji. Na niespójności poprzednich wersji nakładane są dalej idące cechy dotyczące wartości zerowych, co nieuchronnie będzie prowadzić do tworzenia dalszych niespójności. Przekonają się o tym ci, którzy będą ten język implementować i używać (co - zważywszy na wady specyfikacji SQL-99 - miejmy nadzieję, nigdy nie nastąpi).
Wielu autorów próbowało uporządkować sprawę wartości zerowych. W końcowej konkluzji należy jednak przyjąć tezę Ch.Date, który twierdzi, że zło nie tkwi w nieadekwatnym projekcie środowiska tworzenia bazodanowych aplikacji, lecz jest ulokowane głębiej - w samym pojęciu wartości zerowej. Generalnie Ch.Date konkluduje "wartość zerowa wprowadza znacznie więcej problemów niż rozwiązuje", wobec czego z tego pojęcia należy całkowicie zrezygnować. Postuluje przy tym zastąpienie wartości zerowych wartościami domyślnymi (default values), ale to rozwiązanie w wielu sytuacjach również prowadzi do wad i jest przez to nieakceptowalne.
Temat wartości zerowych lub inaczej nieregularności w danych odrodził się ostatnio w nowym sformułowaniu w wersji tzw. danych półstrukturalnych (semi-structured data), koncepcji języka XML oraz związanych z nim technologii. Wydaje się, że nadal występuje zawężenie zakresu, w którym jest rozpatrywany problem nieregularności w danych, do dość ograniczonych języków zapytań. Niesposób sobie wyobrazić, że jakikolwiek system, w tym system z danymi półstrukturalnymi, byłby ograniczony wyłącznie do języka zapytań. Dane półstrukturalne muszą być przecież wprowadzane, pielęgnowane, aktualizowane, przetwarzane, wyprowadzane; zatem muszą działać na nich aplikacje realizowane w językach z pełną mocą algorytmiczną. To stwarza przesłanki, aby twierdzić, że opisane problemy z wartościami zerowymi odrodzą się w przyszłych technologiach, językach i środowiskach tworzenia aplikacji i dotyczy to zarówno tematu danych półstrukturalnych, jak i technologii opartych na XML.
Istotą pomysłu w zakresie wartości zerowych jest wprowadzenie specjalnej kolekcji, która może przyjmować wyłącznie dwa stany: albo jest pusta, albo zawiera jeden element. Poza tym ograniczeniem kolekcja ta podlegać będzie wszelkim standardowym regułom przetwarzania kolekcji. W związku z tym, do przetwarzania opisanej wyżej specyficznej kolekcji można będzie użyć tych samych operatorów. Pomysł ten nie implikuje żadnych nowych operatorów lub własności środowiska programowania aplikacji, wobec czego jest semantycznie czysty.
Niech np. atrybut Stan obiektów Pracownik będzie zadeklarowany jako opcja. Jeżeli dla Kowalskiego stanowisko nie jest znane, wówczas ta kolekcja będzie pusta, natomiast jeżeli jest znany, wówczas będzie zawierać jeden element, np. string "projektant". Jeżeli opcja jest pusta, wówczas można ją zapełnić wartością używając np. operatora insert (znanego np. z SQL). Odpowiada to sytuacji, kiedy na wartość zerową podstawiamy wartość niezerową. Jeżeli opcja nie jest pusta, to możemy uczynić ją pustą używając np. operatora delete (również znanego z SQL). Opowiada to podstawieniu wartości zerowej na wartość niezerową; np. może być zastosowane w sytuacji, gdy Kowalski utracił uprawnienia projektanta i aktualnie nie posiada stanowiska.
Dalsze konsekwencje tego pomysłu również oznaczają uporządkowanie wielu koncepcji. Np. pusty zbiór oznaczymy {}, równość zbiorów oznaczymy =. Przyjmiemy, że wszystkie puste zbiory są sobie równe. Wówczas zdanie (w rozszerzonym SQL):
P4.20. | select * from Pracownik where Stan = {} |
oznacza wyszukanie wszystkich pracowników bez stanowiska. Zdanie
P4.21 | select * from Pracownik where Stan = "projektant" |
jest typologicznie niepoprawne, gdyż porównuje się zbiór ze stringiem. Natomiast poprawne są zdania:
P4.22. | select * from Pracownik where Stan = {"projektant"} |
Z powyższych trzech zdań pierwsze jest równoważne drugiemu; oba wyszukują pracowników, którzy z pewnością są projektantami. Trzecie zdanie natomiast wyszukuje pracowników, którzy albo są projektantami, albo nie posiadają stanowiska.
Pomysł można obudować bardziej przyjazną składnią, aby użytkownik nie miał wrażenia, że jest "zbyt matematyczny". Jak dotąd, nie rozpatrywano w literaturze sytuacji, w której wartość złożona także może być zerowa. Opisany wyżej pomysł automatycznie obejmuje tę sytuację. Niech np. AdresPracownika będzie złożonym atrybutem. Jeżeli projektant liczy się z tym, że może być nieznany, może wprowadzić ten adres jako opcję.
Można pokazać na przykładach, że pomysł ten jest uniwersalny: jeżeli tylko pewne środowisko tworzenia aplikacji umożliwia przetwarzanie dowolnie zagnieżdżonych kolekcji, wówczas jest całkowicie przygotowane do przetwarzania tak rozumianych wartości zerowych. Dla uproszenia schematu bazy danych można także w każdej konkretnej sytuacji rozważyć, czy zastosować ten pomysł czy raczej skorzystać z wartości domyślnych; np. dla atrybutu Stan wartością domyślną (odpowiadającą wartości NULL ) mógłby być ciąg spacji. Istotą tych koncepcji jest to, że specjalna wartość NULL staje się niepotrzebna, przez co znikają wszystkie semantyczne problemy związane z tą wartością.