Aplikacja korporacyjna z JPA w trybie JTA z GlassFish i PostgreSQL

Jacek Laskowski - Notatnik 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:

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.

Grafika:windows-locale.PNG

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).

Grafika:netbeans-glassfish-postgresql-new-webapp.PNG

Oczywiście nie moglibyśmy nie skorzystać z dobrodziejstw JavaServer Faces 1.2.

Grafika:netbeans-glassfish-postgresql-new-webapp-jsf.png

Utworzenie encji - Pracownik

Ponownie korzystamy z pomocnika NetBeans IDE, tym razem do utworzenia encji.

Grafika:netbeans-glassfish-postgresql-new-entity.png

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.

Grafika:netbeans-glassfish-postgresql-new-persistenceunit.png

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.

Grafika:netbeans-glassfish-postgresql-new-managed-bean.png

Wciskając przycisk Next > otrzymujemy

Grafika:netbeans-glassfish-postgresql-jsf-definition.png

Zatwierdzając zostajemy przywitani...NullPointerException.

Grafika:netbeans-glassfish-postgresql-jsf-definition-warning-npe.png

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):

Grafika:netbeans-glassfish-postgresql-uruchomienie.png

Miłej analizy aplikacji. Komentarze mile widziane!