Java EE harmadik rész

By 2015.12.03JAVA

JavaEE harmadik rész

Ebben a részben – ahogy ígértem – a JPA-t fogom alapszinten bemutatni. Annak ellenére, hogy a technológia alapjait szeretném demonstrálni, és  jó pár dolgot nem részletezek, hosszúra sikerült azok a kihagyhatatlan részek miatt, amelyekkel muszáj előhozakodnom, hogy egy viszonylag kerek képet adjak. Remélem a terjedelem miatt senki nem fog a TL;DR; jeligével élve lemondani erről a részről :)

Mindenek előtt egy kis elmélet…

…mert jól jön az még, ha már a gyakorlati rész előtt van egy homályos képünk arról, mit is fogunk csinálni. Ez a homályos kép fog kitisztulni a végére. Mindenesetre megpróbálok az elméleti részben is a lehető leggyakorlatiasabb lenni :) A következő fejezetekben sorra vesszük a bejegyzésben megjelenő komponenseket, technológiákat, és a végén összekapcsoljuk ezeket.

JPA bevezető

Talán úgy a legegyszerűbb leírni, mi is a JPA, ha egy valós adattárolási probléma megoldását szemléltetjük oly módon, hogy egy relációs adatbázistól eljutunk a JPA-ig. A JPA a Java Persistence API rövidítése, mely mint már tudjuk a JavaEE alapok bejegyzésből a JavaEE része. A nevében is benne van, hogy persistence, tehát valami megtartására hivatott, a mi esetünkben adatbázisban való adatok tárolására és kiolvasására. eclipselinklogo Tegyük fel, hogy van egy Person osztályunk, melynek van egy name, egy birthday mezője és egy phones listája. A phones lista Phone osztály példányokat tartalmaz. Egy egyszerű kis telefonkönyvet modellező struktúra. A getterek és setterek hiányától most tekintsünk el :)


public class Person {
    private String name;
    private Date birthday;
    private List<Phone> phones;
}

public enum PhoneType {
    HOME, WORK
}

public class Phone {
    private PhoneType type;
    private String number;
}

Mint látható, egy személyhez tartozhat bármennyi telefonszám típusú objektum. Tegyük fel, hogy szeretnénk a személy és telefonszám objektumainkat adatbázisban tárolni. A kiválasztott adatbázisunkban létrehozzuk a tábláinkat a fenti osztályok szerint:

CREATE TABLE PERSON (ID BIGINT NOT NULL PRIMARY KEY,
                     NAME VARCHAR(200),
                     DATETIME BIRTHDAY);

CREATE TABLE PHONE (ID BIGINT NOT NULL PRIMARY KEY,
                    TYPE VARCHAR(10),
                    PHONE_NUMBER VARCHAR(30),
                    PERSON_ID BIGINT);

ALTER TABLE PHONE ADD CONSTRAINT
                    PHONE_PERSON_FK (PERSON_ID) REFERENCES PERSON (ID);            

Létrehoztuk a táblákat és egy foreign key-t a PHONE táblában lévő PERSON_ID mezőre, mely a PERSON tábla ID mezőjére mutat. Ezzel a modellel a PHONE táblában több rekord is mutathat ugyanarra a PERSON rekordra. Így tudjuk eltárolni a kapcsolatot a személy és a telefonszámai közt. Most pedig jöjjön a java :) Ekkor jön képbe a JDBC, azaz Java Database Connectivity, ami a Java SE része. Szükségünk van a kiválasztott adatbázisunk JDBC driverét tartalmazó könyvtárra, majd a JDBC API-n keresztül elérve az adatbázist már menthetjük is az osztályainkat.

/* Connect to DB */
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/testDB", "testUser", "s3cret");

/* Insert */
Statement stmt = conn.createStatement();
stmt.executeUpdate("INSERT INTO PERSON(name) VALUES ('Teszt Elek')");

/* Query */
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM PHONE WHERE TYPE = 'WORK'");
while (rs.next()) {
    Object phoneNumber = rs.getObject(3);
    // telefonszám
}

Ekkor elkezdjük az így kiolvasott adatainkból példányosítani a Person és Phone osztályainkat, majd bemásolgatni a lekérdezésekből kapott értékeket? Típus konverziót végzünk? Tranzakciót kezelünk? Folyamatosan más és más lekérdezéseket írunk? Na ne is menjünk tovább, oldjuk meg JPA-val. Hogyan?

RDBMS  JPA { JDBC  ORM  Entity (Java osztály) }

A JPA a Java EE ORM szabvány specifikációja. Az ORM az Object Relational Mapping rövidítése, és a célja az objektum-orientált adat relációs adatbázisra való vetítése, leképzése. Nekünk elég csak a Java osztályokat megírnunk, a JPA implementáció képes elvégezni minden más lépést. Létrehozza a sémánkat az adatbázisban, elmenti vagy frissíti az adatokat a java osztályaink szerint, kiolvassa az adatainkat és a java osztályainkba másolja azokat, fenntartja az adatbázis és az objektumaink közti konzisztenciát, kezeli a gyorsítótárat, tranzakciókat, egyszóval rengeteg dolgot levesz a vállunkról és nagyon kényelmessé teszi az adatbázissal való munkánkat. Innentől kezdve azon Java osztályokat, melyek adatot reprezentálnak az adatbázisunkban entitásoknak fogjuk hívni.

Mapping

Honnan is tudja a JPA, hogy mit hogyan mappeljen? Magától biztosan nem fogja tudni :) Legalább egy minimális beavatkozás szükséges a szoftverfejlesztő részéről. Ezt két módon tehetjük meg. A alacsonyabb szinten lévő ORM szabvány biztosította orm.xml leíró segítségével, vagy a JPA szabvány biztosította annotációk segítségével. Ha mindkettőt használjuk, akkor az orm.xml nagyobb prioritást élvez, ráadásul több lehetőséget is nyújt számunkra. Előny lehet az is, hogy nem szükséges a java fájlok újra fordítása a mapping változásakor, illetve több mapping fájlt is használhatunk (az orm.xml fájlnév az alapértelmezett és a META-INF könyvtárban helyezkedik el. Ettől eltérhetünk, de ebben az esetben ezt a JPA implementációnak jelezni kell, a későbbiekben kitérünk rá, hogy hogyan tehetjük ezt meg). A másik lehetőségünk az entitásaink annotációkkal való kiegészítése. A kezdetekben csak az orm.xml-lel dolgozgattunk, de az annotáció java-ba való bevezetése óta igencsak elterjedt módszer lett. Mint ahogy az EE világban a EJB-ket is már annotációval konfiguráljuk, a JPA-ban is ez a legelterjedtebb módszer. Példa orm.xml -el való mapping definiálására:

<?xml version="1.0" encoding="UTF-8" ?>
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm orm_2_0.xsd" version="2.0">

    <entity class="hu.zerotohero.example.javaee.model.Person">
        <attributes>
            <id name="id">
                <column name="ID" unique="true" nullable="false">
                <generated-value strategy="SEQUENCE" generator="PERSON_SEQ_GEN"/>
                <sequence-generator name="PERSON_SEQ_GEN" sequence-name="PERSON_SEQ" allocation-size="1"/>
            </id>
            <basic name="name">
                <column name="NAME"/>
            </basic>
            <basic name="birthday">
                <column name="BIRTHDAY" column-definition="DATE"/>
                <temporal>DATE</temporal>
            </basic>
        </attributes>
    </entity>

    <entity class="hu.zerotohero.example.javaee.model.Phone">
        <attributes>
            <id name="id">
                <column name="ID" unique="true" nullable="false"/>
                <generated-value strategy="SEQUENCE" generator="PHONE_SEQ_GEN"/>
                <sequence-generator name="PHONE_SEQ_GEN" sequence-name="PHONE_SEQ_GEN" allocation-size="1"/>
            </id>
            <basic name="type">
                <column name="TYPE"/>
                <enumerated>STRING</enumerated>
            </basic>
            <many-to-one name="phones">
                <join-column name="PERSON_ID"/>
            </many-to-one>
        </attributes>
    </entity>

