integracja danych i aplikacji przy użyciu wirtualnych ...subieta/sba_sbql/phds/phd michal...
TRANSCRIPT
Integracja danych i aplikacji przy użyciu wirtualnych repozytoriów
Michał LentnerPolsko-Japońska Wyższa Szkoła Technik Komputerowych
Praca doktorska napisana pod kierunkiemprof. dr hab. inż. Kazimierza Subiety
Polsko-Japońska Wyższa Szkoła Technik Komputerowych
Warszawa, styczeń 2008
Streszczenie
Podstawowym celem rozprawy doktorskiej jest zbadanie nowych paradygmatów ułatwiających
proces implementacji systemów informatycznych. Przedstawiona w niej została propozycja
narzędzia programistycznego umożliwiającemu pracę programiście bazodanowemu na znacznie
wyższym poziomie, niż umożliwiają to popularne współczesne rozwiązania. W tym celu stworzony
został nowy, obiektowy język programowania, bezszwowo zintegrowany z konstrukcjami języka
zapytań (zapytania jako wyrażenia), razem z jego rozproszonym, bazodanowo zorientowanym
środowiskiem wykonawczym. Oprogramowanie to zapewnia funkcjonalność wielu istniejących
narzędzi (takich jak np. języki programowania aplikacji baz danych, języki zapytań do baz danych,
systemy zarządzania bazami danych, różnego rodzaju middleware) w jednym, spójnym,
uniwersalnym, łatwym do wykorzystania i efektywnym w użyciu środowisku programistycznym.
Praca została podzielona na siedem rozdziałów.
W pierwszym rozdziale przedstawiamy problem integracji danych i aplikacji jako jednego
z głównych czynników wpływającego na wydajność inżynierów wytwarzających oprogramowanie
biznesowe. Problem ten definiujemy z punktu widzenia dwóch aspektów: integracji języka
programowania z bazą danych, oraz integracji istniejących aplikacji w systemach rozproszonych.
Rozdział drugi poświęcony jest kilku najbardziej znanym obecnie podejściom do integracji danych
i aplikacji. Omawiamy tutaj zagadnienia związane z metodami łączenia aplikacji z relacyjnymi
i obiektowymi bazami danych, jak również najważniejsze rodzaje middleware.
3
W rozdziale trzecim przedstawiamy skrótowo podejście stosowe do języków zapytań - teoretyczną
bazę na podstawie której zaprojektujemy prototypowy język programowania/zapytań zintegrowany
z obiektową bazą danych, funkcjonującą również jako middleware.
W rozdziale czwartych przedstawimy architekturę zaproponowanego przez nas narzędzia oraz
zaprezentujemy przykładowe scenariusze jego zastosowania w dziedzinie integracji danych
i aplikacji.
Kolejny rozdział, czyli rozdział piąty, jest nieformalnym opisem języka SBQL, rozszerzonego przez
nas na potrzeby niniejszej pracy do kompletnego języka programowania.
W związku z tym, iż język SBQL posiada szereg mechanizmów deklaratywnych, niezbędne jest
zaprojektowanie wydajnych algorytmów optymalizacyjnych dla zapytań. Podstawowe algorytmy
optymalizacyjne dla SBQL w środowisku scentralizowanym zostały już sformułowane, jednak
optymalizacja zapytań rozproszonych wymaga dodatkowych technik. Temu zagadnieniu
poświęcono zatem rozdział szósty.
Rozdział siódmy stanowi szczegółowa dyskusja na temat integracji danych za pomocą
aktualizowalnych perspektyw. Prezentujemy kod źródłowy prostego systemu trójwarstwowego
(klient-serwer integracyjny-serwery kontrybucyjne) oraz objaśniamy mechanizm wymiany danych
w takim środowisku.
Ostatni rozdział, czyli rozdział ósmy, dokumentuje szereg niskopoziomowych decyzji, jakie zostały
podjęte w celu zaimplementowania prototypu zaproponowanego przez nas narzędzia. Obok tekstu
niniejszej pracy, prototyp ten jest jednocześnie podstawowym rezultatem badań podjętych przez
autora rozprawy.
4
Integration of data and applications
using virtual repositories
Summary
The main objective of this Ph.D. thesis is to develop new software construction paradigms that
make the implementation of information systems easier. We have proposed an application
development environment allowing a database programmer to work on a much higher level of
abstraction than it is possible when current solutions are used. To this end, we have created a new
object-oriented programming language, seamlessly integrated with query language constructs
(queries as expressions) and with its distributed, database-oriented runtime environment. Our
software provides functionality of many existing tools (such as: database programming languages,
database query languages, database management systems, several kinds of middleware) in a single,
consistent, universal, easy to use and effective to apply programming environment.
The thesis is divided into seven chapters.
In the first chapter, a brief introduction to the problem of data and application integration is
discussed. The need for integration is presented as one of the most important factors influencing the
work of today’s business application developers. The problem is defined from the point of view of
two aspects: the integration of a programming language and a database management system, as well
as the integration of existing applications being parts of distributed systems.
5
The second chapter is focused on highlighting some of the most popular approaches of data and
application integration. We have discussed several issues associated with existing methods of
connecting applications with relational and object-oriented databases, as well as the most important
types of middleware.
In the third chapter the Stack-Based Approach to query languages is briefly described.
It is presented as a theoretical base allowing us to construct a prototype programming/query
language integrated with an object-oriented database having middleware features.
In the fourth chapter the architecture of our prototype solution is introduced. Several typical
scenarios of its application in the area of data and application integration is also presented.
The fifth chapter contains an informal description of the SBQL language, extended by us to a fully-
fledged database programming language.
Since SBQL contains declarative constructs, it was necessary to design efficient query optimization
algorithms. The most important optimizations for SBQL queries executed in a centralized
environment have already been designed in other works. However, distributed query processing
requires completely new solutions. This issue has been discussed in the sixth chapter.
The seventh chapter contains a detailed discussion on data integration with the use of updateable
views. The source code of a simple, three-tier (client, integration server, contributory server) system
is presented. We have also explain the functionality of several low-level mechanisms responsible
for data exchange between particular nodes of the system.
The last chapter documents several low-level decisions which have been made in order to
implement a prototype of the tool discussed in this thesis. Apart from the text of the disseration,
the prototype is the main product of the research conducted by the Author.
6
Spis treści
.........................................................................................................Rozdział 1: Wprowadzenie 9
.......................................Rozdział 2: Stan sztuki w dziedzinie integracji danych i aplikacji 25
............................................................Rozdział 3: Podejście stosowe do języków zapytań 43
.................................Rozdział 4: Koncepcja architektury narzędzia RAD nowej generacji 59
.............................................................................Rozdział 5: Język SBQL w systemie Odra 71
..............................................................Rozdział 6: Optymalizacja zapytań rozproszonych 87
.....................................................................Rozdział 7: Integracja danych rozproszonych 107
...............................................................................................................Rozdział 8: Prototyp 119
............................................................................................................Podsumowanie pracy 143
.............................................................................Dodatek A: Przykładowy program SBQL 145
....................Dodatek B: Kod źródłowy zintegrowanego systemu połączeń kolejowych 149
....................................................Dodatek C: Ewaluacja przykładowego zapytania SBQL 155
................................................Dodatek D: Przetwarzanie danych XML za pomocą SBQL 157
..............................................Dodatek E: Ważniejsze operacje maszyny wirtualnej SBQL 165
................................................................Lista ważniejszych akronimów użytych w pracy 179
................................................................................................................................Literatura 183
Rozdział 1:Wprowadzenie
Eksplozja informacjiW ostatnich latach obserwowana jest gwałtowna eksplozja ilości informacji, z jaką mają do
czynienia systemy informatyczne współczesnych organizacji. Zbadano, iż w ciągu trzech
poprzednich lat wygenerowano więcej informacji, niż zapisano w ciągu całej historii ludzkości [1].
Niestety, za tą eksplozją informacji oraz rozwojem potrzeb nie nadąża rozwój narzędzi
informatycznych. Nie dziwi więc, iż systemy wielu organizacji często załamują się pod wpływem
wielkiej ilości i różnorodności danych, jakie muszą być przetwarzane przez poszczególne (często
odizolowane od siebie) składniki tych systemów. Okazuje się, że wraz z rozwojem potrzeb
integracyjnych problem przetwarzania danych masowych w rozproszonych i heterogenicznych
środowiskach zdobywa coraz większe znaczenie, a narzędzia wspomagające czynności z tym
związane stają się coraz potrzebniejsze [2]. Nieprzerwanie rosnąca złożoność tych narzędzi wiąże
się jednak z tym, iż programiści coraz rzadziej zdolni są opanować wszystkie czynności, jakie
wynikają z produkcji oprogramowania w takich środowiskach. Ilość technologii, interfejsów
programistycznych, języków, bibliotek, protokołów, serwerów, etc., które współczesny programista
musi znać i używać jest wprost gigantyczna. Ten ciągle rosnący zakres technologii jest jedną
z przyczyn ogromnej zlożoności współczesnego oprogramowania, a co za tym idzie - kosztów jego
wytwarzania oraz pielęgnacji. Ponieważ w przyszłości problem ten będzie narastał, nieodzowne są
prace nad nowymi, prostymi w obsłudze, uniwersalnymi i homogenicznymi narzędziami
9
programistycznymi wspomagającymi tworzenie aplikacji baz danych w środowiskach
rozproszonych.
ObiektowośćWalka ze złożonością oprogramowania wymaga przede wszystkim sięgnięcia do źródeł tej
złożoności, czyli sposobu postrzegania problemów przez człowieka oraz implementacji tych
problemów za pomocą maszyny. Okazuje się, iż im mniejsze są różnice, tym prostsze jest tworzenie
systemów informatycznych oraz tym mniejszy jest koszt ich wytworzenia i pielęgnacji.
Praktyka pokazała, iż jednym z najbardziej zaawansowanych obecnie paradygmatów
umożliwiających niwelowanie tych różnic jest obiektowość. Na przestrzeni lat okazało się, iż
pojęcia takie jak obiekt, klasa, asocjacja, metoda, specjalizacja, generalizacja, polimorfizm itp.,
bliższe są ludzkiemu sposobowi postrzegania świata, a realizowana przy ich pomocy struktura
danych/oprogramowania staje się bardziej intuicyjna. Dziś normą jest obiektowa analiza
i projektowanie oprogramowania za pomocą obiektowych notacji graficznych, oraz implementacja
oprogramowania za pomocą obiektowych języków programowania [6].
Rozwój obiektowości nie jest jednak zakończony. Pomimo wiodącej roli tego podejścia w analizie
oraz projektowaniu systemów oraz w językach programowania, idee obiektowości nie zyskały
dotychczas szerokiego uznania na polu baz danych. Standard ODMG [12], czyli próba
ustandaryzowania obiektowego modelu danych, języka zapytań i interfejsu programistycznego do
obiektowych baz danych okazał się klęską. Zaprezentowane w nim idee okazały się w dużym
stopniu nieimplementowalne dla twórców systemów zarządzania bazami danych, oraz
nieakceptowalne dla programistów aplikacji. Również powstające niezależnie od tego standardu
systemy zarządzania obiektowymi bazami danych nie zyskały dotychczas szerszego uznania,
dobrze sprawdzając się jedynie w niszowych zastosowaniach. Mnogość tymczasowych rozwiązań
zapewniejących mechanizmy odwzorowania relacyjno-obiektowego (realizowanych często ad-hoc
przez środowiska przemysłowe) pokazuje jednak, jak ważne dla współczesnych programistów jest
operowanie na bazach danych chociażby bez konieczności tłumaczenia struktur relacyjnych na
struktury obiektowe i odwrotnie.
Zalety obiektowych baz danych w stosunku do baz relacyjnych akcentowane w ubiegłej dekadzie
nie straciły zatem nic ze swej aktualności [34, 36, 32]. Są to przede wszystkim:
• Znacznie bogatszy model danych.
10
• Brak niezgodności impedancji pomiędzy modelem danych obiektowego języka
programowania i bazy danych.
• Ortogonalna trwałość.
• Potencjalnie wyższa wydajność (zwłaszcza w zakresie przetwarzania asocjacji).
Praktyczna realizacja tych postulatów stała się niestety trudniejsza niż początkowo przypuszczano.
Niektórzy specjaliści uważają wręcz, że idea obiektowych baz danych okazała się niemożliwą do
spełnienia utopią. Do najczęściej wymienianych wad obiektowych baz danych należą:
• Zbyt mocne powiązanie aplikacji z operacjami wykonywanymi na bazie danych.
• Przypadkowość rozwiązań, brak ogólnie przyjętego standardu, takiego jak SQL [87].
• Brak lub utrudnione korzystanie z języka zapytań.
Wszystkie te wady powodują, iż potrzebne są dalsze badania w tej dziedzinie. Niezbędne staje się
opracowanie zupełnie nowych architektur obiektowych baz danych, dramatycznie innych od
obecnie stosowanych. W przeciwnym razie bazy danych tego typu na zawsze skazane będą na
zastosowania niszowe, jako rozszerzenia tradycyjnych języków programowania.
Środowiska programowania aplikacji baz danychBazy danych są krytycznym elementem każdej współczesnej aplikacji biznesowej. Programiści
korzystają z baz danych w celu zapewnienia trwałości danym masowym oraz uzyskania do nich
szybkiego i bezpiecznego dostępu. Ponieważ dane lokalne aplikacji oraz dane przechowywane
w bazie danych należą do dwóch różnych światów, częścią praktycznie każdej współczesnej
aplikacji jest warstwa odpowiedzialna za wymianę danych pomiędzy tymi światami. Implementacja
takiej warstwy osiąga często nawet kilkadziesiąt procent objętości całego systemu, jest
niejednokrotnie źródłem błędów i frustracji programistów. Nie dziwią więc ciągłe próby
podejmowane przez światy naukowy i akademicki, zmierzające do integracji języków
programowania z bazami danych.
W idealnej sytuacji (postulowanej przez tzw. ortogonalną trwałość) dostęp do obiektów
aplikacyjnych i obiektów bazy danych realizowany powinien być z użyciem dokładnie tych samych
konstrukcji językowych. W praktyce taka bezszwowa integracja języka programowania z bazą
danych nie została dotychczas osiągnięta w satysfakcjonujący sposób. Idea ta realizowana jest
obecnie zazwyczaj poprzez rozszerzenie tradycyjnego języka programowania (np. Java) w taki
sposób, by niektóre obiekty (np. będące instancjami specjalnej klasy) nie miały charakteru ulotnego
11
(znikały po wyłączeniu komputera), ale trwały (zapisywane były automatycznie na dysku
komputera). Idea ta została pierwotnie zapoczątkowana przez tzw. języki z trwałością (np.
Persistent Modula 3 [24], PJama [23], etc.), a następnie rozwijana w ramach obiektowych baz
danych. Dzięki tym rozwiązaniom udało się przede wszystkim ujednolicić modele danych. Ten
sam obiektowy model danych znajduje się zatem zarówno po stronie bazy danych, jak i języka
programowania. Programista nie jest dzięki temu zmuszony do utrzymywania dwóch
równoważnych struktur danych (np. tabel i obiektów), ale w przezroczysty sposób operuje na
trwałych obiektach. Technika zapoczątkowana przez języki z trwałością stała się również inspiracją
dla technologii wywodzących się ze środowisk przemysłowych i reprezentowanych przez techniki
odwzorowania relacyjno-obiektowego. Rozwiązania takie jak Hibernate próbują połączyć zalety
ortogonalnej trwałości, oraz dobrze znanych systemów zarządzania relacyjnymi bazami danych.
Realizowane jest to poprzez wprowadzenie dodatkowej warstwy metadanych (tworzonych przez
programistę), odwzorowującej strukturę obiektową aplikacji na strukturę relacyjnej bazy danych.
Języki z trwałością oraz bazujące na nich rozwiązania nie spotkały się z większym uznaniem
z powodu wad, w szczególnością jednej która naszym zdaniem całkowicie je dyskwalifikuje. Wadą
tą jest brak języka zapytań lub też język zapytań w szczątkowej formie. Powstaje z związku z tym
pewien dysonans - operacje aktualizacyjne pozbawione są niezgodności impedancji, natomiast
operacje wyszukiwawcze realizowane są w tradycyjny sposób (poprzez zagnieżdżanie zapytań jako
literałów, proceduralne przetwarzanie za pomocą iteratorów [29], etc.). Brak możliwości wyjścia z
tej sytuacji skłania niektórych autorów do stwierdzenia, że języki zapytań w obiektowych bazach
danych nie mogą być poprawnie zdefiniowane, albo wręcz że nie są potrzebne. Zamiast tego
proponuje się wyrażanie operacji wyszukiwawczych w bazie danych za pomocą języka takiego jak
Java czy C#. Okazuje się tymczasem, że rozwiązania takie jak native queries są nie tylko
nieporównywalnie uboższe nawet w stosunku do standardowego SQL, ale również praktycznie
uniemożliwiają realizację technik optymalizacyjnych, bez których konstrukcje deklaratywne tracą
sens. Naszym zdaniem eliminacja języków zapytań z obiektowych baz danych jest skazaniem ich
na porażkę. To właśnie dzięki językowi SQL mógł mieć miejsce tak przytłaczający sukces
relacyjnych baz danych.
Język zapytań jest niezwykle istotnym składnikiem każdego liczącego się obecnie systemu
zarządządzania bazą danych. Podstawowym zadaniem języka zapytań jest podwyższenie
wydajności programistów poprzez dostarczenie do ich dyspozycji pojęciowych, makroskopowych
12
operacji, pozwalających zapisać złożone operacje związane z przetwarzaniem danych w zwartej,
czytelnej i zrozumiałej formie. Powszechną stała się opinia, że jedno zdanie napisane w języku
zapytań może zastąpić kilka stron programu napisanego w języku programowania ogólnego
przeznaczenia. Ma to krytyczne znaczenie dla tempa wytwarzania oprogramowania, jego kosztu,
pielęgnacyjności i modyfikowalności. Następnym powodem jest dążenie do podwyższenia
niezawodności produktów programistycznych, czemu sprzyja zwartość zapisu programu,
konceptualizacja myślenia programisty i tzw. niezależność danych, przejawiająca się m.in.
w abstrahowaniu od ich fizycznej organizacji. Jeszcze jednym powodem jest otwarcie nowych
możliwości dla automatycznej optymalizacji programów. Operacje makroskopowe dają w tym
względzie znacznie większe możliwości niż języki niskiego poziomu. Ten fakt jest potwierdzony
przez szereg badań dowodzących, że dla złożonych zapytań automatyczny optymalizator daje
lepsze wyniki niż ręczna optymalizacja [4, 39].
Wydaje się że różnice między językami zapytań i tradycyjnymi językami programowania są na tyle
duże, iż ich integracja jest po prostu niewykonalna. Naszym zdaniem nie jest możliwe stworzenie
wersji języka takiego jak Java czy C#, która w czysty semantycznie sposób pozwalałaby połączyć
ze sobą konstrukcje języka proceduralnego i języka zapytań. Przyczyną tego stanu rzeczy jest to,
iż języki zapytań bazują na zupełnie innych założeniach semantycznych i optymalizacyjnych:
konstrukcje imperatywne vs. konstrukcje deklaratywne, optymalizacja podczas kompilacji vs.
optymalizacja podczas czasu wykonania, algorytmy i struktury danych vs. tabele (w przypadku
systemów relacyjnych) i indeksy, wątki vs. transakcje, null jako wskaźnik vs. null jako brak
danych, różnice w składni, systemach typów, sposobie enkapsulacji danych, itp. Różnice te znane
są powszechnie pod nazwą “niezgodność impedancji”.
Jedynym sposobem na rozcięcie tego węzła gordyjskiego jest wprowadzenie nowej klasy języków
programowania, w których wyrażenia byłyby tożsame z zapytaniami, a rezultatami wyrażeń mogły
być kolekcje. Wyrażenia takie mogłyby posiadać właściwości tradycyjnych języków
programowania (literały, nazwy, operatory), ale mogłyby również umożliwiać deklaratywne
przetwarzanie dużych kolekcji danych. Operatory takiego języka powinny dawać się swobodnie
łączyć z innymi konstrukcjami języka, wliczając w to konstrukcje imperatywne, instrukcje sterujące
oraz abstrakcje programistyczne. Język taki powinien służyć zarówno do programowania strony
serwera bazy danych, ale również strony klienckiej. Programy takiego języka powinny być
uruchamiane w środowisku posiadającym cechy współczesnych systemów zarządzania bazami
13
danych (trwałość, indeksy, optymalizacja zapytań, bezpieczeństwo, transakcje, itd.), ale również
maszyn wirtualnych (wirtualny zbiór instrukcji, pamięć, I/O). Poprzez przeniesie poziomu
abstrakcji na znacznie wyższy poziom niż w obecnych językach programowania, języki takie
pozwoliłyby na znaczące skrócenie czasu programowania oraz zwiększenie jakości wytwarzanego
przy ich użyciu oprogramowania.
Integracja rozproszonych danych i aplikacjiWe współczesnych systemach informatycznych zauważyć można tendencję zwiększonego
zapotrzebowania na zapewnienie możliwości komunikacji pomiędzy programami komputerowymi,
czy wręcz całymi systemami informatycznymi tych samych (Enterprise Application Integration,
EAI), bądź całkowicie odrębnych (Business To Business, B2B) organizacji. Użytkownicy domagają
się natychmiastowego dostępu do wszystkich informacji jakie posiada dana organizacja, niezależnie
od systemu, w jakim informacja ta jest przechowywana. Zadaniem projektantów jest zatem
sprawienie, by poszczególne systemy mogły ze sobą współpracować, czyli połączenie tzw. wysp
automatyzacji (odizolowanych od siebie systemów realizujące ściśle określone zadania) w jeden
wirtualny system informatyczny.
Jak dotąd istnieje szereg kontrowersji co do tego, jak taki wirtualny system miałby wyglądać od
strony technicznej. Przykładowo, niektórzy specjaliści postulują rozszerzenie tradycyjnego języka
programowania o zestaw narzędzi (zwykle bibliotek i/lub generatorów kodu źródłowego)
umożliwiających do pewnego stopnia przezroczyste dla programisty wykorzystywanie
rozproszonych zasobów. Inni postulują oparcie integracji na tzw. usługach, komunikujących się ze
sobą za pomocą standardowych protokołów i języków opisu danych. Jeszcze inni są zwolennikami
integracji na poziomie procesów biznesowych, albo poprzez portale biznesowe. Istnieje także grupa
zwolenników integracji aplikacji na poziomie danych - poprzez replikację danych (np. w hurtowni
danych), albo federację baz danych. Cechą wspólną wszystkich tych rozwiązań jest dążenie do
tego, by każda informacja była dostępna natychmiast, niezależniej od tego gdzie i w jakiej postaci
jest ona w rzeczywistości przechowywana. Mechanizmy integracyjne muszą zatem działać w czasie
rzeczywistym, oraz być na tyle sprawne, aby były w stanie współpracować z różnorodnymi bazami
danych, serwerami aplikacji, systemami zarządzania treścią, hurtowniami danych, systemami
przepływu prac, językami programowania, protokołami wymiany danych itp.
14
MiddlewarePodstawowym mechanizmem warunkującym sprawną komunikację w środowisku rozproszonym
jest tzw. wartstwa pośrednia (middleware) [10]. Jest to oprogramowanie znajdujące się pomiędzy
systemem operacyjnym oraz aplikacją po każdej stronie systemu rozproszonego. Istnieje wiele
rodzajów middleware: rozproszone obiekty, serwery aplikacji, monitory transakcyjne, serwery
integracyjne, brokery komunikatów i inne. Najważniejszym zadaniem middleware jest uczynienie
procesu wytwarzania oprogramowania prostszym, poprzez dostarczenie abstrakcji
programistycznych wspólnych dla każdej strony systemu rozproszonego, ukrycie heterogeniczności
oraz dystrybucji systemów komputerowych, jak również niskopoziomowych szczegółów
dotyczących komunikacji.
Warto przy tym zauważyć, że aspekt przezroczystości w komunikacji rozproszonej nie jest jedynie
domeną middleware. Tematyka ta jest również przedmiotem badań w dziedzinie rozproszonych/
federacyjnych baz danych [47, 48, 51]. Na przestrzeni lat specjaliści z tej dziedziny wypracowali
znacznie bardziej zaawansowane aspekty przezroczystości niż te, które charakteryzują współczesne
middleware. Do najważniejszych form przezroczystości zapewnianych przez federacyjne (zwane
również wirtualnymi) bazy danych należą: przezroczystość dostępu, położenia, współbieżności,
heterogeniczności, skalowania, fragmentacji, replikacji, optymalizacji, awarii, migracji, i inne. [89]
Tradycyjne rodzaje middleware nie wspierają większości z tych własności, ponieważ nie są one
nastawione na przetwarzanie kolekcji, ani danych trwałych za pomocą konstrukcji deklaratywnych.
Middleware posiada zwykle charakter proceduralny, czyli znacznie niższy poziom abstrakcji niż
języki zapytań. Konieczności przetwarzania kolekcji w systemie rozproszonym nie można jednak
uniknąć. Problemy z tym związane widać szczególnie na przykładzie technologii rozproszonych
obiektów. Przykładowo, w technologiach takich jak CORBA [25] podjęto próbę ukrycia różnic
w dostępie do danych zdalnych i lokalnych. Ta iluzja okazała się zgubna dla wielu projektów
programistycznych ze względów wydajnościowych. Źródłem problemów była pokusa operowania
na danych zdalnych za pomocą takich samych algorytmów, jakie byłyby użyte gdyby dane te
znajdowały się w pamięci operacyjnej lokalnego komputera. Niestety, okazuje się że ze względu na
naturę pracy w sieciach komputerowych (czas dostępu, awaryjność) algorytmy te nie mogą być
takie same.
Twórcy standardu CORBA podjęli próbę wprowadzenia dodatkowych usług (Query Service,
Persistence Service) mających usprawnić obsługę danych masowych za pomocą języka zapytań.
15
Próba ta nie zakończyła się satysfakcjonującymi rezultatami, głównie ze względu na problem
niezgodności impedancji pomiędzy językami zapytań i tradycyjnymi językami programowania.
Podobna sytuacja dotyczy popularnej technologii EJB (Enterprise Java Beans) [52] - tzw. ziarna
encyjne (Entity Beans) oraz działający na nich język zapytań EJB QL nie rozwiązują naszym
zdaniem problemu przetwarzania rozproszonych danych masowych. Pomijając nawet fakt małych
możliwości tych języków zapytań, są one wręcz bezużyteczne w sytuacji gdy potrzebna jest
aplikacja kombinująca dane z różnych źródeł danych. Ze względu na niskopoziomową (poprzez
API) pracę z middleware sytuacje takie wymagają gigantycznych nakładów programistycznych
związanych z optymalizacją kodu. Optymalizacja taka rzadko jest możliwa do zrealizowania
w idealny sposób poprzez człowieka, ponieważ niejednokrotnie realizacja tej samej operacji
dostępu do danych może mieć wiele (np. tysiące) możliwych sposobów implementacji (tzw. planów
wykonania). Automatyczna optymalizacja rozproszonych zapytań za pomocą technik znanych
z rozproszonych baz danych mogłaby rozwiązać ten problem, niestety współczesne rodzaje
middleware nie posiadają takiej możliwości. Panujące obecnie trendy polegają raczej na ucieczce
od tego złożonego problemu. Przykładowo, najnowsze rozwiązania, oparte na idei tzw. architektury
zorientowanej na usługi (Service Oriented Architecture, SOA) całkowicie ignorują problem
przetwarzania danych masowych. Ich zwolennicy przekonują, iż SOA operuje na poziomie usług,
czyli na znacznie wyższym, niż zwykłe dane (wysokopoziomowa integracja usług vs.
niskopoziomowa integracja danych). Automatycznie zakłada się zatem, że zdalna procedura
(usługa) zaprojektowana została w taki sposób, by obsłużyć wszystkie możliwe sytuacje biznesowe
związane ze zdalnym dostępem do zasobów. Naszym zdaniem jest to złudne założenie, które
prowadzi do odkrywania na nowo dobrze znanych mechanizmów (np. transakcje dla WebServices)
przetwarzania danych w środowisku rozproszonym.
Obiektowa baza danych jako middleware
We współczesnych systemach informatycznych bazy danych wykorzystywane są w projektach
integracyjnych najczęściej wówczas, gdy problem da się rozwiązać za pomocą replikacji, federacji
lub poprzez budowę hurtowni danych. Jeśli projekt integracyjny wymaga wykorzystania takich
pojęć jak logika aplikacji, wówczas wykorzystywane są inne strategie integracyjne, np. integracja
poprzez usługi. Zauważmy jednak że te ograniczenia baz danych wynikają z prostoty relacyjnego
modelu danych i nie dotyczą baz obiektowych. Obiektowe bazy danych potencjalnie posiadają
zarówno zalety relacyjnych baz danych, jak i języków programowania, dlatego każda znana
strategia integracyjna może być wyrażona w kontekście takiego systemu.
16
Obiektowe bazy danych w projektach integracyjnych posiadają szereg znaczących zalet. Znacznie
wyższy poziom abstrakcji, na którym pracuje potencjalny inżynier integracyjny korzystający
z obiektowej bazy danych pozwala często wyrazić w jednej linii kod, który przy użyciu np.
środków opartych na standardzie CORBA przyjąłby postać dużego programu. Ma to znaczenie nie
tylko dla produktywności programistów, łatwości i stabilności tworzonego przez nich
oprogramowania, ale również umożliwia automatyczną optymalizację operacji na rozproszonych
danych. Taka automatyczna optymalizacja nie jest możliwa jeśli dany komponent systemu (np.
usługa) zostanie zaprogramowany w ramach niskopoziomowego języka programowania.
Jeśli integrowane mają być dane pochodzące ze źródeł heterogenicznych i integracja ma zachodzić
w czasie rzeczywistym, wówczas konieczne jest zbudowanie federacyjnej bazy danych. Obiektowe
bazy danych znane są tymczasem ze znacznie bogatszego modelu danych, pozwalającego na
łatwiejsze wyspecyfikowanie kanonicznego modelu danych obowiązującego w federacji. Pozwala
to zintegrować istniejące, rozproszone i heterogeniczne zbiory danych do postaci jednego,
wirtualnego repozytorium wszystkich danych jakimi dysponuje organizacja. Nie zależy to od tego,
czy integrowane zasoby to relacyjne, albo obiektowe bazy danych, repozytoria XML, czy dane
multimedialne. Tak różnorodne modele danych stanowią problem dla relacyjnego modelu danych,
ale nie dla wielu istniejących modeli obiektowych. Zauważmy, że nawet jeśli jako kanoniczny
model danych wybrany zostanie stosunkowo uniwersalny model danych XML, taka decyzja nie
gwarantuje sukcesu w integracji danych. Najpoważniejszymi problemami są tutaj m.in.: sposób
opisu globalnych danych danych, język zapytań i jego optymalizacja, oraz aktualizacji danych
z poziomu globalnego użytkownika. Ze względu na brak globalnego identyfikatora obiektu
(obecnego np. w CORBA, ale nie istniejącego obecnie w innych niż obiektowe modelach danych),
nie jest możliwe stworzenie generalnej metody modyfikowania zintegrowanych danych. Z tego
powodu, choć większość obecnych federacyjnych baz danych bez problemu potrafi udostępniać
dane do odczytu, operacje modyfikujące stan poszczególnych baz danych nie są możliwe w ogóle,
albo tylko w niektórych przypadkach. Ponieważ interfejs do danych globalnych zwykle
w federacyjnych bazach danych definiowany jest jako perspektywa, problem ten można rozszerzyć
do dużo szerszego i znanego od wielu lat problemu aktualizacji perspektyw. Perspektywy znane
z systemów relacyjnych (traktowane jako zapamiętane zapytynia) umożliwiają tymczasem
aktualizację danych wirtualnych tylko w najprostszych przypadkach. Przykładowo, nie można
aktualizować danych uzyskanych poprzez złączenie (z kilkoma drobnymi wyjątkami), albo
wygenerowanych za pomocą operatorów agregujących. Przezroczystość wszystkich operacji
17
realizowanych na perspektywach jest tymczasem szczególnie wymagana w federacyjnej bazie
danych, gdzie perspektywy mogą integrować dane z tysięcy źródeł danych.
Okazuje się, że rozwiązanie problemu aktualizacji perspektyw musi wiązać się z wprowadzeniem
dużo bardziej wyspecjalizowanej struktury pełniącej rolę perspektywy. Takie struktury
zaprojektowane zostały dla obiektowych baz danych, i tylko w ich kontekście ich moc może być
w pełni wykorzystana. Wiąże się to m.in. z koniecznością rozszerzenia identyfikatora obiektu do
postaci umożliwiającej opisywanie wirtualnych danych. Wraz z definiowanymi przez twórców
perspektyw generycznymi procedurami przeciążającymi podstawowe operacje na wirtualnych
obiektach, identyfikatorami posiadającymi informację na temat perspektywy, oraz zapytania za
pomocą którego obiekty wirtualne są generowane, perspektywy takie umożliwiają modyfikację
dowolnych danych wirtualnych.
Strategie integracyjneSpecjaliści z dziedziny EAI zgodni są w jednej kwestii - żadna istniejąca obecnie metoda integracji,
czy też technologia nie jest złotym środkiem na każdy problem integracyjny [3, 11, 102].
Przykładowo, dla pewnej organizacji, niezbędna może być integracja danych zawartych w jej
bazach danych. Organizacja ta wymaga nieograniczonego dostępu do wszystkich swoich
danych i nie jest w stanie przewidzieć wszystkich ścieżek dostępu teraz lub w przyszłości.
Ponieważ zgodnie np. z architekturą SOA dostęp do nowego zbioru danych wiąże się z budową
nowej usługi, oznacza to w takiej sytuacji każdorazową modyfikację systemu docelowego,
co z różnych powodów (np. koszt) może być nieakceptowalne. Federacyjna baza danych jest więc
tutaj najlepszym rozwiązaniem.
Podobny problem może jednak nie dotyczyć takiej organizacji, która może skoncentrować się na
integracji logiki aplikacji, a nie przetwarzaniu rozproszonych danych. Poszczególne operacje
możliwe do realizacji na zintegrowanych systemach należących do tej organizacji są dobrze znane,
a ich interfejsy mają charakter statyczny. Dla takiej organizacji tak niskopoziomowa integracja, jaką
jest integracja danych, może okazać się zbyt skomplikowana. Możliwe dla niej jest zatem
wydzielenie dobrze zdefiniowanej funkcjonalności każdego z integrowanych systemów i globalne
ich opublikowanie jako usługi. Stworzony w ten sposób zbiór usług może stać się następnie
podstawą do integracji procesów biznesowych zachodzących w ramach takiej organizacji. Taka
wysokopoziomowa (i pożądana z punktu widzenia biznesu) integracja nie może być z kolei
wykorzystana w przypadku pierwszej z wymienionych organizacji.
18
Zauważmy, iż obecnie obie opisane wyżej organizacje do osiągnięcia swoich celów musiałyby
używać różnego zestawu narzędzi. Pierwsza z nich prawdopodobnie wykorzystałaby jakąś
relacyjną bazę danych umożliwiającą budowę federacyjnych baz danych (np. DB2 z wbudowanym
modułem Garlic), a druga - rozproszonych obiektów, middleware zorientowanego na komunikaty,
albo WebServices.
Naszym zdaniem wszystkie te rozwiązania możliwe są do wyrażenia za pomocą obiektowej bazy
danych zintegrowanej z językiem zapytań rozszerzonym do kompletnego języka programowania.
Narzędzie takie wykorzystane jako middleware może wnieść zupełnie nową jakość do tematyki
integracji aplikacji. Możliwość wykorzystania tego samego narzędzia do zrealizowania różnych
strategii integracyjnych powinna zwiększyć produktywność programistów, ograniczyć koszty
wytwarzania oprogramowania, skrócić czas nauki oraz zredukować liczbę struktur, koncepcji
i mechanizmów niezbędnych do opanowania przez programistów. W tej pracy skoncentrujemy na
zaprojektowaniu architektury takiego narzędzia.
Cel pracy doktorskiejPodstawowym celem rozprawy doktorskiej jest zbadanie nowych paradygmatów ułatwiających
proces implementacji systemów informatycznych. Zaprojektowana zostanie nowa metodologia
tworzenia aplikacji baz danych, wraz z prototypowym narzędziem, umożliwiającym pracę
programiście bazodanowemu na znacznie wyższym poziomie, niż umożliwiają to współczesne
rozwiązania. W tym celu stworzony zostanie nowy, obiektowy język programowania, bezszwowo
zintegrowany z konstrukcjami języka zapytań (zapytania jako wyrażenia), razem z jego
rozproszonym, bazodanowo zorientowanym, obiektowym środowiskiem wykonawczym.
Technologia ta zapewniać będzie funkcjonalność wielu istniejących narzędzi (takich jak np. języki
programowania aplikacji baz danych, języki zapytań do baz danych, systemy zarządzania bazami
danych, różnego rodzaju middleware) w jednym, spójnym, uniwersalnym, łatwym do
wykorzystania i efektywnym w użyciu środowisku programistycznym. Będzie to jednocześnie
propozycja nowej architektury obiektowych baz danych, konkurencyjna wobec istniejących
architektur obiektowych i relacyjnych SZBD.
Najważniejszym zadaniem takiego środowiska programistycznego będzie integracja rozproszonych
zasobów rozumianych jako dane, dlatego zagadnienie to stanowić będzie najpoważniejszy fragment
rozprawy. Integracja danych zrealizowana zostanie za pomocą koncepcji znanej pod nazwą
wirtualnego repozytorium. Zgodnie z tą koncepcją, wszystkie składniki systemu informatycznego
19
widziane będą jako jedna, wirtualna, rozproszona baza danych i usług, dostępna za pomocą języka
zapytań. Zapewnienie odpowiedniego stopnia przezroczystości (przezroczystość danych/usług,
współbieżności, heterogeniczności, skalowania, fragmentacji, replikacji, migracji, optymalizacji,
itp.) niezbędnego w wirtualnym repozytorium osiągalne będzie w satysfakcjonującym stopniu
jedynie wówczas, gdy programista aplikacyjny zwolniony zostanie z ich ręcznego
zaprogramowania za pomocą języka ogólnego przeznaczenia (jak to ma miejsce w typowych
rozwiązaniach). Zamiast tego, za ich realizację odpowiedzialny powinien być system zarządzania
bazą danych sterowany za pomocą bardzo wysokopoziomowych konstrukcji deklaratywnych. Aby
korzystać z wirtualnego repozytorium, programista nie będzie zatem zmuszony do posługiwania się
złożonym API udostępnianym przez obecne middleware. Zamiast tego, pozwolimy mu pracować na
znacznie wyższym poziomie abstrakcji, ukrywając złożoność procesów komunikacyjnych,
mechanizmów bezpieczeństwa, transakcyjności i optymalizacji. Najważniejszą zaletą naszego
wirtualnego repozytorium będzie możliwość aktualizacji danych z poziomu globalnego
użytkownika. Właściwość tę osiągniemy poprzez wykorzystanie w pełni aktualizowalnych
perspektyw, wykorzystywanych jako osłony/mediatory i nie mających precedensu w istniejących
rozwiązaniach z dziedziny integracji danych i aplikacji.
Zaproponowane przez nas narzędzie posiadało będzie potencjał zostania samodzielnym
środowiskiem programistycznym, dostarczającym możliwości budowy aplikacji baz danych oraz
samych baz danych. Integracja danych i aplikacji zbudowanych za pomocą wyłącznie tego
środowiska będzie najbardziej pożądaną sytuacją, najmniej pracochłonną programistycznie.
Rzeczywiste, rozproszone systemy informatyczne składają się jednak z wielu różnych elementów
zbudowanych przy użyciu setek technologii pojawiających się i znikających na przestrzeni lat.
Ze względu na koszty, jakie ponoszą organizacje przy wdrażaniu poszczególnych składników takich
systemów, ich przepisanie w nowym języku programowania, a często nawet drobna modyfikacja,
jest zwykle nierealne. Oznacza to, iż wirtualne repozytorium musi posiadać możliwość integracji
zasobów spadkowych o heterogenicznym charakterze. Cel ten zostanie zrealizowany zgodnie
z regułami przyjętymi dla federacyjnych baz danych. Zaprojektowany zatem zostanie kanoniczny
model danych zdolny reprezentować popularne modele danych wykorzystywanych we
współczesnych bazach danych i językach programowania. Ten kanoniczny model danych
obowiązywać będzie w całym wirtualnym repozytorium, a każdy integrowany zasób (np. relacyjna
baza danych) będzie mógł być wyrażany w jego kategoriach za pomocą zestawu osłon (adapterów)
lub filtrów (importerów). Dodatkowe różnice w schematach poszczególnych żródeł danych oraz
20
reprezentacji danych (np. różne waluty) rozwiązywane będą za pomocą aktualizowalnych
perspektyw pełniących rolę mediatorów. W szczególności, osobna taka perspektywa będzie mogła
zostać zdefiniowana dla każdego systemu kontrybuującego do repozytorium (tzw. perspektywa
kontrybucyjna) oraz globalna perspektywa prezentująca zawartość całego systemu jako wirtualną
całość (tzw. perspektywa integracyjna). Globalny użytkownik korzystał będzie z repozytorium za
pomocą jednego języka zapytań (rozszerzonego do języka programowania), niezależnie od tego
gdzie fizycznie znajdować się będą integrowane dane/usługi, oraz jakie technologie zostały użyte
do ich wygenerowania. Ponieważ tak utworzona rozproszona baza danych widziana jest przez
zewnętrznego użytkownika jako pojedyncza, wirtualna baza danych, istnieje także możliwość
zbudowania dla niej tradycyjnych interfejsów dostępowych, np. JDBC, czy WebServices. W ten
sposób, technologia ta zgodna będzie z przyjętymi w przemyśle standardami, ale jednocześnie
niezależna od nich. W niniejszej pracy nie będziemy jednak zajmowali się konstrukcją takich
interfejsów dostępowych, ani (w szczególności) wrapperów. Konstrukcja takich modułów (np.
osłony do relacyjnej bazy danych) jest zadaniem nietrywialnym i stanowi treść osobnych prac
doktorskich.
Język programowania/zapytań zaproponowany w rozprawie bazuje na podejściu stosowym (Stack-
Based Approach, SBA) do języków zapytań. SBA jest semantyczną ramą ułatwiającą budowę
mocnego języka zapytań dla praktycznie dowolnego znanego modelu danych, niezależnie od tego
czy jest to model półstrukturalny (np. XML), ustrukturalizowany (np. relacyjna baza danych), czy
nieustrukturalizowany (np. dane multimedialne). Przykładowo, obsługa danych XML zapewniona
jest poprzez możliwość zaimportowania zawartości dowolnego dokumentu do bazy
danych i wyrażenia jego treści w terminach obiektów tej bazy danych. Na takich danych można
następnie pracować w taki sposób, jak gdyby były to natywne dane zdefiniowane za pomocą języka
definicji danych wbudowanego w SZBD.
Budowa kompletnego języka programowania bezszwowo zintegrowanego z konstrukcjami języka
zapytań możliwa jest dzięki temu, iż w podejściu stosowym zapytania traktowane są tak, jak
wyrażenia w tradycyjnych językach programowania. Semantyka zapytań jest zatem oparta na
mechanizmach dobrze znanych z typowych języków - stosie środowisk, stosie arytmetycznym oraz
paradygmacie nazwa-zakres-wiązanie. Pozwala to na precyzyjne określenie semantyki operacyjnej
języków zapytań z uwzględnieniem cech obiektowości (np. klasy, dynamiczne role obiektów),
konstrukcji imperatywnych i abstrakcji programistycznych (np. procedur, perspektyw, modułów).
21
Wykorzystanie SBA umożliwi nam znaczną redukcję liczby języków, z jakimi ma do czynienia
typowy programista baz danych. W idealnej sytuacji może to być jeden język przeznaczony do
programowania aplikacji, definiowania schematu bazy danych, wykonywania zapytań,
transformowania danych, itd.
Najważniejsze aspekty technologii zaprezentowanej w rozprawie zaimplementowane zostały
w postaci prototypu. Jest on podstawowym komponentem systemów e-GovBus oraz VIDE,
realizowanych przez PJWSTK wspólnie z partnerami, oraz finansowanych w ramach grantów
przyznanych przez Unię Europejską.
Teza rozprawy doktorskiejKoncepcja wirtualnego repozytorium oparta na jednorodnym obiektowym środowisku
programistycznym bazującym na języku zapytań zintegrowanym z językiem programowania jest
alternatywą dla wielu obecnie popularnych, eklektycznych technologii. Koncepcja ta potwierdzona
została poprzez stworzone oprogramowanie. Jest ono zatem dowodem tezy naukowej dektoratu
mówiącej o tym, iż jest możliwe stworzenie metodologii oraz odpowiednich dla niej narzędzi
programistych (opartych o zintegrowany język zapytań/programowania), które pozwalą na szybką
i tanią budowę wirtualnego repozytorium opartego o kanoniczny model obiektowy. Zadaniem
takiego repozytorium jest integracja danych i aplikacji znajdujących się pod kontrolą tych samych
lub niezależnych od siebie organizacji, a także umożliwienie szybkiego programowania wydajnych
aplikacji baz danych na bardzo wysokim poziomie abstrakcji.
PodsumowanieW rozdziale krótko przedstawiliśmy problemy wiążące się z integracją danych i aplikacji
w systemach rozproszonych. Przedstawiliśmy w nim nasze stanowisko mówiące o tym, iż
obiektowa baza danych z wbudowanym językiem zapytań zintegrowanym z językiem
programowania stanowi potencjalne narzędzie do budowy zintegrowanych baz danych oraz ich
aplikacji. Dzięki pełnej mocy obliczeniowej porównywalnej z istniejącymi językami
programowania, obiektowa baza danych tego typu jest w stanie przykryć funkcjonalnością dowolny
istniejący współcześnie rodzaje middleware, a także niektóre języki programowania czwartej
generacji. Poprzez zastosowanie w pełni aktualizowalnych perspektyw, system tego typu może
również służyć do budowy federacyjnych baz danych. Tak zbudowana baza danych pozbawiona
jest dwóch poważnych problemów związanych z takimi bazami danych jeśli realizowane są przy
użyciu relacyjnych baz danych:
22
1. uproszczonego modelu danych implikowanego przez relacyjny model danych,
2. braku możliwości aktualizacji sfederowanych zasobów danych od strony globalnego klienta.
Przedstawiliśmy również cel niniejszej pracy doktorskiej, której zamierzeniem jest zaprojektowanie
architektury obiektowej bazy danych nowego typu, jak również zaimplementowanie jej prototypu.
23
Rozdział 2:Stan sztuki w dziedzinieintegracji danych i aplikacji
WprowadzenieW bieżącym rozdziale krótko omówimy najważniejsze teorie, techniki, technologie i inne
rozwiązania związane z integracją danych i aplikacji. Ponieważ w niniejszej pracy będziemy
dowodzili, iż obiektowa baza danych jest doskonałym narzędziem dla EAI/EII, zanim przejdziemy
do dokładniejszego opisu naszej koncepcji, postaramy się pokazać dlaczego naszym zdaniem
istniejące techniki programowania aplikacji baz danych powinny zostać w dużej części
przedefiniowane. W naszym opisie skoncentrujemy się na trzech aspektach integracyjnych:
integracji języków programowania z bazami danych za pomocą języków zapytań, metodach
zapewniania komunikacji pomiędzy heterogenicznymi aplikacjami, oraz deklaratywnych sposobach
integracji różnorodnych baz danych.
Integracja baz danych z językami programowaniaMetody integracji języków programowania z bazami danych studiowane są od wielu lat. W związku
z tym, iż ogólnie przyjętym mechanizmem operowania na bazach danych jest język zapytań,
integracja baz danych z językami programowania wiąże się zazwyczaj z integracją języka
imperatywnego ogólnego przeznaczenia z językiem deklaratywnym operującym na bazie danych.
25
Najbardziej popularna klasyfikacja metod integracji języków programowania z bazami danych
oparta jest na dwóch metodach. Pierwsze z tych podejść, tzn. zanurzanie jako literałów kodu języka
zapytań w kodzie języka programowania, obarczone są poważną wadą znaną pod nazwą
“niezgodność impedancji”. Alternatywne podejścia (np. Pascal/R [43], Napier88 [44], DBPL [41],
LOQIS [31], Fibonacci [28], Oracle PL/SQL) w dużym stopniu pozbawione są tej wady, ponieważ
dążą one do stworzenia jednolitego środowiska programistycznego, a konstrukcje imperatywne
zintegrowane są z konstrukcjami deklaratywnymi.
Poniżej przedstawimy krótki, krytyczny opis najbardziej znanych podejść w dziedzinie integracji
języków zapytań z językami programowania.
Interfejs poziomu wywołań
Najbardziej znanym sposobem dostępu do baz danych jest tzw. interfejs poziomu wywołań (Call
Level Interface, CLI). Technika ta polega na zagniedżaniu kodu języka zapytań odpowiedzialnego
za operacje realizowane na bazie danych jako literały łańcuchów znaków w kodzie źródłowym
programów napisanych w językach proceduralnych, a następnie użyciu pewnej bazodanowej
warstwy pośredniej (database middleware) dostępnej jako API.
Poniższy kod w języku Java ilustruje sposób korzystania z interfejsu poziomu wywołań. Fragment
ten odpowiedzialny jest za wysłanie zapytania SELECT * FROM MyTable do bazy danych, a następnie
wyświetlenie na ekranie wszystkich wierszy i kolumn tabeli wynikowej.
Statement stmt = conn.createStatement();try { ResultSet rs = stmt.executeQuery("SELECT * FROM MyTable"); try { while (rs.next()) { int numColumns = rs.getMetaData().getColumnCount();
for (int i = 1 ; i <= numColumns ; i++) System.out.println("COLUMN " + i + " = " + rs.getObject(i)); }
} finally { rs.close(); }
} finally { stmt.close();}
Na przestrzeni lat podejście takie zostało mocno skrytykowane jako uniemożliwiające statyczną
kontrolę typów zapytań (literały Javy nie są dostępne dla kompilatora SQL), wymagające konwersji
26
typów języka programowania na typy bazy danych, stanowiące zagrożenie dla bezpieczeństwa
systemu (tzw. sql injection), uniemożliwiające operacje wspomagania programowania realizowane
przez środowiska programistyczne (refaktoryzacja, podpowiadanie nazw) i inne. Niedogodności te
próbowano ominąć wprowadzając dodatkowy etap kompilacji za pomocą tzw. prekompilatora (np.
SQLJ), analizującego specjalne bloki kodu źródłowego z zawartością będącą kodem
rozpoznawanym przez prekompilator. Prekompilator zdolny jest analizować wnętrze takich
specjalnych klauzul, kontrolować poprawność odwołań zarówno do Javy, jak i do bazy danych,
a także automatycznie zamieniać wnętrze tych klauzul na odpowiednie wywołania CLI
(w przypadku SQLJ na kod Javy używający JDBC). Niestety okazało się że podejście to jest
niewystarczające.
Języki z trwałością
Języki z trwałością należą do dosyć istotnego nurtu bliskiego bazom danych. Aczkolwiek
eksperymentalne projekty (np. Fibonacci [28], PS-Algol [57], DBPL [41], Napier88 [44],
Machiavelli [46], PJama [23], i in.) stworzone w ubiegłej dekadzie nie zdobyły popularności poza
laboratoriami w których powstały, odegrały one silny wpływ na architekturę obiektowych baz
danych. Języki te były zwykle rozszerzonymi tradycyjnymi językami programowania (np. PJama
jest rozszerzeniem Javy, a DBPL Moduli2) wprowadzającymi pojęcie trwałych zmiennych/
obiektów. Trwała zmienna to taka, która ma wszystkie własności zmiennej języka programowania,
ale zachowuje swoją wartość pomiędzy kolejnymi uruchomieniami programu. Uważano że dzięki
takiemu podejściu dowolne bazy danych można traktować jako zestawy trwałych zmiennych.
Prawdopodobnie najpoważniejszym wkładem tych języków w dziedzinę obiektowych baz danych
stało się pojęcie ortogonalnej trwałości. Koncepcja ortogonalnej trwałości mówi, iż programista
nigdy nie powinien być zmuszany do tworzenia kodu odpowiedzialnego za przenoszenie danych
pomiędzy trwałym i ulotnym składem danych. Zakładająca pełną unifikację definicji i środków
manipulacji trwałymi i ulotnymi danymi, koncepcja ta stanowiła ogromny postęp w zakresie
abstrakcji i estetycznego domknięcia pojęć. Przykładowo, projekt PJama zakładał, iż programista
nie musi w ogóle korzystać z żadnego API zapewniającego trwałość, gdyż jest ona zapewniana na
poziomie systemowym i dotyczy wszystkich zmiennych globalnych. System zapewnia zatem iluzję
ciągłej pracy oprogramowania, nawet pomimo zamierzonych lub niezamierzonych wyłączeń.
Cel ten próbowano osiągnąć poprzez rozszerzenie funkcjonalności wirtualnej maszyny języka Java
w taki sposób, aby w trakcie wystąpienia tzw. punktu kontrolnego bieżący stan aplikacji
27
(danych i procesów) był utrwalany. Utrwalone zostają jedyne te dane, które od czasu ostatniego
punktu kontrolnego zostały zmodyfikowane. Gdy nastąpi punkt kontrolny, system jest zamrażany,
a wirtualna maszyna analizuje całe drzewo obiektów sterty w poszukiwaniu takich obiektów.
Po zatrzymaniu aplikacji i jej ponownym uruchomieniu tak zmodyfikowana maszyna potrafi
kontynuować przetwarzanie od momentu w którym jego stan został utrwalony w trakcie ostatniego
punktu kontrolnego.
W innych językach z trwałością stosowano mniej lub bardziej podobne metody utrwalania danych.
W większości projektów koncentrowano się na zbudowaniu nowego języka (często z bardzo
wymyślną semantyką i systemem typów), albo rozszerzeniu istniejącego o aspekt trwałości,
całkowicie ignorując wiążące się z tym implikacje wynikające z konieczności przetwarzania danych
masowych. Żaden ze stworzonych w ten sposób języków nie pozwalał na wykorzystywanie typowo
bazodanowych konstrukcji, np. indeksów, perspektyw, czy wyzwalaczy. Większość języków
z trwałością nie obsługuje również innych krytycznych dla baz danych mechanizmów, np.
transakcji, dzienników, dynamicznej optymalizacji, współbieżnego dostępu wielu równoległych
sesji, kont użytkowników, uprawnień, itd. Tylko w niewielkiej grupie języków wprowadzone
zostały konstrukcje deklaratywne, niezbędne do wygodnego operowania na danych masowych.
Niestety, zazwyczaj konstrukcje te były nieortogonalne z resztą języka. Przykładowo, w systemie
DBPL można korzystać z języka zapytań, ale tylko w stosunku do specjalnego rodzaju zmiennych
zadeklarowanych jako (zagnieżdżone) relacje (próba połączenia języka programowania z modelem
relacyjnym). Wyjątkiem był tu jedynie system Loqis [30, 31], w którym przyjęto jednak zupełnie
inne założenia. Zamiast rozszerzać język proceduralny o konstrukcje charakterystyczne dla baz
danych, rozszerzono bazę danych oraz jej język zapytań o konstrukcje charakterystyczne dla
proceduralnych języków programowania. Dosyć podobne podejście (aczkolwiek
w nieporównywalnie mniejszym stopniu) dotyczy języka PL/SQL firmy Oracle.
Odwzorowanie relacyjno-obiektowe
Odwzorowanie relacyjno-obiektowe jest innym rodzajem integracji aplikacji z bazą danych. Celem
tej techniki jest stworzenie “wirtualnej obiektowej bazy danych” na bazie relacyjnej bazy danych
oraz obiektowego języka programowania. Zdaniem zwolenników tej metody umożliwia to
wykorzystanie istniejących relacyjnych baz danych i obiektowych języków programowania
w sposób “obiektowy”, czyli bez pracochłonnego tłumaczenia struktur relacyjnych na struktury
28
obiektowe. Rozwiązanie to zapewnia zatem iluzję języka z trwałością korzystającego
z dobrodziejstw relacyjnej bazy danych.
Prawdopodobnie najbardziej znaną obecnie technologią tego rodzaju jest Hibernate [20]. Praca
z Hibernate polega na stworzeniu jednej lub kilku klas Javy, których struktura jest następnie
opisywana za pomocą plików XML. Każda taka klasa posiada przypisaną tabelę w bazie danych,
a każdy jej atrybut odpowiednią kolumnę. Stworzone w ten sposób klasy określane są jako trwałe,
ponieważ Hibernate automatycznie utrwala w bazie danych wszystkie zmiany w stanie obiektów
będących instancjami takich klas. Oprócz atrybutów odpowiadających kolumnom tabeli relacyjnej,
każda trwała klasa powinna posiadać atrybut id, w kórym Hibernate zapisuje wartość klucza
głównego dla danego wiersza tabeli. Podczas operacji aktualizacyjnych pozwala to zapisać w bazie
danych zmiany w stanie obiektu wprowadzone na poziomie Javy. Operacje na bazie danych
realizowane są za pomocą obiektu klasy Session, która reprezentuje połączenie z serwerem. Obiekt
trwałej klasy utworzony z poziomu Javy zapisywany jest w bazie danych za pomocą metody save()
tej klasy. Operacja taka powoduje utworzenie wiersza w tabeli bazy danych z którą związana
została dana klasa trwała za pomocą odpowiedniego wpisu wyrażonego w XML. Wpis ten
wykorzystywany jest przez Hibernate do skonstruowania odpowiedniej instrukcji SQL przesyłanej
do bazy danych. Za pomocą mechanizmu refleksji wyznaczana jest nazwa trwałej klasy, a następnie
wyszukiwana jest odpowiednia pozycja wiążące tę klasę i jej atrybuty z odpowiednią tabelą bazy
danych i jej kolumnami. Jeśli w tabeli bazy danych dla której przypisano klasę trwałą istnieje już
jakiś wiersz, wówczas obiekt klasy trwałej może zostać łatwo skonstruowany za pomocą metody
load() jeśli znana jest wartość klucza głównego tego wiersza. Po załadowaniu z bazy danych, stan
obiektu e odzwierciedla stan odpowiedniego wiersza bazy danych:
Employee e = (Employee) sess.load(Employee.class, id);
Niestety, jeśli wartość klucza głównego nie jest znana, wówczas konieczne jest albo załadowanie
wszystkich obiektów z bazy danych (przetwarzane mogą być następnie za pomocą iteratora [29]),
albo wykorzystanie języka zapytań. Sytuacja komplikuje się również wówczas, jeśli obiekt
ładowany za pomocą metody load() posiada zdefiniowane asocjacje z innymi obiektami (które być
może powiązane są własnymi asocjacjami z jeszcze innymi obiektami). W sytuacji takiej narzędzia
odwzorowania O/R nie są w stanie automatycznie ustalić czy obiekty będące celem asocjacji
powinny zostać załadowane razem z głównym obiektem, czy też dopiero przy pierwszej próbie
dostępu do powiązanego obiektu. Hibernate wymaga w takiej sytuacji jawnego określenia
pożądanego zachowania przez programistę.
29
Jeśli stan obiektu klasy trwałej zostanie zmodyfikowany, wówczas automatycznie konstruowane
jest polecenie update trafiające do bazy danych, wysyłane następnie do bazy danych. Hibernate
posiada własny mechanizm transakcji, niezależny od mechanizmu transakcji docelowej bazy
danych. Jeśli transakcja Hibernate zostanie zatwierdzona, dopiero wówczas może być
zainiacjalizowana transakcja bazy danych. Jeżeli z poziomu języka programowania zmodyfikowano
wiele obiektów, wówczas dla każdego takiego obiektu wygenerowane musi być osobne polecenie
update. Jeśli w ramach transakcji zmodyfikowano wiele obiektów, wówczas do bazy danych trafia
tyle poleceń update ile obiektów zostało zmodyfikowanych. Nie jest to zatem dobra metoda na
realizowanie operacji typu zwiększ pensję każdego pracownika o 1000 złotych. Na szczęście do
realizowania makroskopowych operacji imperatywnych Hibernate umożliwia bezpośrednie
wykorzystanie języka SQL (w sposób podobny do CLI).
Oprócz bezpośredniego wykorzystania SQL, programista może wyszukiwać dane w bazie danych
za pomocą języka HQL oraz niskopoziomowego API podobnego do S.O.D.A (tzw. query by API).
HQL jest językiem wewnętrznym Hibernate podobnym do SQL, ale uwzględniającym niektóre
mechanizmy obiektowości (np. dziedziczenie). HQL jest znacznie prostszym językiem
w porównaniu do SQL, nie umożliwia również wykorzystania rozszerzeń wprowadzonych
w niektórych systemach zarządzania bazami danych (np. wskazówek optymalizatora).
W przypadkach, gdy niezbędne są takie konstrukcje języka zapytań, których nie ma w HQL,
programista zmuszony jest korzystać z SQL. Zarówno SQL, jak i HQL zanurzane są w języku Java
jako literały łańcuchów znaków, a ich wyniki przetwarzane za pomocą iteratorów [29]. Jeśli
zapytanie ma zwracać tylko niektóre kolumny, albo kolumny nie istniejące fizycznie w bazie
danych (np. rezultaty funkcji agregujących), wówczas wyniki zapytań muszą być przetwarzane
pojedynczo, w sposób równie pracochłonny jak w przypadku CLI.
Twórcom Hibernate nie udało się całkowicie ukryć różnic pomiędzy relacyjną bazą danych
a obiektowym językiem programowania. Pomimo iż dzięki mapowaniu obiektów na tabele
relacyjne udało się nieco zniwelować niezgodność impedancji na poziomie modeli danych, operacja
taka nie przynosi wielkich korzyści, jeśli niezgodność ta nadal istnieje na poziomie języka
programowania. Większość operacji w każdej bazie danych realizowana jest za pomocą języka
zapytań, tymczasem technologie O/R nie wprowadzają żadnych ułatwień w ich obsłudze z poziomu
języka programowania. Naszym zdaniem technologie te wprowadzają kolejną warstwę złożoności
30
w aplikacjach, zupełnie nie rozwiązując problemu integracji języków programowania z bazami
danych.
Obiektowe bazy danych a standard ODMG
Obiektowe bazy danych najczęściej kojarzone są ze standardem ODMG [27]. Standard ten pojawił
się w 1993 roku jako zbiór specyfikacji mających umożliwić programistom wytwarzenie
przenośnych aplikacji korzystających z obiektowych baz danych. Ostatnia wersja standardu o
numerze 3.0 została wydana w 2000 roku, po czym grupa ODMG uległa rozwiązaniu. Standard
obejmował następujące elementy:
• ramową architekturę systemu zarządzania obiektową bazą danych,
• model obiektowy, języki specyfikacji schematu (ODL),
• obiektowy język zapytań (OQL),
• wiązania do kilku popularnych języków programowania.
Standard nie znalazł szerszego uznania ze względu na jego istotne braki koncepcyjne i semantyczne
[26]. Wady modelu danych oraz języków ODL i OQL są dobrze znane, zatem nie będziemy ich
tutaj przytaczać. Z punktu widzenia integracji aplikacji z bazą danych największe znaczenie ma dla
nas zaproponowany przez ODMG sposób tworzenia aplikacji baz danych.
Wg standardu ODMG aplikacje baz danych powinny być tworzone w językach programowania
ogólnego przeznaczenia. Przykładowo, dla języka C++ standard przewiduje mechanizm wiązania
z bazą danych oparty o bibliotekę klas i funkcji umożliwiających przetwarzanie zawartości bazy
danych. Semantyka C++ jest rozszerzona w tym celu poprzez wykorzystanie mechanizmu
przeciążania operatorów. Wiązanie respektuje składnię i semantykę C++ oraz rozszerza system klas
języka C++ o klasy dotyczące trwałych obiektów. Wiązanie zakłada również zunifikowany system
typów dla trwałych i nietrwałych obiektów.
Procedura tworzenia aplikacji przebiega w kilku krokach. Programista dostarcza opis schematu
bazy danych zapisanego w języku ODL. Przykładowo, może to być plik o następującej treści:
interface Profesor : Osoba (extent profesorowie keys id_wydziału, nr_pesel): persistent { attribute char id_wydziału[6]; attribute long nr_pesel; attribute Adres adres; attribute set<string> stopnie; relationship set<Student> opiekuje_się inverse Student::opiekun;
31
relationship set<Asystent> zatrudnia inverse Asystent::pracuje_dla; relationship Wydział pracuje_na inverse Wydział::zatrudnia; short Promuje( in string kogo ) raises (Nie_spełnia_warunków); };
Powyższy fragment deklaruje interfejs Profesor dziedziczący z Osoba. Interfejs ten określa
zewnętrzną charakterystykę obiektów danego typu widoczną dla użytkownika obiektu. Ustala także
atrybuty obiektu, operacje jakie można na nim wykonać, jak i związki z innymi obiektami.
W powyższym przykładzie wyspecyfikowano również nazwę pod którą będzie dostępna ekstensja
obiektów zgodnych z interfejsem Profesor.
Standard ODMG zakłada, że po zadeklarowaniu takiego interfejsu trafia on do kompilatora ODL.
Kompilator ten realizuje dwie operacje. Po pierwsze, deklarcje ODL trafiają do bazy danych, gdzie
definowane są wewnętrzne struktury potrzebne do przechowywania obiektów. Po drugie,
kompilator ODL generuje zbiór plików zawierających kod źródłowy sformułowany w języku
programowania którego dotyczy dane wiązanie językowe (np. C++). Pliki te zawierają zbiór
procedur umożliwiających korzystanie z zawartości bazy danych z poziomu języka programowania
w taki sposób, aby obiekty bazy danych były dla programisty nieodróżnialne od obiektów danego
języka programowania. Tak zbudowana aplikacja jest następnie kompilowana i konsolidowana
z kliencką biblioteką czasu wykonania dostarczaną przez dany SZBD.
Programista aplikacji klienckiej może operować na obiektach bazy danych w taki sam sposób, jak
gdyby operował na obiektach języka programowania w którym tworzy aplikację. Ponieważ modele
danych bazy danych oraz języka programowania są obiektowe, dlatego nie ma konieczności
tłumaczenia danych pomiędzy tymi modelami. Zauważmy jednak, że aby można było w ten sposób
operować na obiektach bazy danych, muszą one zostać najpierw wyszukane w bazie danych oraz
skopiowane do przestrzeni adresowej języka programowania. W ODMG do tego celu służy język
zapytań podobny do SQL, ale noszący nazwę OQL. Rozbieżności między OQL i SQL dotyczą
pojęć obiektowych, takich jak złożone obiekty, tożsamość obiektów, wyrażenia ścieżkowe,
polimorfizm, wołanie operacji, późne wiązanie. OQL nie jest jednak kompletny obliczeniowo.
Istnieją zapytania których nie można w nim zadać, nie posiada konstrukcji aktualizacyjnych,
abstrakcji programistycznych, ani konstrukcji sterujących.
Zapytania OQL są zatem zagnieżdżane w kodzie źródłowym języka programowania, a ich wyniki
przetwarzane są za pomocą iteratorów. Niestety, ponieważ standard ODMG nie przewiduje
konstrukcji imperatywnych, w których można byłoby zanurzyć zapytania OQL, dlatego
32
aktualizacje nie mogą być wykonywane makroskopowo. Oznacza to, że kliencka biblioteka czasu
wykonania musi śledzić obiekty które zostały zmodyfikowane i wprowadzać pojedynczo
poszczególne modyfikacje do bazy danych. Jest to zatem poważny problem optymalizacyjny,
bardzo dotkliwy w architekturze klient-serwer.
Przy podejściu zgodnym z ODMG niezgodność impedancji między bazą danych a językiem
programowania nadal istnieje. W rzeczywistości naszym zdaniem problem staje się nawet bardziej
dotkliwy niż w przypadku relacyjnych baz danych programowanych za pomocą CLI. Operacje jakie
musi wykonać programista aby zaprogramować aplikację bazy danych wykorzystując wiązanie do
istniejącego języka programowania są dużo bardziej złożone niż w przypadku baz relacyjnych.
Naszym zdaniem to, oraz szereg poważnych wad koncepcyjnych i semantycznych w specyfikacji
standardu ODMG stało się przyczyną jego znikomej popularności. Niemniej trzeba autorom
przyznać, że mimo wad standard ten stał się osią i punktem odniesienia w przyszłych dyskusjach
dotyczących obiektowych baz danych.
Pomimo wad standardu ODMG najważniejsze decyzje w nim podjęte znajdowały swoje
odzwierciedlenie w różnych formach przez kolejne przedsięwzięcia związane z obiektowymi
bazami danych. Przykładem systemu który bazuje na podobnych założeniach (a nawet
implementuje część standardu ODMG) jest system Objectivity/DB. Cechą najbardziej
charakterystyczną dla produktów zainspirowanych standardem ODMG jest przekonanie,
iż obiektowa baza danych powinna być rozszerzeniem popularnego obiektowego języka
programowania.
Inne rodzaje obiektowych baz danych
Interesującą alternatywą w stosunku do wielu elementów standardu ODMG jest system db4o oraz
jego mechanizm operowania na bazie danych noszący nazwę native queries [13]. W założeniu
twórców tego rozwiązania, native queries powinno usunąć niezgodność impedancji na poziomie
języka programowania oraz języka operowania na bazie danych poprzez realizację konstrukcji
przypominających język zapytań, ale wyrażonych za pomocą języka Java lub C#. Mechanizm ten
reklamowany jest jako 100% typesafe, 100% compile-time checked and 100% refactorable.
Spójrzmy na proste zapytanie napisane z użyciem native queries, pochodzące z dokumentacji
systemu db4o:
List <Pilot> pilots = db.query(new Predicate<Pilot>() { public boolean match(Pilot pilot) {
33
return pilot.getPoints() == 100; }});
“Zapytanie” to ma za zadanie odszukać wszystkich kierowców formuły 1 (pilotów), którzy zdobyli
100 punktów. W tym celu tworzona jest instancja anonimowej klasy implementującej interfejs
Predicate, deklarujący metodę match(). Po zainicjowaniu ewaluacji zapytania, metoda match()
zostanie wywołana dla każdego obiektu klasy Pilot. Jeśli dla danego kierowcy metoda zwróci true,
wówczas reprezentujący go obiekt znajdzie się w kolekcji wynikowej, jeśli false - to nie.
Prostota tego założenia być może dobrze sprawdza się w niszowych zastosowaniach, takich jak
systemy wbudowane, naszym jednak zdaniem nie może stać się podstawą poważnego systemu
zarządzania obiektową bazą danych. Naszą uwagę zwraca przede wszystkim ubogość możliwych
operacji jakie można wyrazić za pomocą “zapytań” sformułowanych z użyciem native queries.
Zbiór możliwych operacji ogranicza się praktycznie jedynie do selekcji - brak jest możliwości
tworzenia złączeń, projekcji, kwantyfikatorów, agregowania, operacji zbiorowych (union, minus,
intersect, etc,.), i wielu innych, uważanych za podstawowe operacje w SQL. Nie jest możliwe
również tworzenie zapytań ad-hoc - każde, nawet najprostsze zapytanie musi być częścią aplikacji.
Najpoważniejszy problem z native queries wiąże się jednak z optymalizacją zapytań. Większość
znanych metod optymalizacyjnych w językach zapytań bazuje na przepisywaniu. Do bazy danych
trafia treść zapytania (jako tekst), po czym jest ono przetwarzane na równoważną semantycznie
postać, która jest jednak wydajniejsza w ewaluacji. Ponieważ w przypadku native queries nie mamy
do czynienia z tekstem zapytania, optymalizacja taka jest znacznie utrudniona, o ile w ogóle
możliwa. W związku z tym, iż treścią metody match() może być absolutnie dowolny kod Javy, nie
bardzo wiadomo w jaki sposób zbudować na jego podstawie tekstową formę zapytania, która
mogłaby trafić do serwera bazy danych i być tam zoptymalizowana. Obecne implementacje próbują
optymalizować zapytania poprzez analizę kodu pośredniego, co naszym zdaniem w ogólnym
przypadku jest niewykonalne. Praktycznie wszystkie znane metody optymalizacji zapytań pracują
na deklaratywnej formie zapytania, a nie na skompilowanej, niskopoziomowej postaci.
Wszystkie te problemy wzmacniają nasze przekonanie o tym, iż native queries nie są odpowiedzią
na problem integracji języków zapytań z językami programowania i mogą znaleźć zastosowanie
jedynie w bardzo niszowych zastosowaniach. Dotyczy to również innych technologii działających
w podobny sposób, np. Linq [45] oraz Cω [44] firmy Microsoft.
34
Integracja rozproszonych danych i aplikacjiJedną z cech współczesnych organizacji jest to, iż różne ich jednostki organizacyjne używają
różnych systemów do tworzenia, przechowywania i przeszukiwania danych mających dla nich
jakieś znaczenie. Różnorodność źródeł danych wiąże się z brakiem koordynacji, różnym tempem
adoptowania nowych technologii, geograficznym oddaleniem, jak i łączeniem się ze sobą różnych
firm. Jedynie poprzez integrację tych wszystkich systemów organizacje mogą skorzystać z pełnej
wartości należących do nich danych. Wraz z rozwojem systemów informatycznych oraz sieci
Internet, coraz częściej mówi się również o integracji aplikacji - zarówno należących do tego
samego przedsiębiorstwa (Enterprise Application Integration, EAI), jak i różnych organizacji
(Business To Business, B2B) [3, 11].
Rozproszone obiekty
Integracja danych i aplikacji może być realizowana na poziomie fizycznym za pomocą wielu
różnorodnych technik. Ze względu na dążenie do zmniejszenia kosztów wytwórczych
oprogramowania, obecnie odchodzi się raczej od implementacji własnych, specyficznych dla
konkretnych systemów protokołów, dążąc do wykorzystania istniejącego już, uniwersalnego
oprogramowania ułatwiającego komunikację między aplikacjami - tzw. middleware. Na przestrzeni
lat zaprojektowano wiele rodzajów middleware: rozproszone obiekty, serwery aplikacyjne, kolejki
komunikatów, serwery integracyjne, monitory transakcyjne, i in. Różne rodzaje middleware
pozwalają wykorzystać różne strategie integracyjne (integracja zorientowana na dane, na usługi, na
komunikaty, na procesy biznesowe, poprzez portale itd.), wykorzystując do tego celu różnego
rodzaju metody łączenia integrowanych elementów (połączenia point-to-point, albo many-to-many)
oraz scenariusze komunikacji (komunikacja synchroniczna i asynchroniczna).
Jednym z bardziej znanych rodzajów middleware są rozproszone obiekty, których prawdopodobnie
najbardziej znanym reprezentantem jest standard CORBA [25, 102]. Rozproszone obiekty
definiowane są w tym standardzie jako fragmenty większych aplikacji, zaprojektowanych do
wzajemnej współpracy, jednak działającymi na zupełnie odrębnych maszynach. Podstawowymi
zaletami rozproszonych obiektów są: obiektowy model danych, zgodność z logiką biznesu,
stusunkowo wysoki poziom abstrakcji, szereg usług (np. transakcyjność) dostępnych dla
programistów, standardowy protokół wymiany danych pomiędzy elementami systemu
rozproszonego, niezależność od użytego w implementacji języka programowania.
35
Programista tworzący aplikację opisuje jej interfejs dostępny dla zdalnych klientów za pomocą
specjalnego, niezależnego od implementacji języka - IDL (Interface Definition Language). Poniżej
zaprezentowano przykład takiej definicji:
module StockObjects { struct Quote { string symbol; long at_time; double price; long volume; };
exception Unknown{};
interface Stock { Quote get_quote() raises(Unknown); void set_quote(in Quote stock_quote);
readonly attribute string description; };};
Pliki zbudowane za pomocą IDL poddawane są następnie działaniu kompilatora IDL, który
generuje kod dla wybranego języka programowania. Przykładowo, dla powyższego interfejsu
i języka Java, mogą zostać wygenerowane następujące pliki: Stock.java (interfejs Stock w postaci
interfejsu Javy), Quote.java (klasa Javy reprezentująca strukturę Quote), StockHelper.java,
QuoteHelper.java (implementacja pomocniczych operacji związanych z operowaniem na typach),
StockHolder.java, QuoteHolder.java (klasa pomocnicza umożliwiająca reprezentowanie w Javie
przekazywania parametrów w trybach in i out), _StockStub.java (klasa używana przez klienta jako
namiastka zdalnego klienta, w przypadku serwera jest ona rozszerzana o implementację zdalnych
operacji).
Głównym zadaniem wygenerowanego w ten sposób kodu jest umożliwienie przyłączenia tworzonej
aplikacji do tzw. obiektowego pośrednika żądań (Object Request Broker). Zadaniem takiego
pośrednika jest komunikacja z innymi brokerami (być może dostarczanymi przez innych
producentów), znajdującymi się na odrębnych maszynach, za pomocą standardowego protokołu
(np. Internet Inter-Orb Protocol, IIOP), zdefiniowanego w standardzie CORBA. Zbiór połączonych
ze sobą pośredników tworzy tzw. szynę CORBA.
Charakterystyczną cechą rozproszonych obiektów jest to, iż z punktu widzenia programisty
tworzącego aplikację kliencką nie ma różnicy w operowaniu na danych trwałych lub nietrwałych.
36
Niestety, z czasem okazało się iż ta unifikacja metod dostępu do danych oraz łatwość dostępu do
danych spowodowała u programistów pokusę ignorowania ograniczeń nakładanych przez sieć
komputerową (czas dostępu, awaryjność) i projektowanie aplikacji rozproszonych w taki sam
sposób, jak gdyby były to aplikacje nierozproszone. Przykładowo, naiwna implementacja operacji
przetworzenia kolekcji zdalnych obiektów w sposób proceduralny (za pomocą iteratorów) oznacza
wielokrotną wymianę danych między serwerem i klientem, proporcjonalną do ilości obiektów.
Ta cecha, w połączeniu z niespójnością samego standardu, złożonym API, niekompatybilnością
pomiędzy oprogramowaniem dostarczanym przez różnych dostawców i innymi poważnymi
problemami stała się przyczyną zmniejszającej się popularności standardu CORBA. Mimo
znaczących sukcesów (7 mln instalacji) standard CORBA podlega ostatnio druzgocącej krytyce, zaś
nowych instalacji praktycznie nie przybywa. Czy oznacza to jednak, że sama technologia
rozproszonych obiektów również skazana jest na porażkę?
Wydaje się, że zunifikowanie dostępu do danych lokalnych i zdalnych jest wadą wówczas, gdy
aplikacja tworzona jest w tradycyjnym języku programowania, gdzie dane przetwarzane są sposób
proceduralny. W takiej sytuacji, programista nie posiadający doświadczenia w tworzeniu
rozproszonych aplikacji tworzy niestabilne i niewydajne oprogramowanie. Programista posiadający
takie doświadczenie, ma z kolei ograniczone pole manewru w dziedzinie optymalizacji, ponieważ
może korzystać wyłącznie z metod zaimplementowanych przez twórcę aplikacji serwerowej.
W przypadku gdy odpowiednia metoda nie jest zaimplementowana, wówczas konieczne jest
przetwarzanie po stronie klienckiej (lub przebudowanie aplikacji serwerowej). W ramach standardu
CORBA stworzona została co prawda usługa zapytań, jednak nie znalazła ona uznania w oczach
specjalistów. Wraz z innymi usługami (np. usługą transakcyjności) dodaje ona dodatkowy poziom
złożoności do i tak już skomplikowanego standardu.
Pewna forma języka zapytań wprowadzona została w innej technologii - Enterprise Java Beans.
Technologia ta od samego początku zaprojektowana była w celu zapewnienia podstawowych usług
związanych trwałością, transakcyjnością i bezpieczeństwem w kontekście języka Java. Niestety,
zagadnienie trwałości rozwiązane jest w sposób podobny do Hibernate, czyli poprzez mapowanie
relacyjno obiektowe. Ponieważ sama technika tworzenia rozproszonych obiektów w środowisku
EJB jest również niezwykle skomplikowana, nie dziwi zatem głośna krytyka EJB ze strony wielu
specjalistów.
37
W rzeczywistości złożoność API jest cechą praktycznie wszystkich współczesnych technologii
wspomagających tworzenie oprogramowania rozproszonego, niezależnie od tego czy jest to
CORBA [25], DCOM [101], EJB [52], Spring [105], JMS [104], czy WebServices [11]. Naszym
zdaniem przyczyną tego stanu rzeczy jest zbyt niski poziom abstrakcji oraz tworzenie
oprogramowania za pomocą tradycyjnych języków programowania, które są nieprzystosowane do
tego celu. Projektując zatem język programowania zintegrowany z językiem zapytań, warto zatem
zastanowić się nad wbudowaniem do niego odpowiednich konstrukcji wspierających komunikację
rozproszoną. Język taki powinien posiadać mechanizmy optymalizacyjne związane
z przetwarzaniem danych masowych w wolnym i niestabilnym środowisku rozproszonym.
Rozproszone i federacyjne bazy danych
Potrzeby niektórych organizacji (np. instytutów naukowo-badawczych) wymagają integracji
dostępnych w nich zasobów na bardzo niskim poziomie, tzn. poziomie prostych danych
składowanych w bazach danych. Dla organizacji takich integracja danych za pomocą usług, czy
z wykorzystaniem rozproszonych obiektów może być nieakceptowalna ze względu na ich zbyt
wysokopoziomowy charakter implikujący pewną “sztywność” w sposobie dostępu. Alternatywnym
rozwiązaniem może być budowa scentralizowanej bazy danych podobnej do hurtowni danych,
jednak rozwiązanie to może być nieakceptowalne ze względu na koszt, czas dostępu do najbardziej
aktualnej wersji danych danych, i in. Integracja zasobów kilku baz danych za pomocą federacji
może okazać się najbardziej bezinwazyjną metodą integracji systemów informatycznych (np.
w przypadku przejęcia jednej firmy przez drugą).
Federacyjna baza danych [47, 48] jest logicznym powiązaniem niezależnych od siebie,
rozproszonych baz danych, tworzącym pojedynczy, zintegrowany system bazodanowy. Integrowane
w ten sposób źródła danych mogą być nie tylko typowymi bazami danych (np. obiektowymi,
relacyjnymi, repozytoriami XML) różnorodnych producentów, ale również płaskimi plikami,
dokumentami tekstowymi, plikami arkuszy kalkulacyjnych, oraz wieloma innymi rodzajami danych
ustruktualizowanych i nieustrukturalizowanych. Architektura federacyjna powoduje że wszystkie te
dane widoczne są jako jedna, wirtualna całość (stąd czasem używana nazwa - wirtualna baza
danych).
Integrowane bazy danych udostępniają szereg swoich zasobów całej federacji. Zasoby te mogą być
metadanymi (schematy bazodanowe), zwykłymi danymi, czy interfejsami programistycznymi
umożliwiającymi korzystanie z takiej bazy danych. Suma udostępnionych w ten sposób danych
38
razem z centralną, integracyjną bazą danych tworzy całą infrastrukturę federacyjną. Zintegrowane
bazy danych udostępniają część lub całość swojej zawartości innym członkom federacji, pozostając
jednak autonomię w ich lokalnym zarządzaniu.
Nowe źródła danych mogą być dodawane do federacji poprzez utworzenie dla nich tzw. osłon
(wrappers). Osłony są relatywnie nieskomplikowanymi, ale niskopoziomowymi programami
umożliwiającymi fizyczne połączenie z federacją różnorodnych, heterogenicznych źródeł danych.
Przykładowo, zadaniem osłony dla plików programu Microsoft Excel powinno być
zaimplementowanie pewnego API umożliwiającego odczytywanie tych plików i dynamicznym
udostępnianiu jego zawartości dla oprogramowania sterującego funkcjonowaniem federacyjnej
bazy danych (np. w formie sterownika JDBC) zgodnie z modelem danych przyjętym dla niej.
Architektura federacyjnej bazy danych często obejmuje także komponenty zwane mediatorami.
Mediator jest specjalnym modułem oprogramowania umieszczanym po stronie integrowanego
zasobu. Jego zadaniem jest takie przekształcenie lokalnych danych, by mogły być one
wykorzystane przez globalnego użytkownika zgodnie z pewnymi regułami przyjętymi dla całej
federacji. Mediator tłumaczy zapytanie zgodne z globalnym schematem federacji na taką jego
formę, by mogło być one wykonane na danych lokalnych. Oprócz przekształceń związanych
z różnymi schematami danych, przekształcane mogą być również same dane. Przykładem takiego
zastosowania mediatorów jest dynamiczna (wirtualna) konwersja pensji z waluty używanej lokalnie
(np. PLN) do waluty używanej w całej federacji (np. USD). Nawet taki wydawałoby się banalny
problem wymaga rozstrzygnięć i umów, np. ustalenia serwisu bankowego wg którego na bieżąco
będzie przeliczana waluta, czy zamiana aktualizacji wyrażonej w walucie obowiązującej
w federacji na walutę obowiązującą w lokalnej walucie.
Krytyczną cechą federacyjnych baz danych jest stopień w jaki system taki jest w stanie upodobnić
się do scentralizowanej bazy danych, oraz ukryć złożoność mechanizmów związanych z integracją
danych w heterogenicznym i rozproszonym środowisku. Najczęściej mówi się o następujących
poziomach przezroczystości w federacyjnych baz danych:
• Przezroczystość dostępu, czyli dostarczenie jednorodnych metod operowania na danych
lokalnych i odległych,
• Przezroczystość położenia, czyli uwolnienie użytkowników od konieczności posiadania wiedzy
na temat fizycznej lokalizacji danych w systemie rozproszonym,
39
• Przezroczystość współbieżności, czyli umożliwienie wielu użytkownikom jednoczesnego dostępu
do danych przy zachowaniu pełnej spójności danych, bez konieczności umawiania się, czy
niskopoziomowego programowania mechanizmów synchronizacyjnych,
• Przezroczystość heterogeniczności, czyli umożliwienie jednolitego traktowania danych
pochodzących z różnych źrodeł, zapisanych tam za pomocą różnych modeli danych,
• Przezroczystość skalowania, czyli umożliwienie dodawania nowych elementów systemu
rozproszonego bez wpływu na działanie starych aplikacji i pracę użytkowników,
• Przezroczystość fragmentacji, czyli automatyczne scalanie obiektów, tabel lub kolekcji, których
fragmenty przechowywane są w różnych miejscach,
• Przezroczystość replikacji, czyli umożliwienie tworzenia i usuwania kopii danych w innych
miejscach geograficznych z bezpośrednim skutkiem dla efektywności przetwarzania, ale bez
skutków dla postaci programów użytkowych lub pracy użytkownika końcowego,
• Przezroczystość optymalizacji, czyli możliwość wykorzystania bez wiedzy użytkownika szeregu
strategii optymalizacyjnych czasu kompilacji lub wykonania, umożliwiających przyspieszenie
wykonywania zapytań realizowanych na rozproszonej bazie danych,
• Przezroczystość awarii, czyli umożliwienie nieprzerwanej pracy większości użytkowników
rozproszonej bazy danych w sytuacji, gdy niektóre z jej węzłów lub linie komunikacyjne uległy
awarii,
• Przezroczystość migracji, czyli umożliwienie przenoszenia zasobów danych do innych miejsc bez
wpływu na pracę użytkowników,
• i in.
Federacyjne bazy danych na przestrzyni lat były przedmiotem wielu projektów badawczych, a także
kilku rozwiązań komercyjnych. Spośród pierwszych, pionierskich prób budowy takich baz danych
wymienić można systemy TSIMMIS [33] oraz HERMES. W obu tych systemach wykorzystano
koncepcje bazodanowe do implementacji “mediatorów” posługujących się nieproceduralnymi
specyfikacjami do integracji specyficznych źródeł danych. Systemy DISCO [49] i Pegasus [50] były
bliższe postaci współczesnych federacyjnych baz danych. Twórcy DISCO koncentrowali się na
wydajnej integracji zasobów heterogenicznych. Pegasus miał swój własny model danych oraz język
zapytań. System Garlic [56] był pierwszym projektem badawczym zmierzającym do budowy
40
federacyjnej bazy danych w oparciu o relacyjną bazę danych. Najważniejsze fragmenty tego
prototypu są obecnie częścią DB2 [51].
Konfiguracja prostego systemu federacyjnego w DB2 składa się z kilku kroków. Po pierwsze,
w serwerze federacyjnej bazy danych rejestrowana jest (za pomocą operacji CREATE WRAPPER)
osłona do integrowanego zasobu. Następnie tworzony jest (za pomocą instrukcji CREATE SERVER)
obiekt bazodanowy reprezentujący zdalny serwer. W kolejnym kroku musi zostać opisana struktura
każdej zdalnej tabeli, do której odwoływać się ma serwer integracyjny (operacja CREATE
NICKNAME). Nazwy podane podczas ostatniej z tej operacji dostępne są w przezroczysty sposób dla
języka SQL. Przykładowo, istnieje możliwość zbudowania perspektywy bazodanowej (CREATE
VIEW) odwołującej się zarówno do lokalnych jak i zdalnych danych serwera federacyjnej bazy
danych. Choć na pierwszy rzut oka wydawać by się mogło że perspektywa zrealizowana w taki
sposób ukrywa przed użytkownikiem fakt rozproszenia danych, w rzeczywistości nie jest to do
końca prawdą. Podstawowy problem z transparentnością takiego rozwiązania powstaje
w momencie, gdy użytkownik SZFBD usiłuje zaktualizować dane zwrócone przez taką
perspektywę, okazuje się że w wielu przypadkach nie jest jest to możliwe, wskutek dobrze znanego
problemu aktualizacji perspektyw. W opracowaniach dotyczących DB2 poleca się zatem
zaimplementowanie aktualizacji za pomocą wyzwalaczy INSTEAD OF.
Poważnym problemem w dziedzinie federacyjnych baz danych jest optymalizacja zapytań
rozproszonych. Ze względu na bardzo duży czas dostępu do rozproszonych zasobów, potrzebna jest
specjalna strategia ewaluacji zapytań operujących na takich danych. W relacyjnych bazach danych
zwłaszcza operacja złączenia tabel wymaga specjalnego sposobu ewaluacji. Najczęściej
wykorzystuje się tutaj techniki dekompozycji zapytań na podzapytania wysyłane do różnych
serwerów, jak również półzłączenia (semi-joins) wraz z przesyłaniem między serwerami danych
potrzebnych do wykonania operacji cząstkowych.
Charakterystyczną cechą istniejące na rynku systemów zarządzania bazami danych jest to, iż oprócz
integracji danych w postaci federacji i klastrów, pozwalają również na budowę (np. w języku PL/
SQL) aplikacji pracujących po stronie serwera bazy danych. W środowisku takim procedury
składowane mogą być udostępniane jako usługi seciowe (WebServices), aplikacje mogą
komunikować się zarówno za pomocą RPC jak i kolejek, a wszystkie operacje realizowane są pod
kontrolą silnych mechanizmów dostarczanych przez SZBD do ochrony danych (np. mechanizmy
transakcji, bezpieczeństwa, dzienniki, itp.). Z punktu widzenia twórcy oprogramowania
41
uruchamianego w kontekście serwera bazy danych te bardzo złożone mechanizmy są praktycznie
niewidoczne. Jest to zatem sytuacja zupełnie niż w przypadku middleware, gdzie programista
zmuszony jest korzystać ze złożonego API.
Obiektowe bazy danych dodają do tego obrazu obsługę technologii rozproszonych obiektów.
Dodanie do obiektowej, federacyjnej bazy danych silnego języka programowania, mocno
zintegrowanego z konstrukcjami deklaratywnym, oraz zmniejszenie rozmiarów samego
oprogramowania do objętości dzisiejszych maszyn wirtualnych popularnych języków
programowania stwarza możliwość wykorzystania takiej bazy danych jako narzędzia
programistycznego, które nie jest ograniczone do pracy po stronie serwera. Uzyskane w ten sposób
narzędzie posiada potencjał reprezentowania wielu różnych metod integracji danych i aplikacji
w ramach tego samego narzędzia. Wydaje się, iż mechanizmy przezroczystości oraz
wysokopoziomowy charakter pracy w środowisku takiego narzędzia powinny doprowadzić do
znaczącego zmniejszenia kosztu produkcji oprogramowania oraz jego utrzymania.
PodsumowanieW rozdziale omówiliśmy najbardziej popularne techniki łączenia aplikacji z bazami danych oraz
aplikacji rozproszonych. Krótko umówiliśmy rozwiązania podjęte przy projektowaniu takich metod
dostępu do danych jak interfejs poziomu wywołań, języki z trwałością, odwzorowanie relacyjno-
obiektowe, czy obiektowe bazy danych. Przeanalizowaliśmy również najważniejsze wady i zalety
technologii rozproszonych obiektów oraz federacyjnych baz danych. Rozdział podsumowaliśmy
stwierdzeniem, iż zastosowanie rozproszonych obiektów w kontekście rozproszonych baz danych
daje nadzieję na rozwiązanie wielu problemów inherentnych dla standardu CORBA oraz
podobnych technologii.
42
Rozdział 3:Podejście stosowe do języków zapytań
WprowadzeniePodejście stosowe (Stack-Based Approach, SBA) do języków zapytań [4] jest spójną teorią
umożliwiającą stworzenie silnego języka zapytań dla praktycznie dowolnego znanego obecnie
modelu danych. Podstawą podejścia stosowego jest założenie, że języki zapytań są odmianą
języków preogramowania. Do języków zapytań powinno się zatem stosować pojęcia,
koncepcje i metody znane i skuteczne w językach programowania. Przykładowo, głównym
pojęciem semantyki i implementacji większości języków programowania jest stos środowisk. Jest
on podstawą mechanizmu określania zakresu nazw, wiązania nazw, wywoływania procedur [94]
(włączając wołania rekurencyjne), komunikowania parametrów oraz realizacji pojęć obiektowości,
takich jak hermetyzacja, dziedziczenie i polimorfizm. Istotą podejścia stosowego do języków
zapytań jest wykorzystanie mechanizmu stosu środowiskowego do definiowania i implementacji
operatorów charakterystycznych dla języków zapytań, takich jak operatory selekcji, projekcji,
nawigacji i złączenia. Dzięki semantyce opartej na pojęciu stosu środowisk, w SBA osiągnięto
pełną ortogonalność i kompozycyjność operatorów, a także możliwość bezszwowej integracji
języka zapytań z konstrukcjami imperatywnymi i innymi abstrakcjami programistycznymi
(procedury, klasy, typy, itd.). W niniejszym rozdziale przedstawimy skrótowy opis najważniejszych
pojęć charakterystycznych dla tego podejścia. Szczegółowy opis SBA dostępny jest w [4].
43
Modele danychPodejście stosowe jest zdefiniowane dla szeregu uniwersalnych modeli składu obiektów.
W zależności od złożoności modelu, określane są one nazwami M0, M1, M2 i M3. Każdy kolejny
model rozszerza poprzedni o nowe właściwości. Naturalnie zbiór ten nie wyczerpuje wszystkich
możliwych modeli składów, a jedynie najbardziej znane obecnie.
Model M0 jest zbudowany zgodnie z zasadami relatywizmu i wewętrznej identyfikacji obiektów.
Jest to bardzo prosty model danych, zdolny reprezentować dane półstrukturalne [93]. W modelu M0
każdy obiekt składa się z identyfikatora wewnętrznego (niedostępnego dla programisty),
identyfikatora zewnętrznego (nazwy dostępnej dla programisty), oraz wartości. Istnieją trzy rodzaje
obiektów: atomowe, referencyjne, złożone. Zakładając że I oznacza zbiór wszystkich
dopuszczalnych identyfikatorów wewnętrznych, N oznacza zbiór wszystkich dopuszczalnych nazw
zewnętrznych obiektów, V zbiór wartości prostych takich jak liczby, napisy, itd., a O zbiór
dowolnych obiektów modelu M0, wówczas możemy napisać że obiekty modelu M0 to trójki
następujących rodzajów (dla i1, i2 ∈ I, n ∈ N, oraz v ∈ V):
• Obiekty atomowe <i1, n, v>. Są to najprostsze rodzaje obiektów. Identyfikowane są jednoznacznie
za pomocą identyfikatora wewnętrznego i1, posiadają nazwę n, przechowują wartość atomową v.
• Obiekty referencyjne <i1, n, i2>. Obiekty te służą do modelowania powiązań między obiektami.
Podobnie jak w poprzednim przypadku, identyfikowane są jednoznacznie za pomocą
identyfikatora wewnętrznego i1 i posiadają nazwę n. Wartością i2 tych obiektów są identyfikatory
wewnętrzne wskazywanych obiektów.
• Obiekty złożone <i1, n, O>. Obiekty te służą do modelowania zagnieżdżania obiektów. Obiekt
o identyfikatorze i1, oraz nazwie n, składa się z obiektów należących do O. Innymi słowy
elementy O są podobiektami obiektu i1.
Przykładowo, pewna baza danych może składać się z następującego zbioru obiektów (obiekt entry
jest systemowym obiektem pełniącym rolę korzenia bazy danych):
<i0, entry, <i1, Employee, <i4, Name, “J. Smith”> <i5, Salary, 65000> > <i2, Employee, <i6, Name, “S. Bush”> <i7, Salary, 45000>
44
> <i3, Department, <i8, Name, “Sales”> <i9, Location, “London”> >>
Model M1 jest rozszerzeniem M0 o klasy i dziedziczenie. Klasa jest zwykłym obiektem złożonym,
zawierającym podobiekty reprezentujące niezmienniki pewnej grupy obiektów. Między obiektami
reprezentującymi klasy można deklarować relację dziedziczenia. Oprócz relacji dziedziczenia,
występuje również relacja przynależności obiektu do klasy.
Model M2 wprowadza pojęcie dynamicznej roli obiektu. Każdy obiekt może przyjmować jedną lub
kilka takich ról. Posiadanie jakiejś roli przez obiekt przypomina sytuację gdy obiekt ten jest
instancją klasy. Podczas jednak gdy dziedziczenie ma charakter statyczny, w czasie wykonania
obiekt może zyskiwać nowe oraz tracić stare role, zaś dziedziczenie pomiędzy rolami ma charakter
dynamiczny [99].
Model M3 wprowadza mechanizm hermetyzacji. Model ten może być zarówno rozszerzeniem
modelu M1, jak i modelu M2. Przyjmuje się, że każda klasa może być wyposażona w listę
eksportową, czyli zbiór nazw pól klasy które są widoczne na zewnątrz implementującego ją
obiektu. Pozostałe nazwy nie są widoczne, i traktowane są jako prywatne.
StosyIstotą podejścia stosowego jest zastosowanie dwóch stosów do objaśnienia semantyki języka
zapytań. Istnieją dwa stosy: stos rezultatów oraz stos środowiskowy.
Pierwszy z tych stosów, czyli stos rezultatów (Query Result Stack, QRES), służy do
przechowywania tymczasowych oraz końcowych rezultatów zapytań. Oznaczmy przez R zbiór
wszystkich rezultatów zapytań, zaś przez SR ⊂ R - zbiór rezultatów zapytań nie będących
kolekcjami.
Do zbioru rezultatów zapytań R, mogą należeć następujące elementy r:
• Wartość prosta (liczba, ciąg znaków, wartość logiczna, etc.).
Wartości takie są rezultatami wyrażeń będących literałami, bądź powstają na skutek dereferencji
obiektu atomowego przechowywanego w bazie danych.
• Referencja (identyfikator wewnętrzny obiektu).
Wartości takie zwykle są rezultatami wyrażeń odwołujących się poprzez nazwę do obiektu
45
przechowywanego w składzie danych (inaczej mówiąc: są rezultatami wiązania nazw). Mogą być
również rezultatami dereferencji (pobrania wartości) obiektów referencyjnych.
• Binder (para <n, r>, gdzie n ∈ N, r ∈ R).
Wartości takie powstają na skutek działania operatorów wprowadzających nazwę pomocniczą (as,
groupas), albo jako rezultat operacji nested ix, gdzie ix jest identyfikatorem obiektu
wskaźnikowego. Binder jest zapisywany w postaci n(r).
• Struktura ( struct { r1, r2, ..., rn }, r1, r2, ..., rn ∈ SR)
Jest to ciąg pojedynczych rezultatów (elementów zbioru SR, czyli zbioru R z wyjątkiem kolekcji).
Struktury najczęściej powstają dzięki operatorom przecinka (konstrukcja struktury) lub join, albo
jako rezultat operacji deref iy, gdzie iy jest identyfikatorem wewnętrznym obiektu złożonego.
• Kolekcja pojedynczych rezultatów (bag { r1, r2, ..., rn }, sequence { r1, r2, ..., rn }, r1, r2, rn ∈ SR)
Elementem kolekcji może być dowolny element zbioru R, z wyjątkiem innej kolekcji. Kolekcje
rezultatów nie mogą być elementami kolekcji, chyba że są wartościami binderów. Kolekcje
powstają najczęściej jako wynik wiązania nazw, albo jako rezultat operatorów zbiorowych (np.
union). Istnieją dwa rodzaje kolekcji: wielozbiór (bag) i sekwencja. Pierwszy nie zachowuje
kolejności elementów, a drugi zachowuje. Możliwe są także inne rodzaje kolekcji (np. tablica),
różniące się od omówionych powyżej zestawem generycznych operatorów oraz (niekiedy)
sposobem fizycznej implementacji.
Drugi z wymienionych stosów, czyli stos środowiskowy (Environment Stack, ENVS) służy do
kontrolowania zakresu nazw. Stos środowiskowy składa się z sekcji, a każda sekcja zawiera 0 lub
więcej binderów. Binder jest tworem który służy do wiązania nazwy z odpowiednim bytem czasu
wykonania. W dalszej części pracy bindery będziemy zapisywać jako n(r), gdzie n ∈ N, r ∈ R.
Operacja bindW podejściu stosowym każda nazwa występująca w zapytaniu jest wiązana z odpowiednim bytem
czasu wykonania, zgodnie z zakresem tejże nazwy. Wiązanie nazw realizowane jest przez operację
nazywaną w naszym opisie jako bind. Operacja bind działa na stosie środowiskowym, i jej
zadaniem jest odpowiednie przeszukanie sekcji tego stosu. Na początku ewaluacji zapytania stos
składa się z jednej sekcji (tzw. sekcji bazowej), w której przechowywane są bindery do wszystkich
obiektów korzeniowych. W trakcie ewaluacji zapytania na stosie pojawiają się i znikają dodatkowe
sekcje (puste lub zawierające bindery), ale sekcja bazowa zawsze na nim pozostaje.
46
Mechanizm wiązania nazwy n polega na przeszukiwaniu stosu środowisk w kierunku od szczytu
w dół w poszukiwaniu pierwszej takiej sekcji, która zawiera przynajmniej jeden binder
o poszukiwanej nazwie. Ponieważ nazwy binderów mogą się powtarzać w ramach danej sekcji,
dlatego operacja bind może wyszukać taką sekcję, w której znajduje się kilka binderów o wiązanej
nazwie. W takim przypadku wynikiem wiązania jest kolekcja zawierająca wartości wszystkich
znalezionych binderów. W szczególności, jeśli żadna sekcja nie zawiera bindera o nazwie n,
wówczas rezultatem wiązania jest pusta kolekcja. Alternatywnie, (szczególnie przy mocnej kontroli
typologicznej) brak odpowiedniego bindera sygnalizowany jest jako błąd typologiczny.
Operacja nestedOperacja nested służy do wyznaczania wnętrza nowej sekcji, która umieszczana jest na stosie
środowiskowym. Operacja pobiera jako argument dowolny rezultat zapytania, natomiast zwraca
zbiór binderów. W zależności od rodzaju argumentu, mogą pojawić się następujące sytuacje:
• Jeśli argumentem jest referencja do obiektu złożonego, wówczas rezultatem operacji nested jest
zbiór składający się z binderów utworzonych na podstawie podobiektów tego obiektu złożonego.
Dla każdego podobiektu tworzony jest binder o takiej nazwie jak ten podobiekt, oraz wartości
będącej identyfikatorem wewnętrznym tego podobiektu.
• Jeśli argumentem jest referencja do obiektu wskaźnikowego, wówczas rezultatem operacji nested
jest zbiór zawierający binder o nazwie takiej, jak nazwa docelowego obiektu wskazywanego
przez ten obiekt wskaźnikowy, oraz wartości będącej identyfikatorem wewnętrznym tego
wskazywanego obiektu.
• Jeśli argumentem jest binder, wówczas rezultatem jest zbiór zawierający binder o takiej samej
nazwie i wartości jak binder wejściowy.
• Jeśli argumentem jest strukturą, wówczas rezultatem operacji nested jest zbiór będący sumą
mnogościową rezultatów funkcji nested wykonanych dla każdego pola tej struktury.
• Dla pozostałych argumentów rezultatem operacji jest zbiór pusty.
Wyrażenia i ich ewaluacjaPodejście stosowe traktuje zapytania w taki sam sposób, jak tradycyjne języki programowania
traktują wyrażenia. Terminu wyrażenie można zatem używać zamiennie z terminem zapytanie.
Pomimo, iż SBA jest niezależne od składni, do objaśnienia niektórych konstrukcji semantycznych
używa się składni abstrakcyjnej noszącej nazwę SBQL (Stack-Based Query Language).
47
Podstawową cechą SBQL jako składni jest dążenie do unikania lukru składniowego,
w szczególności charakterystycznej dla SQL konstrukcji select..from..where.
Wyrażenia SBQL posiada własność kompozycyjności. Podobnie jak w językach programowania,
najprostszymi zapytaniami są nazwy i literały. Bardziej złożone zapytania uzyskuje się poprzez
łączenie podzapytań za pomocą operatorów. W ten sposób każde zapytanie składa się z szeregu
podzapytań. Nie istnieją żadne ograniczenia dotyczące zagnieżdżania zapytań.
SBA posługuje się semantyką operacyjną do zdefiniowania operatorów mogących być częścią
zapytania. Poniżej prezentujemy listę najważniejszych operatorów oraz opis ich semantyki:
• +, - — tradycyjne, unarne operatory arytmetyczne.
Ewaluacja: 1. Wykonaj podzapytanie. 2. Podnieś element ze stosu QRES. 3. Sprawdź czy jest to
po jedynczy element. Jeśli nie, podnieś wyjątek czasu wykonania. 4. Jeśli wynik jest referencją,
wykonaj dereferencję. 5. Sprawdź typ rezultatu. Jeśli operator nie może być zastosowany dla
takiej wartości, podnieś błąd czasu wykonania. 6. Wykonaj operację związana z danym
operatorem. 7. Umieść rezultat na QRES.
• – +, -, *, /, =, <>, <, >, <=, >=, or, and — tradycyjne binarne operatory arytmetyczne,
logiczne i porównania. Ewaluacja: 1. Wykonaj oba podzapytania. 2. Podnieś dwa elementy
z QRES. 3. Sprawdź czy są to pojedyncze wartości. Jeśli nie, podnieś błąd czasu wykonania.
4. Jeśli którykolwiek z rezultatów jest referencją, wykonaj dereferencję. 5. Sprawdź typy danych
obu rezultatów. Jeśli operator nie może być zastosowany do takich wartości, podnieś błąd czasu
wykonania. 6. Wykonaj operację związaną z operatorem. 7. Umieść rezultat na QRES.
• – , (przecinek) — iloczyn kartezjanski (uogólniony dla bagów).
Ewaluacja: 1. Zainicjalizuj pusty bag (będziemy odnosić się do niego jako eres ). 2. Wykonaj oba
podwyrażenia. 3 Podnieś jeden element z QRES (będziemy odnosić się do niego jako e2res ).
4. Podnieś jeden element z QRES (będziemy odnosić się do niego jako e1res ). 5. Dla każdego
elementu e1 z e1res wykonaj: 5.1 Dla każdego elementu e2 z e2res wykonaj: 5.1.1 utwórz struct
{ e1, e2 }. Jeśli e1 i/lub e2 jest strukturą, weź jej pola. 5.1.2 Dodaj strukturę do eres. 6. Umieść
eres na QRES.
• bag, sequence — konstruktory bagów i sekwencji. Ewaluacja: 1. Zainiacjalizuj pusty bag
(będziemy odnosić się do niego jako eres ). 2. Wykonaj podzapytanie. 3. Podnieś wynik z QRES.
4. Potraktuj go jako strukturę. Każde pole tej struktury dodaj do eres. 5. Umieść eres na QRES.
48
• union — suma zbiorów (uogólniony dla bagów).
Ewaluacja: 1. Zainicjalizuj pusty bag (będziemy odnosić się do niego jaki eres). 2. Wykonaj oba
podwyrażenia. 3. Podnieś dwa elementy z QRES. 4. Dodaj wszystkie elementy z obu rezultatów
do eres. 5. Umieść eres na QRES.
• minus, intersect, in — tradycyjne operatory zbiorowe (uogólnione dla bagów).
Ewaluacja: podobnie jak w union. W celu porównania wartości operator może dokonywać
dereferencji rezultatów.
• sum — suma elementów kolekcji.
Ewaluacja: 1. Zainicjalizuj wartość, która będzie służyć do przechowywania sumy (będziemy
odnosić się do niej jako eres ). 2. Wykonaj podzapytanie. 3. Podnieś jeden element z QRES. 4.
Dla każdego elementu e wykonaj: 4.1 Jeśli rezultat jest referencją, wykonaj dereferencję. 4.2 Jeśli
rezultat (po ewentualnej dereferencji) nie jest liczbą, podnieś błąd czasu wykonania. 4.3 Dodaj
liczbę do eres. 5. Umieść eres na QRES.
• min, max, unique, exists, count — tradycyjne operatory agregujące.
Ewaluacja: podobnie jak sum. Ostatnie dwa operatory nie dokonują dereferencji rezultatów.
• . (kropka) — projekcja/nawigacja (niealgebraiczny).
Ewaluacja: 1. Zainicjalizuj pusty bag (będziemy się do niego odnosić jako eres). 2. Wykonaj lewe
podzapytanie. 3. Podnieś jego rezultat ze stosu QRES. 4. Dla każdego elementu e z rezultatu
otrzymanego w poprzednim kroku wykonaj: 4.1. Otwórz nową sekcję na ENVS. 4.2. Wykonaj
operację nested(e). 4.3. Wykonaj prawe podzapytanie. 4.4 Podnieś jego rezultat z QRES. 4.5.
Dodaj go do eres. 5. Połóż eres na QRES.
• where — selekcja (niealgebraiczny).
Ewaluacja: podobnie jak w . (kropka). 4.5. Jeśli wynik ewaluacji prawego wyrażenia nie jest
pojedynczą wartością logiczną, podnieś błąd czasu wykonania. 4.6. Jeśli rezultatem prawego
podzapytania jest true, dodaj ten wynik do eres.
• join — złączenie zależne (niealgebraiczny).
Ewaluacja: podobnie jak w . (kropka). 4.5. Zamiast dodawania rezultatu do eres, wykonaj iloczyn
kartezjański e z rezultatem wykonania prawego podzapytania. 4.6. Dodaj otrzymaną strukturę do
eres. 5. Połóż eres na QRES.
49
• forall — kwantyfikator ogólny (niealgebraiczny).
Ewaluacja: podobnie jak w . (kropka). 4.5. Jeśli rezultat prawego wyrażenia nie jest pojedynczą
wartością logiczną, podnieś błąd czasu wykonania. 4.6. Jeśli rezultat prawego wyrażenia jest
false, umieść wartość false na QRES i przerwij ewaluację operatora.
• forany — kwantyfikator szczegółowy (niealgebraiczny).
Ewaluacja: podobnie jak w . (kropka). 4.5. Jeśli rezultatem prawego wyrażenia nie jest
pojedyncza wartość logiczna, zgłoś błąd czasu wykonania. 4.6. Jeśli rezultatem prawego
wyrażenia jest true, umieść true na QRES i przerwij ewaluację operatora.
• orderby — sortowanie (niealgebraiczny).
Ewaluacja: 1. Wykonaj operację join. 2. Podnieś wynik z QRES. 3. Posortuj otrzymane struktury
wg drugiego pola każdej z tych struktur, później trzeciego, czwartego, etc. 3. Usuń wszystkie
z wyjątkiem pierwszego pola otrzymanych struktur. 4. Odłóż kolekcję struktur na QRES.
• as — przypisanie nazwy pomocniczej elementom kolekcji.
Ewaluacja: 1. Wykonaj podzapytanie. 2. Podnieś rezultat z QRES. 3. Każdy element otrzymanej
kolekcji zastąp binderem o nazwie podanej jako parametr operatora i wartości będącej
zastępowanym rezultatem. 4. Umieść wynikowa kolekcje na QRES.
• groupas — przypisanie nazwy pomocniczej całemu rezultatowi.
Ewaluacja: 1. Wykonaj podzapytanie. 2. Podnieś jego rezultat z QRES. 3. Utwórz binder o nazwie
podanej jako parametr operatora oraz wartości otrzymanej w poprzednim kroku. 4. Połóż go na
QRES.
• :< — przeniesienie obiektu w nowe miejsce (imperatywny).
Ewaluacja: 1. Wykonaj prawe podzapytanie. 2. Podnieś jego rezultat z QRES (będziemy nazywać
go dalej res). 3. Jeśli rezultat ten nie jest po jedynczym binderem, zgłoś błąd czasu wykonania.
4. Wykonaj lewe podzapytanie. 6. Podnieś rezultat z QRES (będziemy nazywać go dalej ref ).
6. Jeśli rezultat ten nie jest pojedynczą referencją, zgłoś błąd czasu wykonania. 7. Utwórz w bazie
danych nowy obiekt i podłącz go pod ref. Rodzaj tego obiektu powinien odzwierciedlać poziom
zagnieżdżenia binderów w res. Jeśli wartoscią bindera res jest wartość prosta, wówczas będzie to
obiekt prosty. Jeśli będzie to binder lub struktura res, wówczas utworzony powinien być obiekt
złożony (być może z kolejnymi podobiektami, jeśli bindery są zagnieżdżone, lub ich wartości są
strukturami), itd.
50
• update — modyfikacja wartości (imperatywny).
Ewaluacja: 1. Wykonaj podzapytanie. 2. Podnieś jego rezultat z QRES. 3. Dla każdego elementu
z otrzymanej kolekcji wykonaj: 3.1. Jeśli element ten nie jest strukturą o dwóch polach, w której
pierwsze pole jest referencją, zgłoś błąd czasu wykonania. 3.2. Jeśli typ obiektu bazy danych
o referencji w pierwszym polu tej struktury posiada typ niezgodny z typem drugiego pola tej
struktury, zgłoś błąd czasu wykonania. 3.3. Zmodyfikuj obiekt w bazie danych o referencji
w pierwszym polu analizowanej struktury, nadając mu wartość drugiego pola struktury.
• delete — usunięcie obiektów (imperatywny).
Ewaluacja: 1. Wykonaj podzapytanie. 2. Podnieś jego rezultat z QRES. 3. Dla każdego elementu
z kolekcji otrzymanej w poprzednim punkcie wykonaj: 3.1. Sprawdź czy rezultat jest referencją.
3.2. Jeśli nie, podnieś błąd czasu wykonania. 3.3. Jeśli tak, usuń z bazy danych obiekt o podanym
identyfikatorze. 3.4. Włóż pustego baga na QRES.
Metabaza i statyczna analiza zapytańPodczas kompilacji zapytania SBQL mogą zostać poddane analizie statycznej. Analiza statyczna
niezbędna jest do zrealizowania statycznej kontroli typów oraz niektórych operacji
optymalizacyjnych. Mechanizm ten polega na wstępnym wykonaniu zapytania, realizowanym
podczas kompilacji, a nie w czasie wykonania. Zadaniem takiej ewaluacji jest zasymulowanie
możliwie największej liczby sytuacji zachodzących podczas czasu wykonania, ale za pomocą
danych dostępnych podczas kompilacji. Zapytanie wykonywane jest nie na rzeczywistych
obiektach bazy danych, ale na tzw. metabazie. Metabaza jest grafem schematu bazy danych,
skonstruowanym na podstawie deklaracji bytów programistych. Graf schematu bazy danych jest
strukturą podobną do grafu bazy danych, i rownież jest modelowany za pomoca obiektów prostych,
złożonych i referencyjnych. Podstawowa różnicą w stosunku do grafu bazy danych jest to, iż:
1. w metabazie zamiast konkretnych wystąpień obiektów przechowywane są informacje
o minimalnej i maksymalnej dopuszczalnej ich ilości (tzw. liczność).
2. zamiast wartości przechowywane są informacje o typach danych i relacjach (np. dziedziczenia)
między nimi.
3. metabaza może również zawierać dodatkowe informacje na temat danych przechowywanych
w bazie danych, np. statystyki potrzebne przy optymalizacji kosztowej.
Przykładowo, dla następującego fragmentu kodu źródłowego:
51
x : integer [1..5];y : record { a : string; b : string; }
metabaza (zapisana zgodnie z modelem M0) może wyglądać następująco:
<i0, entry, <i1, x, <i2, meta_object_kind, META_VARIABLE> <i3, type_kind, PRIMITIVE> <i4, type, INTEGER> <i5, min_card, 1> <i6, max_card, 5> > <i7, y, <i8, meta_object_kind, META_VARIABLE> <i9, type_kind, COMPLEX> <i10, type, i13> <i11, min_card, 1> <i12, max_card, 1> > <i13, $x_struct_type, <i14, meta_object_kind, META_STRUCTURE> <i15, fields, <i16, x, <i17, meta_object_kind, META_VARIABLE> <i18, type_kind, PRIMITIVE> <i19, type, STRING> <i20, min_card, 1> <i21, max_card, 1> > <i22, x, <i23, meta_object_kind, META_VARIABLE> <i24, type_kind, PRIMITIVE> <i25, type, STRING> <i26, min_card, 1> <i27, max_card, 1> > > >>
Odpowiednikiem rezultatu zapytania podczas analizy statycznej, jest sygnatura operacji. Istnieją
następujące rodzaje sygnatur: statyczna referencja, statyczny binder, wariant, reprezentacja typu
wartości, statyczna struktura. Sygnatura reprezentująca statyczną referencję zawiera referencję do
pewnego obiektu metabazy. Statyczny binder zawiera nazwę oraz związaną z nim sygnaturę będącą
wartością bindera. Wariant zawiera kilka możliwych sygnatur, jeśli w czasie kontroli typów nie
można jednoznacznie wyznaczyć jednej sygnatury. Sygnatura reprezentująca wartość zawiera
identyfikator typu prostego jaki reprezentuje (dotyczy zwykle literałów oraz statycznych referencji
52
obiektów prostych poddanych dereferencji). Sygnatura reprezentująca strukturę zawiera zbiór
sygnatur reprezentujących pola tej struktury. Każda z tych sygnatur może również przechowywać
pewne dodatkowe informacje, np. liczności. Liczności pozwalają określić przybliżoną liczbę
rezultatów zwracanych przez operację reprezentowaną przez daną sygnaturę.
Oprócz sygnatur, analizator kontekstowy zapytań wyposażony jest również w statyczne
odpowiedniki stosu środowiskowego i stosu rezultatów zapytań. W odróżnieniu od ich
odpowiednikow z czasu wykonania, struktury te działaja na sygnaturach oraz grafie schematu bazy
danych, a nie na rezultatach zapytań.
Kontrola typówAnaliza zapytań podczas kompilacji pozwala m.in. na zrealizowanie operacji statycznej kontroli
typologicznej [9]. Na podstawie reguł wnioskowania o typach zaprojektowanych dla każdego
operatora, kompilator jest w stanie wywnioskować typ wartości zwracany przez złożone zapytanie,
analizując jego poszczególne części. Przykładowo, pojedyncza reguła dla operatora union może
wyglądąć następująco:
bag [a..b] (typ) union bag [c..d] (typ) ⇒ bag [a + c, b + d] (typ)
Reguła ta mówi, iż sumując zbiór A, w którym może być od a do b wartości oraz zbiór B, mogący
potencjalnie składać się z od c do d wartości, w wynikowym zbiorze nigdy nie znajdzie się mniej
rezultatów niż a + c, oraz więcej niż b + d.
Zbiór podobnych reguł jest częścią wewnętrznego systemu typów każdego języka programowania.
Ponieważ jednak SBQL jest językiem zapytań, dlatego jego sygnatury operacji wzbogacone są
o informacje dotyczące kolekcji: liczności i uporządkowanie.
Reguły dla operatorów arytmetycznych zwykle są bardziej restrykcyjne. Przykładowo, dla
operatora + można zaprojektować następującą regułę wnioskowania:
wartość [1..1] (integer) + wartość [1..1] (real) ⇒ wartość [1..1] (real)
Reguła przedstawiona powyżej zakłada, że operację dodawania na wartościach integer i real
można wykonać tylko wtedy, gdy lewy i prawy argument posiadają liczność [1..1] (czyli
odpowiednie wartości zawsze występuja w bazie danych), w przeciwnym razie następuje błąd
typologiczny. Można się zastanawiać, czy założenie dotyczące liczności w powyższym przykładzie
nie jest zbyt restrykcyjne i czy w związku z tym część kontroli typologicznej nie powinna zostać
53
przeniesiona do czasu wykonania. Alternatywna postać powyższej reguły może zatem również
przyjąć następującą postać:
wartość [0..*] (integer) + wartość [0..*] (real) ⇒ wartość [1..1] (real)
W tym przypadku operator + dopuszcza sytuację w której w czasie kompilacji nieznana jest
liczność jego argumentów. Właściwa kontrola następuje podczas czasu wykonania. W takiej
sytuacji jeśli lewe lub prawe podzapytanie nie zwróci pojedynczej wartości, interpreter zgłasza błąd
czasu wykonania. Takie założenie prowadzi do systemu typów, który możemy określić jako
półmocny (semi-strong).
Poniżej przedstawiono przykładowe zapytanie ilustrujące przyczynę dla której półmocny system
typów jest wygodniejszy dla programisty, niż bardzo restrykcyjny:
(employee where name = “Smith”).salary + 500
W przykładzie tym wykorzystuje się założenie, że istnieje dokładnie jeden pracownik o nazwisku
Smith. To założenie jest kontrolowane dynamicznie. Jeżeli nie jest spełnione, wówczas w czasie
wykonania sygnalizuje się błąd typu. W przypadku restrykcyjnego systemu typów, powyższa
konstrukcja musiałaby zostać odrzucona przez kompilator.
Optymalizacja zapytańDrugim kluczowym zastosowaniem analizy statycznej jest optymalizacja zapytań przez
przepisywanie [7]. Metoda ta polega na wykorzystaniu informacji dotyczącej wysokości stosu
środowiskowego podczas ewaluacji różnych części zapytania. Dzięki temu, podczas analizy
zapytania każdemu operatorowi niealgebraicznemu można przypisać liczbę oznaczającą numer
otwieranej przez niego sekcji, zaś każdej z nazw można przypisać wysokość stosu (liczba sekcji na
ENVS) w chwili, gdy dana nazwa jest wiązana, oraz numer sekcji, w której ta nazwa jest wiązana.
Jeśli wszystkie nazwy danego podzapytania wiązane są w sekcji innej niż ta, która na stos kładzie
aktualnie ewaluowany operator niealgebraiczny, to może ono być obliczone zanim ten operator
otworzy swoją sekcje, czyli wcześniej, niż to wynika z tekstowego umieszczenia tego podzapytania
w zapytaniu. Ma to duże znaczenie dla optymalizacji operatorów niealgebraicznych, ponieważ
pozwala uniknąć sytuacji w których dane podzapytanie liczone jest wielokrotnie, pomimo że
zawsze zwraca ten sam wynik. Przykładowo, wykorzystując tę metodę optymalizator może
przekształcić następujące zapytanie:
employee where salary = (employee where name = "Smith").salary;
54
do następującej postaci:
((employee where name = "Smith").salary groupas x).(employee where salary = x);
Dzięki temu przekształceniu podzapytanie wyznaczające zarobek Smitha wyliczane jest tylko raz,
na samym początku (później jego wynik jest wielokrotnie wykorzystywany), zamiast dla każdego
pracownika osobno.
Innym rodzajem optymalizacji, jaki może zostać zastosowany podczas analizy statycznej, jest
wykorzystanie rozdzielności operatorów. Operator niealgebraczny ∆ jest rozdzielny względem
operatora union, jeżeli dla dowolnego stanu składu bazy danych, stanu ENVS oraz zapytań q1, q2,
q3 zapytanie
(q1 union q2) ∆ q3
jest semantycznie równoważne zapytaniu:
(q1 ∆ q3) union (q2 ∆ q3)
Równoważnie, operator ∆ jest rozdzielnywzględem union, jeżeli dla dowolnego zapytania o postaci
q1 ∆ q2, gdzie q1 zwraca bag{r1, r2, ..., rk}, końcowy rezultat może być wyznaczony jako
(r1 ∆ q2) union (r2 ∆ q2) union ... union (rk ∆ q2)
Dzięki właśności rozdzielności operatorów możliwe są dodatkowe przekształcenia, np. znane
z systemów relacyjnych przesunięcie selekcji przed złączenie:
(q1 join q2) where q3 ⇒ q1 join (q2 where q3)
Oprócz przedstawionych wyżej dwóch mechanizmów, SBQL może być również optymalizowany
z użyciem takich struktur jak indeksy, czy takich metod, jak usuwanie niepotrzebnych nazw
pomocniczych, martwych podzapytań, optymalizacja kosztowa, i in. Metody te opisane są
szczegółowo w pracy [7].
Aktualizowalne perspektywyPerspektywa jest wirtualnym obrazem danych zapisanych w bazie danych. Definicja perspektywy
przechowywana jest w komputerze w postaci definicji, natomiast definiowane przez nie wirtualne
dane istnieją wyłącznie w wyobraźni użytkownika lub programisty. Użytkownik formułujący
zapytanie nie potrzebuje rozróżniać, czy ma do czynienia z zapamiętaną czy też z wirtualną daną
(przezroczystość).
55
Do ważniejszych zastosowań perspektyw należą: możliwość przystosowania zawartości bazy
danych do potrzeb użytkownika, wspomaganie mechanizmów bezpieczeństwa, ewolucji schematu,
integracji schematów i heterogenicznych zasobów, etc.
Perspektywy w SBQL są w pełni aktualizowalne [53]. Oznacza to, że obiekty wirtualne mogą być
nie tylko odczytywane, ale również tworzone, usuwane i modyfikowane. Ta cecha odróżnia
perspektywy SBQL od perspektyw występujacych w relacyjnych bazach danych, które mogą być
aktualizowane tylko wtedy, gdy spełnione zostaną pewne warunki związane z budową zapytania.
Przykład instrukcji definiującej perspektywę relacyjną, która nie może zostać poddana działaniu
operacji aktualizacyjnych:
create view aggsalv as select avg(salary) from emp
Istotą podejścia do aktualizowalnych wirtualnych perspektyw w SBQL jest mechanizm
przeciążania generycznych operatorów imperatywnych (wiązanie nazwy, dereferencja, wstawianie,
usuwanie, aktualizowanie) stosowanych do wirtualnych obiektów. Przeciążanie następuje przez
programistę, który samodzielnie definiuje operacje jakie mają zostać wykonane zamiast domyślnej
operacji imperatywnej. Jeśli system wykrywa, iż operuje na wirtualnym obiekcie, wówczas nie
wykonuje domyślnych operacji, jakie związane są z obiektami rzeczywistymi. Zamiast tego
automatycznie wywoływane są odpowiednie procedury przeciążające umieszczone wewnątrz
definicji perspektywy przez programistę.
Drugim istotnym elementem perspektyw w SBQL jest pojęcie ziarna i związany z nim identyfikator
obiektu wirtualnego. Ziarno jest to element należący do kolekcji rezultatów zapytania definiującego
perspektywę. Ziarno służy do tworzenia wirtualnych obiektów i jest częścią wirtualnego
identyfikatora. Liczba wirtualnych obiektów generowanych przez perspektywę jest równa liczbie
rezultatów zwracanych przez zapytanie wyznaczające ziarna danej perspektywy.
Ziarna zazwyczaj są binderami, ponieważ procedury przeciążające generyczne operacje
perspektywy muszą się do tych ziaren odwoływać.
Wirtualny identyfikator jest odpowiednikiem rzeczywistego identyfikatora obiektów i przyjmuje
nastepująca formę:
<jestem wirtualny,
<OID perspektywy1, ziarno1>,
<OID perspektywy2, ziarno2>,
56
...
<OID perspektywyn, ziarnon>>
Flaga jestem wirtualny pozwala odrożnić identyfikatory wirtualne od identyfikatorów
zapamiętanych obiektów. Wartości ziarno1, ziarno2, ziarnon wyznaczają w sposób jednoznaczny
obiekt wirtualny. Na podstawie wartości OID perspektywy1, OID perspektywy2, OID perspektywyn,
wiadomo z której perspektywy pochodzi wirtualny obiekt posiadający dany identyfikator. Ponieważ
perspektywy mogą być zagnieżdżone, dlatego częścią wirtualnego identyfikatora muszą być
ziarna i identyfikatory wszystkich nadperspektyw perspektywy, która generuje dany obiekt
wirtualny. Dzięki wirtualnym identyfikatorom każdy obiekt wirtualny może być jednoznacznie
identyfikowany, a w razie potrzeby system jest w stanie znaleźć perspektywę zawierającą definicję
operacji przeciążającej odpowiednią dla danego obiektu wirtualnego.
Poniższa perspektywa (nazwana PracSzef) dla wszystkich pracowników zwraca nazwisko
pracownika NazwPrac i nazwisko szefa NazwSzefa jako ciągi znaków. Podstawienie ciągu znaków
na nazwisko szefa powoduje przeniesienie pracownika do działu, którego szef ma takie nazwisko,
jak to nazwisko, które zostało użyte w podstawieniu. Nie rozpatrujemy błędnego nazwiska szefa.
Usunięcie obiektu wirtualnego powoduje usunięcie odpowiedniego rzeczywistego obiektu
pracownika.
Składnia poniższej definicji perspektywy charakterystyczna jest dla naszego prototypu.
view PracSzefDef { virtual objects PracSzef : p(ref Prac) [0..*] { return Prac as p; }
on delete { delete p; }
view NazwPracDef { virtual objects NazwPrac : np(ref string) { return p.Nazwisko as np; } on retrieve : string { return np; } }
view NazwSzefaDept { virtual objects NazwSzefa : ns(ref string) { return p.PracujeW.Dzial.Szef.Prac.Nazwisko as ns; }
on retrieve : string { return ns; }
on update NowySzef : Prac { p.PracujeW := ref Dzial where (Szef.Prac.Nazwisko) = NowySzef; } }}
57
Perspektywa zwraca tyle ziaren, ilu jest pracowników (zapytanie Prac as p). Każde ziarno ma postać
bindera p(iprac), gdzie p(iprac) jest referencją do obiektu Prac. Dla tej perspektywy jest zdefiniowana
tylko operacja usuwania, która w reakcji na usunięcie wirtualnego obiektu PracSzef kasuje
odpowiedni rzeczywisty obiekt Prac. Perspektywa ma dwie podperspektywy:
NazwPracDef i NazwSzefaDef.
Dla NazwPracDef zdefiniowano tylko dereferencję, która zwraca nazwisko pracownika jako ciąg
znaków. Podobnie dla NazwSzefaDef, ale dla tej podperspektywy zdefiniowano także operator
podstawiania. Powoduje on podstawienie na PracujeW referencji do działu posiadającego szefa
z nazwiskiem znajdującym się po prawej stronie podstawienia na nazwisko szefa.
Przykładem wykorzystania tej perspektywy może być następujące zapytanie:
(PracSzef where NazwPrac = “Kowalski”).NazwSzefa := “Nowak”;
W efekcie pracownik którego dotyczy to podstawienie zostanie przeniesiony do działu kierowanego
przez osobę o nazwisku podanym po prawej stronie operatora podstawiania.
PodsumowanieW rozdziale skrótowo omówiliśmy podejście stosowe do języków zapytań oraz prototypowy język
zapytań SBQL. Przedstawiliśmy podstawowe mechanizmy tego podejścia: stos środowiskowy
(ENVS), stos rezultatów zapytań (QRES), operatory niealgebraiczne, bindery. Wspomnieliśmy
również o podstawowych metodach optymalizacyjnych, statycznej kontroli zapytań,
aktualizowalnych perspektywach.
58
Rozdział 4:Koncepcja architektury narzędzia RAD nowej generacji
WprowadzenieJak wspomnieliśmy w pierwszym rozdziale niniejszej pracy, walka ze złożonością oprogramowania
wymaga przede wszystkim sięgnięcia do źródeł tej złożoności, czyli sposobu postrzegania
problemów przez człowieka oraz implementacji tych problemów za pomocą maszyny. Im mniejsze
są różnice, tym prostsze jest tworzenie systemów informatycznych oraz tym mniejszy jest koszt ich
wytworzenia i pielęgnacji. Celem technik wspomagających tworzenie aplikacji powinno być zatem
zniwelowanie tych różnic poprzez zwiększenie stopnia abstrakcji na jakim pracują programiści.
Takie podejście przyświecało chociażby twórcom asemblera, języków programowania wysokiego
poziomu, pierwszych systemów zarządzania bazami danych, middleware, jak również obiektowych
metod wytwarzania oprogramowania.
Architektura narzędzia (stworzonego w ramach projektu Odra - Object Database for Rapid
Application development) prezentowanego w niniejszej pracy opiera się na następujących
założeniach:
1. Tradycyjne języki programowania nie spełniają dobrze swojej roli jako narzędzia
programowania baz danych. Podstawowym ograniczeniem tych języków jest brak pojęcia
59
kolekcji jako rodzaju rezultatu wyrażenia. Konsekwencją jest brak możliwości wprowadzenia
do języka programowania operacji makroskopowych, niezbędnych do przetwarzania dużych
zbiorów danych. Potrzebna jest zatem nowa klasa języków programowania wyposażonych w tę
własność oraz pozbawionych w ten sposób negatywnego efektu niezgodności impedancji.
Zintegrowanie konstrukcji deklaratywnych z konstrukcjami imperatywnymi w ramach tego
samego języka pozwala również na zapewnienie mechanizmu statycznej kontroli typologicznej.
Cecha ta jest krytycznym elementem niemal każdego liczącego się języka programowania i jest
praktycznie nieznana w istniejących językach zapytań ze względu na ich ograniczenia
w integracji z językami programowania. Podejście stosowe do języków zapytań idealnie
wpisuje się w nasze wymagania.
2. Pożądaną cechą języka programowania aplikacji baz danych jest ortogonalna trwałość, czyli
umożliwienie operowania na danych trwałych i nietrwałych za pomocą dokładnie tych samych
środków językowych. Ortogonalna trwałość przydatna jest nie tylko po stronie serwera bazy
danych, ale również po stronie klienta.
Język z ortogonalną trwałością jest mało użyteczny jeśli w tym samym czasie tylko jedna sesja
może korzystać z przechowywanych danych. Implikuje to konieczność wprowadzenia
mechanizmów współbieżności znanych z baz danych, jak również modułu odpowiedzialnego za
przetwarzanie transakcji. Do poprawnego funkcjonowania operacji na danych masowych
niezbędny jest natomiast zestaw silnych metod optymalizacji zapytań, m.in. opartych na
indeksach. Techniki optymalizacyjne języków zapytań zazwyczaj realizowane są w czasie
wykonania, różnią się więc znacząco od typowych języków programowania (optymalizowanych
w czasie kompilacji).
Wymagania te utwierdzają nas w przekonaniu o tym, że środowiskiem uruchomieniowym
(odpowiednikiem wirtualnej maszyny Java) języka programowania o przedstawionych wyżej
cechach powinien być pełnoprawny system zarządzania obiektową bazą danych. Takie
założenie pozwala nie tylko zapewnić mechanizmy niezbędne do funkcjonowania trwałości,
ale również na wprowadzenie do języka programowania koncepcji typowo bazodanowych,
jak perspektywy czy wyzwalacze. Konstrukcje takie praktycznie nie są znane w istniejących
językach programowania. Otwierają również zupełnie nowe możliwości w dziedzinie
programowania aplikacji baz danych.
60
3. W językach programowania ogólnego przeznaczenia komunikacja rozproszona realizowana jest
na bardzo niskim poziomie, poprzez API do jakiegoś middleware. Czasem (np. w przypadku
rozwiązań zgodnych ze standardem CORBA) programowanie aplikacji rozproszonych bywa
zadaniem niezwykle niewygodnym, a stworzenie stabilnie i wydajnie działającego
oprogramowania bardzo utrudnione. Bazy danych oferują tutaj znacznie bardziej
wysokopoziomowe abstrakcje, niż tylko API. Przykładowym mechanizmem komunikacyjnym
dostarczanym przez te systemy są połączenia bazodanowe (database links). Są to obiekty bazy
danych reprezentujące połączenia ze zdalnymi bazami danych. Dzięki nim wszystkie zasoby
systemu rozproszonego widziane są tak, jak gdyby przechowywane były w jednym
repozytorium. Poprzez poprzedzenie nazwy zdalnego obiektu za pomocą nazwy połączenia,
programista jest w stanie operować na zdalnych zasobach (danych, procedurach, itp.) w sposób
całkowicie przezroczysty, nawet jeśli w zapytaniu występują odwołania do kilku odrębnych
źródeł danych. Oczywiście takie podejście wymaga mechanizmu optymalizacyjnego
minimalizującego ilość danych przesyłanych przez sieć. Optymalizator taki musi być zdolny do
rozbicia głównego zapytania na podzapytania, które następnie mogą zostać rozesłane do
poszczególnych serwerów, a następnie do odpowiedniego scalenia przychodzących rezultatów.
Uważamy że dzięki automatycznej optymalizacji operacji działających na rozproszonych
danych masowych zapewniona może być znacznie większa wydajność takich operacji, niż
gdyby były oprogramowywane za pomocą tradycyjnych metod dostępnych dla tradycyjnych
middleware.
4. W celu integracji ze spadkowymi źródłami danych lub aplikacjami, system może zostać
wyposażony w szereg dodatków, takich jak zestaw filtrów, osłon oraz bram. Filtry (np. filtr dla
XML) odpowiedzialne są za importowanie danych do bazy danych Odra. Osłony (np. osłona dla
systemu Oracle) udostępniają mechanizmy pozwalające na korzystanie z zewnętrznych źródeł
danych bez importowania ich do bazy danych Odra. Bramy (np. brama dla WebServices,
sterownik JDBC) umożliwiają korzystanie z bazy danych Odra z poziomu innej technologii/
języka programowania. Oprócz tego, dla systemu Odra przygotowywany jest również
niskopoziomowy mechanizm komunikacyjny umożliwiający wywoływanie dowolnego kodu
języka Java (pozwala to m.in. na wprowadzenie do systemu możliwości korzystania z GUI).
Elementy te jednak przygotowywane są w ramach odrębnych podprojektów projektu Odra i nie
są przedmiotem opisu w niniejszej pracy. Zadaniem rozprawy jest natomiast udokumentowanie
61
decyzji podjętych podczas projektowania architektury oraz implementacji jądra takiego
narzędzia.
Scenariusze zastosowaniaSystem Odra zaprojektowany jest w taki sposób, aby pełnić mógł wiele różnych ról. W najprostszej
sytuacji system oraz zintegrowany z nim język programowania/zapytań może być odpowiednikiem
popularnych języków skryptowych (np. Python, Ruby). Korzystając z podstawowych usług
(trwałość, optymalizacja, język zapytań, transakcje, itp.) zbudować jednak można za jego pomocą
znacznie bardziej rozbudowane narzędzia. Przykładowo, poniższy rysunek reprezentuje
architekturę narzędzia integracyjnego, jakie budowane jest na bazie systemu Odra na potrzeby
europejskiego projektu eGovBus.
Communication Bus
Contributory View Contributory View Contributory View
Odra
DatabaseOdra
Database
Odra
Database
Odra
Database
Integration View
Odra
Database
Odra
Database
Oracle
Wrapper
CORBA
Wrapper
Web Services
Wrapper
SBQL
Application
JMS
Wrapper
Relational
Database
CORBA
Application
Java
Application .NET Application
Contributory View
Odra
Database
Odra
Database
XML
Database
SBQL Client
Application
CORBA Client
Application
.NET Client
Application
Tamino
Wrapper
Web Services
GatewayCORBA Gateway
Java Client
Application
JDBC Gateway
Rys. 1: Docelowa architektura wirtualnego repozytorium opartego o system Odra
Systemy baz danych oparte na Odra mogą pracować w architekturze scentralizowanej,
jak i rozproszonej. W tym drugim przypadku nie zakładamy odgórnie jakiejś konkretnej
konfiguracji (np. architektura trójwarstwa, architektura zorientowana na usługi, itd.). System Odra
jest raczej narzędziem dostarczającym mechanizmów umożliwiających zbudowanie takich
konfiguracji. Ze względu na wysokopoziomowe usługi dostarczane przez system (takie jak np.
62
język programowania/zapytań, trwałość, transakcje, bezpieczeństwo, komunikacja rozproszona,
itd.) i w dużym stopniu deklaratywny charakter programowania, użytkownik może użyć systemu
Odra do wyrażenia wielu różnych konfiguracji systemów baz danych. Poniżej zaprezentujemy kilka
przykładowych, popularnych sytuacji jakie można zrealizować na bazie systemu Odra.
System samodzielny (scentralizowany)
Najprostsza sytuacja zachodzi wówczas, gdy system Odra pełni rolę scentralizowanego środowiska
uruchomieniowego programów SBQL. Jeśli budowana jest aplikacja biznesowa, realizująca
złożone operacje na danych masowych, wówczas SBQL mógłby potencjalnie zastąpić tradycyjne
języki programowania służące do tego celu. Zintegrowanie języka programowania z konstrukcjami
języka zapytań oraz inne mechanizmy znane z baz danych pozwalają znacząco przyspieszyć
programowanie aplikacji baz danych w stosunku do języków programowania ogólnego
przeznaczenia, czy nawet popularnych języków skryptowych.
jOdraAplikacja
SBQLProgram
SBQL
AplikacjaJava
CLI
Filtr XML
Dokument XML
Rys. 2: Architektura systemu scentralizowanego
Rysunek przedstawia przykładową konfigurację systemu scentralizowanego. System Odra pracuje
w tle, zarządzając danymi przechowywanymi na dysku i pamięci operacyjnej. Z systemu korzystają
dwie aplikacje wyposażone w GUI. Pierwsza z nich jest napisana w Javie i łączy się bazą danych w
tradycyjny sposób, za pomocą JDBC. Druga z nich napisana jest w SBQL, jest przechowywana
oraz uruchamiana przez bazę danych.
Bazą danych można administrować za pomocą narzędzia linii poleceń o nazwie CLI (Command
Line Interface). System Odra posiada zestaw “filtrów”, których zadaniem jest importowanie do
bazy Odra danych pochodzących z różnych, heterogenicznych źródeł. Przykładem takiego filtru jest
filtr dla XML, który dzięki zdolności do importowania dokumentów XML pozwala na
manipulowaniu ich zawartością za pomocą SBQL (zamiast np. XQuery). Ponieważ model danych
63
języka SBQL wspiera obsługę danych półstrukturalnych, możliwe jest wyrażenie w nim dowolnego
dokumentu XML.
Architektura klient-serwer
Odra może być również wykorzystana do budowy systemów zgodnych z architekturą klient-serwer.
Ponieważ w systemie Odra złożoność procesów komunikacyjnych jest przezroczysta dla
programisty, wywołanie zdalnej procedury wiąże się często z wpisaniem jednej linii kodu.
jOdra jOdra
jOdra
Program SBQL
Program SBQL
SerwerWWW
AplikacjaSBQL
Program SBQL
Program SBQL
AplikacjaC#
SOAP
IDE
Przegl!darka
Brama WS
Rys. 3: Architektura systemu zbudowanego zgodnie z 3-warstwową architekturą klient-serwer
Powyższy rysunek przedstawia przykładową konfigurację systemu baz danych zbudowanego
w oparciu o trójwarstwową architekturę klient-serwer. Instalacja systemu Odra umieszczona
w prawym górnym rogu pełni rolę serwera bazy danych, instalacja z lewego górnego rogu
koncentruje się na realizacji logiki biznesowej (serwer aplikacyjny), komputer w prawym dolnym
rogu odpowiedzialna jest za warstwę prezentacji (cienki klient). Instalacja w lewym dolnym rogu
reprezentuje maszynę wykorzystywaną przez programistę pracującego nad aplikacją SBQL
działającą jako gruby klient. Cały kod aplikacji, łącznie z GUI może być napisany w SBQL.
Z językiem zintegrowane są mechanizmy języka zapytań oraz komunikacji rozproszonej, znacznie
zmniejszając (w porównianiu do tradycyjnych języków programowania) rozmiar kodu potrzebnego
do zaimplementowania w celu zapewnienia komunikacji. Oprócz tego na rysunku zaznaczono
również aplikację C# łącząca się z serwerem aplikacji opartym na Odra poprzez bramę dla
64
WebServices. Brama taka udostępnia wybrane procedury bazodanowe jako usługi sieciowe,
umożliwiając wołanie ich za pomocą protokołu SOAP.
Poniżej przedstawiamy przykładowy kod źródłowy w języku SBQL realizujący prostą funkcję
biznesową w trójwarstwowej architekturze klient-serwer.
module client {dblink aps appuser/[email protected]
main() {aps.fire_employee(“Kowalski”);
}}
module appserver {dblink dbs dbuser/[email protected]
fire(name : string) {delete dbs.emp where lname = name;
}}
module dbserver {emp [0..*] : record { fname : string; lname : string; salary : integer; }
}
Rys. 4: Przykładowa aplikacja SBQL w architekturze klient-serwer
Federacyjna baza danych
Podstawową zaletą federacyjnych baz danych jest zapewnienie wirtualnego widoku na całość
rozproszonych danych, przy zachowaniu autonomii poszczególnych źródeł danych kontrybuujących
do federacji. W federacyjnych bazach danych opartych na systemach relacyjnych widok taki
implementuje się za pomocą mechanizmu perspektyw. Niestety takie rozwiązanie nie jest w pełni
przezroczyste ze względu na trudności aktualizacji zintegrowanych w ten sposób danych z poziomu
globalnego klienta federacyjnej bazy danych.
Ze względu na zastosowanie mechanizmu aktualizowalnych perspektyw stworzonego w SBA,
system Odra pozbawiony jest tej wady. Dzięki wirtualnym referencjom zwracanym przez takie
perspektywy oraz możliwości przeciążania działających na nich generycznych operacji, możliwa
jest aktualizacja dowolnych danych lokalnych.
Poniższy rysunek przedstawia przykładową sytuację, w której zastosowano mechanizm
aktualizowalnych perspektyw SBA. Centralną rolę odgrywa tutaj tzw. serwer integracyjny. Rolą
65
serwera integracyjnego jest zapewnienie wirtualnej, globalnej przestrzeni rozproszonej bazy
danych, z której korzystają klienci.
jOdra jOdra
jOdraPerspektywa integracyjna
jOdraAplikacja
SBQL
Perspektywa kontrybucyjna
Perspektywa kontrybucyjna
Oracle
Oracle Wrapper
SQLServer
SQLServer Wrapper
Rys. 5: Architektura federacyjnej bazy danych opartej na aktualizowalnych perspektywach
Najważniejszym elementem serwera integracyjnego jest tzw. perspektywa integracyjna [14],
definiowana automatycznie lub przez programistę dokonującego integracji zasobów. Podstawowym
elementem takiej perspektywy jest wyrażenie będące zapytaniem do integrowanych serwerów.
Zapytanie to może mieć dowolną formę, ale zazywczaj zapewne będzie to zapytanie składające się
kilku podzapytań połączonych za pomocą operatora union (fragmentacja horyzontalna). Oprócz
tego, twórca perspektywy integracyjnej definiuje jedną lub kilka operacji generycznych: virtual
objects, on insert, on delete, on update, on retrieve. Operacje te dokładnie określają czynności,
jakie powinny być wykonane w przypadku wiązania nazwy pod którą dostępne są wirtualne dane
zwracane przez perspektywę, gdy do wnętrza wirtualnego obiektu o określonym globalnym
identyfikatorze wstawiany jest inny obiekt, gdy wirtualny obiekt jest usuwany z bazy danych,
aktualizowany, lub poddawany dereferencji. Zauważmy, iż ponieważ operacje virtual
objectsvirtual objects, on insert, on delete, on update i on retrieve implementowane są jako
procedury, istnieje możliwość zaimplementowania w ich wnętrzach operacji dowolnie złożonych.
66
Przykładowo, można w ten sposób zaprogramować replikację danych, logowanie operacji dla celów
późniejszej analizy związanej z bezpieczeństwem, zabronić korzystania z całości lub części
sfederowanej bazy danych w określonych godzinach, itp.
Perspektywa integracyjna nakłada na integrowane zasoby konieczność dostosowania ich do
określonego schematu, tzw. schematu integracyjnego. Jeśli schemat lokalny integrowanych
zasobów różni się od schematu integracyjnego, wówczas po stronie integrowanego zasobu
potrzebna jest perspektywa pełniąca rolę mediatora [38, 42]. Dzięki mechanizmom wbudowanym
w aktualizowalne perspektywy SBA, perspektywa taka może nie tylko przekształcać jeden schemat
na inny, ale również przekształcać dane (np. zarobki z jednej waluty na drugą).
Tworzenie aplikacjiSystem Odra dostarcza własnego języka zapytań opartego na SBQL. Służy on jednak nie tylko do
zadawania zapytań i modyfikowania bazy danych, ale również do tworzenia kompletnych aplikacji
baz danych. Mogą to być zarówno aplikacje pracujące po stronie serwera bazy danych/aplikacji
(a’la PL/SQL), jak również aplikacje klienckie (także takie, które są zaopatrzone są w GUI).
Aczkolwiek SBQL świetnie nadaje się do programowania logiki biznesowej, w wielu sytuacjach
dostęp do bazy danych Odra może być wymagany z poziomu tradycyjnego języka programowania
(np. Java, albo C#). W odpowiedzi na takie zapotrzebowanie, przewidziana została możliwość
dostępu do bazy danych za pomocą dobrze znanych interfejsów bazodanowych, takich jak JDBC,
czy ADO. W sytuacji takiej SBQL może być wykorzystywany jako język zapytań oraz język
programowania do tworzenia oprogramowania pracującego po stronie serwera bazy danych (a’la
PL/SQL).
W systemach relacyjnych praktycznie jedyną jednostką organizacyjną danych jest schemat
użytkownika, a wszystkie tabele istnieją w jednolitej, ogólnie dostępnej przestrzeni. System Oracle
posiada oprócz tego możliwość grupowania programów napisanych w PL/SQL w jednolite bryły
zwane pakietami. Charakterystyczną cechą języków programowania jest tymczasem
przechowywanie zarówno deklaracji zmiennych, jak i kodu innych abstrakcji programistycznych
w ramach tzw. modułów [66].
Podstawową jednostką organizacyjną bazy danych Odra jest również moduł. Podobnie jak
w nowoczesnych obiektowych językach programowania, moduł jest samodzielnym komponentem
systemu, grupującym zbiór obiektów bazy danych i posiadającym dobrze zdefiniowany interfejs.
67
Moduły wspomagają enkapsulację oraz ponowne użycie. Pozwalają także zorganizować bazę
danych w niezależne komponenty, nad którymi osobno pracować mogą różni programiści.
Podstawową różnicą w porównaniu z modułami znanymi z języków programowania jest to,
iż moduły w językach programowania mają charakter wirtualny i istnieją jedynie podczas
kompilacji (są bytami drugiej kategorii programistycznej). W systemie Odra tymczasem, podczas
czasu wykonania zarówno same moduły, jak i ich zawartość przyjmują postać fizyczną (są bytami
pierwszej kategorii programistycznej). Zawartością modułów mogą być zarówno procedury, klasy,
perspektywy, indeksy, ale również same dane zdefiniowane przez użytkownika. Obiekty te również
są bytami pierwszej kategorii programistycznej.
Kod źródłowy aplikacji napisanych w SBQL przechowywany jest w plikach systemu operacyjnego
po stronie klienta, gdzie może być zarządzany przez pewne środowisko programistyczne.
Po zmodyfikowaniu kodu źródłowego modułu przez programistę zainicjowaniu przez niego
kompilacji, środowisko takie wysyła kod źródłowy modułu do serwera bazy danych. Serwer
porównuje przesłane definicje obiektów pod względem ich zgodności ze skompilowanymi
wcześniej strukturami i nanosi odpowiednie poprawki na strukturę bazy danych i metabazy. Istnieje
również możliwość pracowania bezpośrednio na obiektach bazy danych (np. procedurach)
w sposób podobny jak w bazach danych - za pomocą narzędzia linii poleceń i odpowiednich
instrukcji DDL (np. dla procedur - instrukcji create procedure, delete procedure, itp.). Dowolna
zmiana struktury modułu powoduje również konieczność automatycznego przekompilowania
modułów zależnych od takiego modułu (t.j. importujących go).
Połączenia bazodanowe i komunikacja rozproszonaSystem Odra umożliwia przezroczystą integrację rozproszonych zasobów, zarówno danych,
jak i “usług”. Podstawowym mechanizmem zapewniającym komunikację rozproszoną jest obiekt
połączenia bazodanowego. Najważniejsze informacje jakie przechowuje taki obiekt to: nazwa hosta
docelowego, port na którym nasłuchuje proces LSNR docelowej bazy danych, nazwa modułu
docelowego oraz hasło użytkownika (nazwa użytkownika jest zawsze pierwszym elementem
globalnej nazwy modułu).
Jeśli nazwa obiektu połączenia bazodanowego jest częścią zapytania, wówczas podzapytania
wykonywane w środowisku tego obiektu w rzeczywistości wykonują się po stronie serwera na
który wskazuje dane połączenie bazodanowe. Przykładowo, jeśli użytkownik utworzy połączenie
68
bazodanowe o nazwie link1 wskazujące na serwer odra1.pjwstk.edu.pl oraz połączenie o nazwie
link2 wskazujące na serwer odra2.pjwstk.edu.pl, wówczas następujące zapytanie:
link1.do_something(“par1”; “par2”; 999);
spowoduje zdalne wywołanie procedury na serwerze odra1.pjwstk.edu.pl.
do_something(”par1”; “par2”; 999)
wynik
Rys. 6: Wywołanie zdalnej procedury
Bardziej skomplikowane przypadki mają miejsce wówczas, gdy odwołania do zdalnych serwerów
są częścią bardziej skomplikowanego zapytania, np.:
print ((link1.emp union link2.emp) where ename = “Smith”).salary;
deref (emp where ename = “Smith”).salary
deref (emp where ename = “Smith”).salarywynik2
wynik1
wynik1unionwynik2
Rys. 7: Dekompozycja zapytań w środowisku rozproszonym
W takiej sytuacji zapytanie musi zostać zdekomponowane na podzapytania zgodnie z pewną regułą
dekompozycji. Reguła ta może mówić np., że zapytanie emp.name powinno zostać wysłane na
serwer odra1.pjwstk.edu.pl, a zapytanie dept.name powinno trafić na serwer odra2.pjwstk.edu.pl.
Rezultaty tych podzapytań mogą zostać następnie lokalnie zsumowane oraz wyświetlone na
ekranie.
PodsumowanieW rozdziale przedstawiliśmy naszą koncepcję narzędzia RAD wspomagającego tworzenie aplikacji
baz danych. Wspomnieliśmy, że narzędzie to rozpatrywać można na kilku różnych płaszczyznach:
obiektowego języka programowania zintegrowanego z językiem zapytań, systemu zarządzania
obiektową bazą danych, middleware opartym na koncepcjach znanych z federacyjnych baz danych.
Przedstawiliśmy również kilka przykładowych scenariuszy, w jakich takie narzędzie mogłoby
zostać zastosowane.
69
Rozdział 5:Język SBQL w systemie Odra
WprowadzenieJęzyk SBQL stanowi jądro, wokół którego zbudowany jest system Odra. Podobnie jak PL/SQL
systemu Oracle, SBQL jest językiem wewnętrznym systemu zarządzania bazą danych w Odra.
Na tym jednak podobieństwa się kończą. SBQL jest obiektowym językiem programowania
z mocno zintegrowanym językiem zapytań. Zapytania przyjmują w nim formę wyrażeń,
a powiązanie pomiędzy konstrukcjami imperatywnymi i deklaratywnymi można określić jako
bezszwowe. W niniejszym rozdziale przedstawimy nieformalny opis składni i semantyki tego
języka. Nie opisujemy kilku bardziej zaawansowanych konstrukcji (np. klas), ponieważ ich
realizacja była przedmiotem osobnego podprojektu.
ModułyPodstawową jednostką organizacyjną programów SBQL jest moduł. Podobnie jak w nowoczesnych
obiektowych językach programowania, moduł jest samodzielnym komponentem grupującym zbiór
bytów programistycznych i posiadającym dobrze zdefiniowany interfejs. Moduły wspomagają
enkapsulację oraz ponowne użycie, pozwalają zorganizować aplikację w niezależne komponenty,
nad którymi osobno pracować mogą różni programiści.
Podstawową cechą odróżniającą moduły systemu Odra od modułów typowych języków
programowania jest to, iż ich skompilowana reprezentacja oprócz kodu aplikacji przechowuje
również obiekty bazy danych: wystąpienia zmiennych, indeksy, perspektywy, etc. Każda
71
modyfikacja wprowadzona w module z ma charakter trwały. Istnieje możliwość zarówno tworzenia
i usuwania wystąpień zmiennych, jak i np. dodawania nowych procedur. W naszym prototypie nie
obsługujemy nietrwałych ani sesyjnych zmiennych globalnych, aczkolwiek wydaje się że
wprowadzenie ich nie powinno stanowić wielkiego problemu.
Poniżej przedstawiamy przykładowy kod źródłowy modułu wg składni jaką przyjęliśmy:
module test { import michal.firma;
prac : record { imie : string; nazwisko : string; pensja : integer; stanowisko : string; } [0..*]
policz_pracownikow() : integer { return count prac; }}
Moduł taki po przesłaniu do bazy danych jest kompilowany i dekomponowany na zbiór obiektów
bazy danych zgodnie ze strukturą podaną w osobnym rozdziale. Kod źródłowy modułu działa
zatem jak pewien rodzaj skryptu, którego zadaniem jest utworzenie obiektów bazodanowych.
Uproszczona gramatyka:
mod_decl ::= module name { mod_body_opt }
mod_body_opt ::= | mod_body
mod_body ::= import_list modfield_list | import_list | modfield_list
import_list ::= import_element | import_list import_element
import_element ::=
import cmpname ;
modfield_list ::= var_decl | proc_decl | typedef_decl | view_decl | link_decl
72
| modfield_list var_decl | modfield_list proc_decl | modfield_list typedef_decl | modfield_list view_decl | modfield_list link_decl
Typy, nazwy i zmienneW naszym języku obsługujemy następujące typy wbudowane: integer, real, string, boolean.
Oprócz wbudowanych typów prostych, wprowadziliśmy również jeden typ złożony - record.
Zmienne typu rekordowego można deklarować zgodnie ze schematem in-line lub out-of-line.
W pierwszym przypadku definicję typu podajemy w miejscu przeznaczonym na nazwę typu,
w drugim - używamy konstrukcji definiującej typ o nazwie type (działa jako makro). Przykład:
type jakis_typ is record { x : integer; y : integer; }
a : jakis_typ; // deklaracja out-of-lineb : record { x : integer; y : integer; } // deklaracja in-line
Najważniejszą różnicą pomiędzy zmiennymi w systemie Odra oraz zmiennymi w tradycyjnych
językach programowania jest pojęcie liczności. Liczność określa dopuszczalną minimalną oraz
maksymalną liczbę wystąpień danej zmiennej. Drugą z tych wartości może być *, co oznacza że nie
istnieje górna granica ilości wystąpień zmiennej. Poniżej przykładowe deklaracje obejmujące
liczności:
a : integer [0..5];b : record { x : integer; y : integer; } [2..*];
Pierwsza deklaracja zakłada, że w pewnym środowisku może wystąpić od zera do pięciu wartości
typu integer o nazwie a. Po skompilowaniu programu, rezultatem zadeklarowania zmiennej a
będzie jedynie odpowiedni wpis w metabazie. Druga deklaracja zakłada istnienie co najmniej
dwóch złożonych wartości b. Kompilacja takiej deklaracji powoduje: a) umieszczenie
odpowiedniego wpisu w metabazie, b) utworzenie w bazie danych dwóch obiektów złożonych
z podobiektami x i y, oraz zainicjalizowanych wartościami domyślnymi. Jeśli zmienna
zadeklarowana jest z licznością niedomyślną (1..1), wówczas podczas czasu wykonania istnieje
możliwość wprowadzania nowych wystąpień tej zmiennej i usuwania istniejących za pomocą
instrukcji create i delete. Instrukcje te są tak zdefiniowane, by podczas wykonania związane z nimi
operacje nie naruszały ograniczeń nakładanych przez liczności (kontrola realizowana jest
dynamicznie).
73
Specjalną formą typu jest typ referencyjny, służący do określania związków pomiędzy obiektami.
W przeciwieństwie do typowych języków programowania, w miejscu po dwukropku nie podajemy
nazwy typu, ale nazwę zmiennej na którą wskazywać ma obiekt referencyjny. Przykładowo,
następująca deklaracja:
c : ref a;
oznacza, że c jest referencją na pewne wystąpienie (obiekt) zmiennej a. Konstrukcja taka jest
bardziej wygodna w modelowaniu pojęciowym (w UML [77, 79] asocjacje prowadzą do nazw
obiektów, a nie do typów), pozwala uniknąć kilku problemów charakterystycznych dla języków
posiadających wskaźniki, jak również umożliwia zrealizowanie kilku innych konstrukcji języka.
Domyślnie obiekt c inicjalizowany jest na wartość null. Wartość null można również później
podstawić na taką zmienną. Wprowadzono także operator is null, zwracający true gdy wartość
odpowiada null, i false gdy nie odpowiada. Dereferencja null powoduje błąd czasu wykonania.
W operacjach wyszukiwawczych (nawigacja, agregacja) obiekty z wartościami null traktowane są
jak nieistniejące dane. Wyjątkiem jest operator count, który podaje liczbę wszystkich obiektów
niezależnie od tego, czy ich wartość to null czy nie.
W celu uniknięcia zjawiska wiszących referencji, wprowadzony został mechanizm tzw. referencji
zwrotnych. Referencja zwrotna jest wewnętrznym (ukrytym przed programistą) wskaźnikiem
prowadzącym od obiektu wskazywanego przez obiekt referencyjny do obiektu referencyjnego.
Dzięki temu, jeśli obiekt wskazywany jest usuwany, automatycznie kasowane są równie obiekty
referencyjne wskazujące na niego. Jeśli skasowanie obiektu spowodowałoby naruszenie minimalnej
liczności którejś ze wskazujących na niego zmiennych referencyjnych, wówczas obiekt
referencyjny nie jest usuwany, a jego wartość ustawiana jest na null.
Charakterystyczną cechą zaprojektowanego przez nas zewnętrznego systemu typów jest możliwość
deklarowania zmiennych reprezentujących wartości nazwane pomocniczą zmienną. W ten sposób
odwzorowujemy sygnatury binderowe w zewnętrznym systemie typów. Przykładowo:
zmienna1 : x(integer);zmienna2 : x(y(integer [0..*]));
Pierwsza z powyższych deklaracji reprezentuje obiekty typu integer nazwane zmienną pomocniczą
x. Związanie nazwy zmienna1 powoduje zwrócenie bindera, którego nazwą jest x, a wartością
liczba całkowita. Druga deklaracja zakłada, że zmienna2 reprezentuje kolekcje wartości integer
nazwane zmienną pomocniczą y. Każdy z takich binderów nazwany jest nazwą pomocniczą x.
74
Poniżej kilka przykładowych operacji wykonanych na tak zadeklarowanych zmiennych:
zmienna1.x := 1;zmienna2.x.(create 2 as y);zmienna2.x.(create 3 as y);count zmienna2.x.y;
Uproszczona gramatyka konstrukcji umożliwiających wprowadzanie nowych zmiennych oraz
typów:
var_decl ::= name : type_decl
optcard ::= | card
card ::= [ INTEGER_LITERAL .. INTEGER_LITERAL ] | [ INTEGER_LITERAL .. * ]
typedef_decl ::= type name is type_decl
type_decl ::= name optcard ; | unnamed_rec_decl optcard | ref name optcard ; | name ( type_decl optcard ) optcard ;
type_decl_no_semicolon ::= name optcard | unnamed_rec_decl optcard | ref cmpname optcard | name ( type_decl optcard ) optcard
unnamed_rec_decl ::= record { rec_fields_opt }
rec_fields_opt ::= | rec_fields
rec_fields ::=
var_decl | rec_fields var_decl
cmp_name ::= name | cmpname . name
name ::= NAME_LITERAL
75
Procedury
Procedury w systemie Odra mają charakter typowy dla języków programowania. Podobnie jednak
jak większość elementów definiowanych w ramach modułów, procedury są bytami pierwszej
kategorii programistycznej. Oznacza to, że w każdym momencie procedury zdefiniowane w jakimś
module mogą być usuwane, jak również istnieje możliwość dodawania nowych procedur. W takiej
sytuacji moduł w którym nastąpiła tego rodzaju zmiana oraz moduły importujące go są
automatycznie przekompilowywane (sprawdzana jest poprawność odwołań do nazw, typy użytych
zmiennych itp.).
Poniżej kod przykładowej procedury:
a : integer [0..*];b : boolean;c : integer;
proca(val : integer [0..*]; cmp : ref integer) : boolean { return for any (val as x) x > cmp;}
Słowo kluczowe ref w deklaracji parametru formalnego procedury oznacza, że argument
przekazywany jest przez referencję, a nie przez wartość.
Istnieje kilka zasadniczych własności w składni definiującej procedury, które są charakterystyczne
wyłącznie dla systemu Odra. Różnice te przedstawimy korzystając z następującego przykładu:
a : integer [0..*];
procb(x : ref integer) : z(ref a) {
return (a as tmpa where tmpa > x).tmpa groupas z;
}
W powyższym listingu zaobserwować można następujące charakterystyczne własności:
1. Rezultatem działania naszej procedury może być wartość nazywana w SBA binderem
(nazwanym rezultatem). Rezultat taki deklaruje się podając nazwę bindera, a w nawiasie
dopuszczalny rezultat.
2. W przeciwieństwie do najbardziej popularnych obecnie języków programowania,
zdecydowaliśmy się zabronić zwracania referencji do obiektów lokalnych procedury.
Referencje do obiektów można zwracać, ale tylko do obiektów globalnych. Referencji takich
nie deklaruje się podając nazwy typu, ale nazwę zmiennej. Pamięć przeznaczona na
przechowywanie obiektów lokalnych rezerwowana jest na stercie, lecz zwalniana jest
76
automatycznie po wyjściu z procedury. Między innymi dzięki tej własności uniknęliśmy
konieczności wprowadzania do systemu mechanizmu zbieracza nieużytków (garbage
collector).
3. Zarówno parametrem aktualnym, jak i rezultatem procedury może być dowolny rezultat
zapytania, w szczególności kolekcja wartości. Przykładowo, procedurę proca można wywołać
w następujący sposób: proca(bag 1, 2, 3; c).
Poniżej przedstawiamy gramatykę konstrukcji umożliwiających definiowanie procedur:
proc_decl ::= name ( proc_par_opt ) proc_type_opt { stmt_list_opt }
proc_type_opt ::= | : type_decl_no_semicolon
proc_arg_opt ::= | proc_arg_list
proc_arg_list ::= formal_par_decl | proc_par_list ; formal_par_decl
formal_par_decl ::= name : type_decl_no_semicolon
Instrukcje
Instrukcje mają podobny charakter, jak w typowych językach programowania. Poniżej ich
uproszczona gramatyka:
stmt_list_opt ::= | stmt_list
stmt_list ::= stmt | stmt_list stmt
stmt_block ::= { stmt_list_opt }
stmt ::= stmt_block | foreach_stmt | while_loop_stmt | do_while_loop_stmt | break_stmt | continue_stmt | return_stmt | update_stmt
77
| insert_stmt | create_stmt | assign_stmt | variable_decl_stmt | if_stmt ;
variabe_decl_stmt ::= var_decl ;
assign_stmt ::= expr := assign_expr ;
assign_expr ::= expr
return_stmt ::= return expr ; | return ;
break_stmt ::= break ;
continue_stmt ::= continue ;
for_stmt ::= for ( assign_stmt ; expr ; assign_stmt ) stmt
for_each_stmt ::= foreach ( expr ) stmt
while_do_loop_stmt ::= while ( expr ) do stmt
do_while_loop_stmt ::= do stmt_block while ( expr ) ;
if_stmt ::= if ( expr ) stmt | if ( expr ) stmt else stmt
Wyrażenia
Wyrażenia w naszej implementacji SBQL przyjmują w większości typową formę zaproponowaną
w SBA. Semantyka ważniejszych operatorów objaśniona została w rozdziale 5. W tym punkcie
omówimy jedynie mechanizm działania operatorów create, :< (insert) oraz update.
Operator create służy do tworzenia nowych obiektów w postaci wystąpień zmiennych. Jego
argumentem jest zapytanie zwracające nazwaną wartość (binder), a wynikiem zbiór referencji do
utworzonych obiektów. Binder musi mieć taką samą nazwę jak zmienna zadeklarowana
w środowisku, w którym wykonywana jest operacja create. Nowe wystąpienie tej zmiennej
przyjmie wartość będącą wartością bindera. W szczególności, jeśli wartością bindera jest kolekcja
wartości, wówczas utworzone zostanie kilka wystąpień zmiennej. Jeśli utworzony ma zostać obiekt
złożony, wówczas binder powinien posiadać jako wartość zagnieżdżone bindery odpowiadające
78
poszczególnym polom rekordu podanym podczas deklaracji zmiennej. Przed wykonaniem
operatora create system sprawdza czy nowe wystąpienie nie przekroczy maksymalnej liczności
obiektów.
Poniżej kilka przykładów wykorzystania operatora create:
x : integer [0..5];y : record { a : integer; b [0..*] : string; } [0..5];...create 1 as x;create 2 as x;create (1 as a, (bag “Ala ma kota”, “Kot ma Ale”) groupas b) as y;
Operator update jest podobny do :=. Jedyna różnica polega na tym, iż ma on charakter
makroskopowy. Ewaluacja operatora := zakończy się błędem czasu wykonania jeśli lewa lub prawa
strona zwróci kolekcję wartości. Rezultat zapytania podanego jako parametr dla update musi
zwrócić kolekcję dwuelementowych struktur, w których pierwsze pole oznacza l-wartość, a prawe -
r-wartość. Przykład wykorzystania operatora update podano poniżej. Zadaniem pierwszej operacji
jest zwiększenie wartości wszystkich wystąpień zmiennej x o 10, jeśli pierwotna wartość była
większa od 10. Druga operacja zwiększa wartość każdego pola a zmiennej y o 10.
update x as tmpx where tmpx > 10 join tmpx + 10;update y.(a, a + 10);
Operator :< umożliwia wstawianie dowolnych obiektów do wnętrza innych obiektów. Jego
działanie przypomina kombinację operacji create i delete. Utworzone obiekty posiadają takie
wartości, jakie posiadają obiekty zwrócone przez prawą stronę operatora. Lewa strona musi zwrócić
pojedynczą referencję, prawa strona - kolekcję referencji. Rezultatem są referencje obiektów
zwrócone przez create. Przykład wykorzystania:
(dept where name = “Sales”) :< (emp where sal < 3000);
Operacja delete (zdefiniowana jako instrukcja) realizuje funkcję odwrotną do create, czyli usuwa
wystąpienia zmiennych, których referencje zwraca zapytanie podane jako parametr. Przykład
zastosowania:
delete (x as tmpx where tmpx > 5).tmpx;delete y where a > 5;
Składnia wyrażeń podana jest poniżej:
expr ::= primary_expr | name_expr | unary_expr
79
| range_expr | sets_expr | additive_expr | multiplicative_expr | conditional_expr | logical_expr | relational_expr | equality_expr | cast_expr | non_algebraic_expr | proc_call_expr | set_constr_expr | insert_expr
primary_expr ::= STRING_LITERAL | INTEGER_LITERAL | REAL_LITERAL | BOOLEAN_LITERAL | ( expr ) | null
name_expr ::= name
range_expr ::= expr [ expr ]
unary_expr ::= - expr | + expr | ref expr | deref expr | not expr | count expr | avg expr | min expr | max expr | unique expr | sum expr | exists expr | update expr | delete expr | create expr | bag expr | expr as name | expr groupas name | expr is null
sets_expr ::= expr union expr | expr minus expr | expr intersect expr
80
| expr in expr | expr , expr
additive_expr ::= expr + expr | expr - expr
multiplicative_expr ::= expr % expr | expr * expr | expr / expr
conditional_expr ::= if ( expr ) expr | if ( expr ) expr else expr
logical_expr ::= expr or expr | expr and expr
relational_expr ::= expr < expr | expr > expr | expr <= expr | expr >= expr
equality_expr ::= expr = expr | expr <> expr
cast_expr ::= expr cast name
insert_expr ::= expr :< expr ;
non_algebraic_expr ::= expr . expr | expr where expr | expr join expr | expr orderby expr asc_desc_opt | forall ( expr ) expr | forany ( expr ) expr
asc_desc_opt ::= | asc | desc
proc_call_expr ::= name ( expr_list_opt )
expr_list_opt ::=
81
| expr_list
expr_list ::= expr | expr_list ; expr
Perspektywy
Perspektywy w naszej implementacji składają się z czterech elementów: nazwy perspektywy, listy
opcjonalnych procedur redefiniujących operacje generyczne, listy opcjonalnych podperspektyw,
listy opcjonalnych pól dostępnych wewnątrz perspektywy.
Nazwa perspektywy ma charakter administracyjny. Posługuje się nią użytkownik chcący operować
na perspektywie jako rzeczywistym obiekcie bazy danych (np. w celu usunięcia perspektywy).
Operacja virtual objects odpowiedzialna jest za przeciążenie operacji wiązania nazw. Parametrem
tej operacji jest deklaracja zmiennej pod jaką widoczne będą wirtualne obiekty zwracane przez
perspektywę. Operacja on retrieve umożliwia przeciążenie operacji dereferencji realizowanej na
pojedynczej wirtualnej referencji. Zwracana wartość musi być zgodna z typem podanym jako
parametr. Operacja on update umożliwia przeciążenie operacji aktualizacji obiektu wirtualnego
(operatory update, :=, itd.). Operacja on delete przeciąża operację usuwania wirtualnego obiektu.
Operacja on insert przeciąża wstawianie obiektu do obiektu wirtualnego.
Poniższy listing przedstawia kod przykładowej perspektywy:
view dzialy_krakow_view { virtual objects dzialy_krakow : dk(ref dzialy) [0..*] { return (dzialy where “Krakow” in lokalizacja) as dk; }
on retrieve : nazwa(string) { return dk.nazwa as nazwa; }
view nazwa_view { virtual objects nazwa : n(ref nazwa) { return dk.nazwa as n; }
on retrieve : string { return n; } }
view zatrudnia_view { virtual objects zatrudnia : z(ref zatrudnia) [0..*] {
82
return dk.zatrudnia as z; }
on insert (p : ref pracownicy) { d.(create ref p as zatrudnia); } }}
Poniżej prezentujemy gramatykę konstrukcji odpowiedzialnych za definiowanie perspektyw.
view_decl ::= view name { view_body_opt }
view_body_opt ::= | view_fld_list
view_fld_list ::= view_fld | view_fld_list view_fld
view_fld ::= on_bind | on_retrieve | on_update | on_delete | on_create | var_decl | proc_decl | typedef_decl | view_decl
on_bind ::= virtual objects name opt_card : type_decl_no_semicolon { stmt_list_opt }
on_retrieve ::= on retrieve : type_decl_no_semicolon { stmt_list_opt }
on_update ::= on update ( name : type_decl_no_semicolon ) { stmt_list_opt }
on_insert ::= on insert ( name : ref name ) { stmt_list_opt }
on_delete ::= on delete { stmt_list_opt }
Połączenia bazodanowePołączenie bazodanowe jest obiektem reprezentującym kanał komunikacyjny ze zdalnym serwerem
Odra. Każde takie połączenie zawiera następujące informacje: nazwa hosta docelowego, port na
83
którym nasłuchuje proces LSNR docelowej bazy danych, nazwa modułu docelowego oraz hasło
użytkownika (nazwa użytkownika jest zawsze pierwszym elementem globalnej nazwy modułu).
Poniżej składnia konstrukcji deklarującej połączenie bazodanowe:
link_decl ::= dblink name cmpname / name @ cmpname : INTEGER_LITERAL | dblink name name_list
Porównywanie i podstawianie strukturalneW implementacji SBQL dla systemu Odra przyjęta została strukturalna zgodność typów. Dzięki tej
decyzji programista aplikacji bazy danych jest w stanie zrealizować znacznie więcej operacji za
pomocą jednej konstrukcji.
Relacja bycia podtypem zdefiniowane jest następująco. Typ strukturalny B jest podtypem typu A
wówczas, gdy wszystkie pola typu B posiadają nazwy takie jak przynajmniej część pól typu A, oraz
typy porównywanych pól są ze sobą zgodne. Kolejność wystąpienia poszczególnych pól w obu
typach jest dowolna. Typy rekurencyjny są zabronione.
Jeśli typ B jest podtypem typu A, wówczas zmienna tego typu może znaleźć się po prawej stronie
operatora podstawiania (wliczając operator update) oraz operatora porównania. W czasie
wykonania, porównywanie dwóch wartości polega na zanalizowaniu czy poszczególne pola
zmiennej typu B odpowiadające polom struktury równe są takim samym polom w obiekcie
zmiennej typu A. Podstawianie strukturalne przebiega podobnie jak porównywanie. Poniżej prosty
przykład ilustrujący omawiane założenie:
x : record { a : integer; b : integer; c : integer; }y : record { b : integer; a : integer; }z : record { a : integer; b : integer; d : integer; }
x := y;x := 1 as a, 2 as b, 3 as c;x := z; // zabronione!y := x; // zabronione!y := 1 as a, 2 as b, 3 as d; // zabronione!
if (x = y) print “Struktury x i y sa sobie rowne”;
W ramach osobnego podprojektu system typów systemu Odra rozbudowany został o obsługę
nazwowej zgodności typów. Programista SBQL może zatem korzystać z obu tych technik.
84
DereferencjaDereferencja jest operacją polegającą na uzyskaniu wartości obiektu na podstawie jego referencji.
Dla obiektów prostych rezultatem operacji dereferencji jest po prostu wartość pobrana
z odpowiedniego obiektu bazy danych. Dla obiektu złożonego dereferencja jest strukturą binderów
odzwierciedlających poszczególne pola tego obiektu. Przykładowo, dla obiektu złożonego i0
<i0, “emp”, <i1, “name”, “Jan Kowalski”> <i2, “sal”, 8000> <i3, “works_in”, i4>>
rezultatem dereferencji jest następująca struktura:
struct { name(“Jan Kowalski”), sal(8000), works_in(i4) }
PodsumowanieW rozdziale przedstawiliśmy skrótowy, nieformalny opis składni i semantyki języka
programowania powstałego poprzez rozbudowanie języka SBQL. Język ten może pełnić funkcję
typowego języka programowania aplikacji biznesowych, jak i tradycyjnie rozumianego,
obiektowego języka zapytań. Podstawową cechą naszego rozszerzenia SBQL jest to, iż zapytania
traktowane są jako wyrażenia oraz są bezszwowo zintegrowane z pozostałymi kontrukcjami języka.
85
Rozdział 6:Optymalizacja zapytań rozproszonych
WprowadzenieZe względu na bardzo duży czas dostępu do rozproszonych zasobów, potrzebna jest specjalna
strategia ewaluacji zapytań operujących na takich danych. W tym rozdziale prezentujemy kilka
bardzo podstawowych metod optymalizacji zapytań SBQL opartych na ich dekompozycji na
podzapytania. Podzapytania te przesyłane są na serwery w postaci tekstowej, a zwracane przez nie
wyniki są następnie odpowienio lokalnie scalane. Czasami nie można w wygodny sposób rozdzielić
zapytania na podzapytania. W takich sytuacjach korzystamy z szeregu technik umożliwiających
przepisanie zapytania do równoważnej postaci, ale dającej się łatwiej zdekomponować na
podzapytania. Inna technika jaką stosujemy podobna jest do półzłączeń (semi-joins) znanych
z relacyjnych baz danych. W naszym przypadku polega ona na przesyłaniu razem z zapytaniem
głównym rezultatu jakiegoś jego podzapytania ewaluowanego na innym serwerze.
Połączenia bazodanowe Podstawowym mechanizmem komunikacyjnym wprowadzonym do systemu Odra są połączenia
bazodanowe. Połączenie bazodanowe reprezentuje kanał komunikacyjny pomiędzy dwoma
serwerami bazodanowymi oraz ukrywa przed programistą SBQL złożoność procesów
komunikacyjnych. Przykładowo, połączenie zadeklarowane poniżej
dblink testlink michal.myapp.hr/[email protected]:8888;
reprezentuje kanał komunikacyjny z serwerem odra.pjwstk.edu.pl na porcie 8888. W naszym
prototypie deklaracja taka może być umieszczona w kodzie źródłowym modułu, albo też dodawana
87
ręcznie za pomocą polecenia administracyjnego create dblink. Parametr lokalizujący docelową
bazę danych posiada również podstawowe informacje związane z bezpieczeństwem (nazwa
użytkownika, hasło), jak również globalną nazwę modułu mającego stanowić środowisko
wykonania zdalnych operacji SBQL. Następująca instrukcja
testlink.test();
spowoduje wykonanie procedury test() na serwerze odra.pjwstk.edu.pl, znajdującej się w module
michal.myapp.hr (pod warunkiem, że podano poprawne hasło użytkownika michal). Podobnie,
instrukcja
print testlink.(emp where name = “Kowalski”).salary;
spowoduje wyszukanie pensji pracownika o nazwisku Kowalski w zdalnej bazie danych.
Oprócz zwykłych połączeń bazodanowych wprowadzono również połączenia grupowe. Połączenie
grupowe reprezentuje kilka połączeń pojedynczych dostępnych pod jedną nazwą. Poniżej przykład
deklaracji takiego połączenia:
dblink systemsab using systema, systemb;
Wywołanie dowolnego zapytania w kontekście połączenia systemsab realizowane jest jako suma
rezultatów tego zapytania wykonanych osobno na serwerach systema i systemb. Przykładowo,
zapytanie systemsab.connections rozwijane jest podczas kompilacji do zapytania
systema.connections union systemb.connections
Referencje do zdalnych obiektówW związku z tym, iż niektóre zapytania do zdalnych zasobów mogą zwracać referencje, a nie
wartości proste, niezbędne stało się rozszerzenie identyfikatora obiektu (OID) o informacje
umożliwiające jednoznaczne zidentyfikowanie zdalnego obiektu. Identyfikator taki przyjmuje
postać podobną do IOR w CORBA, oraz reprezentowany jest przez czwórki o następującej postaci:
<adres serwera, port nasłuchu, nazwa schematu użytkownika, lokalny identyfikator obiektu>
Taki identyfikator jest traktowany na równi ze zwykłymi referencjami do lokalnych zmiennych.
Może znajdować się na stosach, przekazywany jako argument do procedury, przetwarzany przez
operatory makroskopowe etc. Zabraniamy jednak możliwości tworzenia obiektów referencyjnych
do zdalnych obiektów.
Ze względu na podstawowe mechanizmy bezpieczeństwa jakie zdecydowaliśmy się zapewnić,
dereferencja zdalnej referencji również wymaga specjalnego potraktowania. Realizacja takiej
88
operacji polega na sprawdzeniu, czy w schemacie użytkownika realizującego operację dereferencji
znajduje się przynajmniej jedno połączenie bazodanowe prowadzące do zdalnego serwera którego
adres przechowywany jest w referencji. Oprócz tego, porównywane są nazwy schematów. Jeśli
zidentyfikowane w ten sposób połączenie bazodanowe umożliwia kontakt ze zdalnym serwerem,
wówczas realizowana jest za jego pomocą zdalna operacja dereferencji.
Ewaluacja zapytań bez optymalizacji rozproszonej Zapytania korzystające z zasobów zdalnych mogą być realizowane z użyciem standardowych
metod implementacyjnych przyjętych dla SBQL. Warunkiem jest przedefiniowanie operacji nested
w taki sposób, aby operacja ta działając na identyfikatorze obiektu reprezentującego połączenie
bazodanowe wyznaczała nowe środowisko na podstawie środowiska zdalnego. Koncepcyjnie,
rezultatem takiej operacji dla obiektu złożonego byłby zbiór binderów z wartościami będącymi
zdalnymi referencjami do jego podobiektów. Bindery takie można umieścić na stosie
środowiskowym, a następnie przetwarzać w normalny sposób (uwzględniając oczywiście operację
dereferencji dla zdalnych obiektów).
Ze względu na znaczne opóźnienia implikowane przez operacje związane z dostępem do danych
poprzez sieć komputerową, w środowisku rozproszonym standardowy sposób ewaluacji zapytań
może nie prowadzić do rezultatu akceptowalnego wydajnościowo. Optymalizacja staje się więc
koniecznością.
Rozpatrzmy następujące zapytanie do zdalnych danych:
testlink.emp where name = "Smith"
Typowa ewaluacja tego zapytania polega na wykonaniu lewej strony operatora where, a następnie
dla każdego elementu będącego wynikiem lewej strony - wykonanie prawej strony. Dzięki
zmodyfikowaniu operacji nested w sposób opisany powyżej, na stos środowiskowy trafiają bindery
ze zdalnymi identyfikatorami obiektów emp. Wykonane zatem może być zapytanie emp, którego
rezultat odkładany jest na stosie rezultatów. Dalsza ewaluacja zapytania przebiega zgodnie
z następującym pseudokodem:
// rezultat operacji where BagResult bag = new BagResult();
// zdjecie ze stosu rezultatu zapytania testlink.emp Result res = stack.pop();
// prawe podzapytanie where
89
foreach (Result r in res) { stack.openScope(); nested(r); // *
// lewe podzapytanie operatora = stack.bind("name");
// prawe podzapytanie operatora = stack.push(new StringResult("Smith"));
// zdjecie rezultatow String s2 = ((StringResult) stack.pop()).value;
ReferenceResult r1 = (ReferenceResult) stack.pop(); String s1 = (StringObject) r1.deref(); // *
// operacja porownania if (s1.equals(s2)) bag.addResult(r); stack.closeScope(); }
qres.push(bag);
Gdyby informacje o pracownikach składowane były w zdalnej bazie danych, a przetwarzanie
zapytania realizowane byłoby po stronie klienckiej, wówczas przedstawiony wyżej algorytm
musiałby odwoływać się do zdalnego serwera w miejscach oznaczonych przez nas za pomocą *
(nie wliczając odwołania związanego z ewaluacją zapytania testlink.emp). W naszym algorytmie
posiadamy dwa takie miejsca:
1. Wyznaczenie wnętrza obiektu przechowywanego na serwerze wymaga przesłania identyfikatora
obiektu i odebrania binderów z globalnymi identyfikatorami jego podobiektów.
2. Zamiana identyfikatora obiektu przechowywanego na serwerze na jego wartość (dereferencja)
wymaga przesłania identyfikatora obiektu i odebrania jego wartości w postaci ciągu znaków.
Co gorsza, obie operacje realizowane są dla każdego pracownika osobno, dodatkowo spowalniając
nasz algorytm. Dla tysiąca pracowników, oznaczałoby to dwa tysiące odwołań do zdalnej bazy
danych. Sytuacja taka jest nieakceptowalna, stąd geneza metody optymalizacji zapytań przez
dekompozycję dużego zapytania na podzapytania, przedstawionej w kolejnych punktach tego
dokumentu. Celem tej techniki jest spowodowanie, aby zapytania trafiały do danych, a nie
odwrotnie.
90
Nasze zapytanie mogłoby zostać zrealizowane znacznie wydajniej w środowisku rozproszonym,
gdyby w całości zostało wykonane po stronie serwera. Klient powinien zatem wysłać je w formie
tekstowej na serwer, a następnie odczytać wynik. Problem polega na tym, że niektóre zapytania
mogą odwoływać się jednocześnie do kilku serwerów. Potrzebny jest zatem mechanizm
umożliwiający wyznaczenie który fragment zapytania może zostać wysłany do serwera A, który do
serwera B, który powinien zostać zrealizowany lokalnie, itp.
Dekompozycja zapytańDekompozycja zapytań polega na podzieleniu zapytania na niezależne fragmenty, które mogą
zostać wysłane do wykonania na różnych komputerach. W celu wyznaczenia tych fragmentów,
korzystamy z techniki dekorowania drzewa składniowego nazwami serwerów. Jeżeli pewne
podzapytanie odwołuje się do danych składowanych na zdalnym serwerze, wówczas węzły drzewa
składniowego reprezentujące to podzapytanie dekorowane są nazwą serwera do którego się
odwołują. Jeżeli dane poddrzewo udekorowane jest w całości taką nazwą, wówczas zapytanie
reprezentujące to poddrzewo może być wysłane na ten serwer.
Globalna rozproszona metabaza
Do statycznej analizy programów SBQL niezbędna jest struktura nazywana metabazą. Statyczna
analiza zapytań niezbędna jest do zrealizowania statycznej kontroli typów, jak i wielu rodzajów
optymalizacji. Wykorzystujemy ją również przy dekompozycji zapytań.
Globalną rozproszoną metabazą nazywać będziemy metabazę, która składa się z: 1) metabazy
bieżącego modułu, w którym ewaluowane jest zapytanie 2) metabaz modułów przez niego
importowanych, 3) metabaz zdalnych modułów (i modułów przez nie importowanych) wszystkich
połączeń bazodanowych wykorzystywanych w zapytaniu.
Przed rozpoczęciem procedury dekorowania węzłów drzewa składniowego, w zapytaniu
wyszukiwane są odwołania do połączeń bazodanowych, a do wskazywanych przez nie serwerów
wysyłane są żądania przekazania odpowiednich danych, umożliwiających skonstruowanie globalnej
metabazy. Otrzymane w ten sposób obiekty podłączane są jako podobiekty do obiektów lokalnej
metabazy reprezentujących połączenia bazodanowe.
Po skonstruowaniu globalnej rozproszonej metabazy, przechodzimy do etapu dekorowania węzłów
drzewa składniowego nazwami serwerów.
91
Dekorowanie węzłów drzewa składniowego
W celu zrealizowania opisywanego mechanizmu dekompozycji zapytań, dokonujemy
następujących modyfikacji w strukturach wykorzystywanych przez kompilator SBQL:
1. Wszystkie węzły AST posiadają pole przechowujace informacje o nazwie serwera, na którym
mają zostać zrealizowane. Pole takie może być zaimplementowane np. jako referencja do
obiektu–połączenia bazodanowego reprezentujacego zdalną bazę danych, albo jako globalna
nazwa jednoznacznie identyfikująca dane połączenie w całej bazie danych.
2. Sygnatury operacji (rezultaty operacji podczas statycznej analizy zapytań) posiadają również
jedno pole mówiące o tym, przez jaki serwer zwrócone będą reprezentowane przez nie rezulaty
zapytania.
Procedura dekorowania węzłów drzewa składniowego polega na statycznej ewaluacji zapytania
z użyciem globalnej rozproszonej metabazy. Poczynając od najprostszych elementów, dla każdego
podzapytania wyznaczane są serwery, na których te podzapytania powinny zostać wyewoluowane.
W zależności od rodzaju węzła AST, procedura jego dekorowania, oraz oznaczania sygnatury
operacji jest inna:
• Nazwy. W zależności od wartości uzyskanej poprzez związanie nazwy mogą zdarzyć się dwie
sytuacje:
• Jeśli wartością tą jest referencja, wówczas w sygnaturze oraz w AST ustawiana jest wartość
wskazująca na połączenie bazodanowe uzyskane z metabazy. Zaczynając od uzyskanej
referencji należy w tym celu przenawigować w górę drzewa obiektów metabazy, aż do
napotkania obiektu połączenia bazodanowego. Jeśli obiekt połączenia nie może zostać
znaleziony, wówczas wiadomo że nazwa reprezentuje obiekt lokalny. W takim przypadku
sygnatura oraz węzeł AST oznaczane są nazwą localhost.
• Jeśli uzyskaną wartością nie jest sygnatura referencyjna, wówczas nazwa serwera pobierana
jest z tej sygnatury. Węzeł AST dekorowany jest tą samą nazwą.
• Literały. Pole sygnatury operacji reprezentującej połączenie bazodanowe ustawiane jest na
wartość nieznane, oznaczającą że ewaluacja może nastąpić w dowolnym miejscu. Węzeł AST
dekorowany jest tą samą wartością.
92
• Operatory dwuargumentowe. Po udekorowaniu obu poddrzew danego węzła nazwami
połączeń, musi zostać podjęta decyzja na temat tego, gdzie zostanie wykonana operacja związana
z danym operatorem. Wyobrazić można sobie następujące przypadki:
• Jeżeli sygnatury obu podzapytań oznaczone są nazwą tego samego połączenia, wówczas
nazwa ta przydzielana jest również węzłowi operatora oraz sygnaturze operacji
reprezentowanej przez operator.
• Jeżeli sygnatury obu podzapytań oznaczone są innymi nazwami, wówczas węzeł operatora
oraz sygnatura wynikowa jego ewaluacji otrzymują nazwę localhost.
• Jeśli jedna z sygnatur oznaczona jest wartością nieznane, wówczas węzeł reprezentujący tę
operację oraz sygnatura reprezentująca rezultat jego ewaluacji dekorowane są nazwą
połączenia z sygnatury drugiego podzapytania. Ta sama wartość trafia do sygnatury operacji
reprezentującej operator oraz reprezentującego go węzła AST.
• Jeśli obie sygnatury oznaczone są wartością nieznane, wówczas węzeł operatora dekorowany
jest wartością nieznane. Ta sama wartość trafia do sygnatury operacji reprezentującej operator.
Specjalna sytuacja dotyczy sygnatur wariantowych. Jeśli analizowany jest operator
niealgebraiczny, a jego lewa strona zwraca wariant, wówczas drzewo skladniowe musi zostać
“rozszczepione” na kilka postaci. Każda postać takiego drzewa dotyczy jednej sytuacji określonej
w sygnaturze wariantowej. Dla każdej postaci drzewo składniowe dekorowane jest następnie
w normalny sposób, po czym poszczególne postacie porównywane są ze sobą. Jeśli dla którejś
formy AST jakiś węzeł oznaczony został inną nazwą niż pozostałe węzły, wówczas w finalnej
wersji AST węzeł ten ukolorowany jest nazwą localhost.
• Operatory jednoargumentowe. Węzeł operatora dekorowany jest tą samą nazwą, jaka została
przydzielona dla sygnatury jego argumentu. Tą samą nazwą oznaczana jest sygnatura
reprezentująca rezultat działania operatora.
Po zanalizowaniu całego AST i powrocie do korzenia, wyszukiwane są wszystkie węzły oznaczone
nazwą nieznane. Węzły takie oznaczane są nazwą localhost.
Przesyłanie podzapytań do serwerów Drzewo składniowe udekorowane nazwami serwerów może zostać podzielone na
poddrzewa i przesłane do odpowiednich serwerów celem ich zrealizowania. Idąc od korzenia,
interpreter przechodzi AST sprawdzając nazwy serwerów, jakimi udekorowane zostały
93
poszczególne węzły. Jeśli nazwą tą jest localhost, wówczas wiadomo iż operator musi zostać
wykonany lokalnie. W takim przypadku interpreter kontynuuje proces przechodzenia po drzewie
składniowym. Jeśli nazwa jest inna niż localhost, wówczas wiadomo iż całe poddrzewo może
zostać wysłane na serwer identyfikowany tą nazwą. W tej sytuacji interpreter przerywa
przechodzenie w głąb drzewa, zamienia poddrzewo na tekstowe zapytanie i wysyła je na
odpowiedni serwer.
Aby zapytanie takie miało sens dla docelowej bazy danych, tekstowa forma zapytania pozbawiona
musi być uprzednio wszelkich operacji realizowanych na połączeniach bazodanowych. Ponieważ
wszystkie operacje oprócz . (kropka) są zabronione dla obiektów reprezentujących połączenia
bazodanowe (nie mają sensu), dlatego odwołania do nich zawsze będą miały formę
nazwa_połączenia.zapytanie. Aby dostosować zapytanie do odpowiedniej postaci wystarczy
wyszukać nazwę połączenia bazodanowego, a następnie zastąpić nadrzędną operację . za pomocą
jej prawej strony.
Jako prosty przykład rozpatrzmy następujące zapytanie:
(s1.employee where name = "Kowalski").(imie + "" + nazwisko);
Jak widać na poniższym rysunku, korzeń drzewa składniowego tego zapytania udekorowany został
nazwą s1. Oznacza to, że całe zapytanie
(employee where name = "Kowalski").(imie + "" + nazwisko);
może zostać wysłane na serwer s1 i tam wykonane.
s1
+
imie " "
nazwisko
s1
.
s1
"Kowalski"
where
name
.
s1 employee
s1
s1
s1
s1
s1
=
s1
s1
+s1
s1
Rys. 8: Drzewo składniowe dla zapytania przykładowego zapytania udekorowane nazwami serwerów
Następujący przypadek jest nieco bardziej złożony, ponieważ w zapytaniu wystepują odwołania do
trzech serwerów:
(s1.employee union s2.employee) where salary > s3.sal_limit;
94
>
s3
where
localhost
employee
union
s2
.
s1 employee
s1
s1
s1
localhost
s2
. s2
sal_limit
localhost
salary .
s1
s3 s3
s3
s2
Rys. 9: Drzewo składniowe dla pierwszego wariantu (gdy salary dotyczy s1.employee)
Dodatkowo, rezultatem operatora union podczas kompilacji jest sygnatura wariantowa. Ponieważ
operator where działa na tej sygnaturze, dlatego jego prawa strona musi zostać “rozszczepiona”
w celu obsługi dwóch sytuacji: 1) gdy analizowany jest obiekt employee z serwera s1, 2) gdy
analizowany jest obiekt z serwera s1. W pierwszym przypadku węzeł nazwy salary udekorowany
zostaje nazwą s1, a w drugim - nazwą s2. Po analizie obu form uzyskanych poddrzew dochodzimy
do wniosku, iż wiązanie nazwy salary powinno zostać zrealizowane lokalnie (stąd nazwa localhost
w ostatecznej formie udekorowanego AST).
Końcowe zapytanie może zostać zrealizowane poprzez wysłanie zapytania employee na serwery
s1 i s2, lokalnym zsumowaniu obu wyników, a następnie wysłaniu zapytania sal_limit na serwer s3,
i lokalnego porównania otrzymanej wartości z pensją poszczególnych pracowników.
>
s3
where
localhost
employee
union
s2
.
s1 employee
s1
s1
s1
localhost
s2
. s2
sal_limit
localhost
salary .
s2
s3 s3
s3
s2
Rys. 10: Drzewo składniowe dla drugiego wariantu (gdy salary dotyczy s2.employee)
W dalszej części opracowania oprócz graficznych reprezentacji udekorowanego drzewa
składniowego pisać będziemy zapytania w formie tekstowej. Powyższe zapytanie zgodnie z naszą
notacją przyjmie następującą postać:
(s1s1 .s1 employees1 unionlocalhost s2s2 .s2 employees2) wherelocalhost salarylocalhost >localhost s3s3 .s3 sal_limits3;
95
>
s3
where
localhost
employee
union
s2
.
s1 employee
s1
s1
s1
localhost
s2
. s2
sal_limit
localhost
salary .
localhost
s3 s3
s3
s2
Rys. 11: Ostatecznie udekorowane drzewo składniowe przykładowego zapytania
Przepisywanie zapytańW poprzednim punkcie pokazaliśmy, że jeśli w zapytaniu występuje odniesienie tylko do jednego
serwera, wtedy może ono zostać w całości wysłane do wykonania na ten serwer. Ten scenariusz
w rzeczywistości może być nieco bardziej skomplikowany. Po pierwsze, niektóre zapytania są tak
skonstruowane, iż ich ewaluacja powoduje wielokrotne odwoływanie do zdalnych zasobów, lub też
powoduje przesłanie niepotrzebnych danych. Po drugie, zapytania mogą być tak skonstruowane, że
występujące w nich nazwy odwołujące się do zdalnych zasobów są ze soba na tyle splątane,
iż zapytanie nie może być łatwo zdekomponowane na podzapytania. Okazuje się, że niektóre
zapytania mogą zostać automatycznie przepisana w taki sposob, aby ich ewaluacja w systemie
rozproszonym była mniej kosztowna. W tym punkcie przedstawiamy kilka obserwacji związanych
z taką optymalizacją.
Przemienność
Wyrażenia składające się z ciągu podwyrażeń połączonych operatorami algebraicznymi mogą być
w prosty sposób zoptymalizowane przy użyciu własności przemienności i łączności niektórych
operatorów. Przykładowo ewaluacja poniższego zapytania:
s1.val1 + s2.val2 + s1.val3;
bez optymalizacji powoduje konieczność dwukrotnego skontaktowania się z serwerem s1. Pierwszy
raz w celu uzyskania wartości zmiennej val1, drugi raz w celu uzyskania wartości zmiennej val3.
Ilustruje to poniższy zapis, odzwierciedlający miejsce wykonania poszczególnych operatorów:
s1s1 . s1val1s1 +localhost s2s2 .s2 val2s2 +localhost s1s1 .s1 val3s1;
Prosta zamiana miejsc zmniejsza liczbę operacji na serwerze s1 do jednej:
s1s1 . s1val1s1 +s1 s1s1 .s1 val3s1 +localhost s2s2 .s2 val2s2;
96
Rozdzielność
Niektóre operatory posiadają cechę rozdzielności względem innych operatorów. Cechę tę można
wykorzystac do takiego przepisania zapytania, aby zmniejszyć ilość danych przesyłanych przez
sieć.
Operatory arytmetyczne
Najprostszym przypadkiem jest rozdzielność mnożenia względem dodawania. Dzięki tej własności
następujące zapytanie:
s.val * s1.val1 + s.val * s2.val2 + s.val * s3.val3;
może zostać przepisane do nastepujacej postaci:
s.val * (s1.val1 + s2.val2 + s3.val3);
W ten sposób, zamiast trzech odwołań do serwera s, konieczne jest tylko jedno.
Operatory sumy zbiorów i iloczynu kartezjańskiego
Dużo większe korzyści niż w poprzednich przykładach może przynieść wykorzystanie
rozdzielności względem sumy zbiorów. Nastepujące zapytanie jest charakterystyczne dla
fragmentacji poziomej rozproszonej bazy danych:
(s1.employee union s2.employee union s3.employee).lname;
Zapytanie to wymaga przesłania na stronę klienta zdalnych referencji do wszystkich pracowników,
a następnie tylko ich nazwisk. Zapytanie to można jednak przepisać do bardziej wydajnej postaci:
s1.employee.lname union s2.employee.lname union s3.employee.lname;
Forma po przekształceniu jest dużo wydajniejsze w ewaluacji, gdyż wymaga przesłania mniejszej
ilości danych (od razu przesyłane są tylko referencje do nazwisk).
Czasami przydatna jest również sytuacja odwrotna. Ilustruje to poniższe zapytanie:
s1.employee join s2.get_date()union s3.employee join s2.get_date() union s4.employee join s2.get_date();
W tym wypadku lepiej jest wyciągnąć wspólną, niezależną od join część przed nawias, by wykonać
ją tylko raz:
(s1.employee union s3.employee union s4.employee) join s2.get_date();
Generalna zasada jest następująca:
97
• (s1.q1 union s2.q2 union s3.q3 ...) ∆ q ⇒ s1.q1∆ q union s2.q2∆ q union s3.q3∆ q ...
∆ oznacza jeden z następujących operatorów: . (kropka), where, join (jeśli q jest zależne od ∆)
• s1.q1∆ s.q union s2.q2 ∆ s.q union s3.q3 ∆ s.q ... ⇒ (s1.q1 union s2.q2 union s3.q3 ...) ∆ s.q
∆ oznacza: , (przecinek) lub join (jeśli s.q jest niezależne od ∆)
• (s1.q1, s2.q2, s3.q3 ...) ∆ q ⇒ s1.q1∆ q, s2.q2∆ q, s3.q3∆ q ...
∆ oznacza jeden z następujących operatorów: . (kropka), where, join (jeśli q jest zalezne od ∆)
Operatory agregujące i kwantyfikatory
Podobna sytuacja dotyczy operatorów agregujących i kwantyfikatorów. Tuta jednak zwykle
potrzebne są dodatkowe operatory łączące komponenty zapytania. Przykładowo, zamiarem
programisty formułującego poniższe zapytanie było policzenie wszystkich pracowników:
count (s1.employee union s2.employee union s3.employee);
Zamiast jednak przesyłać z wszystkich trzech serwerów referencje do wszystkich pracowników
i dopiero lokalnie ich policzyć, można od razu przesłać sumy cząstkowe ze wszystkich serwerów.
Sumy takie mogą posłużyć do wyliczenia sumy całościowej:
count s1.employee + count s2.employee + count s3.employee;
Generalna zasada w stosunku do operatorów agregujących i kwantyfikatorów jest następująca:
• ∆ (s1.q1 union s2.q2 union s3.q3 ...) ⇒ ∆ (∆ s1.q1 union ∆ s2.q2 union ∆ s3.q3)
∆ oznacza jeden z następujących operatorów: distinct, unique, min, max
• exists(s1.q1 union s2.q2 union s3.q3 ...) ⇒ exists s1.q1 or exists s2.q2 or exists s3.q3 ...
• ∆ (s1.q1 union s2.q2 ... union sx.qx) ⇒ ∆ s1.q1 + ∆ s2.q2 ... + ∆ sx.qx
∆ oznacza jeden z następujących operatorów: count, sum.
• avg (s1.q1 union s2.q2 ... union sx.qx) ⇒ (sum s1.q1 + sum s2.q2 ... + sum sx.qx) / x
• forall (s1.q1 union s2.q2 union s3.q3 ...) q ⇒
forall (s1.q1) q and forall (s2.q2) q and forall (s3.q3) q ...
• forany (s1.q1 union s2.q2 union s3.q3 ...) q ⇒
forany (s1.q1) q or forany (s2.q2) q or forany (s3.q3)
98
Przesyłanie wyników podzapytań razem z zapytaniamiCzasami operacje odwołujące się do zasobów osobnych serwerów są ze sobą na tyle splątane, że
przekształcenia zaprezentowane dotychczas nie wystarczają. W tej sekcji prezentujemy metodę
optymalizacyjną przypiminającą technikę semi-joins znaną z systemow relacyjnych.
Wprowadzenie
Rozpatrzmy następujące zapytanie:
(s1.department join s2.employee where ename = dname).(ename, dname, location);
Przypadek ten jest bardzo nieprzyjemny z punktu widzenia optymalizacji zapytań. Drzewo
składniowe tego zapytania przedstawione na rysunku poniżej wskazuje, iż zgodnie z naszymi
dotychczasowymi strategiami optymalizacyjnymi, zapytanie to może być zrealizowane jedynie
poprzez przesłanie referencji do wszystkich departamentów i wszystkich pracowników na komputer
klienta, który realizuje operację złączenia.
Prawdopodobnie jednak tylko niewielki procent pracowników posiada nazwiska takie jak nazwy
departamentów, zatem przesyłanie informacji o wszystkich pracownikach jest całkowicie
niepotrzebne. Dodatkowo, późniejszy dostęp do poszczególnych informacji na temat pracowników
i departamentów wymagał będzie wielokrotnej komunikacji z serwerem bazy danych.
where
. .
s1 department s2 employee
join =
ename dname
,
location,
ename dname
.
s1 s1
s1
s2 s2
s2
localhost
s2 s1
localhost
localhost
s2 s1
s1
localhost
localhost
localhost
Rys. 12: Drzewo składniowe przykładowego zapytania po udekorowaniu nazwami serwerów
Możemy na szczęście zastąpić podzapytanie odwołujące się do serwera s1 lub s2 jego rezultatem
poddanym dereferencji i wysłać je na drugi serwer razem z tak spreparowanym zapytaniem. Przy
takim podejściu powstaje jednak pytanie czy lepiej jest wysłać pracowników na serwer s1, czy tez
departamenty na serwer s2? Intuicyjnie możemy stwierdzić, że liczba departamentów każdej
organizacji jest mniejsza niż liczba ich pracowników. Inne pytanie jakie może się pojawić: czy
99
należy przesyłać wszystkie wartości uzyskane z dereferencji, czy też tylko te które będą potrzebne
w dalszej części zapytania?
Nasze rozwiązanie tego problemu prowadzi do zastosowania optymalizacji kosztowej
zapytań i wykorzystania statystyk gromadzonych przez system i dotyczących obiektów bazy
danych. Przydatne mogą być przede wszystkim informacje na temat obiektów w danej kolekcji, np.
średnia wielkość wartości (np. średnia długość nazwiska), czy ilość obiektów. Statystyki mogą być
umieszczone w metabazie, zatem praktycznie nie zmienia się nic w dziedzinie dostępu do danych
potrzebnych do kompilacji. Rozszerzamy natomiast mechanizm analizy statycznej zapytań
o możliwość dekorowania węzłów drzewa składniowego o przewidywalny rozmiar rezultatu każdej
operacji. Rozmiar ten dosyć łatwo jest wyliczyć na podstawie sygnatury operacji oraz statystyk
przechowywanych w metabazie.
Można zauwazyć, iż splątanie odwołań do nazw obiektów przechowywanych na osobnych
serwerach następuje w bardzo konkretnej sytuacji. Zauważmy, że na powyższym rysunku
występują dwa rodzaje poddrzew udekorowanych tymi samymi nazwami serwerów. Pierwszym
rodzajem jest to, które wywodzi się od nazwy obiektu reprezentującego połączenie ze zdalną bazą
danych (zapytanie główne). Na rysunku reprezentuje ono zapytania s1.department i s2.employee.
Do drugiej grupy należą pozostałe poddrzewa udekorowane tymi samymi nazwami co te
poddrzewa (zapytanie zależne wobec zapytania głównego). Widać że poddrzewa te rozdzielone są
od siebie przynajmniej jednym węzłem operatora niealgebraicznego udekorowanym nazwą
localhost. Ta obserwacja pozwala nam przyjąć generalny wniosek mówiący kiedy należy
spróbować zastosować optymalizację poprzez wysyłanie na zdalne serwery zapytań razem
z wynikami niektórych ich podzapytań. Podstawowym celem tej metody będzie zbudowanie
takiego zapytania do serwera sx, aby mogło ono pobrać wszystkie dane niezbędne do ewaluacji
głównego (nieco zmodyfikowanego) zapytania, ale znajdujące się na tym serwerze sx.
Przykładowo, sięgając na serwer s1 za pomocą następującego zapytania:
(department groupas $s1_0)join $s1_0.(deref dname groupas $s1_1, dname groupas $s1_2, location groupas $s1_3);
będziemy w stanie wysłać rezultat tego zapytania (oznaczony za pomocą X) na serwer s2 razem
z następującym zapytaniem:
100
(X join employee where ename = $s1_1).(ename, $s1_2, $s1_3);
Wyznaczanie planu wykonania zapytania
Pierwszym krokiem w opisywanej metodzie optymalizacyjnej powinno być zidentyfikowanie
największych możliwych poddrzew wywodzących się bezpośrednio z węzłów identyfikujących
połączenia bazodanowe. W ten sposób (po usunięciu węzłów operatorów nawigacji na s1 i s2) dla
przedstawionego powyżej zapytania dysponujemy dwoma głównymi zapytaniami, odpowiednio:
department (które ma zostać wysłane na s1) i employee (które ma zostać wysłane na s2).
Z uzyskanymi w opisany powyżej sposób zapytaniami głównymi muszą zostać związane pozostałe
poddrzewa udekorowane nazwami s1 i s2. Wyszukujemy zatem największe możliwe poddrzewa
udekorowane tymi nazwami i otrzymujemy następujące podzapytania: dname, dname, location
(dla s1) oraz ename, ename (dla s2). Ze względu na możliwość występowania nazw pomocniczych
w zapytaniach, niezbędne jest także wyznaczenie zależności pomiędzy tymi zapytaniami. Zapytanie
X jest zależne od zapytania Y, jeśli wszystkie nazwy występujące w X wiązane są sekcjach stosu
środowiskowego otwartych przez operator niealgebraiczny działający na zapytaniu Y. Jeśli
w zapytaniu X tylko niektóre nazwy zależne są od Y, a pozostałe nazwy zależne są od innego z tych
podzapytań, wówczas zapytanie X rozdzielane jest na kilka dodatkowych podzapytań, w których
taka zależność nie występuje. Takie zapytania powiększają zbiór podzapytań zidentyfikowanych
jako zależne od zapytań odwołujących się bezpośrednio do połączeń bazodanowych.
W kolejnym kroku optymalizator powinien zbudować wszystkie możliwe plany wykonania
zapytania uwzględniające możliwość wysłania rezultatu pewnego zapytania wraz z zapytaniem
głównym. Tworzony jest zatem plan uwzględniający ewaluację całego zapytania po stronie klienta
(zgodnie z pierwotnym udekorowaniem AST), a także wszystkie możliwe plany zakładające
przesłanie wyników pewnych (lub wszystkich) podzapytań razem z zapytaniem głównym.
Zadaniem tych zapytań jest umożliwienie wyznaczenia kosztu transferu danych potrzebnych do
ewaluacji pierwotnego zapytania. Aby wyliczyć ten koszt wykorzystujemy statystyki periodycznie
zbierane przez system zarządzania bazą danych. Statystyki te umieszczane są w metabazie razem
z deklaracjami obiektów. W naszym przypadku dla każdej deklaracji potrzebujemy dwóch wartości:
ilość obiektów w bazie danych oraz średnią wielkość wszystkich obiektów.
Załóżmy, iż metabaza serwera s1 przyjmuje przedstawioną poniżej postać. W nawiasach < >
podano statystyki aktualne w pewnym momencie.
101
department [0..*] <ilosc: 10> : record { dname <ilosc: 1, wielkosc: 8B> : string; location <ilosc: 1, wielkosc: 10B> : string; address <ilosc: 1, wielkosc: 25B>: string;}
Serwer s1 przechowuje 10 obiektów department, z których każdy posiada jedno pole dname
(średnia wielkość 8 bajtów), jedno pole location (średnia wielkość 10 bajtów) oraz jedno pole
address (średnia wielkość 25 bajtów). Zakładając jako jednostkę kosztu ilość przesłanych danych
pomiędzy dwoma komputerami, koszt przesłania z serwera s1 wszystkich danych potrzebnych do
ewaluacji naszego zapytania głównego wynosi w tej sytuacji 430 bajtów.
Podobnie, przyjmijmy że metabaza serwera s2 przyjmuje następującą postać:
employee [0..*] <ilosc: 1000> : record { ename <ilosc: 1, wielkosc: 8B> : string; job <ilosc: 1, wielkosc: 10B> : string; salary <ilosc: 1, wielkosc: 4B>: integer;}
Koszt przesłania wszystkich informacji o pracownikach wynosi więc 22000 bajtów.
Dodatkowo, zakładamy że długość pojedynczej zdalnej referencji wynosi 20 bajtów.
Dysponując tymi danymi, dla analizowanego w tym punkcie zapytania, optymalizator może
wyznaczyć trzy plany wykonania:
1. Wykonanie zapytanie w pierwotnej formie, czyli bez przesyłania wyników. Oznacza to
przesłanie referencji do wszystkich obiektów employee (koszt: 1000 * 20 = 20000B)
i department (koszt: 10 * 20 = 200B).
Na podstawie sygnatury zwróconej poprzez zanalizowanie operatora join uzyskujemy
informację o tym, iż operator where operował będzie na zbiorze wielkości [0..10000].
Dla każdego elementu iloczynu kartezjanskiego zwroconego przez operator join, dwa razy musi
zostać związana nazwa ename (koszt: 2 * [0..10000] * 20 = [0..400000]B), a następnie każda
z otrzymanych w ten sposób referencji musi zostać poddana dereferencji (koszt: 2 * [0..10000]
* 8 = [0..160000]B). Oprócz tego, dwukrotnie musi zostać związana nazwa dname (koszt: 2 *
[0..10000] * 20 = [0..400000]B), oraz raz nazwa location (koszt: [0..10000] * 20 =
[0..200000]B). Referencje do obiektów location poddawane są raz dereferencji (koszt:
[0..10000] * 10 = [0..100000]B). Koszt przesłania wszystkich danych potrzebnych do ewaluacji
tego zapytania wynosi więc [0..400000 + 160000 + 400000 + 200000 + 100000]B =
[0..1260000]B. Klient musi komunikować się z serwerami 2 (wiązanie department i employee)
102
+ 5 * [0..10000] (wiązanie nazw ename, dname, ename, dname, location) + 2 * [0..10000]
(dereferencja ename i dname) = [2..70000] razy.
2. Wysłanie na serwer s2 zapytania w formie
(X join employee
where ename = $s1_1).(ename, $s1_2, $s1_3);
gdzie X oznacza rezultat wykonania na serwerze s1 zapytania (department groupas $s1_0)
join
$s1_0.(deref dname groupas $s1_1,
dname groupas $s1_2,
location groupas $s1_3);
Rezultat tego zapytania składa się z kolekcji struktur, których pierwsze pole jest referencją do
obiektu department (20B), drugie pole jest wartością dname poddana dereferencji (8B), trzecie
pole jest referencją do obiektu dname (20B), a czwarte - referencją do location (20 B). Koszt
przesłania tego rezultatu z serwera s1 na stronę klienta wynosi [0..10 * 68]B = [0..680]B. Ten
sam rezultat musi zostać następnie wysłany razem z zapytaniem
(X join employee where ename = dname).(ename, dname, location);
na serwer s2 (koszt: [0..680]B). Rezultat tego zapytania przesyłany jest następnie na stronę
klienta. Składa się on z kolekcji struktur złożonych z trzech referencji (koszt jednej: 60B).
Biorąc pod uwagę liczność wyliczoną dla sygnatury korzenia tego zapytania, optymalizator
może wyliczyć koszt przesłania tego rezultatu jako [0..10 * 0..1000 * 60]B = [0..600000]B.
Łącznie zatem koszt wykonania tego scenariusza wynosi [0..2 * 680 + 600000]B =
[0..601360]B. Biorąc jednak pod uwagę to, iż do realizacji tego scenariusza klient musi
komunikować się z serwerami tylko dwa razy, jest to scenariusz znacznie wydajniejszy niż
poprzedni.
3. Wysłanie na serwer s1 zapytania w formie
(department join X
where $s1_1 = dname).($s1_2, dname, location);
gdzie X oznacza rezultat wykonania na serwerze s1 zapytania
(employee groupas $s2_0)
join
103
$s2_0.(ename groupas $s1_1,
ename groupas $s1_2);
Rezultat tego zapytania składa się z kolekcji struktur, których pierwsze pole jest referencją do
obiektu employee (20B), drugie pole jest wartością ename poddaną dereferencji (8B), trzecie
pole jest referencją do obiektu dname (20B). Koszt przesłania tego rezultatu z serwera s1 na
stronę klienta wynosi [0..1000 * 48]B = [0..48000]B. Ten sam rezultat musi zostać następnie
wysłany razem z zapytaniem
(department join X
where ename = dname).(ename, dname, location);
na serwer s1 (koszt: [0..48000]B). Rezultat tego zapytania przesyłany jest następnie na stronę
klienta. Składa się on z kolekcji struktur złożonych z trzech referencji (koszt jednej: 60B).
Biorąc pod uwagę liczność wyliczoną dla sygnatury korzenia tego zapytania, optymalizator
może wyliczyć koszt przesłania tego rezultatu jako [0..10 * 0..1000 * 60]B = [0..600000]B.
Łącznie zatem koszt wykonania tego scenariusza wynosi [0..2 * 480000 + 600000]B =
[0..1080000]B. Biorąc jednak pod uwagę to, iż do realizacji tego scenariusza klient musi
komunikować się z serwerami tylko dwa razy, jest to scenariusz znacznie wydajniejszy niż
scenariusz pierwszy. Jest on jednak gorszy niż scenariusz drugi, dlatego nie powinien zostać
wybrany przez optymalizator.
Ewaluacja zoptymalizowanych zapytań
Po wybraniu najlepszego scenariusza wykonania zapytania, optymalizator przekształca pierwotne
drzewo składniowe do formy przystosowanej do ewaluacji. W nowym drzewie składniowym
podzapytania wywodzące się od połączania bazodanowego zastępowane są węzłami ermtsq (od
execute remote subquery). Oprócz nazwy połączenia bazodanowego jest on dodatkowo
udekorowany treścią zapytania mającego trafić na zdalny serwer. Zapytanie to zbudowane jest
w specjalny sposób z podzapytania głównego oraz z podzapytań od niego zależnych. Rezultaty
każdego podzapytania będącego elementem tego zapytania powiązane są ze sobą za pomocą
operatorów groupas oraz operatora konstrukcji struktury (,). Nazwy używane przez groupas są
kolejnymi liczbami porządkowymi rozszerzonymi o nazwę połączenia. Struktura zapytania
wygląda następująco:
(zapytanie_glowne groupas $polaczenie_0)join $polaczenie_0.(zapytanie_zalezne1 groupas $polaczenie_1, zapytanie_zalezne2 groupas $polaczenie_2, ...);
104
Dla zapytania użytego w analizowanym przykładzie, będzie to zatem
(department groupas $polaczenie_0)join $polaczenie_0.(dname groupas $polaczenie_1, dname groupas $polaczenie_2, location groupas $polaczenie_3);
w przypadku gdy założymy, że dane o departamentach powinny zostać wysłane na serwer s2 razem
z pierwotnym zapytaniem, oraz
(employee groupas $polaczenie_0)join $polaczenie_0.(ename groupas $polaczenie_1, ename groupas $polaczenie_2);
jeśli okaże się że wysłanie listy pracowników na serwer s1 będzie korzystniejszą operacją.
Drugą modyfikacją w AST jest zastąpienie wszystkich zapytań zależnych za pomocą nazw
odpowiadających kolejnym parametrom operatora groupas w opisanym wyżej schemacie.
Kolejne modyfikacje dotyczą mechanizmu ewaluacji zapytań. Do zbioru rezultatów zapytań R
wprowadzamy nowy rodzaj rezultatu, który nazwywamy rezutlatem złożonym. Jest to specjalny
rodzaj struktury, przezroczystej dla większości operacji języka zapytań. Typowe operacje działając
na rezultacie złożonym w rzeczywistości operują na pierwszym polu tej struktury. Operacja nested
jest natomiast przeciążona w taki sposób, iż działając na rezultacie złożonym, operacja nested
wywoływana jest rekurencyjnie dla wszystkich za wyjątkiem pierwszego pól reprezentującej ją
struktury. W ten sposób na stosie środowiskowym dostępne będą rezultaty zapytań do których
odwołują się nazwy zastępujące zapytania zależne.
Optymalizowanie dostępu do perspektyw Ważnym warunkiem sprawnego działania wirtualnego repozytorium w zaproponowanej przez nas
architekturze jest wydajne funkcjonowanie perspektyw integracyjnych i kontrybucyjnych. Ogólna
idea optymalizacji wykorzystania aktualizowalnych perspektyw została przedstawiona w [53].
W największym skrócie polega ona przepisaniu zapytania odwołującego się do perspekywy
w następujacy sposób:
1. Zamianie nazwy odwołującej się do obiektów udostępnianych przez perspektywę poprzez
zapytanie wyznaczające ziarna tej perspektywy. Dotyczy to tylko takich sytuacji, gdy operacja
virtual objects składa się wyłącznie z intrukcji return. W podobny sposób zamieniany jest
operator deref (wstawiany automatycznie przez kompilator w czasie kontroli statycznej)
105
poprzez zapytanie podane w on retrieve. Podobnie jak w poprzednim przypadku, jeśli
on retrieve składa się z bardziej złożonego kodu, niż instrukcja return, wówczas optymalizacja
ta nie może zostać zastosowana. Podobnie optymalizowane są instrukcje delete (operacja on
delete) oraz insert (operacja on insert).
2. Zoptymalizowaniu tak otrzymanego zapytania za pomocą technik opisanych w [7], czyli
przeniesienie selekcji przed złączenie, wyciągnięcie niezależnego podzapytania przed operator
niealgebraiczny, wyszukanie martwych podzapytań, zastosowanie indeksów, etc. Następnie
zastosowane mogą zostać techniki przedstawione w niniejszym rozdziale.
Dzięki możliwościom udostępnianym przez perspektywy SBA w dziedzinie definiowania operacji
generycznych, możliwe są również pewne operacje optymalizacyjne implementowane bezpośrednio
przez programistę. Przykładowo, następująca definicja operacji virtual objects (po wprowadzeniu
do języka operatora ping) pozwala na odwołanie się do tego serwera, który w danym momencie jest
najmniej obciążony:
...virtual objects PracSzef : p(ref Prac) [0..*] { if (ping s1 < ping s2) return s1.Prac as p; else return s2.Prac as p;}...
PodsumowanieZagadanienie optymalizacji zapytań jest kluczowym problemem przy budowie języków zapytań.
Nie inaczej jest w przypadku zapytań rozproszonych. Opóźnienia implikowane przez sieć
komputerową uniemożliwiają naiwną (bez optymalizacji) ewaluację takich zapytań.
W rozdziale przedstawiona została propozycja podejścia do optymalizacji rozproszonych zapytań
SBQL. Omówiona została technika dekorowania węzłów drzewa składniowego za pomocą nazw
serwerów, kilka reguł przekształcających zapytania do wydajniejszej w ewaluacji formy, jak
i technika optymalizacji poprzez przesyłanie na serwery częściowo wyewaluowanych zapytań.
Problem optymalizacji w takim środowisku jest olbrzymim tematem, godnym osobnej pracy
doktorskiej. Podstawowe techniki, jakie zostały opisane w niniejszym rozdziale są zaledwie
wierzchołkiem góry lodowej. Niewątpliwie potrzebne są dalsze badania w tej dziedzinie.
106
Rozdział 7:Integracja danych rozproszonych
Wirtualne repozytoriumWirtualnym repozytorium nazywamy w niniejszej pracy zbiór rozproszonych, powiązanych ze sobą
baz danych widocznych dla globalnego użytkownika jako pojedyncza, wirtualna baza danych.
Zgodnie z regułami stworzonymi dla federacyjnych baz danych, wirtualne repozytorium składa się
z szeregu autonomicznych, być może należących do odrębnych organizacji systemów.
Istnieje szereg różnorodnych sytuacji biznesowych dla których pojedyncza organizacja, lub grupa
organizacji (konsorcjum) może podjąć decyzję o stworzeniu wirtualnego repozytorium. Przykładem
może być pewna grupa przedsiębiorstw kolejowych postanawiająca stworzyć wspólny system
informacyjny dotyczący rozkładów jazdy pociągów. System taki umożliwiałby pasażerom
planowanie podróży w oparciu o globalną bazę danych wszystkich istniejących połączeń, dostępną
w jednym miejscu, niezależnie od przedsiębiorstwa obsługującego daną linię. Generalnie można
powiedzieć, że wirtualne repozytorium jest tym bardziej potrzebne, im bardziej niezbędna jest
możliwość przeszukiwania danych geograficznie rozproszonych wg niestandardowych (trudnych do
przewidzenia przez projektantów aplikacji) kryteriów.
Modelowanie wirtualnego repozytoriumStworzenie wirtualnego repozytorium musi wiązać się z uprzednim zaprojektowaniem
odpowiednich reguł dostępu do danych, obowiązujących wszystkie organizacje biorące udział
107
w konsorcjum tworzącym wirtualne repozytorium. Musi się to wiązać m.in. z decyzjami
związanymi z ustaleniami w zakresie:
• schematu danych jaki musi być przestrzegany przez systemy kontrybuujące do repozytorium,
• jednolitego formatu danych dostarczanych dla repozytorium (daty, waluty, etc),
• reguł dostępu i bezpieczeństwa do lokalnych danych.
Zadaniem każdej z organizaji członkowskiej danego konsorcjum jest zatem przede wszystkim:
• wybranie pewnego zbioru danych lokalnych jakie powinny być udostępnione globalnie,
• wirtualne dostosowanie części lub całości schematu lokalnego do postaci globalnej,
• wirtualne przekonwerowanie lokalnych danych do postaci przyjętej w całym repozytorium.
Węzły integracyjne i kontrybucyjneFizyczne przekształcenia danych niezbędne do dostosowania ich do postaci akceptowalnej
w ramach wirtualnego repozytorium zwykle nie jest możliwe, m.in. ze względu na potencjalną
utratę spójności między danymi i istniejącymi aplikacjami. Przekształcanie danych do wymaganej
postaci musi zatem być realizowane w sposób wirtualny, tzn. przezroczysty dla użytkownika
realizującego zapytanie i przy zachowaniu autonomii lokalnych systemów. Z tego powodu
w federacyjnych bazach danych dostosowywaniem formatu i schematu danych do globalnej postaci
zajmują się zwykle perspektywy.
W proponowanej przez nas architekturze mechanizm perspektywy stosowany jest po po stronie
integrowanych systemów (gdzie pełni rolę osłony/mediatora) oraz po stronie serwera
integracyjnego (gdzie pełni rolę mechanizmu unifikującego dane z integrowanych serwerów).
Zgodnie z tymi założeniami, w architekturze wirtualnej bazy danych można wydzielić dwa rodzaje
węzłów: węzły kontrybucyjne oraz węzły integracyjne.
Węzły kontrybucyjne mają za zadanie udostępnienie lokalnych danych w taki sposób, by mogły
być one wykorzystane przez węzły integracyjne. Węzły te muszą posiadać zdolność dynamicznego
odwzorowania danych lokalnych do postaci dopuszczalnej dla danych globalnych i odwrotnie.
Węzły integracyjne mają za zadanie zapewnić wirtualny, globalny obraz rozproszonej bazy danych
poprzez zebranie danych z integrowanych przez nie węzłów kontrybucyjnych, oraz udostępnienie
ich klientom.
108
Ponieważ węzły integracyjne same mogą pełnić rolę kontrybucyjną w stosunku do innych węzłów
integracyjnych, dlatego możliwe są bardzo różne opcje konfiguracyjne wirtualnej bazy danych -
gwiaździste, drzewiaste, sieciowe, i in. Struktura takiej bazy danych może mieć charakter statyczny
oraz dynamiczny. W pierwszym przypadku na etapie projektowania repozytorium dokładnie
wiadomo jakie węzły będą brały udział w federacji. W drugim przypadku brak jest takiej wiedzy -
węzły mogą się podłączać do repozytorium i odłączać od niego (sytuacja podobna do sieci P2P).
Rys. 13: Przykładowe struktury wirtualnej bazy danych
Architektura wirtualnej bazy danych do zaimplementowania funkcjonalność węzłów
kontrybucyjnych oraz węzłów integracyjnych opiera się na mechanizmie aktualizowalnych
perspektyw SBA. Do zaimplementowania węzła kontrybucyjnego używamy zatem perspektywy
(zwanej przez nas kontrybucyjną) pełniącej rolę osłony/mediatora. Najważniejszym elementem
węzła integracyjnego jest z kolei perspektywa (zwana przez nas integracyjną), której zadaniem jest
zebranie danych ze wszystkich integrowanych węzłów oraz przedstawienie ich w postaci dostępnej
dla globalnego użytkownika. Po raz kolejny pozwolimy sobie w tym miejscu wyraźnie podkreślić,
iż wykorzystanie perspektyw SBA pozwala nam zapewnić kompletną przezroczystość wszelkich
operacji realizowanych na obiektach wirutalnych. Jest to jedna z podstawowych cech
odróżniających nasze rozwiązanie od istniejących federacyjnych baz danych.
Przykładowy scenariuszW dalszej części rozdziału zaprezentujemy prosty przykład ilustrujący konstrukcję, a następnie
funkcjonowanie wirtualnego repozytorium kontrolowanego przez system Odra. W przykładzie
posługujemy się scenariuszem rozproszonej bazy danych połączeń kolejowych. Baza danych
budowana jest przez konsorcjum europejskich przedsiębiorstw kolejowych. System ma umożliwić
łatwe planowanie podróży klientom przemieszczającym się pomiędzy krajami Unii Europejskiej.
109
Dla uproszczenia, załóżmy że integrowane mają być systemy dwóch przedsiębiorstw kolejowych:
system A oraz system B.
Poniższy rysunek przedstawia strukturę modułów stworzonej przez nas przykładowej aplikacji.
schedules
pkp.app.public db.apps.pub
testapp
db.schedule.timetablepkp.app.connections
Warstwa kliencka
Warstwa serwera integracyjnego
Warstwa serwerów kontrybucyjnych
System AW systemie A dane dotyczące połączeń przechowywane są w module Connections, którego
struktura zgodna jest z przedstawionym niżej schematem.
Connections
from : stringto : stringduration : integerprice : integer
Connection
d : integerm : integer
Date
h : integerm : integer
Time
depart_date
depart_time
Moduł ten utworzony został poprzez wprowadzenie do bazy danych kodu źródłowego definicji
modułu pkp.app.connections, którego listing przedstawiono w dodatku B.
110
System BW systemie B dane dotyczące połączeń przechowywane są w module TimeTable, a jego struktura
zgodna jest z przedstawionym niżej schematem.
TimeTable
name : stringcountry : string
Location
dep_hour : integerdep_minutes : integerduration : integerprice : integer
Link
from
to
Moduł ten utworzony został poprzez wprowadzenie do bazy danych kodu źródłowego definicji
modułu db.schedule.TimeTable. Jego listing również przedstawiono w dodatku B.
Serwer integracyjnyW celu zintegrowania systemów A i B konsorcjum podjęło decyzję, iż każdy z członków musi
spełnić następujące warunki:
1. Administrator zarządzający serwerem integracyjnym otrzyma od administratorów systemów
lokalnych następujące informacje: adres sieciowy komputera, numer portu nasłuchu, nazwa
użytkownika i jego hasło, nazwa modułu w którym przechowywane są dane mające zostać
upublicznione w wirtualnej bazie danych.
2. Dane na temat wszystkich połączeń kolejowych muszą być zgodne ze schematem
przedstawionym na rysunku poniżej.
3. Wszystkie ceny biletów muszą zostać wyrażone w EUR.
Po otrzymaniu potrzebnych danych, administrator serwera integracyjnego tworzy moduł schedules,
w którym umieszcza definicje połączeń bazodanowych prowadzących do serwerów
kontrybucyjnych oraz definicję perspektywy integracyjnej.
111
Schedules
from : stringto : stringduration : integerprice : integerhour : integermin : integerday : integermonth : integer
Connection
Połączenia bazodanowe do poszczególnych serwerów zdefiniowane są następująco:
dblink systema pkp.app.public/[email protected]:8888; dblink systemb db.apps.pub/[email protected]:7777;
Pierwsze ze zdefiniowanych połączeń prowadzi do serwera alfa.pkp.com.pl, posiadającego moduł
pkp.app.public, zawierający dane zgodne z przyjętym schematem kontrybucyjnym. Drugie
połączenie bazodanowe prowadzi do serwera beta.db.de, w którym z kolei dane integracyjne
przechowywane są w module db.apps.pub.
Oprócz tego, na bazie połączeń systema i systemb zdefiniowane jest połączenie grupowe systemab:
Połączenie systemab wykorzystywane jest przez perspektywę integracyjną do zdefiniowania
operacji virtual objects. Zrealizowane jest to w następujący sposób:
virtual objects connections : con(ref systemsab.connections) [0..*] { return systemsab.connections as con; }
Pełny kod źródłowy modułu serwera integracyjnego przedstawiono w dodatku B.
Perspektywa kontrybucyjna systemów A i BAdministrator systemu A musi dostosować dane przechowywane w swoim systemie do postaci
wymaganej przez serwer integracyjny. W tym celu tworzy nowy nowy moduł zawierający
perspektywę kontrybucyjną. Podobna sytuacja dotyczy administratora systemu B. Kod źródłowy
stworzonych przez nich modułów umieszczono w dodatku B.
Wykonywanie zapytańW niniejszym punkcie objaśnimy w jaki sposób ewaluowane są zapytania realizowane na
wirtualnej bazie danych. W tym celu zbudowana została prosta aplikacja kliencka (jej kod źródłowy
umieszczony został w dodatku B), której najważniejszym elementem z naszego punktu widzenia
jest następująca instrukcja:
112
print db.connections where from = “Katowice” and to = “Warszawa”;
Podczas kompilacji zapytanie będące parametrem instrukcji print dekorowane jest zgodnie
z algorytmem przedstawionym w rozdziale 6. Kompilator wstawia również operację dereferencji
(wymuszoną przez = i print) oraz usuwa odwołanie do nazwy db (pod którą zarejestrowany
w aplikacji klienckiej pod nazwą db). Dzięki temu podczas czasu wykonania na serwer integracyjny
trafia następujące zapytanie:
deref connections where deref from = “Katowice” and deref to = “Warszawa”;
Zapytanie to następnie jest kompilowane i wykonywane przez serwer integracyjny. W normalnym
przypadku związanie nazwy connections powoduje wywołanie operacji virtual objects
perspektywy connections_view. Ponieważ jednak kod tej procedury składa się wyłącznie z jednego
zapytania, dlatego może zostać zastosowana metoda optymalizacji wykorzystania perspektywy
poprzez zamianę nazwy connections na zapytanie będące parametrem return [53]. Podobna
sytuacja dotyczy nazw from i to - zostają zamienione na zapytania pobrane z procedur virtual
objects odpowiadających im podperspektyw. Po zamianie zapytanie to przyjmie następującą
postać:
deref systemsab.connections as con where deref con.from as v = “Katowice” and deref con.to as v = “Warszawa”;
W kolejnym kroku podobna operacja stosowana jest do wystąpień operatora deref. Parametry tego
operatora rozszerzane są o zapytania pobierane z procedur on retrieve poszczególnych
podperspektyw. Zapytania te wstawiane są w drzewie składniowym między operator deref,
a istniejącą wcześniej częścią zapytania. Po zrealizowaniu tej operacji zapytanie przyjmie zatem
następującą postać:
deref (systemsab.connections as con where deref (con.from as v).v = “Katowice” and deref (con.to as v).v = “Warszawa”).con;
Ze względu na własność zbiorowych połączeń bazodanowych opisanych wcześniej, po wykryciu
w zapytaniu nazwy systemsab, kompilator zamienia ją na na nazwy reprezentowanych przez nie
połączeń bazodanowych. Zapytanie przekształcane jest zatem do następującej postaci:
deref (systema.connections as con where deref (con.from as v).v = “Katowice” and deref (con.to as v).v = “Warszawa”).con;unionderef (systemb.connections as con where deref (con.from as v).v = “Katowice” and deref (con.to as v).v = “Warszawa”).con;
113
Takie zapytanie może zostać następnie poddane optymalizacji przez przepisywanie.
W szczególności, po wykryciu niepotrzebnych nazw pomocniczych zapytanie może być
przekształcone do następującej formy:
deref systema.connections where deref from = “Katowice” and deref to = “Warszawa”union deref systemb.connections where deref from = “Katowice” and deref to = “Warszawa”;
Ostatecznie, zapytanie jest dekorowane nazwami serwerów, po czym lewe podzapytanie operatora
union trafia do wykonania na serwer zarejestrowany jako systema, a prawe podzapytanie - na
serwer systemb. Przed wysłaniem usuwane jest odniesienie do nazw połączeń bazodanowych
serwera integracyjnego.
Rozpatrzmy teraz jakie operacje wykonywane są po stronie systemu serwera. Do systemu trafia
następujące zapytanie:
deref connections where deref from = “Katowice” and deref to = “Warszawa”;
Kompilator rozpoznaje nazwę connections jako odwołującą się do perspektywy, po czym zastępuje
ją zapytaniem pobranym z instrukcji return operacaji virtual objects. Podobna operacja
realizowana jest dla każdej pozostałej nazwy występującej w zapytaniu:
deref connection as con where deref con.from as v = “Katowice” and deref con.to as v = “Warszawa”;
Również każda operacja dereferencji rozszerzana jest o operacje pobrane z on retrieve
odpowiednich perspektyw. W naszym przykładzie po wykonaniu tych operacji zapytanie przyjmie
następującą formę:
deref connection as con where deref (con.from as v).v = “Katowice” and deref (con.to as v).v = “Warszawa”).con.( from, to, duration, pln_2_eur(price), depart_time.hour, depart_time.min, depart_date.day, depart_date.month );
Po usunięciu niepotrzebnych nazw pomocniczych przez optymalizator, końcowe zapytanie wygląda
następująco:
deref (connection where deref from = “Katowice” and deref to = “Warszawa”).( from,
114
to, duration, pln_2_eur(price), depart_time.hour, depart_time.min, depart_date.day, depart_date.month );
Zapytanie to ewaluowane jest na rzeczywistych danych, po czym jego rezultat wraca na serwer
integracyjny. Serwer integracyjny scala go z rezultatem zwróconym przez serwerb, a otrzymany
wynik przesyła do klienta.
Wykonywanie operacji aktualizacyjnychW niniejszym punkcie przeanalizujemy sposób wykonywania operacji aktualizacyjnych
realizowanych na obiektach wirtualnych. Objaśnimy działanie instrukcji delete, przyjmując iż
pozostałe operacje aktualizacyjne ewaluowane są w podobny sposób.
Instrukcja delete kierowana jest na serwer integracyjny, który musi przekazać odpowiednie
polecenia do serwerów kontrybucyjnych. Serwery kontrybucyjne muszą przekształcić instrukcję
realizowaną na danych wirtualnych w taki sposób, by mogła być ona wykonana na danych
rzeczywistych.
Zgodnie z podstawowymi regułami składniowo-semantycznymi dotyczącymi perspektyw SBA
i opisanymi w [53], operacja delete realizowana na perspektywie jest zabroniona jeśli
w perspektywie nie została zdefiniowana operacja on delete. Do zapewnienia możliwości usuwania
obiektów wirtualnych (a co za tym idzie - obiektów rzeczywistych) kod perspektywy integracyjnej
oraz perspektyw kontrybucyjnych wzbogacamy następującym fragmentem:
on delete { delete con;}
Załóżmy teraz, że klient globalnego repozytorium podaje następującą instrukcję:
delete db.connections where deref price < 100;
Po udekorowaniu i usunięciu fragmentu db. zapytanie to wysyłane jest na serwer integracyjny.
Po stronie tego serwera nazwa connections zamieniana jest na odwołanie do połączenia
bazodanowego systemsab (zgodnie z procedurą virtual objects). Oprócz tego, argument operatora
deref rozszerzany jest o zapytanie pobrane w procedury on retrieve podperspektywy price_view.
Po zrealizowaniu tych przekształceń zapytanie przyjmie następującą formę:
delete systemsab.connections as con where deref (con.price as v).v < 100;
115
po rozwinięciu grupowego połączenia bazodanowego i usunięciu niepotrzebnej nazwy
pomocniczej:
delete systema.connections as con union systemb.connections as con where deref con.price < 100;
Zgodnie z semantyką perspektyw SBA dla każdego wirtualnego obiektu wykonany musi zostać kod
podany w procedurze on delete. Podobnie jak w przypadku procedury on retrieve, kod tej
procedury podatny jest na pewne optymalizacje. Optymalizacje nie są możliwe jeśli kod procedury
on delete składa się z więcej niż jednej instrukcji. W takim przypadku operacja on delete jest
wywoływana osobno dla każdego rezultatu zapytania będącego parametrem operacji delete.
Jeśli natomiast ciało procedury on delete składa się z pojedynczej instrukcji delete q1, wówczas
instrukcja delete q2 wykonywana na perspektywie może być przekształcona do postaci delete q1.q2.
W naszym przykładzie zapytanie może zostać przepisane do następującej postaci:
delete ((systema.connections as con union systemb.connections as con) where deref con.price < 100).con;
a następnie (dzięki właściom operatorów union i where) przekształcone do dwóch instrukcji
delete:
delete (systema.connections as con where deref con.price < 100).con;delete (systemb.connections as con where deref con.price < 100).con;
Takie instrukcje mogą zostać udekorowane połączeniami bazodanowymi (następnie usuwanymi),
po czym wysłane do wykonania na odpowiednie serwery. Na serwer trafi zatem zapytanie
delete (connections as con where deref con.price < 100).con;
a serwer systemb zapytanie
delete (connections as con where deref con.price < 100).con;
Po stronie serwera systema nazwa connections zastępowana jest zapytaniem pobranym z procedury
virtual objects:
delete (connection as con as con where deref con.price < 100).con;
a zapytanie będące parametrem operacji delete rozszerzone o zapytanie z instrukcji delete podanej
w on delete:
delete (connection as con as con where deref con.price < 100).con.con;
PodsumowanieW rozdziale umówiliśmy mechanizm integracji danych oparty na aktualizowalnych perspektywach
SBQL. Zaprezentowaliśmy prosty przykład integracji, oraz wyjaśniliśmy jak przebiega wymiana
116
danych między węzłami systemu rozproszonego w przypadku zapytań oraz operacji
aktualizacyjnych.
117
Rozdział 8:Prototyp
WprowadzenieW niniejszym rozdziale przedstawimy najważniejsze informacje dotyczące implementacji prototypu
narzędzia o zaproponowanej przez nas architekturze. Prototyp ten oraz badania przeprowadzone
przy jego użyciu są głównymi produktami niniejszej pracy doktorskiej. Podkreślamy tutaj wyraźnie,
że wszelkie prace badawcze w dziedzinie zaawansowanych środowisk programistycznych nie
poparte działającym prototypem są niewiarygodne. Ze względu na ogromną liczbę czynników
wpływających na spójność, kompletność i efektywność takiego środowiska nie jest możliwa
jakakolwiek teoria dająca niepodważalne odpowiedzi na wiele żywotnych pytań.
Omawiany system jest w pełni funkcjonalny i działa z prototypową, rozproszoną bazą danych oraz
ze zintegrowanym językiem zapytań rozszerzonym do kompletnego języka programowania. System
jest napisany w języku Java, oraz rozwijany jest obecnie w ramach osobnych podprojektów na
potrzeby europejskich projektów eGovBus oraz VIDE. Ze względu na ograniczenia objętościowe
niniejszej pracy opisane zostały jedynie najważniejsze decyzje implementacyjne. Szczegółowy opis
znajduje się w dokumentacji technicznej systemu Odra.
Baza danych i instancja bazy danychW systemie Odra wprowadzono rozróżnienie znane z niektórych SZRBD pomiędzy bazą
danych i jej instancją [5].
119
Baza danych ma charakter statyczny, jest zbiorem struktur (“składem danych”) przechowujących
dane zorganizowane w określony sposób. Baza danych w systemie Odra może mieć charakter
trwały lub nietrwały. W pierwszym przypadku dane przechowywane są w pliku systemu
operacyjnego, a w drugim - w pamięci operacyjnej.
Instancja bazy danych jest z zbiorem procesów operujących na tej bazie danych. Obecnie
zaimplementowane są (jako wątki Javy) dwa procesy:
• Proces komunikacji sieciowej (LSNR).
Proces ten odpowiedzialny jest za nasłuchiwanie na połączenia nadchodzące od klientów oraz
realizowanie póżniejszej komunikacji. Komunikacja zaimplementowana jest za pomocą
technologii Java NIO [100, 102, 103].
• Proces serwera (SVRP).
W momencie gdy proces LSNR zarejestruje nadchodzące połączenie, tworzony jest nowy proces
SVRP reprezentujący klienta po stronie serwera i realizujący w jego imieniu polecenia przesyłane
przez sieć. Każdy proces SVRP posiada swój prywatny skład danych nietrwałych (przeznaczony
na przechowywanie obiektów lokalnych, sesyjnych, itp.), jak również korzysta ze składu
globalnego, współdzielonego z innymi procesami SVRP (właściwa baza danych). Także niektóre
moduły systemu (np. kompilator i interpreter SBQL) są prywatne dla każdej sesji.
Architektura systemuNa rysunku poniżej przedstawiono architekturę systemu Odra. W systemie tym można wyróżnić
następujące elementy:
• Programy klienckie (np. interfejs linii poleceń)
Programy klienckie korzystają z biblioteki implementującej niskopoziomowy protokół
komunikacyjny systemu Odra. Biblioteka ta pozwala na komunikację za pomocą interfejsu
poziomu wywołań.
• Analizator składniowy SBQL
Moduł przekształca kod źródłowy SBQL na abstrakcyjne drzewo składniowe.
• Analizator kontekstowy SBQL
Moduł odpowiedzialny za statyczną ewaluację programów. Głównym celem analizy kontekstowej
jest kontrola typów oraz wykrycie odwołań do niezadeklarowanych nazw. Z wyników analizy
120
kontekstowej (np. informacji na temat numeru sekcji, w której związana została nazwa)
korzystają moduły optymalizatora oraz generatora kodu pośredniego.
• Optymalizator zapytań SBQL
Zadaniem optymalizatora jest przekształcenie zapytań w równoważną semantycznie formę,
ale wydajniej ewaluowaną.
• Generator kodu
Moduł odpowiedzialny za przekształcenie drzewa składniowego do bajtowego kodu pośredniego.
• Menedżer uprawnień
Moduł odpowiedzialny za mechanizmy bezpieczeństwa. Wykorzystywany podczas logowania
użytkownika do systemu oraz podczas kompilacji programów.
• Kompilator modułów
Moduł odpowiedzialny za budowę struktur reprezentujących w bazie danych moduły.
• Program ładujący/konsolidator
Moduł odpowiedzialny za zastępowanie logicznych odniesień w kodzie pośrednim na fizyczne
adresy obiektów. Głównie dotyczy to wywołań procedur.
• Bufor procedur
Kod skonsolidowanych procedury umieszczany jest buforze procedur. Bufor procedur przyspiesza
działanie programu konsolidatora.
• Interpreter programów SBQL
Moduł odpowiedzialny za wykonywanie programów SBQL zapisanych w kodzie bajtowym.
• Menedżer modułów
Moduł odpowiedzialny za zarządzaniem wnętrzem modułów, np. tworzeniem i usuwaniem
procedur.
• Moduły
Moduł jest jednostką organizacji bazy danych/aplikacji. Struktura ta zostanie omówiona w dalszej
części niniejszego rozdziału.
• Dane czasu wykonania
Dane dostępne dla interpretera.
121
• Metadane
Dane dostępne dla kompilatora.
• Menadżer obiektów trwałych
Moduł odpowiadający za operacje realizowane na danych trwałych (zapisywanych na dysku).
Tłumaczy operacje wyrażone w kontekście modelu M0 na ciągi bajtów (i owrotnie).
• Menedżer obiektów nietrwałych
Moduł odpowiadający za operacje realizowane na danych nietrwałych (przechowywanych
w pamięci operacyjnej).
• Menedżer transakcji
Moduł odpowiedzialny za przetwarzanie transakcji. Transakcje w systemie Odra posługują się
blokadami na poziomie stron. Mechanizm transakcji jest przedmiotem osobnego podprojektu,
dlatego nie został opisany w niniejszej pracy.
• Koder/dekoder rezultatów
Moduł pozwalający na przekształcanie struktur reprezentujących rezultaty zapytań na postać
łatwą do przesłania przez sieć (i owrotnie).
• Koder/dekoder metadanych
Moduł pozwalający na przeksztalcanie fragmentów bazy danych reprezentujących metadane na
postać łatwą do przesłania przez sieć. Metabaza przesyłana jest między systemami Odra w celu
realizacji statycznej kontroli typów zapytań rozproszonych.
Modularna organizacja bazy danychZgodnie z naszą koncepcją przedstawioną w rozdziale 4, podstawową jednostką organizacyjną bazy
danych jest moduł. Moduł w systemie Odra przechowuje zarówno dane czasu wykonania (baza
danych), jak i dane czasu kompilacji (metabaza). Dotyczy to zarówno obiektów reprezentujących
byty programistyczne, jak i obiektów bazy danych.
Pierwszym (podłączonym do obiektu root) obiektem każdej bazy danych jest moduł o nazwie sys.
Moduł ten jest automatycznie importowany przez wszystkie inne moduły bazy danych. Oprócz
tego, iż moduł ten stanowi korzeń hierarchii modułów, jest również miejscem przechowywania
danych pełniących rolę biblioteki standardowej SBQL. Dodatkowo, w module tym przechowywane
są dane pełnieniące rolę katalogu bazy danych. Niemal wszystkie operacje tworzące, usuwające
i modyfikujące obiekty warstwy bazy danych zorganizowane są w taki sposób, iż automatycznie
122
wstawiają dane do struktur utworzonych w tym module. Przykładowo, dodanie do systemu
procedury w jakimkolwiek module, powoduje automatyczne utworzenie wystąpienia odpowiedniej
zmiennej opisującej tę procedurę w katalogu bazy danych. Użytkownik może następnie użyć
zapytania SBQL, aby uzyskać listę wszystkich procedur zdefiniowanych w systemie, a także
podstawowe informacje na ich temat. Innym rodzajem informacji przechowywanym w katalogu
bazy danych jest lista kont użytkowników zdefiniowanych w systemie, wraz z ich hasłami
(zaszyfrowanymi), oraz innymi elementarnymi informacjami systemowymi. Katalog bazy danych
pełni zatem funkcję zbioru metadanych dotyczących bazy danych i przeznaczonych dla programisty
systemu Odra.
CLI, SBQL IDE, Java, ...
analizator sk!adniowy SBQL
sie"
analizator kontekstowy
optymalizator
kompilator modu!ów
AST
modu!u
AST zapytania
ad-hoc
kod #ród!owy
SBQL
generator kodu
AST
procedury
kod po$redni
procedury
menad%er obiektówtrwa!ych
interpreter
modu! !aduj&cy/konsolidator
bufor procedur
mened%er transakcji
mened%er uprawnie'
menad%er modu!ów
modu!ymodu!
modu!
dane czasu wykonania
procedury
perrspektywy
wyst&pienia zmiennych
klasy
po!&czenia bazodanowe
indeksy
metadane
procedury
perrspektywy
deklaracje zmiennych
klasy
po!&czenia bazodanowe
indeksy
definicje
typów
operatory
i typy
wbudowane
menad%er obiektów
nietrwa!ych
koder rezultatów
dekoder rezultatów
sie"
koder/dekoder rezultatów
koder/dekoder metadanych
odra odra odra
Rys. 14: Architektura systemu Odra
Bezpośrednio do modułu sys podłączane są moduły reprezentujące schematy bazy danych
poszczególnych użytkowników. Każde konto użytkownika utworzone w bazie danych posiada taki
globalny moduł. Wszystkie dane utworzone wewnątrz tego modułu (w tym podmoduły) należą do
123
odpowiadającego mu użytkownika, który może w ich ramach tworzyć struktury bazy danych, takie
jak procedury, perspektywy, deklarować zmienne, jak i instancjonować je.
Poniższy rysunek przedstawia koncepcyjny, przykładowy stan bazy danych. Baza danych zawiera
dwa moduły użytkowników raist i scott. Moduły te zawierają podobiekty reprezentujące inne
moduły, klasy, indeksy, perspektywy, procedury globalne, obiekty czasu wykonania będące
wystąpieniami zmiennych, itd. Obiekty emp i dept to instancje klas Employee i Department.
hr
Department
emp_ename_idx
employ
emp_avg_sal_view
Employee
raist
scott
emp
emp
dept
emp
dept
public
public
employ
sys
Rys. 15: Przykładowe drzewo bazy danych
Skład danychSkład danych jest mechanizmem organizującym podstawowe struktury bazy danych na dysku lub
w pamięci operacyjnej. Istnieją dwa rodzaje składów danych: nietrwały i trwały. Pierwszy z nich
jest typowy dla języków programowania, gdzie występuje pod nazwą “sterta”. Trwały skład danych
z reguły występuje w bazach danych. Zawartość trwałego składu danych nie ulega utracie po
wyłączeniu komputera, jest współdzielona z innymi sesjami. Architektura składu danych
w systemie Odra zainspirowana została przez podobną strukturę systemu Loqis [30, 31, 92].
124
W przeciwieństwie do tradycyjnych, dyskowych SZBD, system Odra nie posiada specjalnych
struktur danych ani algorytmów wspierających optymalne wykorzystanie dysków twardych
komputerów. Zamiast tego, system przystosowany jest do intensywnego wykorzystania możliwie
największych fragmentów dostępnej pamięci operacyjnej. System zakłada, iż cała lub przynajmniej
większa część bazy danych znajduje się w pamięci operacyjnej. Trwałość zapewniana jest przez
mechanizm znany pod nazwą memory mapped files. Mechanizm ten wykorzystuje funkcjonalność
struktur systemu operacyjnego odpowiedzialnych za działanie pamięci wirtualnej, zwalniając nas
z konieczności implementowania mechanizmów I/O oraz buforowania. System operacyjny
automatycznie ładuje potrzebne dane do pamięci operacyjnej oraz zapisuje je na dysku. Dla baz
danych wielkości do kilkuset megabajtów jest to mechanizm sprawdzający się dostatecznie dobrze,
by mógł stać się bazą prototypu, jakim jest Odra.
Warstwa obiektów bazy danych i metabazy
(moduły, procedury, klasy, perspektywy, linki bazodanowe, etc.)
Warstwa obiektów składu danych
(obiekty proste, złożone, referencyjne)
Warstwa fizyczna składu danych
(pliki, pamięć, bloki danych)
Rys. 16: Warstwowa struktura bazy danych
Implementacja składu danych podzielona została na trzy warstwy. Najbardziej podstawową jest
warstwa fizyczna, implementująca mechanizmy zarządzania wolną przestrzenią w ramach plików
danych. Bazująca na tej implementacji warstwa obiektów składu danych implementuje podstawową
funkcjonalność zgodną z modelem danych danych SBA znanym pod nazwą M0. Warstwa obiektów
składu danych jest z kolei podstawą warstwy obiektów bazy danych. Jej zadaniem jest wyrażenie
modeli danych wyższych niż M0 w terminach modelu M0. Interpreter SBQL korzysta z warstwy
najbardziej szczytowej.
Trwały skład danych przechowywany jest w tzw. pliku danych. Plik ten montowany jest w systemie
Odra jako plik mapowany w pamięci, co oznacza że jego zawartość widziana jest dla systemu jako
jedna, wielka tablica bajtów. Wszystkie operacje I/O na tym pliku realizowane są przez system
operacyjny, bez udziału systemu Odra.
125
Niezależnie od tego czy pracujemy z trwałym, czy nietrwałym składem danych, fizycznie jest on
widziany wewnątrz systemu jako tablica bajtów Javy. Aby w obszarze tym można było zapisywać,
potrzebne są mechanizmy zarządzania wolną przestrzenią. Mechanizmy te dostępne są dla warstwy
obiektów bazy danych jako metody malloc() i free(). Podobnie jak w języku C, metoda malloc()
pobiera argument w postaci wielkości obszaru jaki ma być zarezerwowany, a zwraca liczbę będącą
pozycją w tablicy bajtów (adresem) określającym początek zarezerwowanego obszaru. Brak
dostatecznej ilości wolnej pamięci sygnalizowany jest wyjątkiem. Metoda free() pobiera adres
zwrócony przez malloc(), i zwalnia wskazywany przez niego obszar.
Zarówno w trwałym, jak i nietrwałym składzie danych pamięć zarządzana jest w taki sam sposób.
Odpowiedzialny jest za to nieco zmodyfikowany, choć wciąż bardzo prosty algorytm, znany pod
nazwą sequential-fit. Algorytm ten zakłada, iż pamięć dzieli się na szereg bloków pamięci
połączonych w listę dwukierunkową. Każdy węzeł takiej listy składa się z następujących
elementów: stan (0 - wolny, 1 - zajęty), adres poprzedniego węzła (0 jeśli brak), adres następnego
węzła (rozmiar sterty + 1 jeśli brak kolejnego węzła), dane.
Na bazie warstwy fizycznej zbudowana została warstwa zarządzająca podstawowymi operacjami na
najważniejszych rodzajach obiektów wywodzących się z podejścia stosowego. Każdy obiekt jest
strukturą następującej postaci:
rodzaj(1 bajt)
nazwa (4 bajty)
obiekt nadrzędny (4 bajty)
referencje zwrotne(4 bajty)
wartość(4 bajty)
Aby utworzyć obiekt w bazie danych, warstwa obiektów składu danych rezerwuje 17 bajtów
w bazie danych za pomocą malloc(), a następnie zapisuje pod otrzymanym adresem strukturę przedstawioną powyżej.
Obsługiwane są następujące rodzaje obiektów:
• COMPLEX_OBJECT
Obiekty z wartością w postaci ciągu adresów podobiektów. Reprezentują obiekty złożone w modelu M0.
• STRING_OBJECT
Obiekty z wartością w postaci adresu bloku danych zawierającego ciąg znaków i jego długość,
• INTEGER_OBJECT Obiekty z wartością w postaci liczb typu integer,
126
• DOUBLE_OBJECT
Obiekty z wartością w postaci adresu bloku danych zawierającego liczbę typu double.
• BOOLEAN_OBJECT
Obiekty z wartością w postaci true lub false,
• REFERENCE_OBJECT
Obiekty z wartością w postaci adresu obiektu trwałego (obsługuje wiszące wskaźniki),
• POINTER_OBJECT
Obiekty z wartością w postaci adresu trwałego (brak obsługi wiszących wskaźników),
• BINARY_OBJECT
Obiekty z wartością w postaci wskażnika do bloku zawierającego binarny ciąg znaków i jego
długość,
• AGGREGATE_OBJECT
Obiekty złożone służące do modelowania kolekcji obiektów o tych samych nazwach. Jest to
swoista optymalizacja fizyczna, przyspieszająca wyszukiwanie obiektów o tych samych nazwach.
Pole nazwy obiektu przyjmuje numer będący pozycją w tzw. indeksie nazw (opisanym dalej).
Trzecie pole jest adresem obiektu nadrzędnego COMPLEX_OBJECT lub AGGREGATE_OBJECT.
Pole referencji zwrotnych jest adresem bloku pamięci przechowującym zbiór adresów obiektów
REFERENCE_OBJECT wskazujących na dany obiekt (mających jego adres jako wartość).
Ostatnie pole może przyjąć jedną z dwóch form:
• jest wartością samą w sobie (INTEGER_OBJECT, BOOLEAN_OBJECT,
REFERENCE_OBJECT, POINTER_OBJECT) jeśli wartość ta zawsze może być zapisana przy
użyciu maksymalnie 4 bajtów,
• jest adresem osobnego bloku pamięci w którym przechowywana jest właściwa wartość obiektu,
być może razem z długością tej wartości (AGGREGATE_OBJECT, COMPLEX_OBJECT,
DOUBLE_OBJECT, STRING_OBJECT, BINARY_OBJECT).
Referencje zwrotne są mechanizmem zapewniającym funkcjonalność automatycznego zarządzania
pamięcią. Trzecim polem każdego obiektu jest referencja do nadmiarowego bloku danych
(o strukturze takiej jak w przypadku obiektów COMPLEX_OBJECT i AGGREGATE_OBJECT)
przechowującego adresy obiektów REFERENCE_OBJECT posiadających adres tego obiektu jako
127
wartość (czyli wskazujących na niego). Jeśli obiekt nie posiada żadnych referencji zwrotnych,
wówczas wartością tego pola jest 0.
875732845 (warto!")0 (ref. zwrotne)11 (nazwa)9 (REFERENCE_OBJECT)
123 (warto!")322564323 (ref. zwrotne)12 (nazwa)1 (INTEGER_OBJECT)
5233332 (adres 1)1 (ilo!" referencji
zwrotnych)
Obiekt referencyjny
Obiekt wskazywany
Rys. 17: Przykładowy sytuacja pomiędzy obiektem referencyjnym i obiektem wskazywanym
Referencje zwrotne pozwalają nam uniknąć efektu wiszących wskaźników. W momencie gdy
kasowany jest jakiś obiekt, sprawdzane są jego referencje zwrotne, a obiekty
REFERENCE_OBJECT o adresach będących referencjami zwrotnymi są automatycznie usuwane
zanim usunięty zostanie właściwy obiekt. Referencje zwrotne są zawsze aktualne, tzn. każda
operacja usunięcia obiektu REFERENCE_OBJECT powoduje usunięcie odpowiedniego wpisu ze
zbioru referencji obiektu wskazywanego. Podobnie, operacja zmiany wartości takiego obiektu
powoduje usunięcie odpowiedniej referencji zwrotnej z obiektu który był wskazywany przed
aktualizacją, oraz umieszczenie nowej referencji zwrotnej w obiekcie wskazywanym przez
referencję po aktualizacji.
Tworzenie programówObiekty bazy danych mogą być tworzone na dwa sposoby. Pierwszy z nich jest charakterystyczny
dla baz danych i polega na wykorzystaniu poleceń DDL do zdefiniowania odpowiednich struktur.
Programista dysponuje do tego celu narzędziem linii poleceń podobnych do SQL*Plus systemu
Oracle. Utworzenie zatem nowego indeksu polega na wydaniu polecenia create index, dodanie
nowej procedury - create procedure, itd. Wszystkie obiekty muszą być tworzone w ramach
utworzonego wcześniej modułu. Moduły tworzy się za pomocą polecenia create module. Polecenie
to posiada formę bezargumentową i argumentową. Pierwsza forma tworzy pusty moduł.
Argumentem drugiej formy jest pełny kod źródłowy modułu, przypominający kod źródłowy
sformułowany za pomocą typowych języków programowania. Kod źródłowy pełni rolę “skryptu”,
128
którego przetworzenie przez bazę danych odpowiada kolejnym wywołaniom operacji create
procedure, create class, etc.
Kompilacja aplikacji z poziomu środowiska programistycznego wiąże się z przesłaniem do serwera
bazy danych kodów żródłowych wszystkich modułów, które zostały zmodyfikowane od czasu
ostatniej kompilacji. Serwer bazy danych zawiera kompilator i interpreter programów SBQL.
Każdy przesłany moduł jest zatem parsowany, kontrolowany typologicznie, optymalizowany, po
czym generowane są struktury bazy danych oraz kod wykonywany później przez interpreter.
Poniższy rysunek ilustruje sytuację, gdy do bazy danych trafia polecenie create module, którego
zadaniem jest utworzenie nowego modułu. Parametrami tej operacji jest globalna nazwa modułu
nadrzędnego (w tym przypadku scott) oraz kod źródłowy tego modułu. Po prawej stronie
przedstawione są struktury bazy danych jakie tworzone są po skompilowaniu tego modułu. W bazie
danych utworzone zostaną dwa obiekty reprezentujące wystąpienia zmiennej x, jeden obiekt
reprezentujący procedurę, oraz jeden obiekt reprezentujący indeks założony na zmiennej x.
hr
sayHello
xx
x
sayHello
x_idx
Metabaza(dane czasu kompilacji)
Baza danych(dane czasu wykonania)
Modu! u"ytkownika scott (stworzony automatycznie)
Modu! u"ytkownika raist (stworzony automatycznie)
#rodowisko standardowe i biblioteka standardowa
Modu! aplikacyjny (stworzony przez r$cznie przez u"ytkownika)
scott
raist
test
sys
module hr { x [2..*] : integer; sayHello(name : string) { print “Hello, “ + name; }
index x_idx on x;}
Rys. 18: Baza danych po utworzeniu modułu hr
Jeśli moduł hr istnieje już w bazie danych, wówczas nie jest on usuwany razem ze swoją
zawartością, a jedynie jego struktura dostosowywana jest do postaci zgodnej z przesyłanym kodem
źródłowym.
129
• Obiekty bazy danych które są zgodne ze swoimi deklaracjami w kodzie źródłowym nie są
modyfikowane.
• Obiekty których deklaracje nie istnieją w kodzie źródłowym są usuwane.
• Obiekty których struktura różni się w stosunku do kodu źródłowego są dostosowywane do nowej
postaci (procedury) lub są usuwane i tworzone na nowo (egzemplarze zmiennych).
Domyślnie obiekty które muszą zostać usunięte z bazy danych kasowane są automatycznie (tryb
force polecenia create module). Istnieje jednak możliwość wyłączenia tego zachowania (tryb
noforce), który w przypadku wykrycia konfliktu między stanem bazy danych oraz przesyłanym
kodem źródłowym zgłasza błąd, a użytkownik musi ręcznie dostosować bazę danych do pożądanej
formy.
hr
sayHelloWorld
x
Metabaza(dane czasu kompilacji)
Baza danych(dane czasu wykonania)
Modu! u"ytkownika scott (stworzony automatycznie)
Modu! u"ytkownika raist (stworzony automatycznie)
#rodowisko standardowe i biblioteka standardowa
Modu! aplikacyjny (stworzony przez r$cznie przez u"ytkownika)
scott
raist
test
sys
module hr { x [2..*] : integer; sayHelloWorld() { print “Hello, world!”; }
index x_idx on x;}
x
x
x_idx
sayHelloWorld
Rys. 19: Baza danych po zmodyfikowaniu modułu hr
Poniższy rysunek ilustruje sytuację która nastąpiła po przedstawionej powyżej. Do bazy danych
trafia zmodyfikowany kod modułu hr. W nowej wersji usunięto procedurę sayHello, natomiast
dodano procedurę sayHelloWorld. Ponieważ pierwsza z tych procedur nie posiada swojego
odpowiednika w kodzie źródłowym, dlatego zostaje usunięta z bazy danych. Pozostałe obiekty
z bazy danych pozostały nietknięte (nie tylko ich struktura, ale również stan). Podobne zmiany
zaszły w metabazie.
130
Konstrukcja, kompilacja i inicjalizacja modułówKonstrukcja modułów jest stosunkowo złożonym procesem, niezbędnym jednak przede wszystkim
do poprawnego zrealizowania statycznej kontroli typów. Informacje wyznaczone podczas tego
procesu wykorzystujemy również do zastąpienia niektórych dynamicznych wiązań wiązaniami
statycznymi (w celu optymalizacji niektórych operacji).
Kod źródłowy modułu trafia do bazy danych, gdzie jest parsowany. Tworzony jest nowy obiekt
bazy danych reprezentujący moduł, a w jego wnętrzu powstają obiekty reprezentujące byty
programistyczne zawarte w kodzie źródłowym (zarówno obiekty czasu wykonania, jak i obiekty
metabazy). Procedury nie są w tym momencie kompilowane, natomiast kod źródłowy każdej
procedury zostaje dopisany do obiektu reprezentującego tę procedurę. W skonstruowanym w ten
sposób module ustawiona zostaje flaga invalid. Flaga ta uniemożliwia korzystanie z modułu. Moduł
musi zostać najpierw skompilowany i zanicjalizowany.
W tym momencie poszczególne moduły aplikacji są od siebie ciągle niezależne. Można dodawać
nowe moduły, jak i usuwać istniejące. Poprawność odwołań do obiektów globalnych w tych
modułach nie jest kontrolowana.
Przed każdą pierwszą próbą skorzystania z zawartości modułu (np. poprzez wywołanie jego
procedury) zrealizowaną w ramach jakiegoś zewnętrznego narzędzia (np. interfejsu linii poleceń
przeznaczonego dla użytkownika), system sprawdza stan flagi invalid. Jeśli jest ona opuszczona,
wówczas następuje interpretacja polecenia przekazanego przez użytkownika. Jeśli flaga jest
ustawiona, wówczas inicjowany jest proces kompilacji.
Kompilacja realizowana jest rekurencyjnie dla każdego modułu importowanego przez
kompilowany moduł. Jeśli któryś z importowanych modułów posiada opuszczoną flagę invalid,
wówczas nie jest kompilowany. Celem procesu kompilacji jest analiza statyczna poszczególnych
bytów programistycznych zawartych w module. Dla każdego takiego bytu realizowany jest proces
statycznej analizy składającej się z kontroli kontekstowej, a dla procedur również optymalizacja
oraz generacja kodu. Statyczny stos środowiskowy potrzebny do analizy statycznej inicjalizowany
jest poprzez umieszczenie w pierwszej sekcji binderów utworzonych na podstawie wszystkich
globalnych elementów kompilowanego modułu. Do takiej sekcji dodawane są następnie bindery
utworzone na podstawie bytów programistycznych zawartych w importowanych modułach. Na tak
zainicjalizowanym stosie realizowana jest statyczna analiza danego bytu.
131
Modyfikacje programówJeśli usuwany moduł posiadał opuszczoną flagę invalid, wówczas dla wszystkich modułów
importujących ten moduł flaga ta jest automatycznie ustawiana. Wymagają one odtąd rekompilacji.
Podobna sytuacja ma miejce jeśli w ramach poprawnego modułu usuwany jest jedynie pojedynczy
byt (np. procedura). Podobna sytuacja ma miejsce w przypadku bardziej drobnoziarnistej ewolucji
schematu. Jeśli skompilowany moduł A odwołuje się (poprzez listę importową) modułu B, oraz byt
ten jest usuwany lub modyfikowany, wówczas moduł A staje się niepoprawny i wymaga ponownej
kompilacji. Problem ten rozwiązaliśmy poprzez utworzenie referencji do każdego modułu który
importuje dany moduł. Jeśli pewien moduł jest modyfikowany, wówczas dzięki takim referencjom
możemy uzyskać listę modułów zależnych od tego modułu. W każdym z tych modułów ustawiana
jest flaga valid na false. Moduł z taką flagą wymaga ponownej kompilacji i konsolidacji z resztą
modułów.
Kod pośredniKażdy program SBQL jest kompilowany do postaci kodu pośredniego. Dotyczy to zarówno
procedur, jak i zapytań ad-hoc. Przykładowo, zapytanie 1 * 2 + 3 może być przekształcone do ciągu
następujących operacji:
ldi 1 ldi 2 muli ldi 3 addi
Kod pośredni jest przed wykonaniem przekształcany do postaci kodu wykonywalnego.
Przekształcenie to polega obecnie na usunięciu wiązania dynamicznego w kilku przypadkach
i zastąpienie go wiązaniem statycznym. W przyszłości istnieje możliwość dalszych optymalizacji
w tym zakresie, łącznie z kompilacją do postaci kodu bajtowego wirtualnej maszyny Javy, czy też
wręcz do kodu maszynowego.
Każda instrukcja kodu bajtowego posiada następującą formę:
kod operacji(4 bajty)
parametr(8 bajtów)
Parametr jest liczbą całkowitą, która w zależności od rodzaju operacji ma inne znaczenie. Na
przykład w przypadku operacji ldi, parametr jest wkładany na stos rezultatów jako liczba całkowita.
132
Z kolei w przypadku ldcs, parametr jest indeksem w tzw. puli stałych, a w przypadku cri jest to
numer pozycji w indeksie nazw.
Pula stałych jest ciągiem bajtów przechowywanym razem z kodem bajtowym. Jej wnętrze stanowi
zbiór wartości mogących potencjalnie przyjmować dowolne długości (więcej niż 8 bajtów).
Przechowywane w niej wartości zazwyczaj pochodzą z kodu źródłowego, gdzie przyjmują postać
literałów.
Indeks nazw jest globalną strukturą stworzoną dla całej bazy danych, przechowującą pary
<identyfikator, nazwa>. Nazwy obiektów pojawiające się w kodzie pośrednim występują jako
liczby całkowite reprezentujące identyfikator.
W kilku kolejnych punktach pokażemy kilka przykładowych przypadków wygenerowanego kodu
pośredniego.
Generowanie kodu dla zapytań
Dla zapytania 1 union 2 generator kodu stworzy następujący kod pośredni:
ldi 1 ; umieszczamy 1 na stosie rezultatow bag ; konwertujemy szczytowy element QRES na bag ldi 2 ; umieszczamy 2 na stosie rezultatow bag ; konwertujemy szczytowy element QRES na bag unn ; zdejmujemy dwa elementy z QRES i realizujemy operacje sumy bagow
Poniżej kod jaki może zostać wynerowany dla zapytania emp.dept:
ldbag ; ladujemy pusty bag przeznaczony na rezultaty czastkowe ldi 0 ; umieszczamy na QRES liczbe 0 stcntr ; inicjalizujemy nia licznik bnd emp ; wiazemy nazwe emp dup ; kopiujemy wynik w celu policzenia jego elementow cnt ; zliczamy ilosc elementów w kolekcji bedacej wynikiem wiazanialoop: dup ; kopiujemy rozmiar elementow kolekcji ; otrzymanej przez zwiazenie emp inccntr ; zwiekszamy licznik o 1 ldcntr ; ladujemy licznik elementów na QRES grei ; sprawdzamy czy licznik jest wiekszy lub rowny rozmiarowi kolekcji brt end ; jesli tak, wowczas konczymy ewaluacje ldcntr ; ladujemy licznik extr ; umieszczamy na stosie element kolekcji o numerze rownym licznikowi crenv ; tworzymy nowa sekcje na stosie srodowiskowym nstd ; wypelniamy nowa sekcje binderami do ; wnetrza analizowanego obiektu bnd dept ; wiazemy nazwe dept dsenv ; usuwamy sekcje stosu ENVS ins2 ; umieszczamy rezultat w kolekcji przeznaczonej na rezultaty czastkowe
133
bra loop ; przechodzimy do nastepnego elementu empend: pop ; usuwamy rozmiar kolekcji otrzymanej przez zwiazanie emp fltn ; spłaszczamy kolekcje bedaca wynikiem (jesli to mozliwe)
Obsługa modułów
W związku z tym, iż każdy program SBQL musi być wykonany w ramach modułu, w czasie
wykonania stos środowiskowy musi być odpowiednio zainicjalizowany binderami do globalnych
obiektów danego modułu oraz modułów importowanych przez niego. Kod podobny do poniższego
poprzedza kod wygenerowany dla każdego zapytania ad-hoc:
crgenv ; nowa sekcja ENVS nie polaczona linkiem statystycznym ; z sekcja bazowa ldmn “modul” ; ladujemy na stos referencje do modulu “modul” eimpo ; wnetrza modulow importowanych przez modul trafiaja na ENVS ldmn “modul” ; ladujemy na stos referencje do modulu “modul” ldmen ; zaladowanie referencji do korzenia bazy danych modulu nstd ; zrealizowanie operacji nested na korzeniu bazy danych modulu
Generowanie kodu dla procedur
Dla następującej procedury:
divide(x : integer; y : integer) : real { if (y == 0) return -1; else return x / y; }
wygenerowany zostanie następujący kod:
initle: crbnd y ; utworzenie bindera na ENVS na podstawie parametru y crbnd x ; utworzenie bindera na ENVS na podstawie parametru xbody: bnd y ; zwiazanie nazwy y ldi 0 ; zaladowanie wartosci 0 eqi ; sprawdzenie czy y = 0 brf else ; jesli y nie jest rowne 0, skok do etykiety else ldi -1 ; zaladowanie wartosci -1 retv ; zakonczenie dzialania proceduryelse: bnd x ; zwiazanie nazwy x bnd y ; zwiazanie nazwy y divi ; podzielenie x przez y retv ; zakonczenie dzialania procedury
Wołanie procedury realizowane jest za pomocą instrukcji call. Następujące zapytanie:
divide(6; 2);
134
tłumaczone jest do następującej postaci:
ldi 6 ; zaladowanie na stos wartosci parametru aktualnego ldi 2 ; zaladowanie na stos wartosci parametru aktualnego call ”michal.mod1.divide” ; wywolanie procedury “divide”
Instrukcja call działa jako makro tożsame z następującym ciągiem operacji:
crgenv ; utworzenie nowej sekcji globalnej ldmn “michal.mod1” ; zaladowanie na stos referencji do ; modulu “michal.mod1” eimpo ; wnetrza modulow importowanych ; przez modul na ENVS ldmn “michal.mod1” ; zaladowanie na stos referencji ; do modulu “michal.mod1” ldmen ; zaladowanie referencji do korzenia ; bazy danych modulu nstd ; realizacja operacji nested na korzeniu bazy danych crenv ; utworzenie sekcji lokalnej wywolanej procedury
Następnie wykonywany jest skompilowany kod procedury, ktorej nazwa pobierana jest ze stosu
QRES. Po zakończeniu wykonywania operacji call, automatycznie realizowane są następujące
operacje:
dsenv ; usuniecie szczytowej sekcji ENVS dsenv ; usuniecie szczytowej sekcji ENVS
W związku z tym, iż podczas kompilacji modułu kompilator dysponuje pelnymi informacjami na
temat lokalizacji poszczególnych obiektów bazy danych, istnieje możliwość optymalizacji kodu
bajtowego poprzez usunięcie części wiązań dynamicznych i zastąpieniem ich wiązaniami
statycznymi. Przykładowo, każdą instrukcję ldmn zamienić można na instrukcję alternatywną,
której argumentem jest adres docelowego modułu, a nie jego nazwa. Optymalizacja tego typu nie
będą jednak omawiane w niniejszej pracy.
Reprezentacja obiektów bazy danychW niniejszym punkcie skrótowo opiszemy w jaki sposób zaimplementowano podstawowe byty
programistyczne języka SBQL za pomocą modelu danych M0.
Moduły
Moduł w bazie danych przyjmuje formę obiektu złożonego (COMPLEX_OBJECT) składającego
się z kilku podobiektów systemowych. Obiekty systemowe dla odróżnienia od obiektów
użytkownika (dostępnych dla operacji wiązania) posiadają nazwy poprzedzone znakiem $.
Strukturę modułu przedstawia poniższy rysunek.
135
Korzystamy z następującej notacji: (C) - COMPLEX_OBJECT, (I) - INTEGER_OBJECT,
(B) - BINARY_OBJECT, (S) - STRING_OBJECT, (D) - DOUBLE_OBJECT,
(R) - REFERENCE_OBJECT, (N) - BINARY_OBJECT, (P) - POINTER_OBJECT.
$kind (I)
$name (S)
$invalid (B)
$imports (C)
$refs (C)
$meta (C)
$data (C)
$submodules (C)
nazwa modułu (C)
Obiekt $kind zawiera informację o tym, że cała struktura reprezentuje moduł. Obiekt $name
przechowuje globalną nazwę modułu. Obiekt $invalid wskazuje czy procedury danego modułu
muszą być przekompilowane. Jeśli w strukturze modułu lub kodzie procedur nastąpią jakieś
zmiany, wówczas wartość tego obiektu ustawiana jest na true. Obiekt $imports zawiera podobiekty
typu STRING_OBJECT z wartościami będącymi globalnymi nazwami modułów importowanych
przez moduł bieżący. Obiekt $refs zawiera referencje modułów importujących dany moduł. Obiekt
$meta jest korzeniem metabazy modułu. Obiekt $data jest korzeniem bazy danych modułu. Obiekt
$submodules zawiera moduły podrzędne.
Procedury
Procedury są obiektami przechowującymi skompilowany kod SBQL oraz dane potrzebne do
optymalizowania zapytań w czasie wykonania.
$kind (I)
$ast (C)
$obj (N)
$cst (N)
nazwa procedury (C)
Obiekt $ast posiada podobiekty BINARY_OBJECT z wartościami będącymi zserializowanymi (za
pomocą mechanizmu serializacji Javy) drzewami składniowymi wszystkich bądż tylko niektórych
(decyduje o tym generator kodu) zapytań występujących w procedurze. W czasie wykonania
interpreter może odczytywać i przetwarzać te drzewa, co jest niezbędne do dynamicznej
optymalizacji zapytań. Obiekt $obj zawiera kod bajtowy procedury. Obiekt $cst zawiera listę
136
wartości (zwykle literałów kodu źródłowego), których długość jest większa od 4B, albo nie daje się
przewidzieć.
Procedury w metabazie służą do opisania nazw i typów parametrów formalnych oraz zwracanej
przez procedurę wartości.
$kind (I)
$mincard (I)
$maxcard (I)
$typenameid (I)
$ref (I)
$args (C)
nazwa procedury (C)
Obiekty $mincard i $maxcard zawierają wartości reprezentujące minimalną i maksymalną liczność
rezulatu procedury. Wartość obiektu $typenameid reprezentuje typ zwracanej przez procedurę
wartości. Wartość obiektu $ef mówi czy rezulat procedury jest obiektem wskaźnikowym (>0) czy
też nie (0). Podobiektami obiektu $args są obiekty reprezentujące parametry formalne procedury
(podane jako struktury reprezentujące deklaracje zmiennych).
Deklaracje zmiennych
Obiekty te reprezentują deklaracje zmiennych, czyli “podpowiadają” modułowi kompilatora
realizującemu operację analizy kontekstowej jakich danych (typ, ilość obiektów, itd.) można
spodziewać się w czasie wykonania w danym środowisku. Występują tylko w metabazie.
$kind (I)
$mincard (I)
$maxcard (I)
$typenameid (I)
$ref (I)
nazwa zmiennej (C)
Obiekty $mincard i $maxcard zawierają wartości reprezentujące minimalną i maksymalną liczność
zmiennej. Wartość obiektu $typenameid reprezentuje typ zmiennej. Wartość obiektu $ref mówi czy
typ zmiennej jest referencyjny (>0) czy też nie (0). Interpretacja tej wartości zależy od systemu
typów.
137
Struktury
Obiekty te opisują pola typów strukturalnych. Ich nazwa nie powinna być dostępna dla wiązania,
dlatego powinna być rozszerzona o przedrostek $. Jeśli potrzebny jest nazwany typ strukturalny, wówczas należy utworzyć strukturę, a następnie wskazać ją jako typ bazowy obiektu
reprezentującego definicję typu. Występują tylko w metabazie.
$kind (I)
$fields (C)
nazwa struktury (C)
Obiekt $fields posiada podobiekty zgodne ze strukturą deklaracji zmiennej. Podobiekty te stanowią
opis poszczególnych pól struktury.
Typy
Obiekty te wprowadzają nowe, nazwane typy danych. Każdy typ posiada nazwę oraz typ bazowy.
Występują tylko w metabazie.
$kind (I)
$ref (I)
nazwa typu (C)
$typenameid (I)
Wartość obiektu $typenameid reprezentuje typ bazowy (np. typ prosty, albo struktura). Wartość
obiektu $ref mówi czy typ zmiennej jest referencyjny (>0) czy też nie (0).
Operatory binarne
Obiekty te reprezentują proste, arytmetyczne operatory binarne, których operandami są wartości
typu prostego. Tworzone są automatycznie podczas operacji tworzenia bazy danych w metabazie
modułu sys, jako część środowiska standardowego SBQL.
Wartość obiektu $terminal jest ciągiem znaków reprezentującym tekstową formę operatora.
W czasie kontroli typów nazwa ta stanie się nazwą bindera wkładanego do sekcji bazowej
statycznego ENVS. Obiekt $restype jest wskaźnikiem na obiekt deklaracji prostego typu danych,
określającego typ wartości zwracanej po zastosowaniu operatora. Obiekt $ltype jest wskażnikiem
na obiekt deklarujący prosty typ danych dopuszczalny dla lewego operandu, a obiekt $rtype -
prawego. Obiekt $lcrc jest wskażnikiem na obiekt deklarujący prosty typ danych, określający typ
do którego musi nastąpić koercja lewego operandu aby możliwe było zastosowanie operatora.
138
Obiekt $rcrc ma takie samo znaczenie, ale w stosunku do prawego operandu. Obiekt $opcode jest
kodem operacji kodu pośredniego reprezentującego dany operator.
$kind (I)
$terminal (S)
$restype (P)
$ltype (P)
$rcrc (P)
$rtype (P)
$lcrc (P)
$opcode (I)
nazwa operatora (C)
Operatory unarne
Obiekty te reprezentują proste operatory unarne, których operandami są wartości typu prostego.
Tworzone są automatycznie podczas operacji tworzenia bazy danych w metabazie modułu sys, jako
część środowiska standardowego SBQL.
$kind (I)
$terminal (S)
$restype (P)
$argtype (P)
$opcode (I)
nazwa operatora (C)
Wartość obiektu $terminal jest ciągiem znaków reprezentującym tekstową formę operatora.
W czasie kontroli typów nazwa ta stanie się nazwą bindera wkładanego na stos. Obiekt $restype jest
wskażnikiem na obiekt MBPrimitiveType określający typ wartości zwracanej po zastosowaniu
operatora. Obiekt $argtype jest wskażnikiem na obiekt MBPrimitiveType określającym
dopuszczalny typ operandu. Obiekt $opcode jest kodem operacji kodu pośredniego
reprezentującego dany operator.
Typy proste
Obiekty te reprezentują typy proste wbudowane w język SBQL i tworzone są automatycznie
podczas operacji tworzenia bazy danych w metabazie modułu sys jako część środowiska
standardowego.
139
$kind (I)
$typeid (I)
nazwa typu (C)
Wartość obiektu $typeid oznacza specyficzny numer oznaczający wbudowany typ prosty (1 -
integer, 2 - real, itd.).
Interfejs użytkownikaSystem Odra wyposażony jest w dwa narzędzia administracyjne. Pierwsze z nich jest interfejsem
linii poleceń o nazwie Odra CLI (Odra Command-Line Interface). Narzędzie to pozwala
w interaktywny sposób wprowadzać instrukcje i zapytania języka SBQL, a także obserwować
wyniki ich działania. Oprócz tego, dostępne są w nim polecenia administracyjne umożliwiające np.
zatrzymanie serwera bazy danych. Specjalna grupa instrukcji create/drop pozwala tworzyć i usuwać
obiekty bazy danych.
Rys. 20: Narzędzia administracyjne systemu Odra
W drzewie obiektów bazy danych przechodzi się w podobny sposób, jak w narzędziach linii
poleceń systemów operacyjnych. Przykładowo, polecenie cm (od change module) pozwala na
zmianę bieżącego modułu, a polecenie ls (od list) wyświetla listę obiektów metabazy bieżącego
modułu.
140
Oprócz tego, zbudowane zostało przez nas również drugie narzędzie, którego zadaniem jest
realizacja podstawowych operacji administracyjno-programistycznych w środowisku graficznym.
Okno nawigatora przedstawia hierarchiczną strukturę bazy danych, okno edytora kodu pozwala
wprowadzać kod źródłowy modułów.
Rys. 21: Narzędzia administracyjne systemu Odra
PodsumowanieW rozdziale omówiliśmy szczegóły techniczne związane z funkcjonowaniem prototypowego
systemu Odra. Omówiliśmy architekturę systemu, organizację modularną bazy danych, kod
pośredni oraz przyjęty przez nas sposób reprezentowania różnorodnych bytów programistycznych
języka SBQL w ramach bazy danych. Rozdział zakończyliśmy prezentując proste narzędzia
administracyjno-programistyczne jakie zostały stworzone na potrzeby systemu.
141
Podsumowanie pracy
W pracy zaprezentowaliśmy wizję nowoczesnego narzędzia programistycznego przeznaczonego do
tworzenia obiektowych baz danych i ich aplikacji w środowisku rozproszonym oraz
scentralizowanym. Przedstawiliśmy naszą opinię mówiącą o tym, iż każda próba rozszerzenia
języka programowania takiego jak Java o obsługę baz danych skazana jest z góry na klęskę.
W rzeczywistości satysfakcjonujący stan integracji języków programowania z bazami danych może
być osiągnięty jedynie poprzez zaprojektowanie nowej klasy języków programowania, traktujących
kolekcje jako możliwe rezultaty wyrażeń (traktowanych jako zapytania) i dostarczających
operatorów umożliwiających makroskopowe przetwarzanie takich danych. Naszym zdaniem język
taki powinien również zostać rozszerzony o specjalne konstrukcje zapewniające komunikację
między aplikacjami pracującymi w środowisku rozproszonym. Okazuje się bowiem, iż
przetwarzanie danych masowych za pomocą tradycyjnego middleware prowadzi do niepotrzebnych
komplikacji owocujących niestabilnością oprogramowania oraz jego niską wydajnością. Naszym
zdaniem problemy te może zniwelować język zapytań oraz middleware w postaci systemu
zarządzania rozproszoną bazą danych.
Jako dowód koncepcji będących wynikiem badawczym niniejszej pracy, zbudowaliśmy system
Odra - prototypowy system zarządzania bazą danych wyposażony w obiektowy język
programowania zintegrowany z konstrukcjami programistycznymi. W ramach pracy doktorskiej nie
jest możliwe zbudowanie kompletnego narzędzia tego typu, gdyż jest to zadanie dla bardzo dużego
zespołu dysponującego budżetem. Dlatego poprzestaliśmy jedynie na kluczowych zagadnieniach.
Niezbędne są dalsze prace koncypcyjne oraz implementacyjne zmierzające do wyposażenia
143
systemu w wiele niezbędnych mechanizmów, np. transakcji, czy bezpieczeństwa. Prace te będą
kontynuowane w ramach projektów VIDE oraz eGovBus, realizowanych przez PJWSTK.
144
Dodatek A: Przykładowy program SBQL
Poniżej przedstawiamy przykładowy prosty, samodzielny program SBQL. Jest to implementacja
popularnego schematu pracownik-dział, znanego zwłaszcza z systemów relacyjnych. W obecnej
postaci aplikacja ta ma charakter scentralizowany, a interfejsem użytkownika jest konsola.
Wykorzystując komponenty GUI przygotowane dla systemu Odra w ramach osobnego projektu,
aplikacja może zostać rozszerzona o graficzny interfejs użytkownika. Innym scenariuszem
wykorzystania jest wykorzystanie tradycyjnego scenariusza klient-serwer i wykorzystanie JDBC do
podłączenia aplikacji napisanej w języku Java.
module firma { type tpracownik is record { imie : string; nazwisko : string; stanowisko : string; pensja : integer; pracuje : ref dzial; }
type tdzial is record { nazwa : string; lokalizacja : string; zatrudnia : ref prac [0..*]; }
prac : tpracownik [0..*];
145
dzial : tdzial [0..*];
inicjalizacja() { d1 : ref dzial := create ( “Sprzedaz” as nazwa, “Warszawa” as lokalizacja ) as dzial;
d2 : ref dzial := create ( “Badania” as nazwa, “Warszawa” as lokalizacja ) as dzial;
d3 : ref dzial := create ( "Produkcja” as nazwa, “Poznan” as lokalizacja ) as dzial;
p1 : ref prac := create ( “Jan” as imie, “Kowalski” as nazwisko, “Programista” as stanowisko, 14000 as pensja, d3 as pracuje ) as prac;
p2 : ref prac := create ( “Jozef” as imie, “Mlynarski” as nazwisko, “Programista” as stanowisko, 12000 as pensja, d2 as pracuje ) as prac;
p3 : ref prac := create ( “Grzegorz” as imie, “Lubczyk” as nazwisko, “Sprzedawca” as stanowisko, 850 as pensja, d1 as pracuje ) as prac;
p4 : ref prac := create ( “Stefan” as imie,
146
“Nowak” as nazwisko, “Sprzedawca” as stanowisko, 800 as pensja, d1 as pracuje ) as prac;
d1.(create p4 as zatrudnia); d1.(create p3 as zatrudnia); d2.(create p2 as zatrudnia); d3.(create p1 as zatrudnia); }
main() { // wyszukanie wszystkich pracownikow zarabiajacych > 100 print prac where pensja > 100;
// drugi, trzeci, piąty pracownik pod wzgledem najlepiej zarabiajacych print (prac orderby pensja desc).(bag 2, 3, 5);
// pracownicy zatrudnieni w dziale Sprzedaz print (dzial where nazwa = “Sprzedaz”).zatrudnia.prac;
// departamenty w ktorych nie pracuje zaden programista print dept where not exists (zatrudnia.prac where stanowisko = “Programista”); }
}
148
Dodatek B:Kod źródłowy zintegrowanegosystemu połączeń kolejowych
Kod źródłowy modułu pkp.app.connections (system A)module connections { type conntype is record { from : string; to : string; duration : integer; price : integer; depart_date : record { d : integer; m : integer; } depart_time : record { h : integer; m : integer; } }
connection : conntype [0..*];}
Kod źródłowy modułu db.schedule.timetable (system B)module timetable { type linktype is record { dep_hour : integer; dep_minutes : integer; duration : integer; price : integer; from : ref location; to : ref location; }
149
type locationtype is record { name : string; country : string; }
link : linktype [0..*]; location : loctype [0..*];}
Perspektywa kontrybucyjna systemu Amodule public { import pkp.app.connections;
type connection_type is record { from : string; to : string; duration : integer; price : integer; hour : integer; min : integer; day : integer; month : integer; }
pln_2_eur(pln : integer) : integer { // wywołanie zewnetrznej uslugi posiadajacej dostep do aktualnego kursu walut // i przeliczenie za jego pomoca sumy w PLN do EUR }
view connections_view { virtual objects connections : con(ref connection) [0..*] { return connection as con; }
on retrieve : connection_type { return connection.( from, to, duration, pln_2_eur(price), depart_time.hour, depart_time.min, depart_date.day, depart_date.month); }
view from_view { virtual objects from : v(ref connection.from) { return con.from as v; }
150
on retrieve : string { return v; } }
view to_view { virtual objects to : v(ref connection.to) { return con.to as v; }
on retrieve : string { return v; } }
// podobnie dla pozostalych pol typu conn_type // ...
view price_view { virtual objects price : v(ref connection.price) { return con.price as v; }
on retrieve : string { return pln_2_eur(v); } }
view hour_view { virtual objects hour : v(ref connection.depart_time.h) { return con.depart_time.h as v; }
on retrieve : string { return v; } }
// podobnie dla pozostalych pol typu conn_type // ... }}
Perspektywa kontrybucyjna systemu Bmodule pub { import odra.time; import db.schedule.timetable;
type connection_type is record { from : string; to : string;
duration : integer; price : integer; hour : integer; min : integer; day : integer; month : integer; }
view connections_view { virtual objects connections : link(ref link) [0..*] { return link as link; }
on retrieve : connection_type { return link.( from.location.name, to.location.name, duration, price, dep_hour, dep_min, get_current_day_of_month(), // implementacja w odra.time get_current_month()); // implementacja w odra.time }
view from_view { virtual objects from : v(ref link.from.name) { return link.from.location.name as v; }
on retrieve : string { return v; } }
view to_view { virtual objects to : v(ref link.to.name) { return link.to.location.name as v; }
on retrieve : string { return v; } }
// podobnie dla pozostalych pol typu conn_type // ...
view price_view { virtual objects price : v(ref link.price) { return link.price as v; }
152
on retrieve : string { return v; } }
view hour_view { virtual objects hour : v(ref link.dep_hour) { return link.dep_hour as v; }
on retrieve : string { return v; } }
// podobnie dla pozostalych pol typu conn_type // ... }}
Perspektywa integracyjnamodule schedules { dblink systema pkp.app.public/[email protected]:8888; dblink systemb db.apps.pub/[email protected]:7777; dblink systemsab using systema, systemb;
type conn_type is record { from : string; to : string; duration : integer; price : integer; hour : integer; min : integer; day : integer; month : integer; }
view connections_view { virtual objects connections : con(ref systemsab.connection) [0..*] { return systemsab.connections as con; }
on retrieve : conn_type { return con; }
view from_view { virtual objects from : v(ref con.from) { return con.from as v; }
on retrieve : string { return v; } }
view to_view { virtual objects to : v(ref con.to) { return con.to as v; }
on retrieve : string { return v; } }
view duration_view { virtual objects duration : v(ref con.duration) { return con.duration as v; }
on retrieve : string { return v; } }
// podobnie dla pozostalych pol typu conn_type // ... }}
Aplikacja klienckamodule testapp { dblink db schedules/[email protected]:8888;
main() { print db.connections where from = “Katowice” and to = “Warszawa”; }}
154
Dodatek C:Ewaluacja przykładowego zapytania SBQL
Poniżej przedstawiamy poszczególne kroki ewaluacji prostego zapytania zgodnego
z przedstawionymi wyżej założeniami. Zapytaniem tym jest Employee where Name = “J. Smith” and
salary > 10000. Zapytanie to powinno zwrócić identyfikatory wewnętrzne (referencje) wszystkich
obiektów bazy danych reprezentujących pracowników zarabiających więcej niż 10000. W naszym
przykładzie zapytanie to wykonywane jest dla bazy danych przedstawionej poniżej:
<i0, entry, <i1, Employee, <i4, Name, “J. Smith”> <i5, Salary, 65000> > <i2, Employee, <i6, Name, “S. Bush”> <i7, Salary, 45000> > <i3, Department, <i8, Name, “Sales”> <i9, Location, “London”> >>
155
Employee(i1), Employee(i2), Department(i3)
1. Initialize ENVS and QRES
2. Execute bind Employee
3. Pop one element from QRES
5. Execute bind Name
Employee(i1), Employee(i2), Department(i3)
Employee(i1), Employee(i2), Department(i3)
Name(i4), Salary(i5)
Employee(i1), Employee(i2), Department(i3)
6. Push "J. Smith"
Employee(i1), Employee(i2), Department(i3)
Name(i4), Salary(i5)
7. Pop two elements, dereference i4, compare "J. Smith", and "J. Smith", push true
Employee(i1), Employee(i2), Department(i3)
Name(i4), Salary(i5)
8. Execute bind "Salary"
Employee(i1), Employee(i2), Department(i3)
Name(i4), Salary(i5)
9. Push 10000
Employee(i1), Employee(i2), Department(i3)
Name(i4), Salary(i5)
10. Pop two elements, dereference i4, compare "J. Smith", and "J. Smith", push true
Employee(i1), Employee(i2), Department(i3)
Name(i4), Salary(i5)
11. Pop two elements, dereference i4, compare them, push true
Employee(i1), Employee(i2), Department(i3)
4. Create a new ENVS section. Execute nested i1
Employee(i1), Employee(i2), Department(i3)
Name(i4), Salary(i5)
12. Pop one element, since the value is true, add i1 to eres. Remove one section from ENVS.
Employee(i1), Employee(i2), Department(i3)
14. Execute bind Name
Employee(i1), Employee(i2), Department(i3)
Name(i6), Salary(i7)
15. Push "J. Smith"
Employee(i1), Employee(i2), Department(i3)
Name(i6), Salary(i7)
16. Pop two elements, dereference i6, compare "S. Bush", and "J. Smith", push false
Employee(i1), Employee(i2), Department(i3)
Name(i6), Salary(i7)
17. Execute bind Salary
Employee(i1), Employee(i2), Department(i3)
Name(i6), Salary(i7)
18. Push 10000
Employee(i1), Employee(i2), Department(i3)
Name(i6), Salary(i7)
19. Pop two elements, dereference i7, compare 10000 and 45000, push true
Employee(i1), Employee(i2), Department(i3)
Name(i6), Salary(i7)
20. Pop two elements, compare them, push false
Employee(i1), Employee(i2), Department(i3)
13. Create a new ENVS section. Execute nested i2
Employee(i1), Employee(i2), Department(i3)
Name(i6), Salary(i7)
21. Pop one element, since the value is false, do not add i2 to eres. Remove one section from ENVS.
Employee(i1), Employee(i2), Department(i3)
bag(i1, i2)
i4
i4
"J. Smith"
true
true
i5
true
i5
10000
true
true
true
i6
false
i6
"J. Smith"
false
i7
false
i7
10000
false
true
false
22. Push eres onto QRES
Employee(i1), Employee(i2), Department(i3) i1
156
Dodatek D:Przetwarzanie danych XML za pomocą SBQL
Język XML [18] stał się standardem zapisu i wymiany informacji w Internecie. Dotyczy to
zwłaszcza narzędzi przeznaczonych do integracji danych i aplikacji. Przykładowo, w niektórych
systemach ESB (Enterprise Service Bus), XML wykorzystywany jest m.in. do budowy
komunikatów przenoszonych następnie pomiędzy usługami sieci Web, gdzie są analizowane,
przekształcane i wysyłane dalej. Operacje te realizowane są przy współudziale dodatkowych
języków (np. XSLT, XMLSchema, XQuery i in.) i programowane w typowych językach niskiego
poziomu (np. C#) lub językach skryptowych (np. JavaScript).
Jako iż SBA od samego początku swego istnienia zaprojektowane zostało m.in. do obsługi danych
półstrukturalnych, naturalna jest zatem próba zastosowania języka SBQL do przetwarzania danych
XML. Dotyczy to zarówno operacji związanych z językiem zapytań, jak i statycznej kontroli typów,
transformacji danych i in. Zadanie to jest o tyle łatwe, iż model danych M0 w podejściu stosowym
bardzo przypomina model danych XML.
SBA może zostać wykorzystane do stworzenia języka dostosowanego składniowo do XML,
podobnego np. do XQuery [19]. Podejście zaprezentowane w niniejszej pracy (oraz kilku
wcześniejszych prototypów zbudowanych przez autora) jest jednak inne. Celem było osiągnięcie
157
takiej sytuacji, by na danych XML można było pracować za pomocą dokładnie takich samych
konstrukcji, jakich używamy do operowania na “zwykłych” danych. Dążymy do tego, aby XML
widoczny był jedynie na wejściu (import) oraz na wyjściu (export, wyświetlanie rezultatów
zapytań) systemu Odra. Po zaimportowaniu dokumentu XML, zawarte w nim dane nie różnią się
wewnętrznie od typowych danych systemu Odra. Choć SBQL może działać na danych
półstrukturalnych bez wiedzy dostępnej podczas kompilacji na temat ich struktury, my zakładamy
iż każdy przetwarzany dokument posiada odpowiadającą mu definicję typu.
Import dokumentu XML polega na wskazaniu pliku z danymi oraz zadeklarowaniu zmiennej, na
którą przypisany zostanie rezultat zwrócony z modułu importującego te dane. Istnieje również
możliwość automatycznego wygenerowania odpowiedniego typu danych poprzez odzwierciedlenie
danych ze wskazanego schematu XML na definicje typów SBQL (zostało to zrealizowane
w ramach osobnego podprojektu).
Przyjęto następującą formę odwzorowania modelu danych XML na model M0:
• każdy element XML jest obiektem złożonym w sensie modelu M0,
• każdy atrybut elementu XML jest reprezentowany jako obiekt prosty, którego nazwa poprzedzona
jest znakiem @,
• każdy węzeł tekstowy jest reprezentowany jako obiekt prosty, którego nazwa poprzedzona jest
znakiem #.
Znaki @ i # nie mają żadnej semantyki dla kompilatora zapytań. Nie są to operatory, a jedynie
zwykłe znaki będące częścią nazwy obiektu. Interpretowane są one wyłącznie przez moduły
generujące dokumenty XML na podstawie zawartości bazy danych/rezultatów zapytań.
Powyższe założenia nie wspierają oczywiście bezpośrednio bardziej zaawansowanych konstrukcji
XML, np. przestrzeni nazw, encji, sekcji CDATA, i in. Wprowadzenie takich konstrukcji nie wydaje
się jednak zadaniem specjalnie złożonym.
Jako przykład rozpatrzmy następujący dokument:
<a> b <c d=”xxx”>e</c> f <g>aaa</g></a>
Taki dokument wyrażony może być w składzie danych za pomocą następującego zbioru obiektów:
158
<i0, “a”, <i1, “#text1”, “b”> <i2, “c”, <i5, “@d”, “xxx”> <i6, “#text”, “e”> > <i3, “#text2”, “f”> <i4, “g”, <i7, “#text”, “aaa”> >>
W zewnętrznym systemie typów typ reprezentujący ten dokument można zadeklarować
w następujący sposób:
type doctype is record { #text1 : string; c : record { @d : string; #text : string; } #text2 : string; g : #text(string);}
Pozwolimy sobie w tym miejscu zaznaczyć, iż dereferencja wartości nazwanej nazwą pomocniczą
zwraca dokładnie taki sam rezultat jak dereferencja struktury z pojedynczym polem. Oznacza to, że
poniższe deklaracje są całkowicie równoważne:
zmienna1 : record { pole : integer; } zmienna2 : pole(integer);
Wartość będąca rezultatem dereferencji obiektu typu doctype o stanie odpowiadającym
przedstawionemu wyżej dokumentowi przyjmie następującą postać:
a(struct { #text1(“b”), c(struct { @d(“xxx”), #text(“e”) }), #text2(“f”), g(#text(“aaa”)) }
Dokładnie taką samą wartość można uzyskać formułując zapytanie przy użyciu literałów oraz kilku
typowych operatorów. Rezultat takiego zapytania może być przekazany do procedury, podany jako
prawa strona operatora podstawiania, itp. Przykładowo, w następujący sposób zadeklarować można
zmienną doc typu doctype, utworzyć jej nowy egzemplarz oraz przypisać mu odpowiednią wartość:
159
doc : doctype [0..*];create (“b” as #text1, (“xxx” as @d, “e” as #text) as c, “f” as #text2, “aaa” as #text as g) as a as doc;
Realizując operację odwrotną, dowolny wynik zapytania można w łatwy sposób zamienić na tekst
zgodny składniowo z XML.
Poprzez kombinację opisanych wyżej cech razem z operatorami pomocniczej nazwy as i groupas
łatwo również można przekształcić jeden dokument w inny. Przypomnijmy, że operator as
wprowadza nazwę pomocniczą dla każdej wartości kolekcji będącej jego argumentem. Operator
groupas działa podobnie, ale nazwę pomocniczą wprowadza dla całego rezultatu niezależnie od
jego rodzaju. Oznacza to zatem, iż przykładowo:
(bag 1, 2, 3) as x zwraca bag { x(1), x(2), x(3) }, czyli
<x>1</x><x>2</x><x>3</x>
(bag 1, 2, 3) groupas x zwraca x(bag { 1, 2, 3 }), czyli
<x> 1 2 3</x>
Ze względu na “płaską” strukturę plików XML, zarówno pola struktur, jak i elementy kolekcji
wyświetlane są w dokumentach XML w podobny sposób. Oznacza to, że podobnie jak
w powyższym przykładzie, również rezultat poniższego zapytania w postaci XML jest podobny:
(1, 2, 3) as x zwraca x(struct { 1, 2, 3 }), czyli
<x> 1 2 3</x>
Rozpatrzmy teraz następujący dokument wejściowy:
<books> <book> <title>Ogniem i mieczem</itle> <author_first>Henryk</author_first> <author_last>Sienkiewicz</author_last> <isbn>434-3235-3323</isbn>
160
</book> <book> <title>Lalka</itle> <author_first>Boleslaw</author_first> <author_last>Prus</author_last> <isbn>433-3235-3213</isbn> </books> </books>
Dokument taki może zostać zaimporowany do bazy danych z zewnętrznego źródła. Jeśli podany
zostanie dodatkowo schemat XML, wówczas obok obiektów reprezentujących listę książek, na jego
podstawie utworzone mogą być również odpowiednie typy oraz zmienne. Jeśli obiekt
reprezentujący daną zmienną zostanie poddany dereferencji, wówczas jego wynik tożsamy będzie
z wynikiem następującego zapytania:
( (“Ogniem i mieczem” as #text as title, “Henryk” as #text as author_first, “Sienkiewicz” as #text as auhor_last, “434-3235-3323” as #text as isbn) as book union (“Lalka” as #text as title, “Boleslaw” as #text as author_first, “Prus” as #text as author_last, “433-3235-3213” as #text as isbn) as book ) groupas books;
Aby przekształcić ten rezultat do poniższej postaci (typowe zastosowanie XSLT):
<html> <head> <title>Lista ksiazek</title> </head> <body> ksiazka: <book>“Ogniem i mieczem”</book>,↵ autor: <author>Henryk Sienkiewicz</author><br/> ksiazka: <book>“Lalka”</book>,↵ autor: <author>Boleslaw Prus</author><br/> </body> </html>
wystarczy następujące zapytanie:
books.( “Lista ksiazek” as #text as title as head, (book.(“ksiazka: \”“ as #some_text1, title.#text as #title as book, “\”, autor: “ as #some_text2, (author_first.#text + “ “ + author_last.#text) as #text as author),
161
“” as br) groupas body) ) as html;
Przekształcenia takie można realizować także w formie bardziej typowych programów SBQL.
Poniższy program realizuje tę samą operację co przedstawione wyżej zapytanie. Dostępna jest tutaj
pełna moc obliczeniowa SBQL.
module trans_test { // typ reprezentujacy dokument wejsciowy type input_doc is record { books : book(input_book) [0..*]; }
// typ reprezentujacy ksiazke w dokumencie wejsciowym type input_book is record { title : #text(string); author_first : #text(string); author_last : #text(string); isbn : #text(string); }
// typ represzentujacy dokument wyjsciowy type output_doc is { html : record { head : record { title : string := “Lista ksiazek”; }
body : record { book : output_book [0..*]; } } }
// typ reprezentujacy ksiazke w dokumencie wyjsciowym type output_book is { #intro : string; book : #text(string); #writtenby : string; author : #text(string); br : string; }
// przeksztalcenie ksiazki z dokumentu wejsciowego // do ksiazki z dokumentu wyjsciowego transform_book( book : input_book ) : output_book { return book.( “ksiazka: \”“ as #text as intro, title.#text as book, “\”, autor: “ as #text as writtenby, (author_first.#text + “ “ + author_last.#text) as author);
162
}
// przeksztalcenie dokumentu wejsciowego w dokument wyjsciowy transform_doc( books : input_doc ) : output_doc { output : output_doc;
foreach (input_doc.books.book as b) output.html.body.(create transform_doc(b) as book);
return output_doc; }}
W podobny sposób wyobrazić można sobie zastosowanie aktualizowalnych perspektyw SBQL do
transformacji danych danych XML. Zamiast materializacji danych, podejście oparte na
perspektywach umożliwia dynamiczne generowanie nowych dokumentów, jak i ich
modyfikowanie w taki sposób, by każda zmiana była automatycznie odzwierciedlona
w dokumencie źródłowym.
Jak widać, SBQL daje praktycznie nieograniczone możliwości w zakresie przetwarzania danych
XML bez konieczności wprowadzania specjalnej składni charakterystycznej dla narzędzi
przetwarzających XML. Naturalnie specjalna składnia w każdej chwili może zostać wprowadzona
(być może nawet w ramach tego samego narzędzia), jednak użyte w niej konstrukcje powinny być
odpowiednio tłumaczone na wewnętrzne operacje podejścia stosowego.
Przykładowo, do składni SBQL można by wprowadzić konstrukcję nazwa(zapytanie) równoważną
z zapytaniem zapytanie as nazwa. Przy tym założeniu, operację utworzenia nowego egzemplarza
zmiennej doc (zdefiniowanej wcześniej) można byłoby wyrazić w nieco czytelniejszej formie:
create doc( a( #text1(“b”), c(@d(“xxx”), #text(“e”)), #text2(“f”), g(#text(“aaa”)) ));
Inna możliwość, to bezpośrednie wprowadzenie do składni SBQL składni XML. Pomimo innej
składni, zaprezentowana niżej operacja powinna być semantycznie równoważna z operacją podaną
powyżej:
create <a> b <c d = “xxx”>e</c>
163
f <g>aaa</g> </a> as doc;
Generalnie, przedstawiony wyżej schemat postępowania z danymi XML jest tylko jednym z wielu
możliwych podejść do tego samego tematu. Podstawową trudnością z danymi XML jest takie ich
reprezentowanie za pomocą modelu danych M0 podejścia stosowego, by zachować możliwie
największy stopień funkcjonalności SBQL przy jednoczesnej łatwości obsługi i możliwie
największym zbiorze struktur XML dostępnych dla języka zapytań. “Bezinwazyjne” przetwarzanie
danych XML za pomocą języka SBQL jest jak najbardziej możliwe przy zachowaniu elegancji
rozwiązania niemożliwej do osiągnięcia przez relacyjne bazy danych (np. w XML DB firmy
Oracle).
164
Dodatek E:Ważniejsze operacje maszyny wirtualnej SBQL
Poniżej przedstawiono listę operacji kodu bajtowego. Opis każdej operacji składa się z jej
tekstowego objaśnienia oraz tabelki przedstawiającej parametry zapisane razem z operacją,
pobierane przez nią argumenty ze stosu QRES, jak i rezultat jaki wkładany jest na ten stos.
parametr operacji
argument zdejmowany z QRES
argument nie zdejmowany z QRES
rezultat operacji wkładany na QRES
Stos rezultatów
dup (duplicate)
Kopiuje szczytowy element stosu QRES bez jego zdjemowania.
Result Result
165
ldi, ldd (load integer, load double)
Ładuje wartość całkowitą lub rzeczywistą na stos rezultatów.
Integer IntegerResult
Double DobuleResult
ldt, ldf (load true, load false)
Umieszcza true lub false na szczycie stosu rezultatów.
BooleanResult
ldn (load null)
Umieszcza referencję z wartością null na szczycie stosu rezultatów.
ReferenceResult
lda (load address)
Umieszcza na szczycie stosu rezultatów referencję do trwałego obiektu przechowywanego w bazie danych pod adresem
podanym jako argument. Operacja ta zawzwyczaj umieszczana jest przez moduł ładujący kod do interpretera zamiast
operacji ldfn i ldmn gdy znana jest dokładna lokalizacja obiektów wskazywanych przez te operacje za pomocą nazw.
Integer ReferenceResult
ldsc (load string constant)
Umieszcza na szczycie stosu QRES wartość pobraną z puli stałych, przechowywaną tam pod indeksem podanym jako
parametr.
Integer StringResult
Stos środowiskowy
crenv (create environment)
Tworzy nową sekcję na stosie środowiskowym. Statyczny link będzie wskazywał na poprzednią (po utworzeniu nowej)
sekcję.
crgenv (create global environment)
Tworzy nową sekcję na stosie środowiskowym. Statyczny link będzie równy null, czyli bindery znajdujące się
w sekcjach poniżej tworzonej sekcji nie będą mogły być związane.
dsenv (destroy environment)
Usuwa szczytową sekcję ze stosu środowiskowego.
166
nstd (nested)
Realizuje operację nested, a jej wynik (jeśli istnieje) umieszcza w szczytowej sekcji stosu środowiskowego.
Result
bnd (bind)
Realizuje operację bind, umieszczając jej wynik na stosie rezultatów. Parametrem jest nazwa przechowywana
w indeksie nazw.
Integer Result
crbnd (create binder)
Tworzy nowy binder w szczytowej sekcji stosu ENVS. Binder posiada nazwę taką, jak parametr operacji, a wartością
bindera jest wartość pobrana ze stosu QRES.
Integer Result BinderResult
elvb (enter local variable binder)
Wkłada na stos środowiskowy binder do zmiennej lokalnej identyfikowanej w lokalnym składzie danych pod numerem
będącym parametrem. Adres korzenia lokalnego składu danych w którym przechowywane są obiekty bieżącego
środowiska pobierany jest ze szczytowej sekcji stosu środowiskowego.
Integer
rmb (remove binder)
Wiąże nazwę na stosie środowiskowym, a następnie usuwa z niego wszystkie znalezione bindery. Parametrem jest
nazwa przechowywana w indeksie nazw.
Integer
ldcntr (load counter)
Ładuje na QRES zawartość licznika przechowywanego w szczytowej sekcji ENVS.
IntegerResult
stcntr (store counter)
Ustawia licznik przechowywany w bieżącej sekcji stosu środowiskowego na wartość pobraną ze szczytu stosu QRES.
IntegerResult
inccntr (increment counter)
Zwiększa o 1 wartość licznika pochodzącego z bieżącej sekcji stosu środowiskowego.
deccntr (decrement counter)
Zmniejsza o 1 wartość licznika pochodzącego z bieżącej sekcji stosu środowiskowego.
Skład danycheimpo (enter imported objects)
Umieszcza w szczytowej sekcji stosu środowiskowego referencje do obiektów $data modułów importowanych przez
moduł którego referencję pobiera ze stosu rezultatów. Referencje te pobierane są z podobiektów $linkedimports
bieżącego modułu. Referencję modułu można umieścić na QRES za pomocą ldmn.
ReferenceResult
crlse (create local store entry)
Tworzy nowy obiekt złożony $locobj w stercie i zapisuje jego referencję w specjalnym polu szczytowej sekcji stosu
ENVS. Do obiektu tego podłączane będą obiekty lokalne (np. zmienne deklarowane w procedurach). Operacja zwraca
referencję utworzonego w ten sposób obiektu.
ReferenceResult
ldlse (load local store entry)
Ładuje na QRES referencję do obiektu $locobj przechowywanego w stercie i utworzonego za pomocą crlse. Referencja
ta odczytywana jest ze specjalnego pola w szczytowej sekcji stosu środowiskowego.
ReferenceResult
crsse (create session store entry)
Tworzy w stercie obiekt złożony $sesobj oraz zwraca jego referencję. Oprócz tego, referencja ta wraz z referencją do
bieżącego modułu (będącą argumentem operacji) są zapamiętywane w specjalnej zmiennej sesobj, przechowywanej
przez proces SVRP w obiekcie reprezentującym sesję. Umożliwia to później umieszczenie na stosie ENVS binderów do
obiektów sesyjnych zdefiniowanych w module o określonym adresie. Operacja umieszczana jest zwykle w procedurze
systemowej $initso.
ReferenceResult ReferenceResult
ldsse (load session store entry)
Umieszcza w szczytowej sekcji stosu środowiskowego referencję do korzenia obiektów sesyjnych modułu, którego
adres podany został jako argument. Przy pierwszym odwolaniu do tego modułu w ramach bieżącej sesji, obiekty
sesyjne są automatycznie tworzone (przez wywołanie procedury $initso, zawierającej kod odpowiedzialny za
utworzenie zmiennych sesyjnych).
168
ReferenceResult ReferenceResult
ldfn (load field by name)
Ładuje na QRES referencję do obiektu globalnego jakiegoś modułu. Obiekt wyszukiwany jest wg nazwy składającej się
z globalnej nazwy modułu oraz nazwy obiektu rozdzielonych znakiem #. Podczas ładowania kodu do interpretera (czyli
wówczas, gdy adres obiektu jest już znany), nazwa ta zamieniana jest na właściwą referencję, a operacja ldfn
zastępowana jest przez lda.
String ReferenceResult
ldmn (load module by name)
Ładuje na QRES referencję do modułu o globalnej nazwie podanej jako parametr. Podczas ładowania kodu do
interpretera, nazwa zamieniana jest na właściwą referencję, a operacja ldmn zastępowana jest przez lda.
String ReferenceResult
ldmen (load module entry by name)
Działa podobnie jak ldmn, ale na QRES ładowana jest referencja do obiektu $data modułu którego nazwa jest
argumentem.
String ReferenceResult
Operacje na obiektach
dynder (dynamic dereference)
Realizuje operację dereferencji nie znając rodzaju obiektu, na którym będzie realizowana (sprawdza to w czasie
wykonania). Jeśli argument nie jest obiektem ReferenceObject, wówczas nic się nie dzieje.
Result Result
deri, derd, ders, derb, derr, derc (dereference integer, double, string, boolean, reference, complex).
Realizuje operację dereferencji znając rodzaj obiektu na którym operacja będzie realizowana. Przykładowo, operacja
deri zakłada że referencja prowadzi do obiektu INTEGER_OBJECT, zatem może łatwo odczytać wartość tego obiektu
i umieścić na stosie QRES obiekt IntegerObject z tą wartością. Operacja derc (dereferencja obiektu złożonego)
umieszcza na stosie QRES obiekt StructResult składający się z binderów o nazwach będących nazwami podobiektów
danego obiektu złożonego oraz wartościach będących wartościami tych obiektów. Jeśli na tym samym poziomie
hierarchii znajduje się większa niż 1 obiekt o tej samej nazwie, wówczas wartością bindera jest bag z wartościami
będącymi wartościami tych obiektów.
ReferenceResult Result
sti, sts, std, stb, str (store integer, string, boolean, reference)
Zapisuje wartość integer, string, boolean lub referencję w obiekcie którego referencję podano jako pierwszy argument.
ReferenceResult IntegerResult
ReferenceResult StringResult
ReferenceResult DoubleResult
ReferenceResult BooleanResult
ReferenceResult ReferenceResult
ststr (store struct)
Przypisuje wartości podane w strukturze będącej drugim argumentem podobiektom obiektu którego referencję podano
jako pierwszą wartość. Struktura musi mieć odpowiedni format, tzn. składać się z binderów posiadających takie nazwy,
jak nazwy podobiektów (patrz kopiowanie strukturalne opisane w rozdziale o SBQL).
ReferenceResult StructResult
cri, crs, crd, crb, crc, crr, cra (create integer, string, double, boolean, complex, reference, aggregate)
Tworzy obiekty INTEGER_OBJECT, STRING_OBJECT, DOUBLE_OBJECT, BOOLEAN_OBJECT,
COMPLEX_OBJECT, REFERENCE_OBJECT, AGGREGATE_OBJECT. Obiekt posiadał będzie nazwę o
identyfikatorze (w indeksie nazw) podanym jako parametr. Argumentem jest referencja do obiektu nadrzędnego, a
rezultatem referencja do utworzonego obiektu.
Integer ReferenceResult ReferenceResult
ren (rename)
Zmienia nazwę obiektów których referencje podane zostaną jako elementy kolekcji będącej argumentem operacji.
Nazwę zmienia tylko obiekt będący w bazie danych, metabaza pozostaje bez zmian.
Integer CollectionResult<ReferenceResult>
mov (move)
Przenosi obiekty o identyfikatorach będących elementami kolekcji podanej jako pierwszy argument, podłączając je do
obiektu będącego drugim parametrem. Przenoszony jest tylko obiekt będący w bazie danych, metabaza pozostaje bez
zmian. Podczas przenoszenia aktualizowane są również wszystkie referencje wskazujące na przenoszone obiekty w taki
sposób, że po przeniesieniu wskazywać będą na nową lokalizację. Do wyszukania obiektów referencyjnych
170
wykorzystywane są referencje zwrotne przechowywane razem z przenoszonymi obiektami.
CollectionResult<ReferenceResult> ReferenceResult
del (delete)
Usuwa obiekt o identyfikatorze podanym jako argument. Usuwany jest tylko obiekt będący w bazie danych, metabaza
pozostaje bez zmian.
CollectionResult<ReferenceResult>
Operacje sterujące
nop (no operation)
Brak operacji.
bra (branch always)
Realizuje operację skoku bezwarunkowego do pozycji w kodzie bieżącej procedury (licząc od 0) podanej jako parametr
operacji.
Integer
brt, brf (branch if true, branch if false)
Realizuje operację skoku warunkowego do pozycji w kodzie bieżącej procedury (licząc od 0) podanej jako parametr
operacji. Skok realizowany jest tylko wówczas, gdy drugi argument jest równy true.
Integer BooleanResult
call (call)
Wywołuje procedurę wskazywaną przez referencję pobraną ze stosu rezultatów.
ReferenceResult
ret (return)
Umieszcza na QRES pustego baga i kończy wykonywanie procedury.
BagResult
retv (return value)
Kończy wykonywanie procedury. Zakłada że na stosie rezultatów znajduje się rezultat zwracany przez procedurę.
Result
Konwersjei2s, i2d (integer to string, integer to double)
Konwertuje liczbę typu integer pobraną ze szczytu QRES do wartości string lub double.
IntegerResult StringResult
IntegerResult DoubleResult
d2i, d2s (double to integer, double to string)
Konwertuje liczbę double pobraną ze szczytu QRES do wartości integer lub string.
DoubleResult IntegerResult
DoubleResult StringResult
s2i, s2d (string to integer, string to double)
Konwertuje wartość string pobraną ze szczytu QRES do wartości integer lub double.
StringResult IntegerResult
StringResult DoubleResult
b2s (boolean to string)
Konwertuje wartość boolean pobraną ze szczytu QRES do wartości string.
BooleanResult StringResult
dyn2s, dyn2i, dyn2d (dynamic to string, integer, double)
Konwertuje wartość pobraną ze szczytu stosu do wartości string, integer lub double.
SingleResult StringResult
SingleResult IntegerResult
SingleResult DoubleResult
fltn (flatten)
Sprawdza czy argument jest kolekcją jednoelementową. Jeśli tak, wówczas przestaje on być kolekcją. W przeciwnym
razie pozostaje bez zmian.
172
Result SingleResult
bag
Przeciwieństwo operatora fltn. Pobiera rezultat ze stosu QRES i zamienia go na bag. Jeśli na stosie był jeden element,
wówczas bag wynikowy zawiera jeden element. Jeśli na stosie był bag, wówczas nic się nie dzieje.
Result CollectionResult
Nazwy pomocnicze
as
Tworzy bindery na podstawie szczytowej wartości na stosie QRES. Jeśli argumentem jest kolekcja, wówczas jest to
kolekcja binderów. Jeśli argumentem jest pojedyncza wartość, wówczas jest to pojedynczy binder. Bindery posiadają
nazę taką, jaka została podana jako parametr operacji (wartość jest numerem pozycji w indeksie nazw).
Integer Result Result
gas (group as)
Tworzy pojedynczy binder na podstawie szczytowej wartości na stosie QRES. Binder posiada nazę taką, jaka została
podana jako parametr operacji.
Integer Result BinderResult
Operacje na kolekcjach
cnt (count)
Zlicza elementy kolekcji.
CollectionResult<SingleResult> IntegerResult
exts (exists)
Sprawdza czy kolekcja jest pusta, czy nie.
CollectionResult<SingleResult> IntegerResult
extr (extract)
Wybiera element o podanym indeksie z kolekcji wartości.
CollectionResult<SingleResult> IntegerResult SingleResult
ins2 (insert to second)
Wkłada wartość pobraną QRES do kolekcji będącej drugim elementem (po zdjęciu argumentu) na szczycie tego stosu.
CollectionResult<SingleResult> Result SingleResult
sum (sum)
Dodaje do siebie elementy jakiejś kolekcji zawierającej wartości integer lub double. Przed zrealizowaniem operacji
elementy kolekcji muszą zostać poddane dereferencji.
CollectionResult<SingleResult> IntegerResult
in
Sprawdza czy wszystkie elementy jednej kolekcji należą do drugiej kolekcji. Porównywanie takich rezultatów zachodzi
strukturalnie. Przed zrealizowaniem porównania referencje obu kolekcji muszą zostać poddawane dereferencji.
CollectionResult<SingleResult> CollectionResult<SingleResult> IntegerResult
unn (union)
Sumuje dwie kolekcji na zasadzie sumy zbiorów..
CollectionResult<SingleResult> CollectionResult<SingleResult> CollectionResult<SingleResult>
intsct (intersect)
Wylicza część wspólną dwóch kolekcji. Przed zrealizowaniem porównania referencje obu kolekcji muszą zostać
poddawane dereferencji.
CollectionResult<SingleResult> CollectionResult<SingleResult> CollectionResult<SingleResult>
crpd (carthesian product)
Wylicza iloczyn kartezjański dwóch kolekcji.
CollectionResult<SingleResult> CollectionResult<SingleResult> CollectionResult<StructResult>
diff (difference)
Wylicza różnicę dwóch zbiorów. Przed zrealizowaniem porównania referencje obu kolekcji muszą zostać poddawane
dereferencji.
CollectionResult<SingleResult> CollectionResult<SingleResult> CollectionResult<SingleResult>
ordr (order)
Sortuje kolekcję rezultatów. Kolekcja musi składać się ze struktur <SingleResult, SingleResult1, SingleResult2, ...,
SingleResultn>. Obiekty SingleResultx to klucze, wg których będzie realizowane sortowanie. Klucze muszą zostać
poddane uprzedniej dereferencji. Wynikiem jest kolekcja składająca się wyłącznie z pierwszych pól tych struktur.
CollectionResult<StructResult> CollectionResult<SingleResult>
174
unq (unique)
Usuwa powtórzenia z kolekcji, nie znając rodzajów rezultatów w niej przechowywanych. Przed wykonaniem operacji
argument powinien zostać poddany dereferencji.
CollectionResult<SingleResult> CollectionResult<SingleResult>
max (maximum)
Wylicza maksymalną wartość znajdującą się w kolekcji. Kolekcja może zawierać tylko rezultaty będące liczbami
integer, albo double. Jeśli znajduje się w niej choć jeden inny rezultat, wówczas zgłaszany jest błąd czasu wykonania.
Przed wykonaniem operacji argument powinien zostać poddany dereferencji
CollectionResult<SingleResult> CollectionResult<SingleResult>
min (minimum)
Wylicza minimalną wartość znajdującą się w kolekcji. Kolekcja może zawierać tylko rezultaty będące liczbami integer,
albo double. Jeśli znajduje się w niej choć jeden inny rezultat, wówczas zgłaszany jest błąd czasu wykonania. Przed
wykonaniem operacji argument powinien zostać poddany dereferencji
CollectionResult<SingleResult> CollectionResult<SingleResult>
avg (average)
Wylicza średnią rezultatów będących elementami kolekcji. Kolekcja może zawierać tylko rezultaty będące liczbami
typu integer, albo double. Jeśli znajduje się w niej choć jeden inny rezultat, wówczas zgłaszany jest błąd czasu
wykonania. Przed wykonaniem operacji argument powinien zostać poddany dereferencji.
CollectionResult<SingleResult> CollectionResult<SingleResult>
Operacje arytmetyczne i konkatenacja
addi, addd, adds (add integer, double, string)
Dodaje dwie liczby integer, albo dwie liczby double. Operacja adds konkatenuje ciągi znaków.
IntegerResult IntegerResult IntegerResult
DoubleResult DoubleResult DoubleResult
StringResult StringResult StringResult
muli, muld (multiplicate integer, double)
Mnoży dwie liczby integer, albo dwie liczby double.
IntegerResult IntegerResult IntegerResult
DoubleResult DoubleResult DoubleResult
subi, subd (static subtract integer, static subtract double)
Odejmuje od siebie dwie liczby integer, albo dwie liczby double.
IntegerResult IntegerResult IntegerResult
DoubleResult DoubleResult DoubleResult
divi, divd (divide integer, divide double)
Dzieli przez siebie dwie liczby typu integer, albo dwie liczby double.
IntegerResult IntegerResult IntegerResult
DoubleResult DoubleResult DoubleResult
modi, modd (module integer, module double)
Wylicza resztę z dzielenia dwóch liczb typu integer lub dwóch liczb double.
IntegerResult IntegerResult IntegerResult
DoubleResult DoubleResult DoubleResult
negi, negr (negation integer, negation double)
Zamienia znak liczby integer lub liczby double na przeciwny.
IntegerResult IntegerResult
DoubleResult DoubleResult
dynadd, dynmul, dyndiv, dynmod, dynneg (dynamic add, multiplicate, divide, modulo, negation)
Dynamicznie dodaje, mnoży, dzieli, wylicza resztę z dzielenia lub zamienia znak na przeciwny.
SingleResult SingleResult
176
Operacje logiczneand, or
Wylicza iloczyn lub sumę logiczną.
BooleanResult BooleanResult BooleanResult
gri, grd (greater integer, greater double)
Sprawdza czy jedna liczba integer lub double jest większa od drugiej.
IntegerResult IntegerResult BooleanResult
DoubleResult DoubleResult BooleanResult
grei, gred (greater or equals integer, greater or equals double)
Sprawdza czy jedna liczba integer lub double jest większa lub równa od drugiej.
IntegerResult IntegerResult BooleanResult
DoubleResult DoubleResult BooleanResult
loi, lod (lower integer, lower double)
Sprawcza czy jedna liczba typu integer lub double jest mniejsza od drugiej.
IntegerResult IntegerResult BooleanResult
DoubleResult DoubleResult BooleanResult
loei, loed (lower or equals integer, lower or equals double)
Sprawdza czy jedna liczba integer lub double jest mniejsza lub równa drugiej.
IntegerResult IntegerResult BooleanResult
DoubleResult DoubleResult BooleanResult
eqi, eqd, eqb, eqs, eqr (equals integer, equals double, equals boolean, equals string, equals reference)
Sprawdza czy dwie liczby integer, boolean, double, string lub referencje są sobie równe.
IntegerResult IntegerResult BooleanResult
DoubleResult DoubleResult BooleanResult
StringResult StringResult BooleanResult
DoubleResult DoubleResult BooleanResult
ReferenceResult ReferenceResult BooleanResult
eqstr (equals struct)
Porównuje strukturalnie dwie struktury.
StructResult StructResult BooleanResult
dyngr, dyngre, dynlo, dynloe, dyneq (dynamic greater, lower, lower or equals, equals)
Dynamiczne wersje operaacji większości, mniejszości, mniejszości lub równości, równości.
SingleResult SingleResult BooleanResult
not
Neguje wartość boolean.
BooleanResult BooleanResult
178
Lista ważniejszych akronimów użytych w pracy
ADO - ActiveX Data Objects
API - Application Programming Interface
AST - Abstract Syntax Tree
B2B - Business-To-Business
CLI - Call-Level Interface
CLI - Command-Line Interface
CORBA - Common Object Request Architecture
DBMS - Database Management System
DCOM - Distributed Component Object Model
EAI - Enterprise Application Integration
EII - Enterprise Information Integration
EJB - Enterprise Java Beans
EJB QL - Enterprise Java Beans Query Language
ENVS - Environment Stack
179
ESB - Enterprise Service Bus
HQL - Hibernate Query Language
JDBC - Java Database Connectivity
JMS - Java Messaging Service
LSNR - Listener
ODL - Object Definition Language
ODMG - Object Database Management Group
ODRA - Object Database for Rapid Application development
OID - Object IDentifier
OMG - Object Management Group
OQL - Object Query Language
P2P - Peer To Peer
PJWSTK - Polsko-Japońska Wyższa Szkoła Technik Komputerowych
QRES - Query Result Stack
RAD - Rapid Application Development
SBA - Stack-Based Approach
SBQL - Stack-Based Query Language
SOA - Service Oriented Architecture
SQL - Structured Query Language
SVRP - SerVeR Process
SZBD - System Zarządzania Bazą Danych
SZFBD - System Zarządzania Federacyjną Bazą Danych
SZRBD - System Zarządzania Relacyjną Bazą Danych
SZOBD - System Zarządzania Obiektową Bazą Danych
UML - Unified Modelling Language
VIDE - Visualize all moDel drivEn programming
180
XML - eXtensible Markup Language
XSLT - eXtensible Stylesheet Language Transformations
Literatura
1. P. Lyman, H. Varian, A. Dunn, A. Strygin, and K. Swearingen, How Much Information?,
http://www.sims.berkeley.edu/research/projects/how-much-info/.
2. A.Y. Halevy, N. Ashish, D. Bitton, M.J. Carey, D. Draper, J. Pollock, A. Rosenthal, V. Sikka
(2005). Enterprise information integration: successes, challenges and controversies. SIGMOD
2005, 778-787.
3. M. Roth and D. Wolfson, From Data Management to Information Integration: A Natural
Evolution http://www7b.software.ibm.com/dmdd/library/techarticle/0206roth/0206roth.html
4. K. Subieta, Teoria i konstrukcja obiektowych języków zapytań, Wydawnictwo PJWSTK,
Warszawa 2004.
5. H. Garcia-Molina, J.D. Ullman, J. Widom, Implementacja systemów baz danych, WNT,
Warszawa 2003.
6. K. Subieta, Obiektowość w projektowaniu i bazach danych, Akademicka Oficyna Wydawnicza
PLJ, Warszawa 1998.
7. J. Płodzień, Optimization Methods in Object-Oriented Database Management Systems,
rozprawa doktorska, IPI PAN, Warszawa 2000.
8. K. Kaczmarski, Metodologie i metamodele dla obiektowych baz danych typu grid, rozprawa
doktorska, IPI PAN, Warszawa 2006.
9. K. Stencel, Półmocna kontrola typów w językach programowania baz danych, Wydawnictwo
PJWSTK, Warszawa 2006.
183
10. J.M. Myerson, The Complete Book of Middleware, Auerbach Publications, 2002.
11. D.S. Lithicum, Next Generation Application Integration: From Simple Information to Web
Services, Addison Wesley, 2003.
12. M. Atkinson, R. Morrison. Orthogonally Persistent Object Systems. The VLDB Journal 4(3),
319-401, 1995. R.G.G.Cattell, D.K.Barry (Eds.): The Object Data Standard: ODMG 3.0.
Morgan Kaufmann 2000.
13. W.R. Cook, C. Rosenberger: Native Queries for Persistent Objects A Design White Paper.
http://www.db4o.com/about/productinformation/whitepapers/Native%20Queries
%20Whitepaper.pdf, 2006
14. H. Kozankiewicz, K. Stencel, K. Subieta: Integration of Heterogeneous Resources through
Updatable Views. Workshop on Emerging Technologies for Next Generation GRID
(ETNGRID-2004), June 2004, Proc. published by IEEE.
15. H. Kozankiewicz, J. Leszczylowski, K. Subieta: Updateable XML Views. Proc. of ADBIS’03,
Springer LNCS 2798, 2003, 385-399.
16. J. Plodzien, A. Kraken: Object Query Optimization in the Stack-Based Approach. Proc. ADBIS
Conf., Springer LNCS 1691, 3003-316, 1999.
17. K. Subieta: Stack-Based Approach (SBA) and Stack-Based Query Language (SBQL). http://
www.sbql.pl, 2006.
18. World Wide Web Consortium (W3C) : Extensible Markup Language (XML), http://
www.w3.org/XML/.
19. World Wide Web Consortium (W3): XML Query specifications. http://www.w3.org/XML/
Query/
20. Hibernate - Relational Persistence for Java and .NET. http://www.hibernate.org/, 2006
21. db4o - Native Java & .NET Object Database. http://www.db4o.com.
22. Objectivity - Object Oriented Database. http://www.objectivity.com.
23. M. Atkinson, M. Jordan, L. Daynes and S. Spence. Design Issues for Persistent Java: a type-
safe, object-oriented, orthogonally persistent system, 7th International Workshop on Persistent
Object Systems, Cape May, 1996.
184
24. A.L. Hosking and J. Chen. Persistent Modula 3: An Orthogonally Persistent Systems
Programming Language - Design, Implementation, Performance. In Proceedings of the 25th
International Conference on Very Large Data Bases (Edinburgh, Scotland, September 1999).
Morgan Kaufmann, 1999.
25. Common Object Request Broker Architecture: Core Specification, OMG, 2004,
http://www.omg.org/docs/formal/04-03-12.pdf.
26. S. Alagic. The ODMG Object Model: Does it Make Sense? Proc. OOPSLA Conf., 253-270,
1997.
27. Object Data Management Group: The Object Database Standard ODMG, Release 3.0.
R.G.G.Cattel, D.K.Barry, Ed., Morgan Kaufmann, 2000.
28. A. Albano, G. Ghelli, R. Orsini. Fibonacci: A Programming Language for Object Databases.
The VLDB Journal 4(3), 403-444, 1995.
29. H. Baker. Iterators. Signs of Weakness in Object-Oriented Languages. ACM OOPS Messenger
4(3), 18-25, http://home.pipeline.com/~hbaker1/, 1993.
30. K. Subieta, M. Missala, and K. Anacki. The LOQIS System. Description and Programmer
Manual. Institute of Computer Science of PAS Report 695, 1990.
31. K. Subieta. A Persistent Object Store for the LOQIS Programming System. International Journal
on Microcomputer Applications 13(1), 50-61, 1994.
32. F. Bancilhon. Understanding Object-Oriented Database Systems. Proc. EDBT Conf., Springer
LNCS 580, 1-9, 1992.
33. S.S. Chawathe, H. Garcia-Molina, A. Gupta, J. Hammer, K. Ireland, Y. Papakonstantinou,
D. Quass, A. Rajaraman, Y. Sagiv, J. Ullman, J. Widom. The TSIMMIS Project, 1994-95; http://
www-db.stanford.edu/pub.
34. D. Figura. Obiektowe bazy danych. Akademicka Oficyna Wydawnicza, Warszawa 1996.
35. W. Kim. Wprowadzenie do obiektowych baz danych. Wydawnictwa Naukow-Techniczne,
Warszawa 1996.
36. M.E.S. Loomis. Object Databases: The Essentials. Addison Wesley, 1995.
37. A. Heuer, M.H. Scholl. Principles of Object-Oriented Query Languages. Proc. GI Conf. on
Database Systems for Office, Engineering and Scientific Applications, Springer-Verlag,
178-197, 1991.
38. H. Kozankiewicz, J. Leszczyłowski, K. Subieta. Implementing Mediators through Virtual
Updateable Views. Proc. EFIS’03, IOS Press, 52-62, 2003.
39. M. Jarke, J. Koch. Query Optimization in Database Systems. ACM Computing Surveys 16(2),
111-152, 1984.
40. A. Kemper, G. Moerkotte. Query Optimization in Object Bases: Exploiting Relational
Techniques. (In) Query Processing for Advanced Database Systems. Morgan Kaufmann, 63-98,
1994.
41. F. Matthes, A. Rudloff, J.W. Schmidt, K. Subieta. The Database Programming Language
DBPL, User and System Manual. FIDE, ESPRIT BRA Project 3070, Technical Report Series,
FIDE/92/47, 1992 (również: Fachbereich Informatik Universitaet Hamburg, Bericht Nr. 159,
FBI-HH-B-159/92, 1992).
42. G.Wiederhold. Mediators in the Architecture of Future Information Systems, IEEE Computer
Magazine, 1992.
43. Pascal/R Report, J.W. Schmidt et al, U Hamburg, Fachbereich Informatik, Report 66, Jan 1980.
44. R. Morrison, F. Brown, R. Connor, Q. Cutts, A. Dearle, G. Kirby, D. Munro The Napier88
Reference Manual Release 2.0, 1994.
45. F. Matthes, Persistente Objektsysteme: Integrierte Datenbankentwicklung und
Programmerstellung. Springer-Verlag, 1993.
46. A. Ohori, P. Buneman, V. Breazu-Tannen. Database Programming in Machiavelli a
Polymorphic Language with Static Type Inference (1992).
47. McLeod and Heimbigner. A Federated architecture for information management. ACM
Transactions on Information Systems Vol 3, Issue 3 (1985): 253-278.
48. Sheth and Larson. Federated Database Systems for Managing Distributed, Heterogenous, and
Autonomous Databases. ACM Computing Surveys Vol 22, No.3 (1990): 183-236.
49. A. Tomasic, R. Amouroux, P. Bonnet, The Distributed Information Search Component (DisCo)
and the World Wide Web.
186
50. R. Ahmed, et al. The Pegasus Heterogeneous Multidatabase System. In IEEE Computer, Vol.
24, 1991.
51. L. Haas, E. Lin. IBM Federated Database Technology, IBM 2002.
52. EJB 2.1 Specification, Sun Microsystems, http://java.sun.com/products/ejb/docs.html.
53. H. Kozankiewicz, Updateable Object Views, praca doktorska, IPI PAN 2005.
54. Comega language, http://research.microsoft.com/Comega.
55. LINQ Project, http://msdn2.microsoft.com/en-us/netframework/aa904594.aspx.
56. Garlic Project, http://www.almaden.ibm.com/cs/garlic.
57. U. Strathclyde, The PS-Algol Reference Manual, TR PPR-12-85, CS Dept, University of
Glasgow.
58. S. Ceri and S. Navathe. A methodology for the distribution design of databases. In IEEE
COMPCON 26, San Francisco, 1983.
59. S. Ceri, M. Negri, and G. Pelagatti. Horizontal data partitioning in database design. In
SIGMOD Conference, pages 128–136, 1982.
60. S. Ceri and G. Pelagatti. Distributed databases: Principles and systems. McGraw-Hill, Inc. New
York, NY, USA, 1984.
61. S. Ceri, B. Pernici, and G. Wiederhold. Distributed database design methodologies. In
Proceedings of the IEEE, volume 75, pages 533–546, 1987.
62. DataGrid. The DataGrid Project Website. eu-datagrid.web.cern.ch/eu-datagrid/.
63. C. J. Date. An Introduction to Database Systems. Adison-Wesley, 1995.
64. eGov Bus Consortium. Advanced Egovernment Information Service Bus. website: www.egov-
bus.org, 2006.
65. M. Stonebraker, L. A. Rowe, B. G. Lindsay, J. Gray, M. J. Carey, and D. Beech. The Committee
for Advanced DBMS Function: Third Generation Data Base System Manifesto. In H. Garcia-
Molina and H. V. Jagadish, editors, Proceedings of the 1990 ACM SIGMOD International
Conference on Management of Data, Atlantic City, NJ, May 23-25, 1990, page 396. ACM Press,
1990.
66. N. Wirth. The module: A system structuring facility in high-level programming languages. In
Language Design and Programming Methodology, pages 1–24, 1979.
67. N. Wirth. Programming in Modula 2. Springer, 1982.
68. Oracle Corporation. Oracle database concepts 10g, 2005.
69. Oracle Corporation. PL/SQL Programmer’s manual, 2005.
70. D. L. Parnas. On the criteria to be used in decomposing systems into modules. Commun. ACM,
15(12):1053–1058, 1972.
71. Rational Software Corporation. The UML and data modeling, 2000.
72. J. Rumbaugh, M. Blaha, W. Premerlani, F. Eddy, and W. Lorensen. Object-Oriented Modeling
and Design. Prentice-Hall, 1991.
73. J. Rumbaugh, I. Jacobson, and G. Booch. The Unified Modeling Language Reference Manual.
Addison-Wesley, 1998.
74. I. Sommerville. Software Engineering. Addison-Wesley, 2001.
75. B. G. Lindsay, L. M. Haas, C. Mohan, P. F. Wilms, and R. A. Yost. Computation and
communication in R*: a distributed database manager. ACM Trans. Comput. Syst., 2(1):24–38,
1984.
76. B. Meyer. Object-Oriented Software Construction. Prentice-Hall, 2000.
77. E. Naiburg and R. A. Maksimchuk. UML for Database Design. Addison-Wesley, 2001.
78. S. B. Navathe, S. Ceri, G. Wiederhold, and J. Dou. Vertical partitioning algorithms for
database design. ACM Trans. Database Syst., 9(4):680–710, 1984.
79. Object Management Group. Unified Modeling Language (UML), version 2.0. website:
www.omg.org/technology/documents/formal/uml.htm, 2003.
80. K. Kaczmarski, P. Habela, and K. Subieta. Metadata in a data grid construction. In WETICE,
pages 315–316. IEEE Computer Society, 2004.
81. K. D. Levin and H. L. Morgan. Optimizing distributed data bases: A framework for research. In
Proc. National Computer Conf., pages 473–478, 1975.
188
82. M. Lenzerini. Data integration: a theoretical perspective. In Proceedings of the twenty-first
ACM SIGMOD-SIGACT-SIGART symposium on Principles of database systems, Madison,
Wisconsin, 2002.
83. D. Laurent, J. Lechtenborger, N. Spyratos, and G. Vossen. Complements for data warehouses. In
Proc. of 15th Conference on Data Engineering, pages 490–499, 1999.
84. J. Górski. Inżynieria Oprogramowania w projekcie informatycznym. Mikom, 2000.
85. A. Jaszkiewicz. Inżynieria Oprogramowania. Helion, 1997.
86. IBM Research Laboratory. R*: An overview of the architecture. Technical Report RJ3325, IBM
Research Laboratory, 1982.
87. ISO/IEC 9075:1992. Database language SQL standard. Technical report, ISO, 1992.
88. K. Kaczmarski. Transparent migration of database services. In J. Wiedermann, G. Tel, J.
Pokorny, M. Bielikova, and J. Stuller, editors, SOFSEM, volume 3831 of Lecture Notes in
Computer Science, pages 332–340. Springer, 2006.
89. K. Kaczmarski, P. Habela, H. Kozankiewicz, K. Stencel, and K. Subieta. Transparency in
object-oriented grid database systems. In Paral lel Processing and Applied Mathematics, 6th
International Conference, PPAM 2005, volume 3911 of Lecture Notes in Computer Science,
pages 675–682, 2006.
90. W. Kent. Data and Reality. North-Holland Publishing Company, 1978.
91. K. Subieta: Object-Oriented Standards. Can ODMG OQL Be Extended to a Programming
Language? Cooperative Databases and Applications, World Scientific, pp. 459–468, 1997.
92. K. Subieta, Virtual Persistent Object Store Package (Sun Version), 1991.
93. K. Subieta, J. Leszczyłowski, I. Ulidowski: Processing SemiStructured Data in Object Bases,
ICS PAS Report 852, February 1998.
94. K. Subieta, Y. Kambayashi, J. Leszczyłowski: Procedures in Object-Oriented Query Languages.
Proc. 21-st VLDB Conf., Zurich, pp.182–193, 1995.
95. H. Kozankiewicz, J. Leszczyłowski, K. Subieta: Updateable Views for an XML Query
Language. Proc. of 15th CAiSE, Klagenfurt/Velden, Austria, 2003.
96. H. Kozankiewicz, K. Subieta: SBQL Views – Prototype of Updateable Views. Proc. 8th ADBIS
Conf., Hungary, 2004.
97. CUP Parser Generator for Java, http://www.cs.princeton.edu/~appel.
98. Fast scanner generator for Java, http://jflex.de.
99. A. Jodłowski: Dynamic Object Roles in Conceptual Modelling and Databases, Ph.D. Thesis,
The Institute of Computer Science, The Polish Academy of Sciences, 2002.
100. W.R. Stevens: Unix Network Programming Prentice Hall 1990.
101. S.R. Gopalan: A Detailed Comparison of CORBA DCOM and Java/RMI (with specific code
examples), http://www.execp.com/~gopalan/misc/compare.html.
102. D.E. Comer: Sieci Komputerowe TCP/IP, WNT, Warszawa 1997.
103. JSR 51: New I/O APIs for the JavaTM Platform, Sun Microsystems, http://jcp.org/en/jsr/
detail?id=51.
104. Java Messaging Service, Sun Microsystems, http://java.sun.com/products/jms/.
105. Spring Framework, http://www.springframework.org/.
190