07 lipca 2007

Powrót do lektury specyfikacji JPA - rozdziały 5.2-5.4 na przykładzie

Postanowiłem powrócić do lektury specyfikacji Java Persistence API 1.0 (JPA) od przykładu, który znalazłem w NetBeans IDE 6.0 zbudowanego ze źródeł z 30 czerwca (ostatnia rozwojowa wersja NetBeans IDE to 6.0M10). Przykład dostępny jest w menu New Project > Samples > Enterprise > Customer CMP. Przykład opisany jest jako

Demonstrates using Java Persistence APIs based on the new Java Persistence specification. The EJB interacts with a relational database to store and display information about customer subscriptions to periodicals.

Szybko okazało się, że przykład nie działa poprawnie z Glassfish v2 b50f, gdyż wymagany do poprawnego działania JPA wpis test w JNDI nie istnieje. Skorzystałem z instrukcji uruchomienia źródła danych do PostgreSQL 8.2.3 opisanych w moim artykule Aplikacja korporacyjna z JPA w trybie JTA z GlassFish i PostgreSQL i po modyfikacji pliku konfiguracyjnego JPA - persistence.xml - o wartość elementu jta-data-source mogłem uruchomić przykład.

Analiza kodów źródłowych ujawniła niezalecany sposób tworzenia aplikacji przy pomocy umieszczania elementów języka Java w ramach stron JSP. Dodatkowo, właśnie ze względu na umieszczenie części logiki aplikacji w plikach JSP, zamiast korzystać z wstrzeliwania zależności, przykład proponuje wyszukiwanie bezstanowego sesyjnego ziarna EJB za pomocą JNDI i rejestrowania nazwy logicznej ziarna w deskryptorze aplikacji internetowej (/WEB-INF/web.xml). To przede wszystkim zadecydowało o zaniechaniu prezentacji wiadomości z rozdziału 5 specyfikacji JPA na przykładzie Customer CMP z NetBeans IDE.

Zapewne zostało zauważone moje użycie słowa ziarno odpowiadające angielskiemu bean. Rozpocząłem używanie ziarno w kontekście dawnej nazywanych komponentów EJB. Po ostatniej prezentacji Michała Grzejszczaka zatytułowanej Open Terracotta - klastrowanie dla Javy na spotkaniu Warszawa JUG, gdzie użył tego określenia w stosunku do ziaren w Spring Framework, jakoś przypadło mi to do gustu i tak zostało.

Mimo nierekomendowanych rozwiązań projektowych, warto zapoznać się z kodem źródłowym przykładowej aplikacji Customer CMP. Czytanie kodu źródłowego zawsze dostarcza ciekawych informacji (nawet takich, że nie należy tworzyć aplikacji w ten sposób). I tak w kodzie źródłowym ziarna EJB - enterprise.customer_cmp_ejb.ejb.session.CustomerSession napisano:

Why a facade?
1. session beans are thread safe, and EMs are not necessarily; so injecting a EM into a SessionBean makes it safe.
2. Tx management is taken care of by container
3. of course, because it's a facade [we can combine operations].

Bardzo ważna jest uwaga #1 o odporności ziaren sesyjnych na wielowątkowość. Kontener dba o jednoczesny dostęp do ziarna i nie dopuszcza do sytuacji, w której dwa wątki równocześnie modyfikują jego stan, więc mamy gwarancję, że ziarno sesyjne używane jest przez pojedyńczy wątek. To gwarantuje korzystanie z zarządcy trwałego (ang. entity manager) w ramach pojedyńczego wątku, a to ma niebagatelne znaczenie, gdyż zarządca musi być używany wyłącznie przez jeden wątek (czytaj: nie jest odporny na wielowątkowość).

Druga uwaga jest równie ważna, co stanowi argument dla korzystania z serwera aplikacyjnego Java EE w ogólności - zarządzanie usługami przez kontener. Poza usługą monitora transakcji, inną usługą, która jest udostępniana przez serwer Java EE, niwelując naszą potrzebę poznawania szczegółów, a jedynie kilka adnotacji lub elementów w XML, jest bezpieczeństwo. Za pomocą deklaratywnego podejścia do wykorzystania usług transakcji i bezpieczeństwa możemy konfigurować je na potrzeby naszej aplikacji. (Jeśli ktoś teraz powie, że Spring Framework też to potrafi, więc nie potrzeba serwera aplikacyjnego Java EE, to właśnie wzbudzi mój entuzjazm, który wiąże się z określeniem Spring Framework jako pewnego rodzaju serwera aplikacyjnego, z czym wielu nie może się zgodzić).