</entity-mappings>

A mi példaprogramunkban annotációval fogjuk definiálni a mappinget, ezért erre most nem írok példát, ott minden kiderül majd :) Most hogy megvan a mapping, felmerül a kérdés, hogy mégis hogyan mondjuk meg a JPA-nak, hogy menteni akarunk valamit, vagy épp szeretnénk lekérdezni az adatbázisból? Itt jön képbe az EntityManagerFactory, és itt fogjuk megemlíteni a persistence.xml-t és a tranzakció kezelést is.

persistence.xml

Ha annotációt használunk a mappingre, egy bizonyos fájlra akkor is szükségünk lesz, nem ússzuk meg. Ez a fájl pedig a persistence.xml. Ez egy olyan leíró fájl, mely persistence unit-okat tartalmaz, melyek összefogják az entitásainkat egy egésszé és a megfelelő adatbázishoz kapcsolják az xml-ben leírt paraméterek szerint. Itt tudjuk megadni melyik JPA implementációt akarjuk használni, milyen tranzakció kezelést szeretnénk, csoportokba rendezhetjük az entitásainkat, megadhatjuk melyik adatbázishoz csatlakozzunk, elkészítse-e a JPA az adatbázis sémánkat a mapping szerint, logoljon-e, milyen szinten, hová, sőt, még azt is megadhatjuk, hogy milyen dialektust használjon az SQL lekérdezések elkészítésére, generálására. Lássunk egy példát egy persistence.xml fájlra:

<?xml version="1.0" encoding="UTF-8" ?>
<persistence version="2.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_2_0.xsd">

    <persistence-unit name="ZTH_PU" transaction-type="RESOURCE_LOCAL">

        <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
        <mapping-file>general-mapping.xml</mapping-file>
        <mapping-file>eclipselink-mapping.xml</mapping-file>

        <jar-file>MySharedModel.jar</jar-file>
        <jar-file>OtherModel.jar</jar-file>

        <class>hu.zerotohero.example.javaee.model.Person</class>
        <class>hu.zerotohero.example.javaee.model.Phone</class>

        <exclude-unlisted-classes>false</exclude-unlisted-classes>

        <properties>
            <property name="eclipselink.ddl-generation" value="create-or-extend-tables"/>
            <property name="eclipselink.ddl-generation.output-mode" value="database"/>
        </properties>
    </persistence-unit>

</persistence>

A fenti xml-ben látszik, hogy létre kell hoznunk egy persistent-unit -ot, melynek most a ZTH_PU nevet adtuk. Több persistent unit is létezhet egy persistence.xml fájlban, ezekre külön tudunk hivatkozni, majd amikor az EntityManager-ünket hozzuk létre, ezt a következő fejezetben láthatjuk majd. A tranzakció típusa a példa fájlban RESOURCE_LOCAL, mely azt jelzi, hogy a tranzakciókat a JPA kezeli. A mi példa programunkban nem így lesz, mivel JavaEE környezetben dolgozunk, maga a container kezeli majd a tranzakciókat. Ha több JPA implementációt használunk egyszerre, akkor megadhatjuk, hogy ez a persistence unit melyik provider-t használja. A példa xml-ben EclipseLink van megadva, mi is ezt használjuk majd, mivel a Weblogic tartalmazza (További JPA implementációk pl.: Hibernate, TopLink, OpenJPA). Itt adhatunk meg saját mapping (orm.xml) fájlt, akár többet is. A jar-file szolgál a külső könyvtárakban szereplő entitások megadására. Több jar-file is használható, de a használata esetén nincs lehetőség a <class> segítségével megadni a konkrét osztályainkat. Az exclude-unlisted-classes true-ra állításával megtehetjük, hogy ebben a persistence-unit-ba csak a class tag-ek által megadott entitások tartozzanak bele. A properties tag alatt pedig a JPA implementációt paraméterezhetjük fel. Ezek a paraméterek az implementációtól függnek.

EntityManager

Ahhoz, hogy az entitásainkat menedzselni tudjuk, EntityManager-re van szükségünk. Az EntityManager tartalmazza a PersistenceContext-ünket, ami az entitásaink és az adatbázis konzisztenciáját hivatott karban tartani, továbbá a cache is itt található. Ha a persistence context tartalmazza az entitás példányunkat, akkor menedzselt entitásról, managed entity-ról beszélünk. A programunkban kétféleképpen hozhatjuk létre az EntityManager példányunkat, ez pedig a tranzakció kezelés típusától függ, tehát ezt a következő pontban írom le. Amit nekünk most tudnunk kell az EntityManager-ről, az az, hogy rajta keresztül tudunk entitásokat menteni, és olvasni az adatbázisba, illetve több módon lekérdezéseket készíteni. Ezek pár típusa szerepelni fog a példa programunkban, ott el is magyarázom őket.

Tranzakció kezelés

A JPA kétféle tranzakció kezelést ismer: RESOURCE_LOCAL, JTA.

RESOURCE_LOCAL

A RESOURCE_LOCAL esetében nekünk kell létrehoznunk az EntityManager-t az EntityManagerFactory segítségével:

@PersistenceUnit(unitName = "ZTH_PU")
private EntityManagerFactory entityManagerFactory;

EntityManager entityManager = entityManagerFactory.createEntityManager();

Ebben az esetben az EntityManagerFactory-t tudjuk injektélni a @PersistenceUnit annotáció segítségével. A createEntityManager metódus hívásakor létrejött EntityManager tartalmazza a PersistenceContext-et és a cache-ünket. Ügyelnünk kell arra, hogy ne hívjuk meg többször a metódust, mert ahányszor ezt megtesszük, egy újabb PersistenceContext és egy újabb cache jön létre. Ezt nyilván nem szeretnénk. Továbbá a RESOURCE_LOCAL típusú tranzakció kezelés esetében magunknak kell gondoskodnunk a tranzakciós blokkok kezeléséről az EntityTransaction API segítségével.

EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();
try {
    savePersons();
    transaction.commit();
} catch (Exception e) {
    transaction.rollback();
}
JTA

Mivel JavaEE környezetben vagyunk, mi ezt fogjuk választani és nem is bánjuk, lássuk miért: JTA esetében a PersistenceContext és a cache az alkalmazásszerver által van kezelve, így nekünk csak az EntityManager-t kell injektálnunk a következő módon:

@PersistenceContext(unitName = "ZTH_PU")
private EntityManager entityManager;

Ha csak egy persistence unit-unk van, a unitName paraméter elhagyható, hisz egyértelmű, melyiket szeretnénk használni. JTA esetében a tranzakciók automatikusan kezelve vannak, de van lehetőségünk felülbírálni annotációk segítségével. Továbbá a JTA együtt kezeli a tranzakcióinkat a JavaEE többi komponensével is, mint pl a JMS (Java Message Service).

JavaDB, avagy Derby

Szükségünk lesz adatbázisra is. Mivel ez egy tanuló alkalmazás, nincs szükségünk nagy tudású, nagy teljesítményű adatbázis szerverre. derbylogo   A JDK része a JavaDB, ami egy az Oracle által támogatott, teljesen jávában megírt adatbázis szerver. Tehát, ha van JDK, van DB ;) A Weblogic is képes kezelni, ha a megfelelő helyre másoljuk a megfelelő könyvtárakat. Ezt majd a későbbiekben meg is tesszük.

