Rozdział 48 Transpers generyczna mikroarchitektura warstwy dostępu do danych Streszczenie. Rozdział podejmuje tematykę nowoczesnych rozwiązań bazodanowych w aplikacjach J2EE (ang. Java 2 Platform, Enterprise Edition). Autorzy prezentują własną koncepcję mechanizmu (Transpers) służącego do przechowywania obiektów spełniających specyfikację JavaBeans w relacyjnych bazach danych. Krótki przegląd istniejących rozwiązań służy w rozdziale jako punkt wyjścia, do określenia pożądanej funkcjonalności dla mikroarchitektury. Podstawowe założenia to przezroczysty zapis obiektów, osiągnięcie minimum konfiguracji oraz współpraca z dowolnym system RDBMS. Sedno rozdziału stanowi prezentacja przyjętych rozwiązań (architektury Transpers, mechanizmu rekonfiguracji oraz hermetyzacji kluczy). 1 Wstęp Większość aplikacji biznesowych wykorzystuje relacyjne bazy danych jako sprawdzony sposób przechowywania danych. Pomimo rozwiniętych bibliotek, które mają wspierać programistów w tworzeniu aplikacji opartych na bazach danych, wiele z nich jest niskopoziomowa. Zazwyczaj użytkownik musi stworzyć obiekt reprezentujący połączenie z bazą danych, następnie stworzyć i skonfigurować obiekt realizujący zapytanie SQL oraz dokonać pewnych operacji na danych. Z tego powodu praca z bibliotekami tego typu jest źródłem wielu błędów; kod nie nadaje się do ponownego użycia, często błędem jest pozostawienie otwartych połączeń z bazą danych. Nawet doświadczeni programiści mają duże problemy z wyeliminowaniem błędów wynikających z korzystania z niskopoziomowych obiektów i metod służących do operowania na bazach danych. Autorzy rozdziału są przekonani, że przechowywanie informacji w bazie danych powinno być dla programisty przyjazną operacją. Podstawowym celem rozdziału jest zaprezentowanie Transpers autorskiej koncepcji mechanizmu do przechowywania obiektów klas w relacyjnej bazie danych. Transpers można określić jako generyczną mikroarchitekturę warstwy dostępu do danych. Nazwa pochodzi od angielskiego słowa transparence (przezroczystość) oraz ma wyrażać sposób operowania na danych przez programistę, który używa zwykłych obiektów na poziomie języka programowania, zaś wszelkie zmiany w danych systemu są odwzorowywane w docelowym RDBMS. Przemysław Krygier: Politechnika Łódzka, Wydział Elektrotechniki i Elektroniki, ul. Stefanowskiego 18/22, 90-924 Łódź, Polska email: kcepk@wpk.p.lodz.pl Mariusz Sieraczkiewicz: FPL Sp. z o.o., ul. Piramowicza 9, 90-254 Łódź, Polska email: m.sieraczkiewicz@fpl.pl
P. Krygier, M. Sieraczkiewicz 2 Czym obecnie dysponuje programista? W celu zaprezentowania mechanizmu Transpers zaprezentowane zostaną istniejące rozwiązania. 2.1 JDBC Jako przykład weźmy fragment kodu, którego zadaniem jest dodanie nowego elementu do bazy danych. Przykład został napisany w języku Java z użyciem standardowej biblioteki JDBC (ang. Java Database Conectivity), która wspiera korzystanie z relacyjnych baz danych. Narzędzie to udostępnia obiektową abstrakcję struktury danych (np. obiekty kursorów, wyników zapytań). Chociaż przytoczony fragment kodu [4] jest związany z konkretnym językiem, to jego wygląd pozostaje analogiczny w przypadku użycia podobnych bibliotek w innych językach np. ADO (ang. ActiveX Data Object) w C++. // Begin of the main part //... Statement stmt = con.createstatement(); String query = "SELECT NAME, SECONDNAME, " + "BIRTHDAY, WEIGHT, HEIGHT FROM PERSONS WHERE PERSONID = 1"; ResultSet rs = stmt.executequery(query); // Validation ommited for clarity Person person = new Person(); if ( rs.next() ) { person.setname( rs.getstring( "NAME" ) ); person.setsecondname( rs.getstring( "SECONDNAME" ) ); person.setbirthdate( rs.getdate( "BIRTHDAY" ) ); person.setweight( new Float( rs.getfloat( "WEIGHT" ) ) ); person.setheight( new Integer( rs.getint( "HEIGHT" ) ) ); } else { throw new SQLException( "Person not found." ); } //End of the main part //... W przypadku tego rozwiązania programista musi uwzględnić kilka elementów. Po pierwsze przed wykonaniem danego fragmentu kodu musi zostać stworzona tabela odpowiadająca osobom w bazie danych, ponadto należy skonfigurować i uzyskać połączenie z bazą danych, następnie stworzyć i wykonać zapytanie SQL, które zwróci pożądane dane. Dostęp do takich danych jest zwykle sekwencyjny (wzorzec iteratora) otrzymujemy obiekt, za pomocą którego możemy przemieszczać się w otrzymanym zestawie rekordów. Przemieszczaniu się po zestawie rekordów towarzyszy zwykle tworzenie docelowego obiektu na podstawie wyłuskanych danych. Korzystanie z takiego rozwiązania nie należy do najprzyjemniejszych. Zauważmy, że wydobycie listy obiektów to kilkanaście (o ile nie kilkadziesiąt) linii kodu, naszpikowanego obsługą wyjątków. Warto się zastanowić nad uproszczeniem tego podejścia, tym bardziej, że intuicyjnie wydaje się, iż nic nie powinno stać na przeszkodzie temu, aby obiektowość relacyjnych baz danych przenieść na wyższy poziom abstrakcji. 400
Transpers generyczna mikroarchitektura warstwy dostępu do danych 2.2 Budowa warstwowa aplikacji biznesowych Warto na chwilę przyjrzeć się tak zwanej warstwowej architekturze aplikacji biznesowych [1]. Podejście to polega na tym, aby wyraźnie odróżnić części tworzonego systemu, które są architektonicznie niezależne. Typowy podział na warstwy kształtuje się następująco: warstwa integracji (dostępu do danych), warstwa logiki biznesowej (zasadnicza część operacyjna systemu), warstwa interfejsu użytkownika (obejmująca zarówno formę zewnętrzną, z jaką ma kontakt użytkownik, jak również logikę interfejsu użytkownika). Rys. 1. Warstwy aplikacji biznesowych Dzięki takiej architekturze możemy niezależnie rozpatrywać podsystemy wchodzące w skład poszczególnych warstw oraz w razie potrzeby zmieniać ich implementacje bez wpływu na pozostałe części aplikacji. Korzystanie z bibliotek w rodzaju JDBC API prowadzi do wielu problemów w realizacji niezależnych komponentów warstwy integracji. Idealnym rozwiązaniem byłby system, który implementacyjnie pokrywałby całą przestrzeń operacji dostępnych w warstwie integracji. Jednocześnie system ten byłby dostępny programiście obiektowemu w postaci fasady z najważniejszymi operacjami bazodanowymi: dodaj, pobierz, usuń i aktualizuj. System ten miałby zapewniać niezależność implementacji operacji od rodzaju bazy danych, która stanowi źródło danych dla systemu. Sprecyzowanie oczekiwań odnośnie do optymalnego narzędzia stanowiło milowy krok przy tworzeniu Transpers biblioteki, która z powodzeniem realizuje warstwę dostępu do danych (patrz rysunek 1). 2.3 Istniejące strategie ułatwiające implementację warstwy dostępu do danych Istnieją rozwiązania, które umożliwiają korzystanie z relacyjnych baz danych poprzez obiekty z dziedziny problemu. Przykładem takiego rozwiązania, nazywanego oprogramowaniem mapowania relacyjno-obiektowego, pochodzącego z rodziny projektów Open Source jest projekt Hibernate [3]. Dzięki jego zastosowaniu programista nie musi niskopoziomowo myśleć o bazie danych. Korzystając z Hibernate'a możemy stworzyć implementację DAO (ang. Data Access Object), tzn. obiekt pomocniczy, który potrafi realizować wszystkie operacje bazodanowe związane z danym obiektem pochodzącym z dziedziny problemu. Przykład takiego wzorca można znaleźć na rysunku 2. Jest to wzorzec projektowy, który często stosuje się w aplikacjach biznesowych w celu wydzielenia odpowiedzialności operowania na danych poza obiekty warstwy biznesowej [4]. 401
P. Krygier, M. Sieraczkiewicz Rys. 2. Przykładowy diagram UML reprezentujący zastosowanie wzorca DAO (PersonDao) dla obiektu (Person) Przykład fragmentu operacji z klasy DAO zaimplementowanej w Hibernate zamieszczamy poniżej [2]. Session session = getsessionfactory().opensession(); Person person = (Person) session.load( Person.class, new Long( 1 ) ); session.close(); Jak widać, rozwiązanie to ma ogromną zaletę, przede wszystkim polegającą na uproszczeniu programistycznych działań do korzystania z obiektów z dziedziny problemu (co dobrze widać porównując tenże kod z pierwszym przykładem). Jednak żeby skorzystać z biblioteki Hibernate, należy odpowiednio skonfigurować mapowania obiektów na tabele baz danych, co uzyskuje się poprzez odpowiednio napisane pliki XML. Przy dużej ilości klas ilość plików mapowania rośnie proporcjonalnie do ilości klas. Ponadto konfiguracje XML wymagają sporej wiedzy związanej z bazami danych, zaś schemat bazy danych należy uprzednio stworzyć. Transpers przenosi nas do jeszcze wyższej warstwy abstrakcji. Zakładamy, że użytkownik (programista) nie jest zobligowany do innej konfiguracji niż określenia parametrów połączenia z bazą, musi mieć możliwość łatwego korzystania z operacji bazodanowych na obiektach z dziedziny problemu. Oznacza to, że system dynamicznie będzie tworzyć odpowiednie elementy schematu bazy danych (tj. tabele, klucze, ograniczenia, pola) oraz mapowania struktury przechowywanych danych na elementy docelowe w bazie danych. Na koniec warto umiejscowić system Transpers w typologii, która nieobca jest specjalistom z zakresu baz danych. Najbliższym skojarzeniem, które może się w tym przypadku nasuwać, są obiektowo-relacyjne systemy zarządzania bazami danych (ORDBMS). Umożliwiają one stosowanie uproszczonych mechanizmów obiektowych, takich jak dziedziczenie czy tworzenie metod dla obiektów bazodanowych, jednakże rozwiązania te są specyficzne dla konkretnej bazy danych oraz nie wspierają obiektowości na poziomie języka programowania. Transpers zatem należy rozumieć jako bibliotekę programistyczną, która upraszcza dostęp do danych. Pozwala na korzystanie z zaawansowanych mechanizmów obiektowych na poziomie języka programowania (m. in. dziedziczenie, polimorfizm) w sposób dla niego naturalny. Ponadto zaproponowane przez nas rozwiązane jest niezależne od docelowej bazy danych, co czyni system bardziej elastycznym. 402
3 Transpers Transpers generyczna mikroarchitektura warstwy dostępu do danych Podrozdział ma na celu przedstawić podstawowe aspekty proponowanego rozwiązania. 3.1 Wstęp Startowym punktem mikroarchitektury jest fabryka menedżera Transpers. Obiekt ten na podstawie bardzo prostej konfiguracji (obligatoryjne jest tylko określenie parametrów połączenia z bazą danych) tworzy pojedynczą instancję TranspersManager'a. Jest to typowe zastosowanie wzorca fabryki i wzorca singleton. TranspersMenager dostarcza wygodny i prosty zestaw metod dla programisty, który umożliwia pracę z obiektami zbudowanymi w oparciu o specyfikację JavaBeans. Na rysunku 3 przedstawiono powiązanie podstawowych obiektów w Transpers. Rys. 3. Powiązanie podstawowych obiektów Transpers Metoda add(object : Object) pozwala zapisać obiekt. Zwracaną wartością jest identyfikator obiektu (wewnętrzna unikatowa wartość na poziomie całego menedżera). Metoda get(id : Long) wydobywa obiekt na podstawie identyfikatora. Metoda getbyexample(object : Object) służy do wydobycia listy obiektów, które mają takie same wartości pól, jak przekazany obiekt. Podejście to stanowi implementację query by example. Naturalnie wszelkie nie ustawione pola w przykładowym obiekcie mogą być dowolnie ustawione wśród listy zwracanych wyników. Metoda find(criteria : Criteria) służy do zadawania złożonych zapytań, dzięki niej można szczegółowo określić, jakie są dozwolone wartości pól. Wymaga zbudowania obiektu Criteria. Metoda find(hqlquery : String) służy do niskopoziomowego zadawania zapytań. Pomysł tej metody został zaczerpnięty z Hibernate. Zapytanie musi posiadać specyficzną składnię. Metoda getall(class : Class) zwraca listę wszystkich obiektów danej klasy składowanych w menedżerze. Metoda remove(id : Long) pozwala usunąć obiekt o znanym identyfikatorze. Metoda removebyexample(object : Object) służy do kasowania obiektów. Korzystanie z tej metody jest bardzo podobne do korzystania z metody getbyexample. Przekazywany argument jest wzorcem, na podstawie którego kasowane są obiekty w bazie. 403
P. Krygier, M. Sieraczkiewicz Metoda update(id, object) pozwala na aktualizację obiektu w bazie o konkretnym identyfikatorze, przydatne szczególnie wówczas, gdy informacje zostają przechowywane poza systemem ( np.: eksport danych ). Metoda update(object : Object) pozwala uaktualnić składowany obiekt. Aby metoda ta nie powodowała wyjątków obiekt przekazywany jako argument musi być wcześniej wydobyty poprzez którąś z odpowiednich metod menedżera Transpers. 3.2 Architektura Podstawą naszej architektury jest fasada TranspersManager. Fasada bazuje na czterech podstawowych aspektach: metadane bazy danych, strategia hermetyzacji kluczy, generowaniu i wykonywaniu Data Definition Language, mapowaniu klas i tabel. Rys. 4. Transpers Manager zastosowanie wzorca fasady Metadane bazy danych to informacje o tym, jakie tabele zostały stworzone w bazie. Dane te są niezbędne przy inicjalizacji menedżera, ponieważ na ich podstawie wykonywane jest startowe mapowanie klas użytkownika/programisty. Strategia hermetyzacji kluczy głównym zadaniem mikroarchitektury jest dostarczenie prostego narzędzia do przechowywania obiektowych danych. Jedynym obwarowaniem, co do typów obiektów jest to, aby spełniały specyfikację JavaBeans. Nie muszą one implementować żadnego interfejsu, ani pochodzić od jakiejkolwiek klasy bazowej, zatem nie muszą mieć żadnych identyfikatorów. Z drugiej strony klucze są bardzo istotnym mechanizmem w bazie danych. Biorąc pod uwagę dwa powyższe fakty logiczną konsekwencją jest potrzeba istnienia wewnętrznego mechanizmu przechowującego klucze bazodanowe, które są typowe dla struktury bazy danych, zaś nie występują w sposób naturalny w językach obiektowych. Powinny być zatem ukryte przed programistą. Generator i wykonawca DDL to moduł odpowiadający za tworzenie i wykonywanie zapytań SQL zmieniających strukturę bazy danych. Każdy składowany obiekt w bazie musi posiadać odpowiednie mapowanie klasa->tabele bazy danych. W przypadku zapisywania obiektów nowych typów (nie podanych w konfiguracji) generator DDL pozwala wygenerować dynamicznie skrypt SQL tworzący odpowiednie struktury w bazie danych, a generator pozwala je wykonać. Generator nie wymaga szczegółowej konfiguracji, jedyną istotną dla niego informacją jest używany dialekt bazodanowy, zapytania są generowane we właściwej dla bazy składni SQL. Mapowanie klas i tabel jest jednym z kluczowych aspektów mikroarchitektury. Jest to sposób odzwierciedlenia pól obiektów na pola tabele baz danych. Implementacja przyjęta 404
Transpers generyczna mikroarchitektura warstwy dostępu do danych w Transpers bazuje na Hibernate narzędziu typu Open Source. Istnieje jednak możliwość dowolnej implementacji tego rozwiązania za pomocą mechanizmów niskopoziomowych. W tym przypadku zostaną one pominięte gdyż wykraczają poza ramy rozdziału. Poniżej warstwy fasady menedżera Transpers i jego komponentów istnieje warstwa oparta o Hibernate i Spring a. Spring jest szkieletem aplikacji, który w istotny sposób wspiera pracę z Hibernate, ponadto stanowi szkielet dla całej mikroarchitektury dostarczając implementacji generycznej fabryki obiektów składających się na system i jest wykorzystywany wewnętrznie. Hibernate jest zaawansowanym narzędziem, które z punktu widzenia architektury stanowi warstwę powyżej JDBC. Samo JDBC jest podstawowym mechanizmem JAVY do współpracy z bazami danych. 3.3 Mechanizm rekonfiguracji Transpers Rekonfiguracja Transpers ma miejsce w przypadku próby zapisu obiektu klasy, dla której jeszcze nie zostało utworzone wewnętrzne mapowanie. Mapowanie takie powstać może w dwóch przypadkach. Pierwszy z nich ma miejsce przy uruchomieniu Transpers, dana klasa jest wyszczególniona przy konfiguracji. Drugi przypadek tworzenia mapowania to sytuacja, w której startowa konfiguracja nie obejmuje mapowania danej klasy i należy ją utworzyć dynamicznie. Przedstawiony powyżej scenariusz może mieć miejsce w przypadku wykonania metody add(object : Object) TranspersManager'a. Jej działanie przedstawione jest schematycznie na poniższym diagramie. Rys. 5. Algorytm metody add 405
P. Krygier, M. Sieraczkiewicz Zgodnie z rysunkiem menedżer Transpers w przypadku zapisu obiektu nowego typu (tj. obiektu klasy, która nie była wcześniej używana w operacjach z Transpers) tworzy dynamicznie mapowanie, aktualizuje schemat bazy danych oraz zapisuje w niej obiekt. Pierwszy zapis obiektu nowego typu jest w związku z powyższym bardzo wydłużony. Opłaca się konfigurować Transpers, np. określić, jakie klasy będą zapisywane, ponieważ dynamiczne mapowanie zostanie utworzone podczas inicjalizacji menedżera Transpers. Każda utworzona definicja jest wewnętrznie składowana w menedżerze, przez cały czas jego istnienia. 3.4 Strategia hermetyzacji klucza Zgodnie z przedstawionymi wcześniej założeniami, odnośnie składowanych obiektów, dogodnym rozwiązaniem jest przechowywanie powiązań typu referencja obiektu wewnętrzny klucz Transpers. Dzięki temu podejściu można wydajnie pracować z obiektami, sprawdzać ich identyfikatory, jest to również sposób na realizację cache a dla obiektów. W oczywisty sposób istnieje konieczność uaktualniania wpisów w przedstawionym rejestrze powiązań klasie ReferenceToIDMapping. Transpers przetwarza obiekty, do których ma dostęp aplikacja, zatem nie ma sposobu na wykrycie momentu, w którym obiekt jest niszczony (w przypadku Javy klasą zarządzającą pamięcią jest GarbageCollector). Naturalnie nie stanowi to zagrożenia dla integralności powiązań kluczy z obiektami, ponieważ nawet w przypadku odnalezienia wpisu w rejestrze powiązań sprawdzane jest, czy obiekt Javy faktycznie istnieje w pamięci. Rys. 6. Diagram UML prezentujący rozwiązanie hermetyzacji kluczy 406
Transpers generyczna mikroarchitektura warstwy dostępu do danych Uaktualnianiem wpisów w rejestrze powiązań zajmuje się wydzielona klasa UpdatingStrategy, na której również spoczywa ciężar określenia momentu, w którym należy odświeżyć rejestr powiązań. Klasa SearchingStrategy to obiekt realizujący wyszukiwanie konkretnych powiązań (kluczy na referencje obiektów i referencji na klucze). Zanim obiekt zostanie wydobyty z bazy za pomocą Transpers musi być wcześniej zapisany. To z gruntu rzeczy proste stwierdzenie daje jednak znaczne możliwości. Weźmy jako przykład sytuację, w której programista chce zapisać zmieniony przez siebie obiekt. Wywołanie metody update(object : Object) oznacza konieczność sprawdzenia, czy istnieje powiązanie referencji obiektu z wewnętrznym kluczem Transpers. Jeżeli takie powiązanie istnieje, to klucz jest wyznaczony jednoznacznie, w przeciwnym wypadku metoda update nie ma sensu i zostaje wygenerowany odpowiedni wyjątek. 4 Podsumowanie Zdaniem autorów zapis obiektów w bazie danych powinien być tak prosty, jak: Person person = new Person(); // setting fields TranspersManager manager = context.gettranspersmanager(); Long storedobjectid = manager.add( person ); Transpers jest narzędziem, która pozwala programiście osiągnąć taką właśnie prostotę kodu. Stanowi on ideę, którą można wykorzystać w innych językach obiektowych. Jest to innowacja, jeżeli chodzi o przechowywanie obiektów w bazach danych, ponieważ pozwala w bardzo wygodny sposób składować obiekty i jednocześnie korzystać w pełni z możliwości relacyjnych baz danych (np. szybkie wyszukiwanie). Projekt Transpers jest udostępniany na zasadach open source i jest dostępny pod następującym adresem: https://transpers.dev.java.net/. Literatura 1. Fowler M., Architektura systemów zarządzania przedsiębiorstwem, Helion 2005. 2. Bauer Ch., King G.: Hibernate in action., Manning 2004. 3. Hibernate Reference Documentation. http://www.hibernate.org/hib_docs/reference/en/html/ 4. Horstmann C. S., Cornell G.: Java 2. Techniki zaawansowane, Helion 2003. 5. Raible M., Spring Live, Source Beat 2004. 407