Trzecia, ostatnia uwaga jest jedynie wzmocnieniem sensowności stosowania wzorca projektowego - Fasada, który polega na upraszczaniu publicznego interfejsu przez ukrycie zawiłych interakcji wewnętrznych między usługodawcami, np. usługą JPA, transakcji, bezpieczeństwa, itp. Wszystko, to jest ukryte w pojedyńczym wywołaniu metody znacząco upraszczając konstruowanie aplikacji klienckiej. Wydaje się, że nie ma o czym debatować o wzorcach projektowych w kontekście JPA, ale nieprawda. Właśnie konstrukcja oprogramowania przy wykorzystaniu Java EE 5, którego częścią jest JPA poprzez EJB3, wymusza stosowanie dobrych praktyk projektowych, a stosowanie wzorca Fasada jest tego potwierdzeniem. Zamiast wymuszać na twórcy klienta (w tym przypadku jest to niewyrafinowana strona JSP, ale możnaby wyobrazić sobie coś bardziej zaawansowanego, np. zdalny klient aplikacyjny w postaci aplikacji desktopowej) znajomość tajników konstrukcji oprogramowania ze wszystkimi detalami z Java EE, które związane byłyby z rozpoczęciem transakcji przed dostępem do danych przechowywanych w bazie (za pomocą JPA), wystarczy jedynie skorzystać z pojedyńczej metody udostępnianej przez bezstanowe ziarno sesyjne EJB, które jest dostępne zdalnie poprzez mechanizm klienta aplikacyjnego (ang. Java EE client application), ale również możnaby udostępnić ją jako część usługi sieciowej (ang. web service) za pomocą prostej modyfikacji polegającej na udekorowaniu klasy ziarna adnotacją @WebService.

W klasie ziarna CustomerSession można zobaczyć dostęp do zarządcy trwałego poprzez adnotację @PersistenceContext.

@javax.persistence.PersistenceContext(unitName="persistence_sample")
private EntityManager em ;

W tym przykładzie istnieje wyłącznie pojedyńcza jednostka trwała związana o nazwie persistence_sample, więc element unitName nie jest konieczny.

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
<persistence-unit name="persistence_sample" transaction-type="JTA">
<provider>oracle.toplink.essentials.ejb.cmp3.EntityManagerFactoryProvider</provider>
<jta-data-source>jdbc/poligon</jta-data-source>
<properties>
<property name="toplink.ddl-generation" value="drop-and-create-tables"/>
</properties>
</persistence-unit>
</persistence>

W przypadku większej ilości definicji PU w pliku konfiguracyjnym JPA użycie elementu unitName byłoby konieczne.

Zarządca trwały dla wybranego kontekstu trwałego pobierany jest z fabryki zarządców. Klasa rozpoczynająca interakcję z JPA to javax.persistence.Persistence (oczywiście wszystko zależy od środowiska, w którym pracuje nasza aplikacja oraz od trybu w jakim wykorzystywane jest JPA - zarządca encji zarządzany przez kontener lub aplikację). W środowisku Java SE mamy do dyspozycji jedynie tryb zarządzania zarządcy encji przez aplikację. Dodam, że aplikacją może być kontener EJB albo szerzej serwer aplikacyjny Java EE, więc klientami mogą być ziarna EJB, komponenty zarządzane JSF, servlety, itp, które będą już korzystali z JPA zazwyczaj w trybie zarządzania zarządcy przez kontener. Różnica między trybami była już przedstawiana w poprzednich odcinkach lektury specyfikacji JPA, ale jest to tak istotna sprawa w poprawnym użyciu JPA, że będzie jeszcze o tym nie raz.

Zobaczmy w jaki sposób dowolna aplikacja może rozpocząć korzystanie z JPA (zakładamy, że wykorzystywaną konfiguracją JPA jest powyższy plik persistence.xml, gdzie zdefiniowano pojedyńcze PU - persistence_sample).

EntityManagerFactory emf = javax.persistence.Persistence.createEntityManagerFactory("persistence_sample");

Skoro każda aplikacja ma taką możliwość, serwer aplikacyjny Java EE również. I właśnie zrozumienie tej kwestii upraszcza zrozumienie kolejnych elementów JPA. Użycie serwera aplikacji Java EE jest jedynie podyktowane potrzebą uproszczenia tworzenia aplikacji, a to jest realizowane przez automatyczne zarządzanie zasobami przez serwer.

Po wywołaniu statycznej metody Persistence.createEntityManagerFactory.createEntityManagerFactory(String persistenceUnitName) otrzymujemy dostęp do gotowej fabryki zarządców - javax.persistence.EntityManagerFactory w aplikacji, która decyduje się samodzielnie zarządzać zarządcami encji (tryb zarządcy trwałego zarządzanego przez aplikację).

Powyższe wywołanie jest realizowane w ramach środowiska serwera za pomocą adnotacji @PersistenceUnit.

@PersistenceUnit(unitName="persistence_sample")
EntityManagerFactory emf;
Korzystając z zarządców zarządzanych przez kontener (w środowisku Java EE), aplikacja korzysta z fabryki pośrednio za pośrednictwem serwera poprzez mechanizm wstrzeliwania zależności lub z drzewa JNDI. Kontener zarządza komunikacją (użyciem) z fabryką zarządców w sposób niezauważalny dla aplikacji, nie wymagając jakichkolwiek dodatkowych kroków po stronie jej twórcy.