JSF

Az előző cikkben JSP-t használtunk. Talán nem árulok el nagy titkot, ha azt mondom, ma már senki se kezd JSP használatával web-et fejleszteni. Viszont van egy JSF-ünk, ami szintén a JavaEE része.

jsflogo

A JSF a JavaServer Faces rövidítése. Nagyon nem szeretnék belemenni, hogy miben jó a JSF vagy miben rossz, én személy szerint nem nagyon kedvelem. Igaz mióta rá vagyok kényszerítve, hogy használjam, kezdek kibékülni vele, de sose lesz a kedvencem. A JSF komponens alapú, template keretrendszer. A JSF első verziója JSP-t használt template-ként, viszont a JSF 2 már Faceleteket. Mivel ez is egy API, itt is léteznek változó implementációk. A referencia implementáció a Mojarra, ezt használja a Weblogic 12c is. Létezik sok különböző komponens csomag, ami a JSF-re épül, ilyen pl. a PrimeFaces, OmniFaces vagy az Apache MyFaces. Ezek közül a PrimeFaces a legelterjedtebb.

EL (Expression Language)

A JSF-en belül EL-t is használni fogunk. Ez egy fontos része a prezentációs réteg (web) és a szerver kommunikációjának. A példában látni fogjuk mennyire hatékony és jól használható :)

Összefoglalás

Most hogy átvettük mit is fogunk használni, foglaljuk össze:

  • Szükségünk lesz egy adatbázis szerverre, ezt a JavaDB (Derby) biztosítja nekünk
  • Az adatokat entitásokon keresztül használjuk, ebben segít nekünk a JPA, definiáljuk, hogy miként képezze le az objektum orientált adatstruktúránkat a relációs adatbázisunkra és elvégzi az adatok mentését és lekérdezését, visszaolvasását
  • Az adatok bevitelére és megjelenítésére a web-et használjuk, ezért egy webes keretrendszerre lesz szükségünk, ebben lesz segítségünkre a JSF

A sok elmélet után jöjjön a gyakorlat

Mielőtt bármibe belekezdenénk, szükségünk lesz az előző részben létrehozott környezetünkre és a példa alkalmazásunkra. Ha ez kimaradt volna, vagy már nincs meg, az előző cikk alapján – ami itt található – nagyon gyorsan összerakható. Ha ezzel megvagyunk, készítsük fel a WebLogic-unkat az adatbázisunk használatára.

WebLogic config

Ahhoz hogy a WebLogic kezelni tudja a Derby adatbázist, szüksége van a kliens könyvtáraira. Ezeket egy előre megadott helyen keresi, tehát nem kell mást tennünk, mint oda bemásolnunk őket (vagy akár szimbolikus linket készítenünk róluk).

weblogiclogo

Ezek után szükség lesz egy Data Source-ra, melynél beállítjuk az adatbázisunk adatait, majd a WebLogic connection pool-t hoz létre, amin keresztül JDBC hozzáférést biztosít az alkalmazások számára. Továbbá megkönnyítjük a dolgunk, és használni fogjuk a WebLogic maven plugin-ját, mely segítségével nagyon könnyen tudunk majd alkalmazást deploy-olni.

Adatbázis telepítése

A JavaDB a következő helyen található: $JAVA_HOME/db vagy Windowson %JAVA_HOME%/db Másoljuk be ezt a könyvtárat a WebLogic alá derby néven, a következő útvonalra: $MW_HOME/wlserver/common/ illetve Windowson %MW_HOME%\wlserver\common\ Itt a common könyvtárban lennie kell egy bin és egy lib könyvtárnak. Ha ezzel megvagyunk, már létre tudjuk hozni az alkalmazásunkhoz szükséges Data Source-t.

Adatbázis szerver elindítása

Mielőtt létrehoznánk az adatforrás beállítást a WebLogic-ban, indítsuk el az adatbázis szerverünket. Ennél a pontnál meg kell jegyezni, hogy a Derby az aktuális könyvtárba fogja létrehozni a szükséges adat állományait, ezért érdemes olyan helyről indítani, ahol joga van ezt megtenni, illetve nem felejtjük el később, hogy honnan is indítottuk. Én a WebLogic domain könyvtárunkat ajánlom, ahonnan magát a WebLogic-ot is indítjuk, így egy helyen lesznek.

Ennél a pontnál van egy kis kompatibilitási hiba a WebLogic szkriptek és a Derby szkriptek közt. Eredetileg a WebLogic automatikusan elindítja a Derby szervert, ha az be van másolva a megfelelő könyvtárba. Ez Windowson lehet, hogy meg is történik, ezt nem ellenőriztem, viszont *NIX rendszereken más az Derby szerver indító fájl neve, ezért nem tudja elindítani. Ezt több féle módon tudjuk javítani, de ez most tovább bonyolítaná a dolgokat, mivel nem elég átnevezni az indító fájlt a megfelelő névre (.sh kiterjesztés hozzáadása), mert ebben az esetben a WebLogic indulása megáll, mivel a startNetworkServer.sh nem kerül háttérbe és nem lép ki (ez is orvosolható természetesen, de most nem ezt tesszük). Ez a megoldás most nekünk sokkal egyszerűbb, mert nem kell szerkesztenünk egyetlen fájlt sem, de itt jegyezném meg, hogy ez nem egy jó megoldás :)

Lépjünk a WebLogic domain-ünk könyvtárába, majd innen indítsuk el az adatbázis szervert a következő módon:

cd $MW_HOME/user_projects/domains/mydomain
$JAVA_HOME/db/bin/startNetworkServer

illetve Windowson:

cd %MW_HOME%\user_projects\domains\mydomain
%JAVA_HOME%\db\bin\startNetworkServer.bat

Ha hasonló üzenetet kapunk, akkor az adatbázis szerverünk elindult:

cd 
Apache Derby Network Server - 10.8.3.2 - (1557835) started and ready to accept connections on port 1527

DataSource létrehozása

Indítsuk a WebLogic-ot a már ismert módon.

cd $MW_HOME/user_projects/domains/mydomain
./startWebLogic.sh

illetve Windowson:

cd %MW_HOME%\user_projects\domains\mydomain
startWebLogic.cmd

Mikor már fut a WebLogic, lépjünk be az Admin Consolba a következő címen: http://localhost:7001/console Lépjünk be a weblogic és welcome1 felhasználói név és jelszó párossal, majd ezután bal oldalon a Domain Structure panelen lévő Services menüben kattintsunk a Data Sources menüpontra.

wl_domain_structure

A következő képernyőn a Name mező legyen zthDS, a JNDI Name legyen jdbc/zthDS, a Database Type legördülő menüből pedig válasszuk ki a Derby-t. Ha a legördülő menüben nem szerepel a Derby, akkor valószínűleg rossz helyre másoltuk a JavaDB fájljait, ezt ellenőrizzük le.

wl_zth_ds_1

Ha ezzel megvagyunk, kattintsunk a Next gombra. A következő oldalon a Database Driver legördülő menüből válasszuk a Derby’s Driver (Type 4 XA) Version:Any menüpontot (elméletileg ez az alapértelmezett), majd kattintsunk ismét a Next gombra. A következő Transaction Options részen nem tudunk mit beállítani, kattintsunk a Next gombra. A következő oldalon töltsük ki a mezőket az alábbi módon:

wl_zth_ds_2

 

Mező neve Érték
Database Name: zth
Host Name: localhost
Port: 1527
Database User Name: zth
Password: zth
Confirm Password: zth

Ha ezzel megvagyunk, kattintsunk a Next gombra. A következő oldalon a Test Configuration gombra kattintva ellenőrizzük, hogy fut-e az adatbázis szerverünk, és a WebLogic eléri-e azt.

