Aplikacja korporacyjna z JPA w trybie JTA z GlassFish i PostgreSQL
Z Jacek Laskowski - Wiki Projektanta Java EE
Przyszła pora na rozpoznanie możliwości uruchomienia Java Persistence API (JPA) w trybie transakcyjnym JTA na serwerze GlassFish i bazą danych PostgreSQL. Do tej pory większość z moich poprzednich artykułów dotyczyła wykorzystania JPA w trybie RESOURCE_LOCAL, co w środowisku serwera aplikacyjnego Java EE 5 nie jest rekomendowaną konfiguracją ze względu na niewielkie wykorzystanie usług samego serwera jako monitora transakcyjnego, czy środowiska dostarczającego mechanizmu wstrzeliwania zależności (ang. DI - dependency injection). Tym artykułem nadrabiamy braki i wypełniamy lukę w potrzebie stworzenia aplikacji będącej przyczółkiem do rozpoczęcia poznawania zaawansowanych konfiguracji usług serwera aplikacyjnego Java EE (w tej roli GlassFish). Aplikacja w obecnej postaci nie korzysta ze wszystkich możliwych uproszczeń (usług) serwera i stąd jedynie potraktowanie jej jako aplikacji wprowadzającej, w której oczekiwać będzie można kolejnych usprawnień, jak wykorzystanie komponentu sesyjnego EJB 3.0 jako fasady dla encji i zniwelowania konieczności użycia mechanizmów obsługi transakcji bezpośrednio w kodzie komponentu zarządzanego JSF oraz skorzystania z narzędzi typu Apache Maven 2 czy alternatywnych dostawców JPA.
Istotnym elementem aplikacji jest wykorzystanie usługi monitora transakcyjnego serwera aplikacyjnego Java EE i mechanizmu DI. Do tej pory baza danych była uruchamiana w trybie wbudowanym podczas, gdy teraz stanowi istotny element architektury aplikacji. Narzędziem do budowania aplikacji jest NetBeans IDE 6.0M9, jednakże użycie narzędzia to jedynie skrócenie czasu na utworzenie aplikacji i dowolne narzędzie programistyczne może zostać użyte podczas lektury artykułu.
Wersje oprogramowania w środowisku uruchomieniowym:
- GlassFish v2 b43
- PostgreSQL 8.2.3 (Postgres)
- NetBeans IDE 6.0M9
Artykuł zakłada, że powyższe oprogramowanie zostało wcześniej zainstalowane i działa poprawnie.
Artykuł był oparty o wcześniejszą moją relację Aplikacja JPA w GlassFish v2 z Firebird v2 i Eclipse IDE 3.3.
Kompletny projekt NetBeans jest dostępny jako glassfish-postgresql.zip. Projekt wystarczy zaimportować do NetBeans IDE i uruchomić.
Spis treści |
Utworzenie bazy danych w PostgreSQL
W dokumentacji sterownika Postgres - Chapter 2. Setting up the JDBC Driver sekcja Creating a Database - znajduje się wzmianka o konieczności poprawnej konfiguracji ustawień locale dla bazy Postgres.
- Do not use a database that uses the SQL_ASCII encoding. [...] If you do not know what your encoding will be or are otherwise unsure about what you will be storing the UNICODE encoding is a reasonable default to use.
Niestety poza komentarzem trudno doszukać się w dokumentacji Postgres odpowiedzi, jakie konkretnie zmiany należy poczynić, aby baza danych akceptowała polskie znaki. Szczęśliwie, na stronie Internacjonalizacja znajduje się przepis na odszukanie jakiego ustawienia locale powiniśmy użyć na platformie MS Windows XP, aby PostgreSQL poprawnie zarządzał polskimi danymi.
Mając dostateczną wiedzę, aby kontynuować pracę, przystępujemy do założenia bazy danych.
jlaskowski@dev /cygdrive/c/apps/PostgreSQL/8.2
$ ./bin/initdb -D "c:/temp/dictionary-db" --locale=Polish_Poland.28592
The files belonging to this database system will be owned by user "jlaskowski".
This user must also own the server process.
The database cluster will be initialized with locale Polish_Poland.28592.
creating directory c:/temp/dictionary-db ... ok
creating subdirectories ... ok
selecting default max_connections ... 100
selecting default shared_buffers/max_fsm_pages ... 32MB/204800
creating configuration files ... ok
creating template1 database in c:/temp/dictionary-db/base/1 ... ok
initializing pg_authid ... ok
initializing dependencies ... ok
creating system views ... ok
loading system objects' descriptions ... ok
creating conversions ... ok
setting privileges on built-in objects ... ok
creating information schema ... ok
vacuuming database template1 ... ok
copying template1 to template0 ... ok
copying template1 to postgres ... ok
WARNING: enabling "trust" authentication for local connections
You can change this by editing pg_hba.conf or using the -A option the
next time you run initdb.
Success. You can now start the database server using:
"c:\apps\PostgreSQL\8.2\bin\postgres" -D "c:/temp/dictionary-db"
or
"c:\apps\PostgreSQL\8.2\bin\pg_ctl" -D "c:/temp/dictionary-db" -l logfile start
Zgodnie z komunikatem podsumowującym tworzenie bazy, uruchamiamy bazę danych za pomocą skryptu pg_ctl.
jlaskowski@dev /cygdrive/c/apps/PostgreSQL/8.2 $ ./bin/pg_ctl -D "c:/temp/dictionary-db" -l "c:/temp/dictionary-db/logfile" start server starting
Kolejnym krokiem jest utworzenie dedykowanego użytkownika do pracy z bazą danych, np. glassfish.
jlaskowski@dev /cygdrive/c/apps/PostgreSQL/8.2 $ ./bin/createuser.exe --createdb --pwprompt --no-superuser --no-createrole glassfish Enter password for new role: Enter it again: CREATE ROLE
, a następnie samą bazę danych.
jlaskowski@dev /cygdrive/c/apps/PostgreSQL/8.2 $ ./bin/createdb.exe --encoding=UNICODE --username glassfish sandbox CREATE DATABASE
Sprawdzenie poprawności działania bazy danych kończy administrację Postgresql.
jlaskowski@dev /cygdrive/c/apps/PostgreSQL/8.2
$ ./bin/psql sandbox
Welcome to psql 8.2.3, the PostgreSQL interactive terminal.
Type: \copyright for distribution terms
\h for help with SQL commands
\? for help with psql commands
\g or terminate with semicolon to execute query
\q to quit
Warning: Console code page (852) differs from Windows code page (1250)
8-bit characters may not work correctly. See psql reference
page "Notes for Windows users" for details.
sandbox=# \l
List of databases
Name | Owner | Encoding
-----------+------------+-----------
sandbox | glassfish | UTF8
(4 rows)
sandbox=# \q
Konfiguracja puli połączeń do PostgreSQL w GlassFish
Instalacja sterownika JDBC dla PostgreSQL
Zgodnie z dokumentacją sterownika - Download - potrzebujemy sterownika o nazwie postgresql-8.2-504.jdbc3.jar, który jest dystrybuowany wraz z samym projektem PostgreSQL w katalogu jdbc.
Szukając informacji o instalacji sterownika JDBC możemy natrafić na sekcję Making the JDBC Driver JAR Files Accessible w dokumentacji GlassFish, w której napisano:
- To integrate the JDBC driver into a Application Server domain, copy the JAR files into the domain-dir/lib/ext directory, then restart the server
To jednak rodzi pytania o możliwe problemy z wieloma wersjami sterownika. Szukając bardziej niezależnego, między aplikacjami, sposobu instalacji sterownika natrafiamy ostatecznie na jego potwierdzenie w rozdziale Using the Java Optional Package Mechanism dokumentacji GlassFish, gdzie napisano, że umieszczenie sterownika JDBC w katalogu domain-dir/lib/ext jest zalecane.
- This is the recommended way of adding JDBC drivers to the Application Server
Przekonani, przekopiowujemy bibliotekę sterownika JDBC dla Postgres - jdbc/postgresql-8.2-504.jdbc3.jar - do katalogu $GLASSFISH_HOME/domains/domain1/lib/ext (domyślna nazwa domeny w GlassFish to domain1).
Definicja puli połączeń
Najpierw uruchamiamy GlassFish.
jlaskowski@dev /cygdrive/c/apps/glassfish $ ./bin/asadmin.bat start-domain domain1 Starting Domain domain1, please wait. Log redirected to c:\apps\glassfish\domains\domain1\logs\server.log. Redirecting output to C:/apps/glassfish/domains/domain1/logs/server.log Domain domain1 is ready to receive client requests. Additional services are being started in background. Domain [domain1] is running [Sun Java System Application Server 9.1 (build b49-beta3)] with its configuration and logs at: [c:\apps\glassfish\domains]. Admin Console is available at [http://localhost:4848]. Use the same port [4848] for "asadmin" commands. User web applications are available at these URLs: [http://localhost:8080 https://localhost:8181 ]. Following web-contexts are available: [/web1 /__wstx-services ]. Standard JMX Clients (like JConsole) can connect to JMXServiceURL: [service:jmx:rmi:///jndi/rmi://dev:8686/jmxrmi] for domain management purposes. Domain listens on at least following ports for connections: [8080 8181 4848 3700 3820 3920 8686 ]. Domain does not support application server clusters and other standalone instances.
Kolejnym krokiem jest wykonanie serii administracyjnych poleceń GlassFish (nudne, ale konieczne).
Tworzymy pulę połączeń.
jlaskowski@dev /cygdrive/c/apps/glassfish $ ./bin/asadmin.bat create-jdbc-connection-pool \ --datasourceclassname org.postgresql.ds.PGSimpleDataSource \ --restype javax.sql.DataSource \ --property portNumber=5432:password=glassfish:user=glassfish:serverName=localhost:databaseName=sandbox \ PostgresPool Command create-jdbc-connection-pool executed successfully.
Sprawdzamy jej poprawność.
jlaskowski@dev /cygdrive/c/apps/glassfish $ ./bin/asadmin.bat ping-connection-pool PostgresPool Command ping-connection-pool executed successfully.
I ostatecznie udostępniamy pulę pod nazwą jdbc/postgres w drzewie JNDI.
jlaskowski@dev /cygdrive/c/apps/glassfish $ ./bin/asadmin.bat create-jdbc-resource --connectionpoolid PostgresPool jdbc/postgres Command create-jdbc-resource executed successfully.
Końcowy krok to sprawdzenie zawartości drzewa JNDI przy pomocy polecenia list-jdbc-resources oraz list-jndi-entries.
jlaskowski@dev /cygdrive/c/apps/glassfish $ ./bin/asadmin.bat list-jdbc-resources jdbc/__TimerPool jdbc/__CallFlowPool jdbc/__default jdbc/postgres Command list-jdbc-resources executed successfully.
jlaskowski@dev /cygdrive/c/apps/glassfish $ ./bin/asadmin.bat list-jndi-entries --context jdbc Jndi Entries for server within jdbc context: postgres: javax.naming.Reference postgres__pm: javax.naming.Reference Command list-jndi-entries executed successfully.
Dla ożywienia warto wspomnieć o ciekawej funkcjonalności skryptu asadmin, która pozwala na odszukanie poleceń administracyjnych korzystając z wyrażeń regularnych (Finding CLI commands in GlassFish is now easier and better), np. wyszukanie wszystkich poleceń związanych z tworzeniem puli połączeń:
jlaskowski@dev /cygdrive/c/apps/glassfish
$ ./bin/asadmin.bat jdbc
Unable to find entry for command, jdbc.
Closest matching command(s):
create-jdbc-connection-pool
create-jdbc-resource
delete-jdbc-connection-pool
delete-jdbc-resource
list-jdbc-connection-pools
list-jdbc-resources
CLI001 Invalid Command, jdbc.
albo
jlaskowski@dev /cygdrive/c/apps/glassfish
$ ./bin/asadmin.bat "^create.*pool$"
Unable to find entry for command, create.*pool$.
Closest matching command(s):
create-connector-connection-pool
create-jdbc-connection-pool
create-threadpool
CLI001 Invalid Command, create.*pool$.
Ot, taki ciekawy przerywnik w nużącej pracy administratora.
Utworzenie aplikacji - glassfish-postgresql
Utworzenie projektu aplikacji w NetBeans IDE
Naszą pracę urozmaicimy korzystając z NetBeans IDE 6.0M9. Jak wspomniano wcześniej, użycie dowolnego innego środowiska programistycznego powinno zakończyć się identycznym wynikiem, a jedynie może się nieznacznie wydłużyć bądź skrócić(zgodnie z regułą wyboru właściwego narzędzia do właściwego zadania dzisiaj stawiamy na NetBeans IDE).
Korzystamy z pomocnika do tworzenia aplikacji internetowej (mimo szumnego tytułu tworzenia aplikacji korporacyjnej tym razem będzie to zwykła aplikacja internetowa).
Oczywiście nie moglibyśmy nie skorzystać z dobrodziejstw JavaServer Faces 1.2.
Utworzenie encji - Pracownik
Ponownie korzystamy z pomocnika NetBeans IDE, tym razem do utworzenia encji.
Za pomocą przycisku Create Persistence Unit... mamy możliwość stworzenia nowej jednostki utrwalania (PU). NetBeans IDE pobiera informacje o dostępnych źródłach danych ze związanego z projektem serwera aplikacyjnego, a tym samym i o naszym jdbc/postgres, z którego skorzystamy.
Korzystamy z domyślnego dostawcy JPA w GlassFish, którym jest TopLink Essentials. Dodatkowo trybem pracy PU jest JTA (atrybut transaction-type). Innymi słowy korzystamy z usług serwera aplikacyjnego tak dalece, jak się tylko da.
Utworzony plik persistence.xml prezentuje się następująco:
<?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="postgresPU" transaction-type="JTA"> <provider>oracle.toplink.essentials.PersistenceProvider</provider> <jta-data-source>jdbc/postgres</jta-data-source> <properties> <property name="toplink.ddl-generation" value="create-tables" /> </properties> </persistence-unit> </persistence>
Przedstawia się bardzo skromnie, ale o to nam właśnie chodziło, aby wykorzystać usługi serwera aplikacyjnego Java EE oraz prostotę tworzenia aplikacji korporacyjnych zgodnych ze specyfikacją Java EE 5.
Definiujemy dwa dodatkowe atrybuty trwałe - imie i nazwisko - oraz nazwę tabeli dla encji Pracownik (adnotacja @Table). Ostatecznie klasa encji Pracownik przedstawia się następująco:
package pl.jaceklaskowski.javaee.postgres;
import java.io.Serializable;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.NamedQuery;
import javax.persistence.Table;
@Entity
@Table(name = "PRACOWNICY")
@NamedQuery(name = "pobierzPracownikow", query = "SELECT p FROM Pracownik p ORDER BY p.nazwisko, p.imie")
public class Pracownik implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String imie;
private String nazwisko;
public void setId(Long id) {
this.id = id;
}
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
public Long getId() {
return id;
}
public String getImie() {
return imie;
}
public void setImie(String imie) {
this.imie = imie;
}
public String getNazwisko() {
return nazwisko;
}
public void setNazwisko(String nazwisko) {
this.nazwisko = nazwisko;
}
}
Utworzenie komponentu zarządzanego JSF - Kadry
Pora na stworzenie części klienckiej dla naszej aplikacji. Utworzymy komponent zarządzany JSF - Kadry, którego celem będzie zarządzanie informacjami w bazie danych za pomocą właśnie stworzonej encji Pracownik. Skorzystamy z bardzo mało popularnego, acz bardzo wyrafinowanego, mechanizmu JSF o nazwie DataModel, który znacząco uprości zarządzanie danymi tabelarycznymi (np. pobranie informacji o wybranym wierszu przez użytkownika).
Więcej informacji o mechaniźmie DataModel można znaleźć w artykule Tabele w JavaServer Faces - znacznik <h:dataTable>
Skorzystajmy z pomocnika Web->JSF Managed Bean w NetBeans IDE.
Wciskając przycisk Next > otrzymujemy
Zatwierdzając zostajemy przywitani...NullPointerException.
Możnaby się tym zmartwić, ale szczęśliwie wszystko jest w należytym porządku, tj. klasa Kadry zostaje utworzona i odpowiednia definicja komponentu zarządzanego pojawia się w faces-config.xml. Reszta nas nie interesuje...przynajmniej na razie.
Skorzystamy z wstrzeliwania zależności dla komponentów zarządzanych, aby skorzystać z JPA w JSF oraz DataModel do obsługi danych tablicowych w JSF (za Przykład 7 - h:dataTable i obsługa wyboru wiersza).
Po kilku uderzeniach w klawiaturę otrzymujemy następującą klasę komponentu zarządzanego JSF - Kadry:
package pl.jaceklaskowski.javaee.postgres;
import java.util.List;
import javax.annotation.Resource;
import javax.faces.model.DataModel;
import javax.faces.model.ListDataModel;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.transaction.UserTransaction;
public class Kadry {
@PersistenceContext(name = "postgresPU")
EntityManager em;
@Resource
UserTransaction tx;
private DataModel pracownicyDM = new ListDataModel();;
public Kadry() {
// UWAGA: Kolejność wykonywania DI - serwer aplikacyjny i JSF
// Korzystając z DI dla EM i korzystając z niego w konstruktorze nie można skorzystać z DI poprzez
// faces-config.xml
}
public DataModel getPracownicy() {
pracownicyDM.setWrappedData(pobierzPracownikow());
return pracownicyDM;
}
public void setPracownicy(DataModel pracownicyDM) {
this.pracownicyDM = pracownicyDM;
}
protected List<Pracownik> pobierzPracownikow() {
return em.createNamedQuery("pobierzPracownikow").getResultList();
}
public int getSize() {
return pracownicyDM.getRowCount();
}
public String zwolnijPracownika() {
try {
tx.begin();
// UWAGA: JPA wykonując merge zwróci trwałą encję i tylko ta będzie trwała, a nie przekazywany parametr
Pracownik pracownik = (Pracownik) em.merge(pracownicyDM.getRowData());
em.remove(pracownik);
tx.commit();
} catch (Exception e) {
// zignoruj tymczasowo jedynie dla uproszczenia przykładu
}
return "success";
}
public String zatrudnijPracownika() {
// UWAGA:
// Niepotrzebnie musimy opiekować się transakcją - najlepiej wynieść funkcjonalność do EJB z odpowiednią
// deklaracją transakcji
try {
tx.begin();
em.merge(getPracownik());
tx.commit();
} catch (Exception e) {
// zignoruj tymczasowo jedynie dla uproszczenia przykładu
}
return "success";
}
// Korzystamy z DI dla JSF - patrz plik faces-config.xml
Pracownik pracownik;
public void setPracownik(Pracownik pracownik) {
this.pracownik = pracownik;
}
public Pracownik getPracownik() {
return this.pracownik;
}
}
Panel administracji pracownikami - kadry.jsp
Utworzenie strony o nazwie kadry.jsp jako domyślnej strony aplikacji internetowej wymaga dodatkowego kroku zdefiniowania jej w deskryptorze aplikacji internetowej (/WEB-INF/web.xml) tak, że uruchomienie aplikacji będzie automatycznie związane z uruchomieniem strony kadry.jsp. W zasadzie moglibyśmy skorzystać również z możliwości przekierowania ze strony index.jsp (domyślnie uruchamiana strona przez serwer) bądź po prostu utworzyć stronę index.jsp zamiast kadry.jsp, ale byłoby to jedynie obejście a nie faktyczne rozwiązanie.
Modyfikujemy plik web.xml, który ostatecznie przedstawia się następująco:
<?xml version="1.0" encoding="UTF-8"?> <web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"> <context-param> <param-name>com.sun.faces.verifyObjects</param-name> <param-value>false</param-value> </context-param> <context-param> <param-name>com.sun.faces.validateXml</param-name> <param-value>true</param-value> </context-param> <context-param> <param-name>javax.faces.STATE_SAVING_METHOD</param-name> <param-value>client</param-value> </context-param> <servlet> <servlet-name>Faces Servlet</servlet-name> <servlet-class>javax.faces.webapp.FacesServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>Faces Servlet</servlet-name> <url-pattern>/faces/*</url-pattern> </servlet-mapping> <session-config> <session-timeout>30</session-timeout> </session-config> <welcome-file-list> <welcome-file>faces/kadry.jsp</welcome-file> </welcome-file-list> </web-app>
Zauważmy użycie ścieżki faces w elemencie welcome-file, która spowoduje wywołanie servletu Faces Servlet, a co za tym idzie i całej infrastruktury JSF.
Uruchomienie aplikacji
Skoro korzystaliśmy z NetBeans IDE do utworzenia aplikacji, skorzystamy z niego również do jej uruchomienia. Sprowadza się to do wciśnięcia klawisza F6.
Początkowo lista pracowników jest pusta, ale po zatrudnieniu kilku pracowników (poprzez funkcjonalność dolnego formularza) aplikacja prezentuje się następująco (oczywiście, z dokładnością do danych pracowniczych):
Miłej analizy aplikacji. Komentarze mile widziane!