W przeciwieństwie do środowiska Java EE, gdzie zlecamy zarządzanie dostępem do kontekstu trwałości kontenerowi, w przypadku korzystania z zarządców trwałości zarządzanych przez aplikację (w środowisku Java SE lub w szczególnych przypadkach w Java EE) aplikacja musi korzystać bezpośrednio z fabryki zarządców, aby zarządzać zarządcą trwałości i cyklem życia kontekstu trwałego za pomocą klas Persistence oraz EntityManagerFactory.

Zarządca trwałości jest zaprojektowany do działania w środowisku jednowątkowym i nie może być współdzielony wśród wielu równolegle wykonujących się wątków. Jest to bardzo istotna cecha zarządców, która często jest zapominana, a która uwidacznia się w środowisku serwera aplikacji, np. w kontenerze servletów, gdzie istnieją różne zasięgi istnienia obiektów - request, session oraz application. Z tego powodu, w środowisku servletów, obiekty o zasięgu request muszą korzystać z wstrzeliwania zależności bądź bezpośrednio korzystać z fabryki zarządców do tworzenia kontekstu trwałego.

Dostęp do zarządcy trwałości zarządzanego przez kontener (w środowisku Java EE) jest możliwy poprzez wstrzeliwanie zależności (adnotacje @PersistenceUnit lub @PersistenceContext) lub bezpośrednie wyszukanie w JNDI. Cała odpowiedzialność za zarządzanie cyklem rozwojowym kontekstu trwałego (utworzenie, zamknięcie) oraz związanego z nim zarządzcy trwałości spoczywa na kontenerze (serwerze Java EE) i jest niewidoczne dla aplikacji (poza wynikającymi uproszczeniami w sposobie dostępu do nich).

Adnotacja @PersistenceContext służy do wstrzelenia zarządcy trwałego odpowiedzialnego za jednostkę trwałości o nazwie wskazanej przez opcjonalny element unitName. Element type określa typ kontekstu trwałego w odniesieniu do transakcji:
  • TRANSACTION - (domyślna wartość) kontekst trwały pojedyńczej transakcji
  • EXTENDED - kontekst trwały jest przedłużony, tj. trwający dłużej niż pojedyńcza transakcja
Przy następującej deklaracji:

@PersistenceContext
EntityManager em;

ustanawiamy zależność aplikacji od zarządcy trwałego dla jednostki trwałej, która jest jedna w aplikacji (plik persistence.xml zawiera wyłącznie pojedyńczy element persistence-unit) w trybie pojedyńczej transakcji.

Poniższa deklaracja jest analogiczna z tym, że korzysta z wyszukania zarządcy w drzewie JNDI.

@Stateless
@PersistenceContext(name="persistence_sample")
public class CustomerSessionBean implements CustomerSessionRemote {

@Resource SessionContext ctx;

public Customer searchForCustomer(String id){
EntityManager em = (EntityManager) ctx.lookup("persistence_sample");
...
}

}

Dostęp do zarządcy trwałego zarządzanego przez aplikację uzyskiwany jest przez aplikację od fabryki zarządców. Korzysta się wtedy z metod interfejsu EntityManagerFactory i jest identyczne bez względu, czy aplikacja uruchamiana jest w ramach środowiska Java SE, czy Java EE (można sobie wyobrazić servlet bedący zarządzcą zarządcy trwałego, który zazwyczaj korzysta z mechanizmu wstrzeliwania zależności, czyli zarządcy trwałego zarządzanego przez serwer).

Każda fabryka zarządców trwałych zwraca egzemplarze zarządców trwałych, które są skonfigurowane w ten sam sposób (korzystają z tej samej bazy danych, używają tych samych ustawień początkowych, itp.).

Może istnieć wiele egzemplarzy fabryki zarządców trwałych. Taki przypadek może wystąpić podczas korzystania z wielu baz danych, skoro zazwyczaj pojedyńczy egzemplarz zarządcy trwałego korzysta z pojedyńczej bazy danych (zastanawiam się nad użyciem słowa typical w notatce na stronie 115 specyfikacji - czy może być inaczej niż pojedyńczy zarządca - pojedyńcza baza danych?).

Metody interfejsu EntityManagerFactory są odporne na wielowątkowość. Metody służą do utworzenia zarządców trwałych zarządzanych przez aplikację. Zakończenie pracy z fabryką przez aplikację powinno być odnotowane przez wywołanie metody EntityManagerFactory.close(), co powoduje, że wszyscy zarządcy trwali utworzeni przez właśnie zamkniętą fabrykę uważani są również za zamkniętych. Metoda EntityManagerFactory.isOpen() pozwala na sprawdzenie stanu fabryki. Podczas tworzenia zarządcy metodą EntityManagerFactory.createEntityManager(Map map) przekazywana mapa zawiera standardowe właściwości posiadające nazwę rozpoczynającą się od javax.persistence oraz właściwości specyficzne dla dostawcy JPA, które muszą być nazwane inaczej niż rozpoczynając się od javax.persistence.