wl_zth_ds_3

Ha minden jól működik, akkor a Connection test succeeded üzenetet kell kapnunk. Ellenkező esetben győződjünk meg arról, hogy fut-e az adatbázis szerverünk, és a DataSource létrehozása közben jól adtuk-e meg az adatokat. Ezután nyomjuk meg a Next gombot, és a következő Select Targets képernyőn jelöljük be a domain-ünket: √ myserver.

wl_zth_ds_4

Legvégül kattintsunk a Finish gombra. Ha minden sikerült, a következő táblázat fogad minket:

wl_zth_ds_5

WebLogic Maven Plugin telepítése

Az előző cikkben elég körülményes módot választottunk az elkészült alkalmazásunk deployolására. Most használjuk ki a WebLogic adta lehetőségeket, és telepítsük a wls-maven-plugin artifact-et, majd használjuk a pom-xml-ünkben: Lépjünk be a WebLogic alá a következő könyvtárba:

cd $MW_HOME/wlserver/server/lib

illetve Windowson:

cd %MW_HOME%\wlserver\server\lib

majd adjuk ki a következő parancsot:

mvn install:install-file -Dfile=wls-maven-plugin.jar -DpomFile=pom.xml

Ezzel telepítettük a lokális tárolónkba a WebLogic maven plugin-jét. Ezt például a következőképpen tudjuk majd használni:

mvn com.oracle.weblogic:wls-maven-plugin:help
mvn com.oracle.weblogic:wls-maven-plugin:deploy
mvn com.oracle.weblogic:wls-maven-plugin:undeploy

Mint láthatjuk ez nem túl kényelmes, ezért lehetőségünk van ezt egyszerűsíteni, ha szerkesztjük a $HOME (illetve %HOMEPATH%) könyvtárunkban található .m2 könyvtár alatt lévő settings.xml fájlunkat. Írjuk bele a következőt a <settings> tag után:

<pluginGroups>
    <pluginGroup>com.oracle.weblogic</pluginGroup>
</pluginGroups>

Ezek után az com.oracle.weblogic groupId-val rendelkező maven plugin-t tudjuk az egyszerűbb formájában is használni, a következő módokon:

mvn wls:help
mvn wls:deploy
mvn wls:undeploy

Maven WebLogic Plugin használatba vétele

Ha már telepítettük a plugint, vegyük is gyorsan használatba, mivel a következő pontban már ezzel fogunk deployolni. Nagyon egyszerűen be tudunk állítani, a pom.xml-ünkben a <build> alatt lévő <plugins> tag alá (a többi pluginhoz hasonlóan) írjuk be a következő módon:

<plugin>
    <groupId>com.oracle.weblogic</groupId>
    <artifactId>wls-maven-plugin</artifactId>
    <version>12.1.3.0</version>
    <configuration>
        <middlewareHome>${env.MW_HOME}</middlewareHome>
        <name>${project.build.finalName}</name>
        <user>weblogic</user>
        <password>welcome1</password>
    </configuration>
</plugin>

A <configuration> rész sokkal több beállítási lehetőséget nyújt, de mivel nekünk egy alapértelmezett beállításokkal telepített WebLogicunk van, nincs szükségünk semmi extra paraméterre. A <middlewareHome> kötelező, de mivel ezt beállítottuk környezeti változóként az operációs rendszerünkben, a maven ezt könnyedén használatba is veheti. Továbbá meg kell adnunk a deployolni kívánt alkalmazásunk nevét. Ahhoz hogy ne térjünk el az eddig névtől, használjuk a maven által automatikusan generált nevet, ez a project.build.finalName placeholderen keresztül érhető el. Ebből a placeholderből látszik, hogy a pom.xml-ünkben a <project> tag alatt lévő <build> tag alatt a <finalName> tag-ben megadhatjuk az alkalmazásunk végleges nevét. Ebben az esetben ilyen néven fog létrejönni a .war fájlunk a projekt könyvtárunk alatt lévő target mappában. A <user> és <password> minden esetben kötelező, ez a WebLogic-unk admin szerveréhez való kapcsolódás autentikációjára szolgál, mivel azon keresztül deployolja az alkalmazásunkat. Ezt a felhasználói nevet és jelszót adtuk meg akkor, amikor az alapértelmezett domainünket hoztuk létre a WebLogic telepítésekor. Vegyük észre, hogy nem kell megadnunk az admin szerverünk elérését, hiszen alapértelmezetten a localhost:7001 -es porton figyel. Ha mégis meg szeretnénk tenni, akkor az <adminurl> tagben ezt megtehetjük.

JSP lecserélése JSF-re

Következő lépésként, hogy a kódhoz nyúlunk, legyen az, hogy lecseréljük az eddigi JSP oldalunkat JSF-re. Megfigyelhetjük, hogy mennyire le fog egyszerűsödni minden :) Ahhoz, hogy fordítási időben ismerjük a JSF osztályait  (és az IDE is lássa), a pom.xml-ünkben adjuk hozzá a következő függőséget a <dependencies> tagek közé:

<dependency>
    <groupId>javax.faces</groupId>
    <artifactId>jsf-api</artifactId>
    <version>2.0</version>
    <scope>provided</scope>
</dependency>

Ezután fogjuk a PersonServlet osztályunkat, és… töröljük ki :) Bizony, rá már nem lesz szükségünk. helyette módosítsuk a PersonBean osztályunkat a következőképpen:

package hu.zerotohero.example.javaee;

import java.io.Serializable;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.SessionScoped;

@ManagedBean
@SessionScoped
public class PersonBean implements Serializable {

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

Az látszik, hogy lecseréltük az annotációkat. Ebben az esetben a @Named annotáció helyett @ManagedBean lett, a @SessionScoped annotáció pedig másik csomagból került be, a javax.faces.bean csomagból. A JSF többféleképp inicializálható. Normális esetben van a WEB-INF könyvtárunk alatt egy web.xml fájlunk, abban felvennénk a FacesServlet osztályt, és a servlet-mapping-el a *.xhtml fájlokra állítanánk. Erre már nincs szükség, mert a JavaEE konténerünk megteszi helyettünk, ha talál a WEB-INF alatt egy faces-config.xml fájlt (nekünk ilyenünk sem lesz), vagy van legalább egy olyan osztály, ami valamelyik JSF-es annotációval van ellátva (ilyenünk viszont van, lást @ManagedBean). Ezek után nincs más hátra, amint létrehozni az új index.xhtml az index.jsp helyett. Hozzuk létre az index.xhtml fájlt a webapp könyvtár alatt a következő tartalommal:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core">
<f:view>
    <h1>Hello World!</h1>
    Person name: ${personBean.name} <br/>

    <h:form>
        <h:inputText value="#{personBean.name}"/>
        <h:commandButton value="Set"/>
    </h:form>
</f:view>
</html>

Utolsó lépésként töröljük ki az index.jsp-t, majd fordítsuk le az alkalmazásunkat, és deployoljuk a már beállított WebLogic Maven pluginünk segítségével (természetesen a projekt könyvtárában adjuk ki a következő parancsot):

mvn clean install wls:deploy

Ha ez sikeresen lefutott, a következő címen már az új JSF-es alkalmazásunk fut: http://localhost:7001/javaee-0.1-SNAPSHOT/index.xhtml

Person mentése, avagy névjegyzék

Következő lépésként egy nagyobb lélegzetvételű dolog következik. Létrehozzuk a persistence.xml-ünket, majd az entitásainkat. Ha ezzel megvagyunk létre kell hoznunk egy DAO-t (Data Access Object), mely majd az adataink hozzáférését és mentését fogja biztosítani, majd létrehozunk egy service osztályt, ami pedig a DAO-nkon keresztül üzleti logikát valósít meg. Legvégül pedig megírjuk a megjelenítéshez szükséges xhtml, azaz JSF fájlunkat.

persistence.xml

Első lépésként hozzuk létre a persistence.xml fájlt a következő útvonalon: (a <project> értelemszerűen a projektünk gyökérkönyvtára. Mint láthatjuk, a resources könyvtár is új lesz számunkra. A maven projektekben a resources könyvtárak olyan könyvtárak, melyek tartalma a csomagolás közben bekerül a .jar, .war esetleg .ear fájlunkba) <project>/src/main/resources/META-INF/persistence.xml a következő tartalommal:

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.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_2_0.xsd">

    <persistence-unit name="ZTH_PU" transaction-type="JTA">
        <jta-data-source>jdbc/zthDS</jta-data-source>
        <exclude-unlisted-classes>false</exclude-unlisted-classes>
        <properties>
            <property name="eclipselink.deploy-on-startup" value="true"/>
            <property name="eclipselink.ddl-generation" value="create-or-extend-tables"/>
            <property name="eclipselink.ddl-generation.output-mode" value="database"/>
            <property name="eclipselink.logging.logger" value="DefaultLogger"/>
            <property name="eclipselink.logging.level" value="FINE"/>
            <property name="eclipselink.logging.level.sql" value="FINE"/>
        </properties>
    </persistence-unit>

</persistence>

Mint láthatjuk, hogy a persistence.xml-ünk tartalmaz egy persistence unit-ot ZTH_PU névvel. Ezt a nevet nem fogjuk használni, mivel csak egy darab persistence unitunk van, tehát egyértelmű a JPA számára, hogy melyiket is kell használnia az Persistence Context létrehozásakor. A tranzakciós típusunk JTA, ezzel jelezve, hogy az alkalmazás szervernek a feladata a tranzakciók kezelése. Mivel JTA a tranzakciós típusunk, az adatforrást a <jta-data-source> tagben tudjuk megadni. Itt megadtuk a WebLogic-ban beállított adatforrásunknál beállított JNDI nevet. Az <exclude-unlisted-classes> false értéket kapott, mert nem szeretnénk, ha kihagyná azokat az entitásokat, melyeket itt nem definiáltunk, mivel egyet sem definiáltunk :) Így automatikusan detektálva lesznek, a @Entity annotáció alapján. Továbbá adtunk meg JPA implementáció függő beállításokat is a properties-ben, ezeket vegyük gyorsan sorra:

  • eclipselink.deploy-on-startup: az alkalmazásunk deploy-olásakor inicializálja az eclipselink-et is. Ez többek közt azért is fontos, hogy már induláskor létrehozza az entitásaink alapján az adatbázis sémánkat.
  • eclipselink.ddl-generation: itt megadjuk, hogy az automatikus séma létrehozás milyen módon történjen. A create-or-extend-tables létrehozza azon entitások tábláit, melyek még nem léteznek, illetve kibővíti azokat, amennyiben az adatbázisban még hiányos táblákat talál. Létrehozza a foreign key-eket is, így ezzel sem kell foglalkoznunk.
  • eclipselink.ddl-generation.output-mode: itt állíthatjuk be a séma generálásának kimenetét. Mi database-t választottunk, tehét eleve az adatbázisba generálja. Van módunk fájlba is generálni, illetve mindkettőbe, de nekünk erre most nincs szükségünk.
  • eclipselink.logging.logger: Mivel szeretnénk látni mit művel az eclipselink, állítsunk be logolást. Itt megadjuk, hogy melyik logger-t használja. Nekünk most a DefaultLogger kell, mivel nem állítunk be sajátot.
  • eclipselink.logging.level: itt megadhatjuk a logolás szintjét. A FINE már elég beszédes lesz.
  • eclipselink.logging.level.sql: mivel szeretnénk a generált SQL utasításokat is látni, ezért ezt a paramétert is be kell állítanunk. Itt szintén megfelel a FINE.

Fontos megjegyezni, hogy az automatikus séma generálást nem tanácsos rábízni a JPA-ra, érdemes ezt magunknak megtenni valamilyen adatbázis verziókezelő rendszerrel, mint pl flyway, vagy a mégjobb liquibase. Mivel ez nem egy komoly alkalmazás, azért hogy a cikk minél egyszerűbb legyen mi ezt most nem tesszük meg (bár már úgy érzem nem tenné sokkal hosszabbá, mint amennyire hosszú már most is :) ).

Entitások

Most pedig hozzuk létre az entitásainkat. Első lépésben készítsünk egy absztrakt ős entitást, mely tartalmazni fogja a közös mezőket, mint például az ID és a VERSION. Az ID-re mindenki tudja, miért van szükségünk, a VERSION viszont azért kell, hogy a JPA el tudja dönteni, hogy az az entitás példány amit ő ismer, az megegyezik-e azzal, mint ami az adatbázisban található, tehát konzisztencia miatt. Hozzuk tehát létre a BaseEntity nevű osztályunkat a következő csomagba: hu.zerotohero.example.javaee.model

package hu.zerotohero.example.javaee.model;

import java.io.Serializable;
import java.util.Objects;
import javax.persistence.*;

@MappedSuperclass
public abstract class BaseEntity implements Serializable {

    @Id
    @GeneratedValue(generator = "ID_GEN", strategy = GenerationType.SEQUENCE)
    @SequenceGenerator(name = "ID_GEN", sequenceName = "ID_SEQ", allocationSize = 10)
    private Long id;

    @Version
    private Long version;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Long getVersion() {
        return version;
    }

    public void setVersion(Long version) {
        this.version = version;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) { return true; }
        if (o == null || getClass() != o.getClass()) { return false; }
        BaseEntity that = (BaseEntity) o;
        return Objects.equals(getId(), that.getId()) &&
                Objects.equals(getVersion(), that.getVersion());
    }

    @Override
    public int hashCode() {
        return Objects.hash(getId(), getVersion());
    }
}

Amit itt elsőnek észrevehetünk, hogy ez az osztály absztrakt, tehát nem példányosítható, és a @MappedSuperclass annotációval van ellátva a @Entity helyett. Ez azt jelenti a JPA számára, hogy ha egy entitás ebből az osztályból származik le, akkor annak is rendelkeznie kell az itt definiált mezőkkel, mint az id és a version, viszont maga az ősosztály nem létezik, mint entitás. Deklaráltuk az id és version mezőket Long típusúként, elkészítettük hozzá a megfelelő getter és setter metódusainkat, valamint megírtuk az equals és hashCode metódusokat. Ezeket az IDE is ki tudja generálni nekünk, ha egy mód van rá, ne azzal töltsük az időnket, hogy ezeket kézzel írjuk meg. Viszont a generált equals és hashCode metódusokat nem árt ellenőrízni. Létezik egy olyan projekt, mely annotációk segítségével szabadít meg minket a sok boilerplate kódtól, mint például az imént említett getter, setter, equals és hashCode metódusok. Ezt a projektet Lombok-nak hívják, érdemes utánajárni, ha valakit érdekel. Mi szeretjük használni, és hasznosnak tartjuk. Az id mezőnk egy különleges mező, mely esetünkben egy automatikusan generált értéket fog kapni, tehát nem mi töltjük ki. Látható, hogy a @Id annotáció mellet még rendelkezik egy @GeneratedValue annotációval, mely megmondja, hogy ez egy generált érték lesz. Az annotáció argumentumaiban meg tudjuk adni a generátor nevét és az érték generálásának stratégiáját, mely esetünkben egy adatbázis szekvencia lesz. Mivel szekvenciát használunk, meg kell adnunk a @SequenceGenerator annotációban a generátorunk nevét és hogy ehhez milyen szekvencia generátor található (ID_SEQ: ilyen névvel fog szerepelni az adatbázisban) A version mezőnk @Version annotációval lett ellátva, ezzel jelezve a JPA-nak, hogy azt a mezőt használhatja a verzió ellenőrzésére.

Rövid példa arra, hogy miért is van szükség a version mezőre: Minden mentés és update ezt a verzió számot növeli eggyel. Tehát ha elmentünk egy új entitást, az megkapja a nullát ebben a mezőben. Tegyük fel, hogy valahol kiolvassuk ezt az entitást. Szerepelni fog a példány a persistence context-ben, a version mezője nulla értékű lesz. Közben egy másik session-ben az alkalmazásunk egy másik felhasználóval szintén kiolvasta ezt az entitást, módosított valamit, majd elmentette. Ekkor az adatbázisban a version már 1-es értékkel rendelkezik, viszont a mi session-ünkben ugyanez az entitás még 0-val. Mi is módosítunk rajta, majd szeretnénk elmenteni, de nem tudjuk. A JPA ellenőrzi, hogy az entitásunkhoz tartozó adatbázis táblában lévő entitás példányunkhoz tartozó rekordban a version értéke megegyezik-e, ha nem akkor jön a mindenki által előbb vagy utóbb megismert OptimisticLockException :)

Most hogy megírtuk az BaseEntity-nket, hozzuk létre a Person entityt ugyanabban a package-ben, Person néven, a következő tartalommal:

package hu.zerotohero.example.javaee.model;

import java.util.Date;
import java.util.Objects;
import javax.persistence.*;

@Entity
public class Person extends BaseEntity {

    @Basic
    private String name;

    @Basic
    @Temporal(TemporalType.DATE)
    private Date birthday;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) { return true; }
        if (o == null || getClass() != o.getClass()) { return false; }
        if (!super.equals(o)) { return false; }
        Person person = (Person) o;
        return Objects.equals(getName(), person.getName()) &&
                Objects.equals(getBirthday(), person.getBirthday());
    }

    @Override
    public int hashCode() {
        return Objects.hash(super.hashCode(), getName(), getBirthday());
    }
}

Itt már láthatjuk, hogy a Person osztályunk az @Entity annotációt kapta, és a BaseEntity-ból származik, valamit már nem absztrakt osztály. Definiáltuk a megfelelő mezőket, mint a name és a birthday a megfelelő JPA annotációkkal. A @Basic annotáció akár el is hagyható, mivel a JPA felismeri a mezőket, és tudja, milyen típust használjon az adatbázisban a megfelelő mezőkhöz. Ha ez nem dönthető el, mint például a birthday mezőnél, melynél csak a dátumra van szükségünk, megadhatjuk ebben az esetben a @Temporal annotációval és annak TemporalType típusú attribútumával. Ha azt szeretnénk, hogy egy bizonyos mező ne kerüljön perzisztálásra, akkor azt a @Transient annotációval tudjuk jelezni.

DAO

Most hogy ez entitásunkkal megvagyunk, készítsük el a DAO-nkat. Hozzuk létre a BaseDao osztályt a következő csomagban: hu.zerotohero.example.javaee.dao

package hu.zerotohero.example.javaee.dao;

import java.util.List;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import hu.zerotohero.example.javaee.model.BaseEntity;

@Stateless
public class BaseDao {

    @PersistenceContext
    private EntityManager entityManager;

    public <E extends BaseEntity> List<E> query(Class<E> entityClass, String queryName) {
        return entityManager.createNamedQuery(queryName, entityClass).getResultList();
    }

    public <E extends BaseEntity> E save(E entity) {
        if (entity.getId() == null) {
            entityManager.persist(entity);
            return entity;
        } else {
            return entityManager.merge(entity);
        }
    }

}

Mint láthatjuk, ez egy @Stateless bean lesz, és egyetlen mezőt tartalmaz, az pedig az EntityManager melyet a @PersistenceContext annotációval injektáltunk be. Létrehoztunk két metódust. A query metódusunk egy BaseEntity-ból leszármazott osztály-t vár, e szerint tudja majd, hogy a visszatérési listában milyen típusú entitások lesznek, továbbá egy nevesített lekérdezés nevét várja. A másik metódusunk pedig egy entitást ment el. Az implementációjában az látszik, hogy ha ez egy új entitás, akkor az id mezője még nem rendelkezik értékkel, ekkor az entityManger persist metódusát használjuk mentésre. Ebben az esetben a mentést követően az adatbázis szekvenciánkból kiolvasott következő érték bekerül az entitásunk id mezőjébe. A második esetben a merge metódust használjuk. Abban az esetben, ha egy olyan entitást akarunk menteni, ami nem menedzselt, tehát nem része a persistence context-ünknek, a mentés után menedzselté válik, és ezt az entitás példányt kapjuk vissza ebben az esetben. Mivel létrehoztunk itt egy olyan metódust, amivel nevesített lekérdezéseket tudunk futtatni, hozzunk létre egy NamedQuery-t a Person entitásunkon. Egészítsük ki a Person osztályunkat a következő módon:

...

@NamedQueries(
    @NamedQuery(name = Person.NQ_FIND_ALL_PERSONS, query = "select p from Person p")
)
@Entity
public class Person extends BaseEntity {
    public static final String NQ_FIND_ALL_PERSONS = "person.findAll";

...

A @Entity annotációnk fölé írjuk meg a @NamedQueries annotációnkat, melynek a nevét konstansban tároljuk, amit a Person osztályunk deklarációjában definiáljunk a fent szemléltetett módon. A named query-ket JPQL-ben kell megírunk, ez nagyon hasonlít a natív SQL query-kre.

Service

Következő lépésben hozzuk létre a PersonService nevű osztályunkat a következő csomagban: hu.zerotohero.example.javaee.service

package hu.zerotohero.example.javaee.service;

import java.util.Date;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;

import hu.zerotohero.example.javaee.dao.BaseDao;
import hu.zerotohero.example.javaee.model.Person;

@Named
public class PersonService {

    @Inject
    private BaseDao baseDao;

    public void addPerson(String name, Date birthday) {
        Person person = new Person();
        person.setName(name);
        person.setBirthday(birthday);
        baseDao.save(person);
    }

    public List<Person> getPersons() {
        return baseDao.query(Person.class, Person.NQ_FIND_ALL_PERSONS);
    }

}

Ez a szervíz osztály fog gondoskodni az adatok és a megjelenítés közti kommunikációról. Mint láthatjuk, ez az osztály @Named annotációt kapott, mivel nevesített bean-re van szükségünk, ha JSF-ben hivatkozni szeretnénk rá. A PersonService-ben találhatjuk a fentebb elkészített BaseDao-nkat injektálva, két metódus mellett. Az addPerson metódus új Person entitást hoz létre a megadott paraméterek alapján, majd elmenti ezt nekünk az adatbázisba a DAO-nk segítségével. A getPersons metódus a DAO-nk segítségével futtatja a nevesített lekérdezésünket, mi szerint szeretnénk megkapni az összes Person típusú entitást.

Persons (web)

Ezek után nincs más hátra, webes felületet kell eszkábálnunk :) Van egy PersonService-ünk, mely már elég lesz egy Persons táblázat megjelenítésére, és egy új Person elmentésére. Hozzuk tehát létre a persons.xhtml fájlt a projektünk webapp könyvtárában a következő tartalommal:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core">
<f:view>
    <h1>Persons</h1>
    <hr/>

    <h:form>

        <h:panelGrid columns="2">
            Name:
            <h:inputText id="personName" binding="#{personName}"/>
            Birthday:
            <h:inputText id="personBirthday" binding="#{personBirthday}" title="Format: yyyy-MM-dd, eg.: 1980-06-25">
                <f:convertDateTime pattern="yyyy-MM-dd"/>
            </h:inputText>
        </h:panelGrid>

        <h:commandButton value="Add person" action="#{personService.addPerson(personName.value, personBirthday.value)}"/>

        <hr/>

        <h:dataTable value="#{personService.persons}" var="person" border="1" cellpadding="2" cellspacing="0">
            <h:column>
                <f:facet name="header">ID</f:facet>
                <h:outputText value="#{person.id}"/>
            </h:column>
            <h:column>
                <f:facet name="header">Name</f:facet>
                <h:outputText value="#{person.name}"/>
            </h:column>
            <h:column>
                <f:facet name="header">Birthday</f:facet>
                <h:outputText value="#{person.birthday}">
                    <f:convertDateTime pattern="yyyy-MM-dd"/>
                </h:outputText>
            </h:column>
        </h:dataTable>

    </h:form>

</f:view>
</html>

A <h:panelGrid> alatt lévő <h:inputText>-ekben lévő binding attríbútúmmal hozzárendeljük ez értékeket a változóhoz, melyet felhasználhatunk a <h:commandButton> action attribútumával, ahol a personService.addPerson metódust hívjuk meg az imént említett paraméterekkel. Mivel ez egy <h:form> része, a gomb megnyomására fordul egyet az oldal, lefut az addPerson metódus, az alább lévő <h:dataTable> pedig újra kitöltődik. A <h:dataTable> value attribútumában a personService.getPersons metódusa hívódik meg, ami pedig lekérdezi nekünk az összes Person-t, majd a var attribútumban megadott névvel minden rekordhoz hozzárendeli, a <h:column> tagekben definiált módon pedig oszloponként lekérdezhetjük valamely mezőjét. Szerkesszük az index.xhtml fájlunkat, és a </f:view> záró tag elé írjük a következő link definíciót:

<h:link outcome="/persons" value="Persons"/>

Ezzel létrehoztunk egy linket, mely az új persons.xhtml oldalunkra mutat.

Make and Deploy

Nincs más hátra, fordítsuk le az alkalmazásunkat, és deployoljuk a WebLogic szerverünkre, majd teszteljük le :)

mvn clean install wls:deploy

Phone entitás, avagy telefonkönyv

Ha kijátszottuk magunkat :) elkezdhetjük egy kicsit bonyolítani a dolgot. Készítsünk telefonkönyvet. Ehhez az első lépés a Phone entitás létrehozása. Viszont mielőtt létrehoznánk a Phone entitást, hozzunk létre egy PhoneType enumot, amivel azt jelöljük majd, hogy a telefonszámunk milyen típusú.

PhoneType enum

A hu.zerotohero.example.javaee.model csomagban (ahol a Person és a BaseEntity is van) hozzuk létre a PhoneType enumot a következő tartalommal:

package hu.zerotohero.example.javaee.model;

public enum PhoneType {
    HOME, WORK
}

Phone entitás

Ezután létrehozhatjuk ugyanebben a csomagban a Phone entitásunkat a következő tartalommal:

package hu.zerotohero.example.javaee.model;

import java.util.Objects;
import javax.persistence.*;

@Entity
public class Phone extends BaseEntity {

    @Basic
    @Enumerated(EnumType.STRING)
    private PhoneType type;

    @Basic
    @Column(name = "PHONE_NUMBER", length = 30)
    private String number;

    @ManyToOne
    private Person person;

    public PhoneType getType() {
        return type;
    }

    public void setType(PhoneType type) {
        this.type = type;
    }

    public String getNumber() {
        return number;
    }

    public void setNumber(String number) {
        this.number = number;
    }

    public Person getPerson() {
        return person;
    }

    public void setPerson(Person person) {
        this.person = person;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) { return true; }
        if (o == null || getClass() != o.getClass()) { return false; }
        if (!super.equals(o)) { return false; }
        Phone phone = (Phone) o;
        return getType() == phone.getType() &&
                Objects.equals(getNumber(), phone.getNumber()) &&
                Objects.equals(getPerson(), phone.getPerson());
    }

    @Override
    public int hashCode() {
        return Objects.hash(super.hashCode(), getType(), getNumber(), getPerson());
    }
}

Ennél az entitásnál láthatjuk, hogy a number mezőnk egy @Column annotációt is tartalmaz. Ezzel tudjuk meghatározni, hogy az adatbázisban ez a mező egy másik néven jelenjen meg, esetünkben a PHONE_NUMBER néven, továbbá a maximális hossza legyen 30 karakter. A Person mezőnk egy @ManyToOne annotációval lett ellátva, mely azt definiálja, hogy a Phone entitásokhoz (Many) tartozik egy Person entitás (One). ManyToOne, tehát több Phone is kapcsolódhat egy Personhoz. Ebben az esetben a JPA egy olyan mezőt hoz létre a Phone entitáshoz tartozó táblában, melynek a típusa a Person entitáshoz tartozó tábla ID mezőjének a típusa, továbbá erre foreign key constraint-et is készít. Nekünk nem kell foglalkoznunk, hogy a Long típusú ID-t kiolvasva megkeressük a Person további mezőit, esetleg join-okat gyártsunk, ezt a JPA elvégzi helyettünk.

Sokat lehetne írni itt arról, hogy a JPA mit miért és hogyan csinál, de ez a cikk is inkább az alapokról szól, ezért nem megyünk most bele adatmodell tervezésbe, JPA optimalizálásba, ez a cikk pusztán az egyszerű JPA világból hivatott ízelítőt adni.

PhoneDao

A telefonkönyvnél szükségünk lesz egy olyan lekérdezésre, melynél egy Person szerint szeretnénk megkapni az összes hozzá tartozó Phone entitást, avagy a személy összes telefonszámát. A hu.zerotohero.example.javaee.dao csomagunkba (ahol a BaseDao is van) hozzuk létre a PhoneDao osztályunkat a következő tartalommal:

package hu.zerotohero.example.javaee.dao;

import java.util.List;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.criteria.*;

import hu.zerotohero.example.javaee.model.Person;
import hu.zerotohero.example.javaee.model.Phone;

@Stateless
public class PhoneDao {

    @PersistenceContext
    private EntityManager entityManager;

    public List<Phone> getPhonesByPerson(Person person) {
        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        CriteriaQuery<Phone> query = criteriaBuilder.createQuery(Phone.class);
        Root<Phone> from = query.from(Phone.class);
        Path<Person> personField = from.get("person");
        Predicate personPredicate = criteriaBuilder.equal(personField, person);
        query.where(personPredicate);
        return entityManager.createQuery(query).getResultList();
    }

}

Láthatjuk, hogy itt is injektáltuk az EntityManager-t, majd létrehoztunk egy metódust, mely a Person paramétere szerint visszaadja a hozzá tartozó Phone listát. Ennél a metódusnál Criteria Query-t használtunk, mely a JPA beépített lekérdező nyelve. Az entityManager-től elkérjük a CriteriaBuilder-t, mely segítségével CriteriaQuery-t és Predicate-eket tudunk gyártani. Ebben a metódusban a criteriaBuilder equals metódusával olyan Predicate-et hoztunk létre, mely a Phone entitásban lévő person mezőt hasonlítja össze a metódus paraméterében megadott personnal, és ha azonos, akkor az a Phone entitás megjelenik a visszatérési listában. Természetesen ebből is egy SQL lekérdezés gyártódik és hajtódik végre a háttérben, de a JPA ezt szépen elfedi nekünk.

BaseDao kiegészítés, find by Id

Következő lépésben ki kell egészítenünk a BaseDao osztályunkat egy új metódussal. Szükségünk lesz egy olyan függvényre, mely ID szerint visszaadja magát az entitást. Erre majd a persons weboldalunkról a phones weboldalunkra való átlépésnél lesz szükségünk. A BaseDao osztályhoz adjuk hozzá a következő metódust:

public <E extends BaseEntity> E find(Class<E> entityClass, Long id) {
    return entityManager.find(entityClass, id);
}

Mint látható, nem sok mindent csinál, csupán meghívja ugyanazt a metódust az entityManager-en.

PersonConverter

Mielőtt nekivágnánk a phones.xhtml megírásának, létre kell hoznunk egy átalakítót, melyet a JSF fog használni akkor, amikor megkapja a Person ID-nkat arra, hogy Person entitást varázsoljon belőle. Hozzuk létre a PersonConverter osztályt a hu.zerotohero.example.javaee.converter csomagban, a következő tartalommal:

package hu.zerotohero.example.javaee.converter;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;
import javax.inject.Inject;
import javax.inject.Named;

import hu.zerotohero.example.javaee.dao.BaseDao;
import hu.zerotohero.example.javaee.model.Person;

@Named
public class PersonConverter implements Converter {

    @Inject
    private BaseDao baseDao;

    @Override
    public Object getAsObject(FacesContext facesContext, UIComponent uiComponent, String s) {
        return baseDao.find(Person.class, Long.parseLong(s));
    }

    @Override
    public String getAsString(FacesContext facesContext, UIComponent uiComponent, Object o) {
        return ((Person) o).getId().toString();
    }
}

Ez az osztály implementálja a Converter interfészt. A getAsObject metódus a String-ként megkapott paraméterből fog nekünk Long típusú Id-t, majd ezután az Id szerint lekérdezett Person objektumot gyártani. A getAsString pedig az o paraméterként kapott Person objektumból olvassa ki a Long típusú Id-t, majd String-ként adja azt vissza.

Biztos sokaknak feltűnt (legalábbis nagyon remélem :) ), hogy nulla hibakezelés van a két metódusban. Korrekt, tényleg nincs benne ilyen, mivel itt a JPA-ra koncentrálunk, nem szerettem volna ezzel még több kódot gyártani, de senki se jegyezze meg, hogy ez így helyes, mert nem az :)

PhoneService

Az üzleti logika biztosítására hozzuk létre a PhoneService osztályt a hu.zerotohero.example.javaee.service csomagban (ahol a PersonService is van) a következő tartalommal:

package hu.zerotohero.example.javaee.service;

import java.util.List;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.inject.Named;

import hu.zerotohero.example.javaee.dao.BaseDao;
import hu.zerotohero.example.javaee.dao.PhoneDao;
import hu.zerotohero.example.javaee.model.Person;
import hu.zerotohero.example.javaee.model.Phone;
import hu.zerotohero.example.javaee.model.PhoneType;

@Named
@RequestScoped
public class PhoneService {

    @Inject
    private BaseDao baseDao;

    @Inject
    private PhoneDao phoneDao;

    private Person person;

    public Person getPerson() {
        return person;
    }

    public void setPerson(Person person) {
        this.person = person;
    }

    public void addPhone(String phoneType, String phoneNumber) {
        Phone phone = new Phone();
        phone.setPerson(person);
        phone.setType(PhoneType.valueOf(phoneType));
        phone.setNumber(phoneNumber);
        baseDao.save(phone);
    }

    public List<Phone> getPhones() {
        return phoneDao.getPhonesByPerson(person);
    }

}

Ami elsőre feltűnhet, az a @RequestScope annotáció, mely a bean-ünk életciklusát hivatott meghatározni. Tehát egy PhoneService példány lesz jelen egy lekérdezés scope-ban. Ez azért fontos, mert ha a JSF először elkéri a phoneSerice bean-t EL-en keresztül, és elmenti az Id-ként megkapott, később Person típusra konvertált person-t a person mezőjébe, a következő phoneService kérésénél, amikor a getPhones metódus hívódna meg a táblázat adatainak elkérése érdekében, ekkor már egy másik phoneService példányt kapna, amiben nem szerepel az általunk átadott person példány, így az null értékkel bír. Ha RequestScoped az életciklusa, akkor az egész xhtml feldolgozásában csak egy phoneService példány szerepel, tehát végig ismerjük a kapott Person példányunkat :)

Linkek a phones oldalra

Ebben a pontban létrehozunk a persons.xhtml fájlunkban lévő táblázatban egy új oszlopot, melynek minden sorában egy link fog szerepelni, ami majd az új phones lapunkra mutat, minden sornak megfelelő person id paraméterrel. A persons.xhtml fájlunkban egészítsük ki a <h:dataTable> tagünket úgy, hogy a benne található utolsó <h:column> blokk után írjunk be egy új <h:column> blokkot, a következőképpen:

<h:column>
    <h:link value="Phones" outcome="/phones">
        <f:param name="personId" value="#{person.id}"/>
    </h:link>
</h:column>

Phones weboldal

Ebben a pontban megírjuk a phones.xhtml oldalunkat. Hozzuk létre a phones.xhtml oldalt ugyanott, ahol a persons.xhtml oldalunk is van, a következő tartalommal:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core">
<f:view>
    <f:metadata>
        <f:viewParam name="personId" value="#{phoneService.person}" converter="#{personConverter}"/>
    </f:metadata>

    <h1>Phones by person</h1>

    <h:link outcome="/persons" value="Back to persons"/>
    <hr/>

    <h:panelGrid columns="2">
        Name:
        <h:outputText value="#{phoneService.person.name}"/>
        Birthday:
        <h:outputText value="#{phoneService.person.birthday}">
            <f:convertDateTime pattern="yyyy-MM-dd"/>
        </h:outputText>
    </h:panelGrid>

    <hr/>

    <h:form>

        <h:panelGrid columns="2">
            Type:
            <h:selectOneMenu id="phoneType" binding="#{phoneType}">
                <f:selectItem itemValue="WORK" itemLabel="Work"/>
                <f:selectItem itemValue="HOME" itemLabel="Home"/>
            </h:selectOneMenu>
            Number:
            <h:inputText id="phoneNumber" binding="#{phoneNumber}"/>
        </h:panelGrid>

        <h:commandButton value="Add phone number to person" action="#{phoneService.addPhone(phoneType.value, phoneNumber.value)}"/>

        <hr/>

        <h:dataTable value="#{phoneService.phones}" var="phone" border="1" cellpadding="2" cellspacing="0">
            <h:column>
                <f:facet name="header">ID</f:facet>
                <h:outputText value="#{phone.id}"/>
            </h:column>
            <h:column>
                <f:facet name="header">Type</f:facet>
                <h:outputText value="#{phone.type}"/>
            </h:column>
            <h:column>
                <f:facet name="header">Number</f:facet>
                <h:outputText value="#{phone.number}"/>
            </h:column>
        </h:dataTable>

    </h:form>

</f:view>
</html>

Az <f:metadata> részben látszik, hogy itt használjuk a converter-t arra, hogy az Id-ként megkapott személyt Person példánnyá alakítsuk, és elmentsük a phoneService bean-ünkben található person mezőbe. Ezek után a persons.xhtml-hez hasonlóan egy táblázatban kiírjuk a kapott person-hoz tartozó telefonszámokat.

Make and Deploy again :)

Most sincs más hátra, fordítsuk le az alkalmazásunkat, és deploy-oljuk a WebLogic szerverünkre, majd teszteljük le :)

mvn clean install wls:deploy

Mi lesz legközelebb?

Legközelebb megnézzük hogy tudunk módosítani, törölni, több a többhöz kapcsolatokat létrehozni, meta osztályokat generálni az entitásainkhoz.