JPA (Java Persistence API) ist ein API für Datenbankzugriffe und objektrelationales Mapping.
Objektrelationales Mapping (O/R-M oder ORM) bietet Programmierern eine objektorientierte Sicht auf Tabellen und Beziehungen in relationalen Datenbank-Management-Systemen (RDBMS). Statt mit SQL-Statements wird mit Objekten operiert.
JPA kann sowohl mit Java SE als auch mit Java EE verwendet werden. Ab Java EE 5 ist JPA in Java EE enthalten, aber es ist nicht in Java SE enthalten.
Die bekanntesten JPA-Implementierungen sind EclipseLink, Apache OpenJPA und Hibernate.
EclipseLink ist die Referenzimplementierung für JPA 2.0 (JSR 317)
(enthalten in Java EE 6).
TopLink Essentials ist die Referenzimplementierung für
JPA 1.0 (Teil von
Java EE 5,
EJB 3.0,
JSR 220).
Die JPA-API-Beschreibung finden Sie in der javax.persistence-Javadoc.
Der folgende Text ersetzt keine Doku zu JPA, aber demonstriert einige Konfigurationsbeispiele und Features anhand einfacher lauffähiger Beispiele und listet "Best Practices" auf.
Die folgende Tabelle zeigt typische JPA-Basiskonfigurationen. Bitte beachten Sie, dass es weitere Konfigurationsmöglichkeiten und auch Mischformen gibt.
Java SE und einfache Servlet-Container |
Servlet/JSP/JSF/... im Java EE Application Server |
EJB im Java EE Application Server | |
---|---|---|---|
Datasource | Direkte JDBC-Connection (z.B. javax.persistence.jdbc... in der persistence.xml) | Container-managed Datasource (<jta-data-source ... in der persistence.xml) | Container-managed Datasource (<jta-data-source ... in der persistence.xml) |
Transaction-Type | Application-managed Transaction (transaction-type="RESOURCE_LOCAL" in der persistence.xml) | JTA-verwaltete Transaktionen (transaction-type="JTA" in der persistence.xml) | Container-managed Transaction (transaction-type="JTA" in der persistence.xml) |
Transaction-Klasse | Datenbank-bezogen: EntityTransaction tx = em.getTransaction(); |
JTA-kontrolliert: UserTransaction utx = (UserTransaction) (new InitialContext()).lookup( "java:comp/UserTransaction" ); |
JTA-kontrolliert: Normalerweise implizit durch EJB-CMT, alternativ auch: @Resource UserTransaction utx; |
EntityManager | Application-managed EntityManager: EntityManagerFactory emf = Persistence.createEntityManagerFactory( meinJpaPuName ); EntityManager em = emf.createEntityManager(); |
Container-managed EntityManager: EntityManager em = (EntityManager) (new InitialContext()).lookup( "java:comp/env/persistence/em" ); |
Container-managed EntityManager: @PersistenceContext EntityManager em; |
Kodeschnipsel für Speichern, Merge und Find |
JPA:
Es gibt viele Persistence- und ORM-Frameworks. Bevorzugen Sie JPA: Es ist zurzeit der aussichtsreichste Standard.
Sie erhöhen Ihre Flexibilität und Zukunftssicherheit, wenn Sie möglichst wenige über den JPA-Standard hinausgehende proprietäre Spezialfeatures Ihrer JPA-Implementierung verwenden.
Links auf Dokus zu JPA finden Sie weiter unten.
Bevorzugen Sie in Java-EE-Anwendungen "container-managed Datasourcen" (<jta-data-source ... in der persistence.xml) gegenüber direkten JDBC-Connections.
Transaction:
Bevorzugen Sie in Java-EE-Anwendungen "JTA-verwaltete Transaktionen" (UserTransaction) (und in EJBs "container-managed Transaction") (vom Java EE Application Server kontrolliert) gegenüber "EntityTransaction" ("Resource-Local Transaction", Datenbank-bezogen).
EntityManager:
Bevorzugen Sie den "container-managed EntityManager" (vom Java EE Container kontrolliert, manchmal auch "container-managed persistence context" genannt) gegenüber dem "application-managed EntityManager" (EntityManager-Lifecycle von Anwendung gesteuert).
Beachten Sie, dass zwar die EntityManagerFactory thread-safe ist, aber nicht der EntityManager. Die Injektion "@PersistenceContext EntityManager em;" darf so deshalb nur in single-threaded Objekten (z.B. Session-EJBs) verwendet werden, aber zum Beispiel nicht in Servlets.
Falls Sie nicht eine EJB-Anwendung erstellen, sondern Servlets (oder JSP, JSF, ...) verwenden, können Sie querschnittliche Funktionalität zentral in einem Servlet-Filter kapseln (siehe hier, hier und hier).
Verwenden Sie bei Assoziationen/Relationen zwischen Tabellen immer die entsprechenden Annotationen @OneToOne, @OneToMany, @ManyToOne und @ManyToMany.
OneToMany unidirektional:
Wenn Sie z.B. in der Entity-Klasse Team definieren:
@OneToMany Set<Person> teamMembers = new HashSet<>();
und in der Person-Entity-Klasse nichts hierzu definieren, haben Sie eine unidirektionale Beziehung: Die Person weiß nichts von der Teammitgliedschaft. Deshalb erweitert JPA auch die Person-Tabelle nicht um eine Foreign-Key-Spalte, sondern muss eine separate Join-Tabelle hinzufügen.
Definieren Sie in der Entity-Klasse Team:
@OneToMany(mappedBy="team") Set<Person> teamMembers = new HashSet<>();
Und definieren Sie in der Entity-Klasse Person:
@ManyToOne Team team;
Jetzt haben Sie eine bidirektionale Beziehung: Die Person-Tabelle wird um eine Foreign-Key-Spalte erweitert und es wird keine separate Join-Tabelle benötigt.
Erweitern Sie Ihre Assoziations-Annotationen um cascade=...-Attribute:
Entity-Klasse Team:
@OneToMany(mappedBy="team", cascade=CascadeType.PERSIST) Set<Person> teamMembers = new HashSet<>();
Und die Entity-Klasse Person:
@ManyToOne(cascade=CascadeType.PERSIST) Team team;
CascadeType.PERSIST sollten Sie nahezu immer verwenden, damit alles konsistent gespeichert wird.
CascadeType.REMOVE verwenden Sie nicht bei "Aggregationen", sondern nur dann, wenn Sie wirklich eine "Komposition" haben, wenn es also die "Teile" nicht ohne das "Ganze" bzw. das "Ganze" nicht ohne die "Teile" gibt. Im Team/Person-Beispiel liegt keine Komposition vor, weil Teams und Personen auch unabhängig voneinander existieren können.
Die anderen CascadeType-Varianten werden seltener benötigt.
Sowohl bei einzelnen Attributen als auch bei Assoziationen können Sie das Ladeverhalten steuern:
Bei fetch=FetchType.EAGER
werden die Daten sofort aus der Datenbank geladen.
fetch=FetchType.LAZY
erlaubt dem JPA-Provider die Daten eventuell nur bei Bedarf aus der Datenbank zu laden (was Vendor-spezifisch unterschiedlich implementiert sein kann).
Beachten Sie, dass das Lazy-Nachladen nicht mehr funktioniert, wenn die Entity-Objekte detached sind oder mit merge() reattached werden. Falls Sie vor dem Detachen noch Collections laden wollen: Es genügt, wenn Sie Collection.size() aufrufen.
Bei @OneToOne und @ManyToOne ist der Default EAGER.
Bei @OneToMany und @ManyToMany ist der Default LAZY.
Falls Sie den Primary Key kennen, verwenden Sie immer em.find() und nicht die Query-Methoden, damit der Persistenz-Context-Cache einbezogen wird.
Wie mit SQL sind auch mit JPQL Updates, Deletes, Joins, Subselects, Aggregationen und viele Sonderfunktionen möglich.
Bevorzugen Sie
"JPQL Queries" gegenüber
"Native SQL Queries":
JPQL ist datenbankunabhängig, native SQL nicht.
JPQL ist sehr ähnlich zu SQL. Wichtige Unterschiede sind:
JPQL-Beispiel:
Query query = em.createQuery( "Select a from MeineEntity a where a.meinAttr like :param" ); query.setParameter( "param", "%abc%" ); List<MeineEntity> list = query.getResultList();
Native-SQL-Query-Beispiel:
Query query = em.createNativeQuery( "Select * from MeineEntity where meinAttr like ?" ); query.setParameter( 1, "%abc%" ); List<Object> list = query.getResultList();
Beachten Sie, dass bei JPQL-Queries normalerweise keine abhängigen Entities mitgeladen werden.
In detached Entities sind Listen assoziierter Elemente leer.
Falls Sie abhängige Entities mitladen wollen, können Sie "left join fetch" verwenden, beispielsweise so:
"Select distinct t from Team t left join fetch t.teamMembers where t.team.name = :teamName";
Bei normalen JPA-Updates werden einzelne Datensätze aus der Datenbank ausgelesen, bearbeitet und zurückgespeichert.
Bei Bulk Updates werden oft mehrere (oder auch sehr viele) Datensätze mit einem einzigen Kommando datenbankseitig geändert,
und diese Datensätze werden nicht vorher in die Anwendung geladen. Beispiel:
Query bulkUpdateQuery = em.createQuery( "Update Person p set p.team = ( select t from Team t where t.name = 'Team30' ) where p.age > 30" );
bulkUpdateQuery.executeUpdate();
Bulk Updates sind sehr performant, aber beachten Sie Folgendes:
Bevorzugen Sie
"Named Queries" gegenüber
"Dynamic Queries":
Bei "Named Queries" kann der Container das Parsen und Kompilieren cachen und "SQL Injection Attacks" sind kaum noch möglich.
Die Namen der "Named Queries" müssen global eindeutig sein, deshalb sollte immer der Entity-Name vorangestellt werden. Um den Namen nur an einer Stelle als String zu hinterlegen, sollte er als Konstante in der Entity definiert werden:
@Entity @NamedQuery( name = MeineEntity.QUERY_BY_MEINATTR, query = "Select a from MeineEntity a where a.meinAttr like :param" ) public class MeineEntity implements Serializable { public static final String QUERY_BY_MEINATTR = "MeineEntity.queryByMeinAttr"; ... }
Verwendung (in anderer Klasse):
Query query = em.createNamedQuery( MeineEntity.QUERY_BY_MEINATTR ); query.setParameter( "param", "%abc%" ); List<MeineEntity> list = query.getResultList();
Falls Sie mehrere NamedQuery benötigen:
@NamedQueries( { @NamedQuery( ... ), @NamedQuery( ... ) } )
Fügen Sie zu Ihren Entity-Klassen ein mit
@Version
annotiertes Attribut hinzu, um parallele Änderungen auf
"stale Data"
zu verhindern.
Verwenden Sie hierzu beispielsweise ein Integer- oder Long-Attribut.
Den ebenfalls möglichen Typ java.sql.Timestamp sollten Sie vermeiden. Sie dürfen ihn nur verwenden,
wenn Sie ganz sicher sind, dass nicht mehrere Schreibzugriffe innerhalb der Zeitstempel-Granularität möglich sind.
Die Zeitstempel-Granularität beträgt häufig 16 Millisekunden.
Die Alternativen zu "Optimistic Locking" sind "Pessimistic Locking", "Explicit Locking" und "Database Locking", die aber seltener Verwendung finden.
Um bei einer geworfenen OptimisticLockException mit einer adäquaten Fehlermeldung reagieren zu können, müssen Sie die empfangene Exception untersuchen. Beachten Sie dabei, dass die OptimisticLockException oft als "nested Exception" gewrapped ist, weshalb Sie iterativ mit getCause() suchen müssen:
public static boolean isOptimisticLockException( Throwable t ) { while( t != null ) { if( t instanceof OptimisticLockException ) return true; t = ( t != t.getCause() ) ? t.getCause() : null; } return false; }
Nach einer OptimisticLockException machen übrigens wiederholte Speicherversuche mit denselben Entity-Objekten keinen Sinn, da diese meistens "stale Data" und in jedem Fall ein ungültiges Version-Attribut enthalten.
Optimistic Locking bei geteilter Transaktion (z.B. in Webanwendung):
In typischen Webanwendungen werden in einer ersten Transaktion Daten gelesen und in einem HTML-Webformular dargestellt. Dann werden die Daten vom Anwender bearbeitet, zurückgesendet und in einer zweiten Transaktion werden die geänderten Daten gespeichert (siehe auch "Long Transaction").
Dabei kann es trotz Optimistic Locking leicht zu Stale-Data-Fehlern kommen, wenn in der zweiten Transaktion die Daten zuerst neu aus der Datenbank gelesen werden, um die Änderungen in die Entity-Objekte zu übertragen. Denn dabei wird der aktuelle "Versions"-Wert gelesen, und nicht der von der ersten Transaktion. Wenn zwischen den beiden Transaktionen ein anderer Anwender die Daten geändert hat, kommt es zu Stale-Data-Fehlern und Datenverlusten, ohne das dies bemerkt wird.
Damit Optimistic Locking über getrennte Transaktionen funktioniert, muss die "Versions"-Information der ersten Transaktion zwischengespeichert werden. Hierfür bieten sich in Webanwendungen zwei Verfahren an:
Um zum Beispiel "non-repeatable Read"-Fehler zu vermeiden, können Sie "Explicit Locking" einsetzen:
em.lock( entity, LockModeType.READ );
Aber seien Sie damit vorsichtig:
Explicit Locking ist sinnvoll, um Konsistenz abzusichern, wenn Sie Entities lesen, aber nicht verändern, sondern nur Daten daraus verwenden, um andere Entities zu verändern und zu speichern.
Beachten Sie, dass nach Ausführung des em.merge()-Kommandos nicht das übergebene Entity-Objekt, sondern nur das returnierte Entity-Objekt das Merge-Ergebnis beinhaltet, also die "managed instance" bzw. die "reattached Entity". Eventuell werden auch nur im returnierten Entity-Objekt Felder (z.B. version) aktualisiert.
Falls Sie mehrere Entity-Klassen mit den gleichen Attributen haben, überlegen Sie diese gleichen Attribute in eine mit @Embeddable annotierte Extraklasse auszulagern, die dann mit @Embedded eingebunden wird.
Vermeiden Sie Vererbung ("Inheritance").
Bevorzugen Sie stattdessen Aggregation und Komposition.
Falls Sie unbedingt Vererbung und Polymorphie benötigen:
Sehen Sie sich die drei üblichen Umsetzungsvarianten an unter:
Vererbung und Polymorphie mit relationalen Datenbanken.
In JPA sind die drei Umsetzungsvarianten benannt mit:
JOINED,
TABLE_PER_CLASS und
SINGLE_TABLE,
und werden gesteuert über die Annotationen
@Inheritance und
@DiscriminatorColumn.
Beachten Sie, dass polymorphe Abfragen von einigen speziellen JPA-Implementierungen nicht unterstützt werden
(z.B. bei Google App Engine (GAE/J)).
Die JPA-1.0-Spezifikation formuliert keine speziellen Forderungen an die equals()- und hashCode()-Methoden in Entity-Klassen. Zu diesem wichtigen Thema gibt es widersprüchliche Meinungen und Diskussionen. Es gibt nicht den einen "richtigen" Weg. Beachten Sie folgende Alternativen und Auswirkungen:
Beachten Sie folgende allgemeine Regeln für equals() und hashCode():
Composite Primary Keys sind aus mehreren Feldern zusammengesetzte Primary Keys. Formuliert werden sie in JPA meistens als @Embeddable-Klasse, welche per @EmbeddedId eingebunden wird (oder alternativ über mehrere mit @Id annotierte Felder, die in einer gesonderten Primary-Key-Klasse zusammengefasst werden, welche per @IdClass eingebunden wird).
Wichtig: Die Primary-Key-Klasse muss equals() und hashCode() adäquat überschreiben.
Composite Primary Keys werden normalerweise nur verwendet, um bestehende "Legacy"-Datenbanken einbinden zu können. In neuen Anwendungen sollten stattdessen "Surrogate Primary Keys" (auch "Artificial Primary Keys" genannt) bevorzugt werden, also "stellvertretende künstliche" zusätzlich hinzugefügte technische ID-Spalten, die keine fachliche Bedeutung haben.
Falls Sie EclipseLink verwenden und die Cache-Konfiguration nicht Ihren Anforderungen genügt, sehen Sie sich an: Cache Architecture, @Cache, flush-clear.cache, Caching Examples und EclipseLink Extensions.
Folgendermaßen entfernen Sie Entity-Objekte aus dem Cache:
em.flush();
em.clear();
((JpaEntityManager)em.getDelegate()).getServerSession().getIdentityMapAccessor().initializeAllIdentityMaps();
Falls Sie nicht JPA 1.0, sondern JPA 2.x verwenden, können Sie die letzte Zeile ersetzen durch:
em.getEntityManagerFactory().getCache().evictAll();
Verlassen Sie sich möglichst nicht auf nicht spezifiziertes Verhalten: Eventuell verhält sich die nächste Version der JPA-Implementierung anders.
Beispiele:
Falls Sie eine neue Entity mit automatischer Id-Generierung mit persist() speichern wollen, aber anschließend ein rollback() erfolgt, kann je nach JPA-Implementierung die Id im Entity-Objekt gesetzt oder nicht gesetzt sein, was zu Problemen bei einem zweiten Speicherversuch führen kann.
Der Versuch, ein Entity-Objekt mit leerer Id mit merge() zu speichern, kann funktionieren (z.B. TopLink 2.1) oder ohne Exception fehlschlagen (z.B. EclipseLink 1.2).
Das LAZY bei "@Lob @Basic( fetch=FetchType.LAZY )" wird bei EclipseLink 2.1.3 weniger stringent gehandhabt als bei EclipseLink 2.4.2. Falls Sie Probleme beim Speichern oder Lesen von LOBs oder CLOBs haben, versuchen Sie "@Lob @Basic( fetch=FetchType.EAGER )".
Sehen Sie sich die JPA Blueprints an.
Das folgende Beispiel demonstriert:
Sie können das Programmierbeispiel als Zipdatei downloaden oder die im Folgenden beschriebenen Schritte durchführen:
Legen Sie ein Projektverzeichnis an (z.B. D:\MeinWorkspace\JpaJavaSE) und darunter folgende Unterverzeichnisse:
mkdir \MeinWorkspace\JpaJavaSE
cd \MeinWorkspace\JpaJavaSE
mkdir bin\META-INF
mkdir lib
mkdir src\entities
mkdir src\main
tree /F
Sie erhalten:
[\MeinWorkspace\JpaJavaSE] |- [bin] | '- [META-INF] |- [lib] '- [src] |- [entities] '- [main]
Downloaden Sie
apache-openjpa-2.4.2-binary.zip.
Entzippen Sie das OpenJPA-Archiv in ein temporäres Verzeichnis.
Sehen Sie sich die Doku im apache-openjpa-2.4.2/docs-Verzeichnis und
die Beispiele im apache-openjpa-2.4.2/examples-Verzeichnis an.
Kopieren Sie openjpa-2.4.2.jar aus dem apache-openjpa-2.4.2-Verzeichnis
und alle Libs aus dem apache-openjpa-2.4.2/lib-Verzeichnis in das
lib-Verzeichnis des JpaJavaSE-Projekts.
Downloaden Sie zusätzlich commons-lang3-3.7.jar aus dem Maven-Repo in das lib-Verzeichnis.
Erzeugen Sie im bin\META-INF-Verzeichnis die JPA-Konfigurationsdatei: persistence.xml
<?xml version="1.0" encoding="UTF-8"?> <!-- Falls JPA 1.0: <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"> --> <!-- Falls JPA 2.0: --> <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="MeineJpaPU" transaction-type="RESOURCE_LOCAL"> <!-- Falls EclipseLink: <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider> --> <!-- Falls OpenJPA: --> <provider>org.apache.openjpa.persistence.PersistenceProviderImpl</provider> <class>entities.MeineDaten</class> <exclude-unlisted-classes>false</exclude-unlisted-classes> <properties> <property name="javax.persistence.jdbc.driver" value="org.apache.derby.jdbc.EmbeddedDriver" /> <property name="javax.persistence.jdbc.url" value="jdbc:derby:/MeinWorkspace/Derby-DB/mydb;create=true" /> <property name="javax.persistence.jdbc.user" value="" /> <property name="javax.persistence.jdbc.password" value="" /> <property name="eclipselink.ddl-generation" value="create-tables" /> <property name="openjpa.jdbc.SynchronizeMappings" value="buildSchema" /> </properties> </persistence-unit> </persistence>
Sehen Sie sich die Bedeutung der Properties im EclipseLink-UserGuide, die allgemeinen Kommentare in PersistenceUnitInfo und die Persistence-Schema-XSD-Datei an.
Erzeugen Sie im src\entities-Verzeichnis die Entity-Klasse: MeineDaten.java
package entities; import javax.persistence.*; import java.io.Serializable; import java.sql.Timestamp; @Entity public class MeineDaten implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; @Version private Timestamp lastUpdate; @Column(nullable = false, length = 200) private String meinText; // Getter/Setter: public Long getId() { return id; } public Timestamp getLastUpdate() { return lastUpdate; } public String getMeinText() { return meinText; } public void setId( Long id ) { this.id = id; } public void setLastUpdate( Timestamp lastUpdate ) { this.lastUpdate = lastUpdate; } public void setMeinText( String meinText ) { this.meinText = meinText; } @Override public String toString() { return "MeineDaten: id=" + id + ", lastUpdate='" + lastUpdate + "', meinText='" + meinText + "'"; } }
Erläuterungen zu den verwendeten Annotationen finden Sie unter @Entity, @Id, @GeneratedValue, @Column und @Version.
Über die @Id-Annotation wird der Primary Key definiert. @Version wird automatisch bei Schreibzugriffen aktualisiert. Letzteres wird für Optimistic Locking verwendet.
Im Beispiel ist jedem Datenfeld eine JPA-Annotation vorangestellt. Das ist nicht immer notwendig. Im Beispiel könnte die @Column-Annotation auch weggelassen werden.
Erzeugen Sie im src\main-Verzeichnis die ausführende Main-Klasse: Main.java
package main; import javax.persistence.*; import entities.MeineDaten; public class Main { EntityManagerFactory emf; public static void main( String[] args ) { (new Main()).test(); } void test() { emf = Persistence.createEntityManagerFactory( "MeineJpaPU" ); try { MeineDaten dat = new MeineDaten(); dat.setMeinText( "Hallo Welt" ); createEntity( dat ); Object id = dat.getId(); System.out.println( "\n--- " + readEntity( MeineDaten.class, id ) + " ---\n" ); } finally { emf.close(); } } public <T> void createEntity( T entity ) { EntityManager em = emf.createEntityManager(); EntityTransaction tx = em.getTransaction(); try { tx.begin(); em.persist( entity ); tx.commit(); } catch( RuntimeException ex ) { if( tx != null && tx.isActive() ) tx.rollback(); throw ex; } finally { em.close(); } } public <T> T readEntity( Class<T> clss, Object id ) { EntityManager em = emf.createEntityManager(); try { return em.find( clss, id ); } finally { em.close(); } } }
Die hier gezeigten Methoden sollen nur das Prinzip verdeutlichen. Um Caching zu ermöglichen sollte normalerweise nicht für jede einzelne Datenbankaktion ein neuer EntityManager erzeugt werden, und es sollte auch nicht jeder einzelne Schreibzugriff in einer eigenen Transaktion erfolgen, sondern es sollte möglichst gesammelt werden (beides zeigen die folgenden Beispiele).
Die Projektstruktur sieht jetzt so aus (das Derby-DB-Verzeichnis entsteht erst bei Ausführung):
cd \MeinWorkspace\JpaJavaSE
tree /F
[\MeinWorkspace] |- [Derby-DB] | '- ... '- [JpaJavaSE] |- [bin] | |- ... | '- [META-INF] | '- persistence.xml |- [lib] | |- commons-beanutils-1.9.2.jar | |- commons-collections-3.2.2.jar | |- commons-dbcp-1.4.jar | |- commons-lang-2.6.jar | |- commons-lang3-3.7.jar | |- commons-logging-1.2.jar | |- commons-pool-1.6.jar | |- derby-10.12.1.1.jar | |- geronimo-jms_1.1_spec-1.1.1.jar | |- geronimo-jpa_2.0_spec-1.1.jar | |- geronimo-jta_1.1_spec-1.1.1.jar | |- geronimo-validation_1.0_spec-1.1.jar | |- openjpa-2.4.2.jar oder eclipselink-2.6.5.jar | |- org.apache.bval.bundle-0.5.jar | |- serp-1.15.1.jar | '- xbean-asm5-shaded-3.17.jar '- [src] |- [entities] | '- MeineDaten.java '- [main] '- Main.java
Im lib-Verzeichnis darf sich nur entweder openjpa-2.4.2.jar oder eclipselink-2.6.5.jar befinden, aber nicht beide gleichzeitig.
Verwenden Sie Java 8 (falls Sie nur Java 9 verwenden können: nehmen Sie statt OpenJPA das im Folgenden verwendete EclipseLink). Öffnen Sie ein Kommandozeilenfenster ('Windows-Taste' + 'R', 'cmd'), bauen Sie das Projekt und starten Sie mehrmals die Main-Klasse:
cd \MeinWorkspace\JpaJavaSE
java -version
javac -version
javac -cp bin;lib/* -d bin src/entities/*.java
javac -cp bin;lib/* -d bin src/main/*.java
java -cp bin;lib/* main.Main
java -cp bin;lib/* main.Main
Sie erhalten:
---- MeineDaten: id=1, lastUpdate='<aktuelles Datum>', meinText='Hallo Welt' ----
Beachten Sie, dass id und lastUpdate automatisch gesetzt werden und id bei weiteren Aufrufen hochzählt.
Folgendermaßen verwenden Sie statt OpenJPA das gebräuchlichere
EclipseLink:
Downloaden Sie EclipseLink in der gewünschten Version, beispielsweise eclipselink-2.6.5.jar aus dem
Maven-Repo.
Falls Sie WebLogic 12.2.1 installiert haben, können Sie alternativ die eclipselink.jar aus dem
\WebLogic\oracle_common\modules\oracle.toplink-Verzeichnis der WebLogic-Installation verwenden.
Kopieren Sie die EclipseLink-Lib in das
lib-Verzeichnis
des JpaJavaSE-Projekts.
Außerdem müssen Sie in der persistence.xml im bin\META-INF-Verzeichnis die sechs Zeilen
<!-- Falls EclipseLink: <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider> --> <!-- Falls OpenJPA: --> <provider>org.apache.openjpa.persistence.PersistenceProviderImpl</provider>
ersetzen durch:
<!-- Falls EclipseLink: --> <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider> <!-- Falls OpenJPA: <provider>org.apache.openjpa.persistence.PersistenceProviderImpl</provider> -->
Mit EclipseLink 2.6.5 können Sie wahlweise Java 8 oder Java 9 verwenden. Löschen Sie das Derby-Datenbankverzeichnis, kompilieren Sie neu und führen Sie die Anwendung mehrmals aus:
cd \MeinWorkspace\JpaJavaSE
rd /S /Q ..\Derby-DB
ren lib\openjpa-2.4.2.jar openjpa-2.4.2.jar.xx
javac -cp bin;lib/* -d bin src/entities/*.java
javac -cp bin;lib/* -d bin src/main/*.java
java -cp bin;lib/* main.Main
java -cp bin;lib/* main.Main
Sie erhalten wieder:
---- MeineDaten: id=1, lastUpdate='<aktuelles Datum>', meinText='Hallo Welt' ----
Falls Sie die Datenbank löschen wollen, löschen Sie einfach das gesamte /MeinWorkspace/Derby-DB-Verzeichnis. Dies müssen Sie zumindest jedesmal machen, wenn Sie auf eine andere JPA-Implementierung wechseln.
Falls Sie die Exception "Internal Exception: java.sql.SQLException: Table/View 'MEINEDATEN' already exists" erhalten: Dann müssen Sie entweder die Datenbank löschen oder die persistence.xml so konfigurieren, dass nicht versucht wird, die Tabellen neu anzulegen.
Falls Sie die Exception "org.apache.openjpa.persistence.ArgumentException: Attempt to cast instance ... to PersistenceCapable failed. Ensure that it has been enhanced." erhalten: Achten Sie darauf, dass in der persistence.xml bei "<class>entities.MeineDaten</class>" die korrekte Entity-Klasse eingetragen ist.
Bitte beachten Sie, dass Sie in reellen Java-SE-JPA-Anwendungen im Buildprozess noch einen zusätzlichen Schritt vorsehen sollten, um den JPA-Provider-spezifischen "JPA-Enhancer" bzw. "Agenten" einzubinden, welcher Optimierungen am Bytecode vornimmt, zum Beispiel um die Performance zu steigern und um Lazy-Loading zu ermöglichen. In Java-EE-Anwendungen ist dieser Schritt nicht erforderlich, weil dies dort automatisch beim Deployment erfolgt.
Das folgende Beispiel demonstriert:
Sie können das Programmierbeispiel als Zipdatei downloaden oder die im Folgenden beschriebenen Schritte durchführen:
Sie benötigen einen installierten Webserver mit Servlet-Container. Führen Sie die Tomcat-9-Installation oder WebLogic-12.2.1-Installation durch.
Falls noch nicht erfolgt, führen Sie die Maven-Installation durch.
Starten Sie ein neues Maven-Projekt:
cd \MeinWorkspace
mkdir JpaServlet
cd JpaServlet
mkdir src\main\java\de\meinefirma\meinprojekt\entities
mkdir src\main\java\de\meinefirma\meinprojekt\servletfilter
mkdir src\main\java\de\meinefirma\meinprojekt\servletimpl
mkdir src\main\java\de\meinefirma\meinprojekt\test
mkdir src\main\resources\META-INF
mkdir src\main\webapp\WEB-INF
tree /F
Erstellen Sie im JpaServlet-Projektverzeichnis die Maven-Projektkonfiguration:
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>de.meinefirma.meinprojekt</groupId> <artifactId>JpaServlet</artifactId> <version>1.0-SNAPSHOT</version> <packaging>war</packaging> <name>JpaServlet</name> <properties> <project.build.sourceEncoding>ISO-8859-1</project.build.sourceEncoding> </properties> <build> <finalName>${project.artifactId}</finalName> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.7.0</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>org.eclipse.persistence</groupId> <artifactId>eclipselink</artifactId> <version>2.6.5</version> <!-- Tomcat: Ohne die folgende Zeile, WebLogic: Mit folgender Zeile: --> <scope>provided</scope> </dependency> <dependency> <groupId>org.apache.derby</groupId> <artifactId>derby</artifactId> <version>10.14.1.0</version> </dependency> <dependency> <groupId>javax</groupId> <artifactId>javaee-api</artifactId> <version>7.0</version> <scope>provided</scope> </dependency> </dependencies> </project>
Erstellen Sie im src\main\webapp\WEB-INF-Unterverzeichnis die Webapp-Konfiguration:
web.xml
<?xml version="1.0" encoding="UTF-8"?> <web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" 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"> <display-name>Meine JPA-WebApp</display-name> <context-param> <param-name>MeinJpaPuName</param-name> <param-value>MeineJpaPU</param-value> </context-param> <filter> <filter-name>JpaServletFilter</filter-name> <filter-class>de.meinefirma.meinprojekt.servletfilter.JpaServletFilter</filter-class> </filter> <filter-mapping> <filter-name>JpaServletFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <servlet> <servlet-name>MeinTestServlet</servlet-name> <servlet-class>de.meinefirma.meinprojekt.servletimpl.MeinTestServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>MeinTestServlet</servlet-name> <url-pattern>/MeinTestServlet</url-pattern> </servlet-mapping> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> </web-app>
Erstellen Sie im src\main\webapp-Unterverzeichnis die JSP-Datei:
index.jsp
<jsp:forward page="MeinTestServlet" />
Erstellen Sie im src\main\resources\META-INF-Verzeichnis die JPA-Konfigurationsdatei:
persistence.xml
<?xml version="1.0" encoding="UTF-8"?> <!-- Falls JPA 1.0: <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"> --> <!-- Falls JPA 2.0: --> <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="MeineJpaPU" transaction-type="RESOURCE_LOCAL"> <!-- Falls EclipseLink: --> <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider> <!-- Falls OpenJPA: <provider>org.apache.openjpa.persistence.PersistenceProviderImpl</provider> --> <class>de.meinefirma.meinprojekt.entities.MeineDaten</class> <exclude-unlisted-classes>false</exclude-unlisted-classes> <properties> <property name="javax.persistence.jdbc.driver" value="org.apache.derby.jdbc.EmbeddedDriver" /> <property name="javax.persistence.jdbc.url" value="jdbc:derby:/MeinWorkspace/Derby-DB/mydb;create=true" /> <property name="javax.persistence.jdbc.user" value="" /> <property name="javax.persistence.jdbc.password" value="" /> <property name="eclipselink.ddl-generation" value="create-tables" /> <property name="openjpa.jdbc.SynchronizeMappings" value="buildSchema" /> </properties> </persistence-unit> </persistence>
Kopieren Sie in das src\main\java\de\meinefirma\meinprojekt\entities-Verzeichnis die Entity-Klasse
MeineDaten.java, wie sie im obigen Beispiel verwendet wurde.
Ersetzen Sie die "package entities;"-Zeile durch "package de.meinefirma.meinprojekt.entities;".
Erzeugen Sie im src\main\java\de\meinefirma\meinprojekt\servletfilter-Verzeichnis das JPA-Servlet-Filter: JpaServletFilter.java
package de.meinefirma.meinprojekt.servletfilter; import java.io.IOException; import javax.persistence.*; import javax.servlet.*; public class JpaServletFilter implements Filter { private static final ThreadLocal<EntityManager> entityManagerHolder = new ThreadLocal<>(); private EntityManagerFactory emf; @Override public void init( FilterConfig filterConfig ) { String meinJpaPuName = filterConfig.getServletContext().getInitParameter( "MeinJpaPuName" ); emf = Persistence.createEntityManagerFactory( meinJpaPuName ); } @Override public void destroy() { emf.close(); emf = null; } @Override public void doFilter( ServletRequest requ, ServletResponse resp, FilterChain chain ) throws IOException, ServletException { EntityManager em = emf.createEntityManager(); EntityTransaction tx = em.getTransaction(); entityManagerHolder.set( em ); try { tx.begin(); chain.doFilter( requ, resp ); tx.commit(); } finally { if( tx != null && tx.isActive() ) tx.rollback(); em.close(); entityManagerHolder.remove(); } } public static EntityManager getEntityManager() { return entityManagerHolder.get(); } }
Erzeugen Sie im src\main\java\de\meinefirma\meinprojekt\servletimpl-Verzeichnis das Test-Servlet: MeinTestServlet.java
package de.meinefirma.meinprojekt.servletimpl; import java.io.*; import java.util.List; import javax.persistence.EntityManager; import javax.servlet.ServletException; import javax.servlet.http.*; import de.meinefirma.meinprojekt.entities.MeineDaten; import de.meinefirma.meinprojekt.servletfilter.JpaServletFilter; public class MeinTestServlet extends HttpServlet { private static final long serialVersionUID = 1L; @Override public void doGet( HttpServletRequest requ, HttpServletResponse resp ) throws ServletException, IOException { EntityManager em = JpaServletFilter.getEntityManager(); String text = requ.getParameter( "text" ); String r = requ.getParameter( "r" ); // Daten persistieren: if( text != null && text.trim().length() > 0 ) { MeineDaten dat = new MeineDaten(); dat.setMeinText( text ); em.persist( dat ); } else { text = "BlaBlupp"; } // Response starten: resp.setContentType( "text/html; charset=ISO-8859-1" ); try( PrintWriter out = resp.getWriter() ) { if( r != null ) { // Nur Thread-Namen returnieren (fuer Multithreading-Test): out.println( "Text: " + text + "; Server-Thread: " + Thread.currentThread().getId() + ", " + Thread.currentThread().getName() ); } else { // Alle gespeicherten Daten lesen und HTML-Output erzeugen (fuer Webseite): List<MeineDaten> datAll = readAllEntities( em, MeineDaten.class ); out.println( "<html>" ); out.println( "<head><title>MeinTestServlet</title></head>" ); out.println( "<body><h2>Mein JPA-Servlet</h2>" ); out.println( "<form method='GET' enctype='application/x-www-form-urlencoded'>" ); out.println( "Mein Text: <input type='text' name='text' value='" + text + "' maxlength=20>" ); out.println( "<input type='submit' value='Speichern'></form>" ); if( datAll != null && !datAll.isEmpty() ) { out.println( "<table border='1' cellspacing='0' cellpadding='5'><tr><td>Id</td><td>LastUpdate</td><td>Text</td></tr>" ); for( MeineDaten d : datAll ) { out.println( "<tr><td>" + d.getId() + "</td><td>" + d.getLastUpdate() + "</td><td>" + d.getMeinText() + "</td></tr>" ); } out.println( "</table>" ); } out.println( "</body></html>" ); } } } @SuppressWarnings("unchecked") public static <T> List<T> readAllEntities( EntityManager em, Class<T> clss ) { return em.createQuery( "Select d from " + clss.getSimpleName() + " d" ).getResultList(); } }
Erzeugen Sie im src\main\java\de\meinefirma\meinprojekt\test-Verzeichnis die Multithreading-Testklasse: TestMultithreading.java
package de.meinefirma.meinprojekt.test; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.Arrays; public class TestMultithreading { // URL und insbesondere Portnummer anpassen: static final String SERVLET_URL = "http://localhost:8080/JpaServlet/MeinTestServlet"; static final String SERVLET_PARMNAME = "?text="; static final String SERVLET_POSTFIX = "&r=threads"; public static void main( String[] args ) { int threadCount = ( args.length > 0 ) ? Integer.parseInt( args[0] ) : 4; for( int i = 0; i < threadCount; i++ ) (new Thread() { @Override public void run() { speichereUndLeseUeberServlet( Thread.currentThread().getName() ); } }).start(); } public static void speichereUndLeseUeberServlet( String text ) { String servletUrl = SERVLET_URL + SERVLET_PARMNAME + text.replace( ' ', '+' ) + SERVLET_POSTFIX; System.out.println( "\nURL: " + servletUrl + "\n" ); long tm = 0; int lenTmp, lenAll = 0; InputStream is = null; try { URL url = new URL( servletUrl ); is = url.openStream(); byte[] buff = new byte[4096]; tm = System.nanoTime(); while( -1 != (lenTmp = is.read( buff )) ) { System.out.println( new String( Arrays.copyOfRange( buff, 0, lenTmp ) ) ); lenAll += lenTmp; } tm = (System.nanoTime() - tm) / 1000000L; } catch( Exception ex ) { System.out.println( "\nError: " + ex ); } finally { if( is != null ) try { is.close(); } catch( IOException ex ) { /* ok */ } } System.out.println( lenAll + " Bytes in " + tm + " ms\n" ); } }
Erzeugen Sie im Projektverzeichnis eine für Ihren Servlet-Server geeignete Batchdatei, zum Beispiel für Tomcat (passen Sie die Pfade an):
cls @echo Bitte auch den Pfad "/MeinWorkspace/Derby-DB/mydb" in src\main\resources\META-INF\persistence.xml anpassen! set _MEIN_PROJEKT_NAME=JpaServlet set TOMCAT_HOME=D:\Tools\Tomcat tree /F @echo JAVA_OPTS=%JAVA_OPTS% @echo JAVA_HOME=%JAVA_HOME% java -version del %TOMCAT_HOME%\webapps\%_MEIN_PROJEKT_NAME%.war rd %TOMCAT_HOME%\webapps\%_MEIN_PROJEKT_NAME% /S /Q pushd . cd /D %TOMCAT_HOME%\bin call startup.bat @echo on popd call mvn clean package @echo on copy target\%_MEIN_PROJEKT_NAME%.war %TOMCAT_HOME%\webapps @ping -n 11 127.0.0.1 >nul start http://localhost:8080/%_MEIN_PROJEKT_NAME%/ pause Tomcat beenden ... pushd . cd /D %TOMCAT_HOME%\bin call shutdown.bat popd
Oder für WebLogic (passen Sie die Pfade an und setzen Sie in der TestMultithreading.java die SERVLET_URL-Portnummer auf 7001):
cls @echo Bitte auch den Pfad "/MeinWorkspace/Derby-DB/mydb" in src\main\resources\META-INF\persistence.xml @echo und die Portnummer in TestMultithreading.SERVLET_URL anpassen! set _MEIN_PROJEKT_NAME=JpaServlet set WEBLOGIC_DOMAIN=C:\WebLogic\user_projects\domains\MeineDomain tree /F del %WEBLOGIC_DOMAIN%\autodeploy\%_MEIN_PROJEKT_NAME%.war pushd . cd /D %WEBLOGIC_DOMAIN%\bin start cmd /C startWebLogic.cmd @echo on popd call mvn clean package @echo on copy target\%_MEIN_PROJEKT_NAME%.war %WEBLOGIC_DOMAIN%\autodeploy @ping -n 20 127.0.0.1 >nul start http://localhost:7001/%_MEIN_PROJEKT_NAME%/ pause WebLogic beenden ... pushd . cd /D %WEBLOGIC_DOMAIN%\bin start cmd /C stopWebLogic.cmd popd
Die Projektstruktur sieht jetzt so aus:
cd \MeinWorkspace\JpaServlet
tree /F
[\MeinWorkspace\JpaServlet] |- [src] | '- [main] | |- [java] | | '- [de] | | '- [meinefirma] | | '- [meinprojekt] | | |- [entities] | | | '- MeineDaten.java | | |- [servletfilter] | | | '- JpaServletFilter.java | | |- [servletimpl] | | | '- MeinTestServlet.java | | '- [test] | | '- TestMultithreading.java | |- [resources] | | '- [META-INF] | | '- persistence.xml | '- [webapp] | |- [WEB-INF] | | '- web.xml | '- index.jsp |- pom.xml |- run-Tomcat.bat '- run-WebLogic.bat
Bitte beachten Sie, dass Sie diesmal nicht manuell Libs zum Projekt hinzukopieren müssen, weil sich darum Maven kümmert.
Bevor Sie kompilieren: Kontrollieren Sie, ob in der pom.xml bei der eclipselink-dependency der Scope korrekt gesetzt ist: Für Tomcat darf kein Scope gesetzt sein und für WebLogic muss der Scope auf <scope>provided</scope> gesetzt sein.
Falls Sie Tomcat und Java 9 verwenden:
Setzen Sie vor dem Aufruf der Tomcat-Batchdatei die Umgebungsvariable JAVA_OPTS geeignet:
cd \MeinWorkspace\JpaServlet
java -version
set "JAVA_OPTS=--add-modules=ALL-SYSTEM"
run-Tomcat.bat
Führen Sie die zu Ihrem Server passende Batchdatei run-...bat aus. Falls die von der Batchdatei gestartete Webseite nicht sofort funktioniert (weil der Server noch nicht fertig ist), führen Sie einige Sekunden später einen Refresh der Webseite durch.
Testen Sie das Speichern: Tragen Sie auf der Webseite verschiedene Texte ein und betätigen jeweils den Speichern-Button. Die Webseite zeigt alle bisher gespeicherten Texte.
Um den Multithreading-Test auszuführen, passen Sie zuerst in src\main\java\de\meinefirma\meinprojekt\test\TestMultithreading.java die SERVLET_URL-Portnummer an und führen (bei laufendem Server) folgende Kommandos aus (ersetzen Sie 8 durch die gewünschte Thread-Anzahl und 8080 durch die passende Portnummer):
cd \MeinWorkspace\JpaServlet
javac -d target\classes src\main\java\de\meinefirma\meinprojekt\test\TestMultithreading.java
java -cp target\classes de.meinefirma.meinprojekt.test.TestMultithreading 8
Falls Sie eine Exception ähnlich zu
"java.lang.NoClassDefFoundError: Ljavax/persistence/EntityManagerFactory" oder
"java.lang.ClassNotFoundException: javax.persistence.EntityManagerFactory"
erhalten: Achten Sie darauf, dass in der pom.xml bei der
eclipselink-dependency der Scope korrekt gesetzt ist.
Falls Sie eine Exception ähnlich zu
"javax.persistence.PersistenceException: ...PersistenceUnitLoadingException ...EntityManagerSetupException ... predeploy for PersistenceUnit ... failed",
"java.lang.IllegalArgumentException: An exception occured while creating a query in EntityManager ... Error compiling the query" oder
"java.lang.IllegalArgumentException: Object: ... is not a known entity type"
erhalten: Achten Sie darauf, dass in der persistence.xml die
"<class>...</class>"-Einträge korrekt und vollständig sind.
Falls Sie eine Exception ähnlich zu
Can't load log handler "1catalina.org.apache.juli.AsyncFileHandler"
java.lang.ClassNotFoundException: 1catalina.org.apache.juli.AsyncFileHandler
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass
erhalten: Dann verwenden Sie wahrscheinlich Java 10. Siehe hierzu:
Bug JDK-8195096.
Falls Sie eine Exception ähnlich zu
java.lang.SecurityException: class "javax.persistence.PersistenceUtil"'s signer information does not match signer information of other classes in the same package
at java.base/java.lang.ClassLoader.checkCerts(ClassLoader.java:1143)
erhalten: Dann verwenden Sie wahrscheinlich verschieden signierte Jar-Libs,
welche Klassen mit gleichen Package-Namen enthalten. Siehe hierzu:
EclipseLink 2.7.0 and JPA API 2.2.0 - signature mismatch und
Bug 525457 - EclipseLink clash as one jar is signed and one unsigned.
Falls Sie auf Probleme stoßen, sehen Sie sich die entsprechende Logdatei D:\Tools\Tomcat\logs\localhost.*.log bzw. C:\WebLogic\user_projects\domains\MeineDomain\servers\AdminServer\logs\AdminServer.log an.
Falls Sie die Datenbank löschen wollen, löschen Sie einfach das gesamte /MeinWorkspace/Derby-DB-Verzeichnis.
Java EE Application Server (z.B. GlassFish und WebLogic)
ermöglichen "container-managed EntityManager" und "JTA-UserTransaction" nicht nur in
EJBs (wie weiter unten noch gezeigt wird),
sondern auch in Servlets.
Hierfür gibt es zwei verschiedene Ansätze, die beide im Folgenden beschrieben werden (aufsetzend auf das letzte Beispiel).
Für beide Ansätze muss in der persistence.xml konfiguriert werden:
<?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="MeineJpaPU" transaction-type="JTA"> <jta-data-source>jdbc/MeinDatasourceJndiName</jta-data-source> <properties> <!-- Nur falls Tabellen automatisch angelegt werden sollen: --> <property name="eclipselink.ddl-generation" value="create-tables" /> <property name="openjpa.jdbc.SynchronizeMappings" value="buildSchema" /> <property name="hibernate.hbm2ddl.auto" value="create" /> </properties> </persistence-unit> </persistence>
Bitte sehen Sie sich hierzu auch die Erläuterungen weiter unten an.
Im Java EE Application Server muss eine DataSource mit dem unter <jta-data-source>...</jta-data-source> eingetragenen JNDI-Namen eingerichtet sein, zum Beispiel wie beschrieben für WebLogic und GlassFish.
Ersetzen Sie im letzten Beispiel den Inhalt von JpaServletFilter.java durch:
package de.meinefirma.meinprojekt.servletfilter; import java.io.IOException; import javax.annotation.Resource; import javax.naming.*; import javax.persistence.*; import javax.servlet.*; import javax.transaction.UserTransaction; @PersistenceContext( unitName="MeineJpaPU", name="persistence/em" ) public class JpaServletFilter implements Filter { private static final ThreadLocal<EntityManager> entityManagerHolder = new ThreadLocal<>(); @Resource private UserTransaction utx; @Override public void doFilter( ServletRequest requ, ServletResponse resp, FilterChain chain ) throws IOException, ServletException { try { EntityManager em = (EntityManager) (new InitialContext()).lookup( "java:comp/env/persistence/em" ); utx.begin(); entityManagerHolder.set( em ); chain.doFilter( requ, resp ); utx.commit(); } catch( Exception ex ) { try { utx.rollback(); } catch( Exception e ) { /* ok */ } throw new ServletException( ex ); } finally { entityManagerHolder.remove(); } } public static EntityManager getEntityManager() { return entityManagerHolder.get(); } @Override public void init( FilterConfig arg0 ) throws ServletException { /* nicht benoetigt */ } @Override public void destroy() { /* nicht benoetigt */ } }
Bitte beachten Sie:
Sowohl der "emf = Persistence.createEntityManagerFactory()"-Aufruf
als auch der "EntityManager em = emf.createEntityManager();"-Aufruf
werden nicht mehr verwendet.
Statt der
Datenbank-EntityTransaction wird die
JTA-UserTransaction verwendet.
Wichtig:
Anders als in EJBs dürfen Sie in Servlets nicht "@PersistenceContext EntityManager em;" verwenden,
weil EntityManager nicht thread-safe ist.
Der hier gezeigte Umweg über den JNDI-Lookup ist dagegen thread-safe.
Siehe hierzu auch:
Design Choices in a Web-only Application (Brydon und Kangath).
Ersetzen Sie im letzten Beispiel den Inhalt von JpaServletFilter.java durch:
package de.meinefirma.meinprojekt.servletfilter; import java.io.IOException; import javax.naming.*; import javax.persistence.*; import javax.servlet.*; import javax.transaction.UserTransaction; public class JpaServletFilter implements Filter { private static final ThreadLocal<EntityManager> entityManagerHolder = new ThreadLocal<>(); @Override public void doFilter( ServletRequest requ, ServletResponse resp, FilterChain chain ) throws IOException, ServletException { UserTransaction utx = null; try { EntityManager em = (EntityManager) (new InitialContext()).lookup( "java:comp/env/persistence/em" ); utx = (UserTransaction) (new InitialContext()).lookup( "java:comp/UserTransaction" ); utx.begin(); entityManagerHolder.set( em ); chain.doFilter( requ, resp ); utx.commit(); } catch( Exception ex ) { try { if( utx != null ) utx.rollback(); } catch( Exception e ) { /* ok */ } throw new ServletException( ex ); } finally { entityManagerHolder.remove(); } } public static EntityManager getEntityManager() { return entityManagerHolder.get(); } @Override public void init( FilterConfig arg0 ) throws ServletException { /* nicht benoetigt */ } @Override public void destroy() { /* nicht benoetigt */ } }
Damit der lookup( "java:comp/env/persistence/em" ) funktioniert, müssen Sie noch die web.xml erweitern:
<?xml version="1.0" encoding="UTF-8"?> <web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" 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"> <display-name>Meine JPA-WebApp</display-name> <persistence-context-ref> <persistence-context-ref-name>persistence/em</persistence-context-ref-name> <persistence-unit-name>MeineJpaPU</persistence-unit-name> </persistence-context-ref> <filter> <filter-name>JpaServletFilter</filter-name> <filter-class>de.meinefirma.meinprojekt.servletfilter.JpaServletFilter</filter-class> </filter> <filter-mapping> <filter-name>JpaServletFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <servlet> <servlet-name>MeinTestServlet</servlet-name> <servlet-class>de.meinefirma.meinprojekt.servletimpl.MeinTestServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>MeinTestServlet</servlet-name> <url-pattern>/MeinTestServlet</url-pattern> </servlet-mapping> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> </web-app>
Diese Variante wird weiter unten in JSP-Webanwendung mit geteilter Transaktion und bidirektionaler OneToMany-Relation in einer vollständigen Demo gezeigt.
Für beide Varianten gilt: Java EE Application Server enthalten eine eigene JPA-Implementierung, weshalb Sie beim Einsatz in Java EE Application Servern in der pom.xml alle OpenJPA-Elemente entfernen können.
Falls Sie den "container-managed EntityManager" wie gezeigt zentral im Servlet-Filter verwalten wollen,
aber die "Transaction Demarcation" nicht (z.B. um leichter bei Exceptions JSF-Fehlerseiten anzeigen können),
können Sie die utx...-Kommandos natürlich auch weglassen.
Sie müssten dann die Transaktionen selbst steuern.
Die dazu notwendige UserTransaction erhalten Sie über:
UserTransaction utx = (UserTransaction) (new InitialContext()).lookup( "java:comp/UserTransaction" );
Die letzte Variante (ohne Injection-Annotationen) hat den Vorteil,
dass Sie den EntityManager und die UserTransaction
nicht nur in managed Klassen wie Servlets (und Servlet-Filter etc.),
sondern auch in aufgerufenen einfachen POJOs leicht erreichen können (auch ohne ThreadLocal-Variablen).
Diese Variante wird weiter unten in
JSP-Webanwendung mit geteilter Transaktion und bidirektionaler OneToMany-Relation
in einer vollständigen Demo gezeigt.
Falls Sie das Beispiel nicht nur mit einem einzigen Java EE Application Server, sondern abwechselnd mit verschiedenen (z.B. GlassFish und WebLogic) auf derselben Datenbank betreiben wollen und die Datenbanktabellen automatisch erzeugen lassen, sollten Sie zwischendurch jeweils die MeineDaten- und die Sequenztabelle in der Datenbank löschen ("Drop Table ..."), um Fehler zu vermeiden.
Falls Sie mit WebLogic und OpenJPA die Exception "org.apache.openjpa.persistence.PersistenceException: Cannot set auto commit to "true" when in distributed transaction" erhalten: Deaktivieren Sie testweise in der WebLogic-Console unter "MeineDomain | Services | JDBC | Datenquellen | MeinMySqlDataSourceName | Transaktion" die Option "Unterstützt globale Transaktionen" ("Supports Global Transactions"). Allerdings sollten Sie normalerweise zumindest in Produktionsumgebungen diese Option unbedingt eingeschaltet lassen.
Das folgende Beispiel demonstriert:
Sie können das Programmierbeispiel als Zipdatei downloaden oder die im Folgenden beschriebenen Schritte durchführen:
Sie benötigen einen installierten Java EE Application Server. Führen Sie die WebLogic-12.2.1-Installation bzw. die GlassFish-2.1-Installation durch.
Im Java EE Application Server muss eine DataSource eingerichtet sein, zum Beispiel wie beschrieben für WebLogic und GlassFish.
Falls noch nicht erfolgt, führen Sie die Maven-Installation durch.
Starten Sie ein neues Projekt:
cd \MeinWorkspace
mvn archetype:generate -DinteractiveMode=false -DarchetypeArtifactId=maven-archetype-webapp -DgroupId=de.meinefirma.meinprojekt -DartifactId=JpaJspOneToMany
cd JpaJspOneToMany
tree /F
md src\main\resources\META-INF
md src\main\java\de\meinefirma\meinprojekt\dao
md src\main\java\de\meinefirma\meinprojekt\entities
md src\main\java\de\meinefirma\meinprojekt\servletfilter
tree /F
Ersetzen Sie im Projektverzeichnis den Inhalt der pom.xml durch:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>de.meinefirma.meinprojekt</groupId> <artifactId>JpaJspOneToMany</artifactId> <version>1.0-SNAPSHOT</version> <packaging>war</packaging> <name>JpaJspOneToMany</name> <properties> <project.build.sourceEncoding>ISO-8859-1</project.build.sourceEncoding> </properties> <build> <finalName>${project.artifactId}</finalName> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.7.0</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>javax</groupId> <artifactId>javaee-api</artifactId> <version>7.0</version> <scope>provided</scope> </dependency> </dependencies> </project>
Ersetzen Sie im src\main\webapp\WEB-INF-Verzeichnis den Inhalt der web.xml durch:
<?xml version="1.0" encoding="UTF-8"?> <web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" 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"> <display-name>Meine JPA-WebApp</display-name> <persistence-context-ref> <persistence-context-ref-name>persistence/em</persistence-context-ref-name> <persistence-unit-name>MeineJpaPU</persistence-unit-name> </persistence-context-ref> <filter> <filter-name>JpaServletFilter</filter-name> <filter-class>de.meinefirma.meinprojekt.servletfilter.JpaServletFilter</filter-class> </filter> <filter-mapping> <filter-name>JpaServletFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> </web-app>
Erzeugen Sie im src\main\resources\META-INF-Verzeichnis die JPA-Konfigurationsdatei: persistence.xml
<?xml version="1.0" encoding="UTF-8"?> <!-- Falls JPA 1.0: <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"> --> <!-- Falls JPA 2.0: --> <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"> <!-- Falls JPA 2.1: <persistence version="2.1" xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"> --> <persistence-unit name="MeineJpaPU" transaction-type="JTA"> <!-- Nur falls eine bestimmte JPA-Implementierung ausgewaehlt werden soll (fuer GlassFish auskommentieren): --> <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider> <jta-data-source>jdbc/MeinDatasourceJndiName</jta-data-source> <properties> <!-- Nur falls Tabellen automatisch angelegt werden sollen: --> <property name="eclipselink.ddl-generation" value="create-tables" /> <property name="openjpa.jdbc.SynchronizeMappings" value="buildSchema" /> <property name="hibernate.hbm2ddl.auto" value="create" /> </properties> </persistence-unit> </persistence>
Einige Java EE Application Server beinhalten mehrere JPA-Implementierungen.
Zum Beispiel WebLogic 12.2.1 enthält OpenJPA und EclipseLink.
Zusätzlich können Sie in Ihrer WAR-Datei weitere JPA-Implementierungen hinzufügen.
Mit der <provider>...</provider>-Zeile können Sie eine bestimmte JPA-Implementierung auswählen.
Falls Sie das nicht wollen, oder falls Sie GlassFish verwenden, lassen Sie einfach die gesamte Zeile weg.
Ebenso können Sie den gesamten <properties>...</properties>-Block weglassen,
falls Sie nicht wollen, dass Tabellen automatisch angelegt werden.
Kopieren Sie in das src\main\java\de\meinefirma\meinprojekt\servletfilter-Verzeichnis das JPA-Servlet-Filter JpaServletFilter.java, wie es oben unter JTA-Transaktionen ohne Injection-Annotationen gezeigt wurde.
Ersetzen Sie im src\main\webapp-Verzeichnis den Inhalt der JSP-Datei index.jsp durch:
<%@ page import = "java.util.*" %> <%@ page import = "de.meinefirma.meinprojekt.dao.*" %> <%@ page import = "de.meinefirma.meinprojekt.entities.*" %> <%! static final String SESSION_ATTR_PERSON_LISTE = "Person-Liste"; static final String SESSION_ATTR_TEAM_LISTE = "Team-Liste"; %> <% String pname = request.getParameter( "pname" ); String tname = request.getParameter( "tname" ); String rpname = request.getParameter( "rpname" ); String rtname = request.getParameter( "rtname" ); String add = request.getParameter( "add" ); String del = request.getParameter( "del" ); PersonTeamDao dao = new PersonTeamDao(); if( pname != null && pname.trim().length() > 0 ) { Person p = new Person(); p.setName( pname ); dao.createEntity( p ); } if( tname != null && tname.trim().length() > 0 ) { Team t = new Team(); t.setName( tname ); dao.createEntity( t ); } pname = tname = ""; String fehlerText = dao.addDelPersonTeam( add, del, rpname, rtname, session.getAttribute( SESSION_ATTR_PERSON_LISTE ), session.getAttribute( SESSION_ATTR_TEAM_LISTE ) ); %> <html> <head><title>JpaJspOneToMany</title></head> <body> <h1>JpaJspOneToMany</h1> <h2>Personen</h2> <form method='GET' enctype='application/x-www-form-urlencoded'> Name der Person: <input type='text' name='pname' value='<%= pname %>' maxlength=20> <input type='submit' value='Neue Person hinzufügen'></form> <% // Daten lesen, in Tabelle anzeigen und fuer Optimistic Locking zwischenspeichern: List<Person> pAll = dao.readAllEntities( Person.class ); session.setAttribute( SESSION_ATTR_PERSON_LISTE, pAll ); if( pAll != null && !pAll.isEmpty() ) { out.println( "<table border='1' cellspacing='0' cellpadding='5'>" + Person.HTML_TABLE_HEADER ); for( Person p : pAll ) out.println( p.toHtmlTableRow() ); out.println( "</table>" ); } %> <h2>Teams</h2> <form method='GET' enctype='application/x-www-form-urlencoded'> Name des Teams: <input type='text' name='tname' value='<%= tname %>' maxlength=20> <input type='submit' value='Neues Team hinzufügen'></form> <% // Daten lesen, in Tabelle anzeigen und fuer Optimistic Locking zwischenspeichern: List<Team> tAll = dao.readAllEntities( Team.class ); session.setAttribute( SESSION_ATTR_TEAM_LISTE, tAll ); if( tAll != null && !tAll.isEmpty() ) { out.println( "<table border='1' cellspacing='0' cellpadding='5'>" + Team.HTML_TABLE_HEADER ); for( Team t : tAll ) out.println( t.toHtmlTableRow() ); out.println( "</table>" ); } %> <h2>Zuordnung</h2> <form method='GET' enctype='application/x-www-form-urlencoded'> Name der Person: <input type='text' name='rpname' maxlength=20><br> Name des Teams: <input type='text' name='rtname' maxlength=20><br> <input type='submit' name='add' value='Person zu Team hinzufügen'> <input type='submit' name='del' value='Person aus Team entfernen'></form> <% if( fehlerText != null && fehlerText.length() > 0 ) out.println( "<h2><font color='red'>" + fehlerText + "</font></h2>" ); %> </body> </html>
Erzeugen Sie im src\main\java\de\meinefirma\meinprojekt\dao-Verzeichnis die für alle Entities geltende Super-DAO-Klasse: AbstractDao.java
package de.meinefirma.meinprojekt.dao; import java.util.List; import javax.naming.InitialContext; import javax.naming.NamingException; import javax.persistence.EntityManager; import javax.persistence.Query; /** Super-Klasse fuer alle DAOs */ public abstract class AbstractDao { protected final EntityManager em; public AbstractDao() throws NamingException { em = (EntityManager) (new InitialContext()).lookup( "java:comp/env/persistence/em" ); } // Erstelle neuen Eintrag: public <T> void createEntity( T entity ) { em.persist( entity ); } // Lies bestimmte Eintraege zu einer Entity-Klasse: @SuppressWarnings("unchecked") public <T> List<T> queryEntities( String namedQuery, String paramName, String paramValue ) { Query query = em.createNamedQuery( namedQuery ); query.setParameter( paramName, paramValue ); return query.getResultList(); } // Lies alle Eintraege zu einer Entity-Klasse: @SuppressWarnings("unchecked") public <T> List<T> readAllEntities( Class<T> clss ) { return em.createQuery( "Select d from " + clss.getSimpleName() + " d" ).getResultList(); } }
Erzeugen Sie im src\main\java\de\meinefirma\meinprojekt\dao-Verzeichnis die DAO-Klasse: PersonTeamDao.java
package de.meinefirma.meinprojekt.dao; import java.util.List; import javax.naming.NamingException; import de.meinefirma.meinprojekt.entities.Person; import de.meinefirma.meinprojekt.entities.Team; /** DAO fuer Person und Team */ public class PersonTeamDao extends AbstractDao { public PersonTeamDao() throws NamingException { super(); } // An HTML-Formular angepasste Methode fuer Zuordnung von Personen zu Teams: @SuppressWarnings("unchecked") public String addDelPersonTeam( String add, String del, String pname, String tname, Object personListe, Object teamListe ) { Person person = null; Team team = null; if( (add == null && del == null) || pname == null || tname == null || pname.trim().length() == 0 || tname.trim().length() == 0 || !(personListe instanceof List) || !(teamListe instanceof List) ) return ""; // Damit Optmistic-Locking-Erkennung funktioniert, muessen die "alten" detached Objekte // verwendet werden (mit den alten "@Version"-Werten) // (personListe und teamListe wurden in der HttpSession zwischengespeichert): for( Person p : (List<Person>) personListe ) if( p != null && p.getName() != null && p.getName().equals( pname ) ) { person = p; break; } for( Team t : (List<Team>) teamListe ) if( t != null && t.getName() != null && t.getName().equals( tname ) ) { team = t; break; } if( person == null ) return "Person nicht vorhanden"; if( team == null ) return "Team nicht vorhanden"; // Merge, damit Optmistic-Locking-Exceptions erkannt werden. // Der Returnwert beinhaltet die "managed instance", also die reattached Entity: person = em.merge( person ); team = em.merge( team ); // Fuehre "del" bzw. "add" aus: if( del != null ) team.removePerson( person ); if( add != null ) { for( Team t : (List<Team>) teamListe ) if( t != null && t.removePerson( person ) ) em.merge( t ); team.addPerson( person ); } return ""; } }
Erzeugen Sie im src\main\java\de\meinefirma\meinprojekt\entities-Verzeichnis die Entity-Klasse: Person.java
package de.meinefirma.meinprojekt.entities; import javax.persistence.*; import java.io.Serializable; import java.sql.Timestamp; @Entity @Table( uniqueConstraints=@UniqueConstraint(columnNames={"personName"}) ) @NamedQuery( name=Person.QUERY_BY_NAME, query="Select p from Person p where p.personName = :pname" ) public class Person implements Serializable { private static final long serialVersionUID = 1L; public static final String QUERY_BY_NAME = "Person.queryByName"; public static final String HTML_TABLE_HEADER = "<tr><th>Id</th><th>LastUpdate</th><th>Name</th><th>Team.Name</th></tr>\n"; @Id @GeneratedValue( strategy = GenerationType.AUTO ) private Long id; @Version private Timestamp lastUpdate; @Column( nullable = false, length = 200 ) private String personName; @ManyToOne( cascade=CascadeType.PERSIST ) private Team team; // Getter/Setter: public Long getId() { return id; } public Timestamp getLastUpdate() { return lastUpdate; } public String getName() { return personName; } public Team getTeam() { return team; } public void setId( Long id ) { this.id = id; } public void setLastUpdate( Timestamp lastUpdate ) { this.lastUpdate = lastUpdate; } public void setName( String name ) { this.personName = name; } public void setTeam( Team team ) { this.team = team; } @Override public boolean equals( Object obj ) { if( !(obj instanceof Person) ) return false; String onm = ((Person) obj).getName(); return personName == onm || (personName != null && personName.equals( onm )); } @Override public int hashCode() { return ( personName == null ) ? 0 : personName.hashCode(); } @Override public String toString() { return superToString( super.toString() ) + ": id=" + id + ", lastUpdate='" + lastUpdate + "', name='" + personName + "', team='" + ((team != null) ? team.getName() : "(kein Team zugeordnet)")+ "'"; } public String toHtmlTableRow() { return "<tr><td>" + id + "</td><td>" + lastUpdate + "</td><td>" + personName + "</td><td>" + ((team != null) ? team.getName() : "(kein Team zugeordnet)") + "</td></tr>\n"; } private static String superToString( String s ) { return s.substring( s.lastIndexOf( '.' ) + 1, s.length() ); } }
Erzeugen Sie im src\main\java\de\meinefirma\meinprojekt\entities-Verzeichnis die Entity-Klasse: Team.java
package de.meinefirma.meinprojekt.entities; import javax.persistence.*; import java.io.Serializable; import java.sql.Timestamp; import java.util.*; @Entity @Table( uniqueConstraints=@UniqueConstraint(columnNames={"teamName"}) ) @NamedQuery( name=Team.QUERY_BY_NAME, query="Select t from Team t where t.teamName = :tname" ) public class Team implements Serializable { private static final long serialVersionUID = 1L; public static final String QUERY_BY_NAME = "Team.queryByName"; public static final String HTML_TABLE_HEADER = "<tr><th>Id</th><th>LastUpdate</th><th>Name</th><th>Person.Name</th></tr>\n"; @Id @GeneratedValue( strategy = GenerationType.AUTO ) private Long id; @Version private Timestamp lastUpdate; @Column( nullable = false, length = 200 ) private String teamName; @OneToMany( mappedBy="team", cascade=CascadeType.PERSIST, fetch=FetchType.EAGER ) private Set<Person> personen = new HashSet<>(); public Set<Person> getPersonen() { return Collections.unmodifiableSet( personen ); } public boolean addPerson( Person person ) { person.setTeam( this ); return personen.add( person ); } public boolean removePerson( Person person ) { boolean b = personen.remove( person ); if( b ) person.setTeam( null ); return b; } // Getter/Setter: public Long getId() { return id; } public Timestamp getLastUpdate() { return lastUpdate; } public String getName() { return teamName; } public void setId( Long id ) { this.id = id; } public void setLastUpdate( Timestamp lastUpdate ) { this.lastUpdate = lastUpdate; } public void setName( String name ) { this.teamName = name; } @Override public boolean equals( Object obj ) { if( !(obj instanceof Team) ) return false; String onm = ((Team) obj).getName(); return teamName == onm || (teamName != null && teamName.equals( onm )); } @Override public int hashCode() { return ( teamName == null ) ? 0 : teamName.hashCode(); } @Override public String toString() { String ps = personenToString( personen ); return superToString( super.toString() ) + ": id=" + id + ", lastUpdate='" + lastUpdate + "', name='" + teamName + "', personen='" + ps + "'"; } public String toHtmlTableRow() { String ps = personenToString( personen ); return "<tr><td>" + id + "</td><td>" + lastUpdate + "</td><td>" + teamName + "</td><td>" + ps + "</td></tr>\n"; } private static String personenToString( Set<Person> personen ) { StringBuilder sb = new StringBuilder(); if( personen != null && !personen.isEmpty() ) for( Person p : personen ) sb.append( p.getName() ).append( ", " ); String ps = sb.toString(); if( ps.length() > 2 ) ps = ps.substring( 0, ps.length() - 2 ); if( ps.length() == 0 ) ps = "(keine Personen zugeordnet)"; return ps; } private static String superToString( String s ) { return s.substring( s.lastIndexOf( '.' ) + 1, s.length() ); } }
Wie bereits oben beschrieben wurde, gibt es für das @OneToMany-Set personen keine normalen Getter und Setter, sondern die Methoden getPersonen(), addPerson() und removePerson().
Erzeugen Sie im Projektverzeichnis eine für Ihren Java EE Application Server geeignete Batchdatei, zum Beispiel für WebLogic (passen Sie die Pfade an):
cls set _MEIN_PROJEKT_NAME=JpaJspOneToMany set WEBLOGIC_DOMAIN=C:\WebLogic\user_projects\domains\MeineDomain tree /F del %WEBLOGIC_DOMAIN%\autodeploy\%_MEIN_PROJEKT_NAME%.war pushd . cd /D %WEBLOGIC_DOMAIN%\bin start cmd /C startWebLogic.cmd @echo on popd call mvn clean package @echo on copy target\%_MEIN_PROJEKT_NAME%.war %WEBLOGIC_DOMAIN%\autodeploy @ping -n 20 127.0.0.1 >nul start http://localhost:7001/%_MEIN_PROJEKT_NAME%/ pause WebLogic beenden ... pushd . cd /D %WEBLOGIC_DOMAIN%\bin start cmd /C stopWebLogic.cmd popd
Oder für GlassFish (passen Sie die Pfade an):
cls @echo --------------------------------------------------------------------- @echo Fuer GlassFish in der src\main\resources\META-INF\persistence.xml die @echo ...org.eclipse.persistence.jpa.PersistenceProvider...-Zeile loeschen! @echo --------------------------------------------------------------------- @echo. set _MEIN_PROJEKT_NAME=JpaJspOneToMany set GLASSFISH_DOMAIN=C:\GlassFish\glassfish\domains\domain1 tree /F del %GLASSFISH_DOMAIN%\autodeploy\%_MEIN_PROJEKT_NAME%.war pushd . call %GLASSFISH_DOMAIN%\..\..\bin\asadmin start-domain domain1 popd call mvn clean package @echo on copy target\%_MEIN_PROJEKT_NAME%.war %GLASSFISH_DOMAIN%\autodeploy @ping -n 5 127.0.0.1 >nul start http://localhost:8080/%_MEIN_PROJEKT_NAME%/ pause GlassFish beenden ... pushd . call %GLASSFISH_DOMAIN%\..\..\bin\asadmin stop-domain domain1 popd
Die Projektstruktur sieht jetzt so aus:
cd \MeinWorkspace\JpaJspOneToMany
tree /F
[\MeinWorkspace\JpaJspOneToMany] |- [src] | '- [main] | |- [java] | | '- [de] | | '- [meinefirma] | | '- [meinprojekt] | | |- [dao] | | | |- AbstractDao.java | | | '- PersonTeamDao.java | | |- [entities] | | | |- Person.java | | | '- Team.java | | '- [servletfilter] | | '- JpaServletFilter.java | |- [resources] | | '- [META-INF] | | '- persistence.xml | '- [webapp] | |- [WEB-INF] | | '- web.xml | '- index.jsp |- pom.xml |- run-GlassFish.bat '- run-WebLogic.bat
Sie brauchen auch diesmal nicht manuell Libs zum Projekt hinzuzukopieren, weil sich darum Maven kümmert.
Bitte beachten Sie, dass für GlassFish in der persistence.xml die Zeile "<provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>" auskommentiert werden muss.
Führen Sie die zu Ihrem Server passende Batchdatei run-...bat aus. Falls die von der Batchdatei gestartete Webseite nicht sofort funktioniert (weil der Server noch nicht fertig ist), führen Sie einige Sekunden später einen Refresh der Webseite durch.
Legen Sie auf der Webseite mehrere Personen, mehrere Teams und mehrere Zuordnungen an. Die Webseite zeigt alle Personen, Teams und Zuordnungen.
Sehen Sie sich das Ergebnis in der Datenbank an (z.B. mit SQuirreL):
Person | |||
---|---|---|---|
Id | LastUpdate | PersonName | Team_ID |
1 | 2010-05-15 23:24:31.0 | Anton | 5 |
2 | 2010-05-15 23:24:44.0 | Berta | 5 |
3 | 2010-05-15 23:24:12.0 | Caesar | <null> |
Team | |||
---|---|---|---|
Id | LastUpdate | TeamName | |
4 | 2010-05-15 23:24:19.0 | Extreme | |
5 | 2010-05-15 23:24:23.0 | SuperDuper |
Beachten Sie, dass nur die Person-Tabelle eine zusätzliche Spalte (Team_ID) für die Relation hat.
Die Webseite sieht für dieses Beispiel so aus:
Testen Sie die Reaktion auf Fehleingaben:
In reellen Anwendungen müssten natürlich die Exceptions abgefangen und als verständliche Fehlermeldung präsentiert werden.
Falls Sie die Programmierbeispiele in derselben Datenbank mit unterschiedlichen JPA-Providern testen (z.B. weil die verschiedenen Beispiele andere Provider verwenden oder weil Sie verschiedene Java EE Application Server wie GlassFish oder WebLogic einsetzen) und die Datenbanktabellen automatisch erzeugen lassen, dann müssen Sie, um Fehler zu vermeiden, vor jedem Wechsel jeweils die Person-, die Team- und die Sequenztabelle in der Datenbank löschen ("Drop Table ...", z.B. mit SQuirreL), zum Beispiel so (der Name der Sequenztabelle kann anders lauten):
DROP TABLE sequence;
DROP TABLE person;
DROP TABLE team;
Falls Sie auf Probleme stoßen, sehen Sie sich die entsprechende Logdatei C:\GlassFish\glassfish\domains\domain1\logs\server.log bzw. C:\WebLogic\user_projects\domains\MeineDomain\servers\AdminServer\logs\AdminServer.log an.
Falls Sie eine ClassNotFoundException erhalten, stellen Sie sicher, dass in der persistence.xml entweder kein oder ein im Java EE Application Server vorhandener <provider>...</provider> eingetragen ist.
Das folgende Beispiel demonstriert:
Sie können das Programmierbeispiel als Zipdatei downloaden oder die im Folgenden beschriebenen Schritte durchführen. Das Programmierbeispiel basiert auf obigem JpaJspOneToMany-Beispiel.
Kopieren Sie das JpaJspOneToMany-Beispiel und erzeugen Sie ein Test-Package:
cd \MeinWorkspace
xcopy JpaJspOneToMany JpaJspOneToManyMitOpenEJBTest\ /S
cd JpaJspOneToManyMitOpenEJBTest
md src\test\java\de\meinefirma\meinprojekt\dao
md src\test\resources\META-INF
rd target /S /Q
tree /F
Löschen Sie das servletfilter-Package-Verzeichnis inklusive des JpaServletFilter:
rd src\main\java\de\meinefirma\meinprojekt\servletfilter /S /Q
Ersetzen Sie im Projektverzeichnis den Inhalt der pom.xml durch:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>de.meinefirma.meinprojekt</groupId> <artifactId>JpaJspOneToManyMitOpenEJBTest</artifactId> <version>1.0-SNAPSHOT</version> <packaging>war</packaging> <name>JpaJspOneToManyMitOpenEJBTest</name> <properties> <project.build.sourceEncoding>ISO-8859-1</project.build.sourceEncoding> </properties> <build> <finalName>JpaJspOneToMany</finalName> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.7.0</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> </build> <dependencies> <!-- Falls fuer die JUnit-Tests nicht der JPA-Provider OpenJPA verwendet werden soll, sondern EclipseLink: In der persistence.xml die Auskommentierung um die Zeile <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider> entfernen. --> <dependency> <groupId>org.eclipse.persistence</groupId> <artifactId>eclipselink</artifactId> <version>2.6.5</version> <scope>test</scope> </dependency> <!-- Falls fuer die JUnit-Tests nicht HSQLDB verwendet werden soll: Hier den fuer die JUnit-Tests gewuenschten JDBC-Treiber eintragen, z.B.: <dependency> <groupId>org.apache.derby</groupId> <artifactId>derby</artifactId> <version>10.14.1.0</version> <scope>test</scope> </dependency> <dependency> <groupId>com.mysql.jdbc</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.31</version> <scope>test</scope> </dependency> <dependency> <groupId>com.oracle.jdbc</groupId> <artifactId>ojdbc8</artifactId> <version>12.2</version> <scope>test</scope> </dependency> --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.activemq</groupId> <artifactId>activeio-core</artifactId> <version>3.1.4</version> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.openejb</groupId> <artifactId>openejb-core</artifactId> <version>4.7.5</version> <scope>test</scope> </dependency> <!-- Um in Tests die Exception java.lang.ClassFormatError: Absent Code attribute in method that is not native or abstract in class file ... zu vermeiden, wird javaee-api von org.apache.openejb verwendet. Falls doch unbedingt javax:javaee-api verwendet werden muss: Dann muss die javaee-api-Dependency als letzte Dependency eingetragen sein (und falls Tests in Eclipse ausgefuehrt werden, muss nach jedem 'mvn eclipse:eclipse'-Aufruf in der '.classpath'-Datei die entsprechende Dependency-Zeile hinter die letzte der '<classpathentry kind="var" path="M2_REPO/..."/>'-Zeilen verschoben werden bzw. das M2Eclipse-Plugin mit aktiviertem 'Enable Dependency Management' verwendet werden). --> <dependency> <groupId>org.apache.openejb</groupId> <artifactId>javaee-api</artifactId> <version>6.0-6</version> <scope>provided</scope> </dependency> </dependencies> </project>
Um die JpaServletFilter-Einträge zu entfernen, ersetzen Sie im src\main\webapp\WEB-INF-Verzeichnis den Inhalt der web.xml durch:
<?xml version="1.0" encoding="UTF-8"?> <web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" 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"> <display-name>Meine JPA-WebApp</display-name> <persistence-context-ref> <persistence-context-ref-name>persistence/em</persistence-context-ref-name> <persistence-unit-name>MeineJpaPU</persistence-unit-name> </persistence-context-ref> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> </web-app>
Ersetzen Sie im src\main\resources\META-INF-Verzeichnis den Inhalt der JPA-Konfigurationsdatei persistence.xml durch:
<?xml version="1.0" encoding="UTF-8"?> <!-- Falls JPA 1.0: <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"> --> <!-- Falls JPA 2.0: --> <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="MeineJpaPU" transaction-type="JTA"> <!-- Nur falls eine bestimmte JPA-Implementierung ausgewaehlt werden soll (fuer GlassFish auskommentieren): --> <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider> <jta-data-source>jdbc/MeinDatasourceJndiName</jta-data-source> <properties> <!-- Nur falls Tabellen automatisch angelegt werden sollen: --> <property name="eclipselink.ddl-generation" value="create-tables" /> <property name="openjpa.jdbc.SynchronizeMappings" value="buildSchema" /> <property name="hibernate.hbm2ddl.auto" value="create" /> </properties> </persistence-unit> </persistence>
Mit der <provider>...</provider>-Zeile können Sie eine bestimmte JPA-Implementierung auswählen.
Da die persistence.xml auch im Testfall ausgewertet wird, und dort die Libs des Java EE Application Servers
nicht zur Verfügung stehen, müssten Sie die Dependencies zu den benötigten Libs in der pom.xml hinzufügen
(mit <scope>test</scope>).
OpenEJB verwendet ohne die <provider>...</provider>-Angabe als Default OpenJPA.
Ersetzen Sie im src\main\java\de\meinefirma\meinprojekt\dao-Verzeichnis den Inhalt der AbstractDao.java durch:
package de.meinefirma.meinprojekt.dao; import java.util.List; import javax.naming.InitialContext; import javax.naming.NamingException; import javax.persistence.EntityManager; import javax.persistence.Query; import javax.transaction.UserTransaction; /** Super-Klasse fuer alle DAOs mit einigen generellen CRUD-Methoden */ public abstract class AbstractDao { protected final EntityManager em; protected final UserTransaction tx; // Konstruktor mit Lookups: public AbstractDao() { try { em = (EntityManager) (new InitialContext()).lookup( "java:comp/env/persistence/em" ); tx = (UserTransaction) (new InitialContext()).lookup( "java:comp/UserTransaction" ); } catch( NamingException ex ) { throw new RuntimeException( ex ); } } // Erstelle neuen Eintrag: public <T> void createEntity( T entity ) { try { tx.begin(); em.persist( entity ); tx.commit(); } catch( Exception ex ) { try { tx.rollback(); } catch( Exception e ) { /* ok */ } throw new RuntimeException( ex ); } } // Lies bestimmte Eintraege zu einer Entity-Klasse: @SuppressWarnings("unchecked") public <T> List<T> queryEntities( String namedQuery, String paramName, Object paramValue ) { Query query = em.createNamedQuery( namedQuery ); query.setParameter( paramName, paramValue ); return query.getResultList(); } // Lies bestimmte Eintraege zu einer Entity-Klasse: @SuppressWarnings("unchecked") public <T> List<T> queryEntities( String namedQuery, List<Object> paramNamesAndValues ) { Query query = em.createNamedQuery( namedQuery ); for( int i = 0; paramNamesAndValues != null && i < paramNamesAndValues.size(); i += 2 ) { query.setParameter( paramNamesAndValues.get( i ).toString(), paramNamesAndValues.get( i + 1 ) ); } return query.getResultList(); } // Native Query: @SuppressWarnings("unchecked") public List<Object[]> queryNative( String sqlCmd, List<Object> paramValues ) { Query query = em.createNativeQuery( sqlCmd ); for( int i = 0; paramValues != null && i < paramValues.size(); i++ ) { query.setParameter( i + 1, paramValues.get( i ) ); } return query.getResultList(); } // Lies alle Eintraege zu einer Entity-Klasse: @SuppressWarnings("unchecked") public <T> List<T> readAllEntities( Class<T> clss ) { return em.createQuery( "Select d from " + clss.getSimpleName() + " d" ).getResultList(); } // Loesche alle Eintraege zu einer Entity-Klasse: public <T> int deleteAllEntities( Class<T> clss ) { try { tx.begin(); em.flush(); em.clear(); int r = em.createQuery( "Delete from " + clss.getSimpleName() ).executeUpdate(); tx.commit(); return r; } catch( Exception ex ) { try { tx.rollback(); } catch( Exception e ) { /* ok */ } throw new RuntimeException( ex ); } } }
Ersetzen Sie im src\main\java\de\meinefirma\meinprojekt\dao-Verzeichnis den Inhalt der PersonTeamDao.java durch:
package de.meinefirma.meinprojekt.dao; import java.util.List; import de.meinefirma.meinprojekt.entities.Person; import de.meinefirma.meinprojekt.entities.Team; /** DAO fuer Person und Team */ public class PersonTeamDao extends AbstractDao { // An HTML-Formular angepasste Methode fuer Zuordnung von Personen zu Teams: @SuppressWarnings("unchecked") public String addDelPersonTeam( String add, String del, String pname, String tname, Object personListe, Object teamListe ) { Person person = null; Team team = null; if( (add == null && del == null) || pname == null || tname == null || pname.trim().length() == 0 || tname.trim().length() == 0 || !(personListe instanceof List) || !(teamListe instanceof List) ) return ""; // Damit Optmistic-Locking-Erkennung funktioniert, muessen die "alten" detached Objekte // verwendet werden (mit den alten "@Version"-Werten) // (personListe und teamListe wurden in der HttpSession zwischengespeichert): for( Person p : (List<Person>) personListe ) if( p != null && p.getName() != null && p.getName().equals( pname ) ) { person = p; break; } for( Team t : (List<Team>) teamListe ) if( t != null && t.getName() != null && t.getName().equals( tname ) ) { team = t; break; } if( person == null ) return "Person nicht vorhanden"; if( team == null ) return "Team nicht vorhanden"; try { tx.begin(); // Merge, damit Optmistic-Locking-Exceptions erkannt werden. // Der Returnwert beinhaltet die "managed instance", also die reattached Entity: person = em.merge( person ); team = em.merge( team ); // Fuehre "del" bzw. "add" aus: if( del != null ) team.removePerson( person ); if( add != null ) { for( Team t : (List<Team>) teamListe ) if( t != null && t.removePerson( person ) ) em.merge( t ); team.addPerson( person ); } tx.commit(); return ""; } catch( Exception ex ) { try { tx.rollback(); } catch( Exception e ) { /* ok */ } throw new RuntimeException( ex ); } } }
Erzeugen Sie im src\test\java\de\meinefirma\meinprojekt\dao-Verzeichnis den JUnit-Test: PersonTeamDaoTest.java
package de.meinefirma.meinprojekt.dao; import java.util.List; import org.junit.*; import de.meinefirma.meinprojekt.entities.*; public class PersonTeamDaoTest { @Test public void testPersonTeamDao() { // Initialisierung von Datenbank, EntityManager und UserTransaction: (new JpaTestUtil()).setupDbEmUtx(); // Konstruiere das zu testende DAO: PersonTeamDao dao = new PersonTeamDao(); // Loesche eventuell vorhandene Eintraege: dao.deleteAllEntities( Person.class ); dao.deleteAllEntities( Team.class ); // Teste createEntity() und queryEntities(): Person p1 = new Person(); Team t1 = new Team(); p1.setName( "p1" ); t1.setName( "t1" ); dao.createEntity( p1 ); dao.createEntity( t1 ); List<Person> p1Lst = dao.queryEntities( Person.QUERY_BY_NAME, "pname", "p1" ); List<Team> t1Lst = dao.queryEntities( Team.QUERY_BY_NAME, "tname", "t1" ); Assert.assertEquals( 1, p1Lst.size() ); Assert.assertEquals( 1, t1Lst.size() ); Assert.assertEquals( "p1", p1Lst.get( 0 ).getName() ); Assert.assertEquals( "t1", t1Lst.get( 0 ).getName() ); // Teste addDelPersonTeam() mit "add" (Zuordnung hinzufuegen): List<Person> pAll = dao.readAllEntities( Person.class ); List<Team> tAll = dao.readAllEntities( Team.class ); dao.addDelPersonTeam( "add", null, "p1", "t1", pAll, tAll ); pAll = dao.readAllEntities( Person.class ); tAll = dao.readAllEntities( Team.class ); Person p = pAll.get( 0 ); Team t = tAll.get( 0 ); Assert.assertEquals( "t1", p.getTeam().getName() ); Assert.assertTrue( t.getPersonen().contains( p ) ); // Teste addDelPersonTeam() mit "del" (Zuordnung entfernen): dao.addDelPersonTeam( null, "del", "p1", "t1", pAll, tAll ); pAll = dao.readAllEntities( Person.class ); tAll = dao.readAllEntities( Team.class ); p = pAll.get( 0 ); t = tAll.get( 0 ); Assert.assertNull( p.getTeam() ); Assert.assertTrue( t.getPersonen().isEmpty() ); } }
Erzeugen Sie im src\test\java\de\meinefirma\meinprojekt\dao-Verzeichnis die Test-Hilfsklasse: JpaTestUtil.java
package de.meinefirma.meinprojekt.dao; import java.util.Properties; import javax.naming.*; import javax.persistence.*; import org.apache.openejb.api.LocalClient; @LocalClient public class JpaTestUtil { @PersistenceContext private EntityManager em; /** Initialisierung von Datenbank, EntityManager und UserTransaction */ public void setupDbEmUtx() { // Falls eine automatische Tabellenanlage nicht ueber die persistence.xml initiiert werden soll, // kann dies fuer Tests auch hier ueber Environmentvariablen vorgegeben werden, z.B. fuer EclipseLink so: System.getProperties().setProperty( "eclipselink.ddl-generation", "create-tables" ); // InitialContext fuer OpenEJB-Container: Properties p = new Properties(); p.put( Context.INITIAL_CONTEXT_FACTORY, "org.apache.openejb.client.LocalInitialContextFactory" ); p.put( "jdbc/MeinDatasourceJndiName", "new://Resource?type=DataSource" ); // HSQLDB (in Memory): p.put( "jdbc/MeinDatasourceJndiName.JdbcDriver", "org.hsqldb.jdbcDriver" ); p.put( "jdbc/MeinDatasourceJndiName.JdbcUrl", "jdbc:hsqldb:mem:MeineDb" ); // Derby (im target-Verzeichnis): // p.put( "jdbc/MeinDatasourceJndiName.JdbcDriver", "org.apache.derby.jdbc.EmbeddedDriver" ); // p.put( "jdbc/MeinDatasourceJndiName.JdbcUrl", "jdbc:derby:./target/Derby-DB/mydb;create=true" ); // MySQL: // p.put( "jdbc/MeinDatasourceJndiName.JdbcDriver", "com.mysql.jdbc.Driver" ); // p.put( "jdbc/MeinDatasourceJndiName.JdbcUrl", "jdbc:mysql://localhost:3306/MeineDb" ); // p.put( "jdbc/MeinDatasourceJndiName.UserName", "root" ); // p.put( "jdbc/MeinDatasourceJndiName.PassWord", "" ); // Oracle-DB: // p.put( "jdbc/MeinDatasourceJndiName.JdbcDriver", "oracle.jdbc.OracleDriver" ); // p.put( "jdbc/MeinDatasourceJndiName.JdbcUrl", "jdbc:oracle:thin:@localhost:1521:XE" ); // p.put( "jdbc/MeinDatasourceJndiName.Username", "xx..." ); // p.put( "jdbc/MeinDatasourceJndiName.Password", "yy..." ); try { (new InitialContext( p )).bind( "inject", this ); try { (new InitialContext()).bind( "java:comp/env/persistence/em", em ); } catch( NameAlreadyBoundException e ) { /* ok */ } } catch( NamingException ex ) { throw new RuntimeException( ex ); } } }
Erläuterungen zu den hier verwendeten und zu weiteren Properties für den OpenEJB-InitialContext finden Sie unter http://openejb.apache.org/3.0/containers-and-resources.html unter "Resources / javax.sql.DataSource".
Erzeugen Sie im src\test\resources\META-INF-Verzeichnis die Dummy-Konfigurationsdatei: application-client.xml
<!-- Notwendig, damit beim Test mit @LocalClient annotierte Klassen erkannt werden: --> <application-client/>
Die Projektstruktur sieht jetzt so aus:
cd \MeinWorkspace\JpaJspOneToManyMitOpenEJBTest
tree /F
[\MeinWorkspace\JpaJspOneToManyMitOpenEJBTest] |- [src] | |- [main] | | |- [java] | | | '- [de] | | | '- [meinefirma] | | | '- [meinprojekt] | | | |- [dao] | | | | '- AbstractDao.java | | | | '- PersonTeamDao.java | | | '- [entities] | | | |- Person.java | | | '- Team.java | | |- [resources] | | | '- [META-INF] | | | '- persistence.xml | | '- [webapp] | | |- [WEB-INF] | | | '- web.xml | | '- index.jsp | '- [test] | |- [java] | | '- [de] | | '- [meinefirma] | | '- [meinprojekt] | | '- [dao] | | |- JpaTestUtil.java | | '- PersonTeamDaoTest.java | '- [resources] | '- [META-INF] | '- application-client.xml |- pom.xml |- run-GlassFish.bat '- run-WebLogic.bat
Führen Sie die JUnit-Tests aus (mit Java 8):
cd \MeinWorkspace\JpaJspOneToManyMitOpenEJBTest
java -version
mvn test
Selbstverständlich funktioniert auch die Webanwendung weiterhin. Führen Sie die zu Ihrem Server passende Batchdatei run-...bat aus. Falls die von der Batchdatei gestartete Webseite nicht sofort funktioniert (weil der Server noch nicht fertig ist), führen Sie einige Sekunden später einen Refresh der Webseite durch. Führen Sie die oben genannten Tests durch.
Falls Sie die Programmierbeispiele in derselben Datenbank mit unterschiedlichen JPA-Providern testen (z.B. weil die verschiedenen Beispiele andere Provider verwenden oder weil Sie verschiedene Java EE Application Server wie GlassFish oder WebLogic einsetzen) und die Datenbanktabellen automatisch erzeugen lassen, dann müssen Sie, um Fehler zu vermeiden, vor jedem Wechsel jeweils die Person-, die Team- und die Sequenztabelle in der Datenbank löschen ("Drop Table ...", z.B. mit SQuirreL), zum Beispiel für OpenJPA so (der Name der Sequenztabelle kann anders lauten):
DROP TABLE openjpa_sequence_table;
DROP TABLE person;
DROP TABLE team;
Zur verwendeten Test-Datenbank gibt es verschiedene Optionen (siehe auch JpaTestUtil.java):
Falls Sie den JPA-Provider oder die Datenbank für den JUnit-Test wechseln wollen:
a) Tragen Sie den JPA-Provider ein in die
persistence.xml.
b) Tragen Sie die JDBC-Parameter ein in die setupDbEmUtx()-Methode in
JpaTestUtil.java.
c) Tragen Sie Dependencies zu benötigten Libs ein in die
pom.xml.
Falls Sie eine Lib verwenden wollen, die es nicht in den üblichen öffentlichen Maven-Repositories gibt, können Sie die Lib entweder nur in Ihr lokales Maven-Repository "installen", oder noch besser, falls Sie einen Repository-Manager wie z.B. Nexus verwenden, dorthin "deployen" oder "uploaden".
Der lokale "Install" erfolgt zum Beispiel für den JPA-Provider EclipseLink 2.6.5.v20170607 aus WebLogic 12.2.1.3 so (siehe auch Maven-Repo):
mvn install:install-file -DgroupId=org.eclipse.persistence -DartifactId=eclipselink -Dversion=2.6.5.v20170607 -Dpackaging=jar -Dfile=/WebLogic/oracle_common/modules/oracle.toplink/eclipselink.jar
Oder für den Oracle-JDBC-Treiber Oracle Database 12.2.0.1 JDBC Driver ojdbc8.jar aus WebLogic 12.2.1.3:
mvn install:install-file -DgroupId=com.oracle.jdbc -DartifactId=ojdbc8 -Dversion=12.2 -Dpackaging=jar -Dfile=/WebLogic/oracle_common/modules/oracle.jdbc/ojdbc8.jar
Oder für den MySQL-JDBC-Treiber mysql-connector-java-5.1.31-bin.jar:
mvn install:install-file -DgroupId=com.mysql.jdbc -DartifactId=mysql-connector-java -Dversion=5.1.31 -Dpackaging=jar -Dfile=mysql-connector-java-5.1.31-bin.jar
Oder für den JTOpen-JDBC-Treiber für DB2/400 auf AS/400 (iSeries), jt400-7.1.0.10.jar:
mvn install:install-file -DgroupId=net.sf.jt400 -DartifactId=jt400 -Dversion=7.1.0.10 -Dpackaging=jar -Dfile=jt400.jar
Falls Sie dem Dateinamen nicht die genaue Versionsnummer entnehmen können (wie z.B. bei ojdbc8.jar und jt400.jar), dann lohnt sich oft ein Blick in die meistens vorhandene Manifest-Datei, beispielsweise für ojdbc8.jar so:
jar xf ojdbc8.jar
type META-INF\MANIFEST.MF
Falls Sie folgende Exception erhalten:
org.apache.openjpa.persistence.PersistenceException: Constraint already exists: UNQ_ in statement [CREATE TABLE ...
Dann müssen Sie (wie bei Person und Team gezeigt):
Falls Sie im JUnit-Test folgende Exception erhalten:
"java.sql.SQLException: Table not found ..." oder
"java.sql.SQLException: Sequence not found ...":
Dann fehlt wahrscheinlich ein Kommando zur automatischen Anlage der Tabellen in der Testdatenbank.
Im hier gezeigten Beispiel ist dieses Kommando zur automatischen Tabellenanlage der Einfachheit halber in der "normalen" persistence.xml eingetragen
(unter <properties>, z.B. mit "create-tables"), was in reellen Anwendungen normalerweise nicht möglich ist.
Um für den Testfall die Tabellen automatisch anzulegen, können Sie
entweder in der persistence.xml eine eigene <persistence-unit> für den Testfall vorsehen,
oder eine eigene Test-persistence.xml hinzufügen,
oder am einfachsten im Unittest die Parameter des Tabellenanlagekommandos in einer Environmentvariable hinterlegen,
zum Beispiel für EclipseLink so:
System.getProperties().setProperty( "eclipselink.ddl-generation", "create-tables" );
Diese Zeile muss ausgeführt werden, bevor der PersistenceContext aufgebaut wird, also vor:
(new InitialContext( p )).bind( "inject", this );
Falls Sie im JUnit-Test folgende Exception erhalten:
javax.naming.NamingException: Unable to find injection meta-data for ... Ensure that class was annotated with @org.apache.openejb.api.LocalClient and was successfully discovered and deployed. See http://openejb.apache.org/3.0/local-client-injection.html
Dann untersuchen Sie Folgendes:
Falls Sie beim Ausführen des JUnit-Tests mit "mvn test" folgende Exception erhalten:
java.lang.ClassFormatError: Absent Code attribute in method that is not native or abstract in class file javax/persistence/Persistence
Dann haben Sie wahrscheinlich statt der org.apache.openejb:javaee-api-6.0-6.jar die javax:javaee-api-...jar in der pom.xml eingetragen. Falls Sie nicht die org.apache.openejb:javaee-api-6.0-6.jar verwenden können, müssen Sie darauf achten, dass die javaee-api-Dependency als letzte Dependency eingetragen ist.
Falls Sie trotz der genannten Maßnahmen weiter "... ClassFormatError: Absent Code ..."-Exceptions beim Ausführen von Unit-Tests erhalten: Eventuell müssen Sie weitere "leere API-Implementierungen" durch "richtige Implementierungen" ersetzen. Im Falle von JSF kann vielleicht "org.apache.myfaces.core:myfaces-api" helfen.
Falls die Komponententests mit "mvn test" funktionieren, aber in Eclipse erhalten Sie die genannten oder folgende Exceptions:
FATAL - OpenEJB has encountered a fatal error and cannot be started: OpenEJB encountered an unexpected error while attempting to instantiate the assembler.
java.lang.ClassFormatError: Absent Code attribute in method that is not native or abstract in class file javax/resource/spi/ResourceAdapterInternalException
oder
java.lang.ClassFormatError: Absent Code attribute in method that is not native or abstract in class file javax/persistence/PersistenceContextType
oder
ERROR - FAIL ... null: Persistence unit not found
oder
ERROR - Invalid ClientModule ... Module failed validation
Dann entspricht die Classpath-Reihenfolge in Eclipse nicht der in Maven. In diesem Fall haben Sie zwei Möglichkeiten:
Falls Sie WebLogic 12.2.1 verwenden, und Exceptions ähnlich zu diesen erhalten:
com.sun.jdi.InvocationException occurred invoking method
java.lang.NullPointerException
at weblogic.persistence.CICScopedEMProvider.getEMForCurrentCIC(CICScopedEMProvider.java:35)
at weblogic.persistence.TransactionalEntityManagerProxyImpl.getPersistenceContext(TransactionalEntityManagerProxyImpl.java:122)
javax.persistence.TransactionRequiredException: Cannot call methods requiring a transaction if the EntityManager has not been joined to the current transaction.
Dann sollten Sie überprüfen, ob die Ursache das unter Probleme mit eigenen Threads in WebLogic 12.2.1 beschriebene Problem ist.
Das folgende Beispiel demonstriert:
Kopieren Sie das JpaJspOneToManyMitOpenEJBTest-JPA-2.0-Beispiel in ein neues Projekt:
cd \MeinWorkspace
xcopy JpaJspOneToManyMitOpenEJBTest JpaJspOneToManyMitTomEEOpenEJBTest\ /S
cd JpaJspOneToManyMitTomEEOpenEJBTest
Ersetzen Sie im neuen JpaJspOneToManyMitTomEEOpenEJBTest-Projekt den Inhalt der pom.xml durch:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>de.meinefirma.meinprojekt</groupId> <artifactId>JpaJspOneToManyMitTomEEOpenEJBTest</artifactId> <version>1.0-SNAPSHOT</version> <packaging>war</packaging> <name>JpaJspOneToManyMitTomEEOpenEJBTest</name> <properties> <project.build.sourceEncoding>ISO-8859-1</project.build.sourceEncoding> </properties> <build> <finalName>JpaJspOneToMany</finalName> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.7.0</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>org.eclipse.persistence</groupId> <artifactId>eclipselink</artifactId> <version>2.6.5</version> <scope>test</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.tomee</groupId> <artifactId>openejb-core</artifactId> <version>7.0.4</version> <scope>test</scope> </dependency> <dependency> <groupId>javax</groupId> <artifactId>javaee-api</artifactId> <version>7.0</version> <scope>provided</scope> </dependency> </dependencies> </project>
Ersetzen Sie im src\main\resources\META-INF-Unterverzeichnis den Inhalt der persistence.xml durch:
<?xml version="1.0" encoding="UTF-8"?> <!-- JPA 2.1: --> <persistence version="2.1" xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"> <persistence-unit name="MeineJpaPU" transaction-type="JTA"> <!-- Nur falls eine bestimmte JPA-Implementierung ausgewaehlt werden soll (fuer GlassFish auskommentieren): --> <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider> <jta-data-source>jdbc/MeinDatasourceJndiName</jta-data-source> <properties> <!-- Nur falls Tabellen automatisch angelegt werden sollen: --> <property name="eclipselink.ddl-generation" value="create-tables" /> <property name="toplink.ddl-generation" value="create-tables" /> <property name="openjpa.jdbc.SynchronizeMappings" value="buildSchema" /> <property name="hibernate.hbm2ddl.auto" value="create" /> </properties> </persistence-unit> </persistence>
Führen Sie die JUnit-Tests aus (mit Java 8):
cd \MeinWorkspace\JpaJspOneToManyMitTomEEOpenEJBTest
java -version
mvn test
Selbstverständlich funktioniert auch die Webanwendung weiterhin. Führen Sie die zu Ihrem Server passende Batchdatei run-...bat aus, beispielsweise für WebLogic 12.2.1:
cd \MeinWorkspace\JpaJspOneToManyMitTomEEOpenEJBTest
run-WebLogic.bat
Führen Sie die oben genannten Tests durch.
Das folgende Beispiel demonstriert:
Sie können das Programmierbeispiel als Zipdatei downloaden oder die im Folgenden beschriebenen Schritte durchführen. Das Programmierbeispiel basiert auf obigem JpaJspOneToManyMitOpenEJBTest-Beispiel.
Kopieren Sie das JpaJspOneToManyMitOpenEJBTest-Beispiel und passen Sie es an:
cd \MeinWorkspace
xcopy JpaJspOneToManyMitOpenEJBTest JpaJspOneToManyMitArquillianTest\ /S
cd JpaJspOneToManyMitArquillianTest
md src\test\resources-glassfish-embedded
md src\test\resources-jbossas-remote
del src\test\java\de\meinefirma\meinprojekt\dao\JpaTestUtil.java
rd src\test\resources\META-INF /S /Q
rd target /S /Q
tree /F
Ersetzen Sie im Projektverzeichnis den Inhalt der pom.xml durch:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>de.meinefirma.meinprojekt</groupId> <artifactId>JpaJspOneToManyMitArquillianTest</artifactId> <version>1.0-SNAPSHOT</version> <packaging>war</packaging> <name>JpaJspOneToManyMitArquillianTest</name> <properties> <arquillian.version>1.0.0.Alpha4.SP1</arquillian.version> <project.build.sourceEncoding>ISO-8859-1</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> </properties> <build> <finalName>JpaJspOneToMany</finalName> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>2.3.2</version> <configuration> <source>1.6</source> <target>1.6</target> <compilerArgument>-proc:none</compilerArgument> </configuration> <executions> <execution> <id>run-annotation-processors-only</id> <phase>generate-sources</phase> <configuration> <compilerArgument>-proc:only</compilerArgument> </configuration> <goals> <goal>compile</goal> </goals> </execution> </executions> </plugin> <plugin> <!-- Fuegt vom JPA-2-Annotation-Processor generierte Sourcen zum Compilier-Path hinzu: --> <groupId>org.codehaus.mojo</groupId> <artifactId>build-helper-maven-plugin</artifactId> <version>1.5</version> <executions> <execution> <phase>process-sources</phase> <configuration> <sources> <source>${project.build.directory}/generated-sources/annotations</source> </sources> </configuration> <goals> <goal>add-source</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.8.1</version> </plugin> </plugins> </build> <profiles> <profile> <id>glassfish-embedded</id> <activation> <activeByDefault>true</activeByDefault> </activation> <dependencies> <dependency> <groupId>org.jboss.arquillian.container</groupId> <artifactId>arquillian-glassfish-embedded-3</artifactId> <version>${arquillian.version}</version> <scope>test</scope> </dependency> <dependency> <!-- GlassFish-Embedded: Versionen 3.0.1 und 3.1-b13 funktionieren, aber Version 3.1 meldet: java.lang.NoClassDefFoundError: org/glassfish/api/embedded/Server$Builder Caused by: java.lang.ClassNotFoundException: org.glassfish.api.embedded.Server$Builder, siehe hierzu http://community.jboss.org/message/582758 --> <groupId>org.glassfish.extras</groupId> <artifactId>glassfish-embedded-all</artifactId> <version>3.0.1</version> <scope>provided</scope> </dependency> </dependencies> <build> <testResources> <testResource> <directory>src/test/resources</directory> </testResource> <testResource> <directory>src/test/resources-glassfish-embedded</directory> </testResource> </testResources> </build> </profile> <profile> <id>jbossas-managed</id> <dependencies> <dependency> <groupId>org.jboss.arquillian.container</groupId> <artifactId>arquillian-jbossas-managed-6</artifactId> <version>${arquillian.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.jboss.jbossas</groupId> <artifactId>jboss-as-client</artifactId> <version>6.0.0.Final</version> <type>pom</type> <scope>provided</scope> </dependency> <dependency> <groupId>org.jboss.spec</groupId> <artifactId>jboss-javaee-6.0</artifactId> <version>1.0.0.Final</version> <type>pom</type> <scope>provided</scope> </dependency> <dependency> <groupId>org.jboss.jbossas</groupId> <artifactId>jboss-server-manager</artifactId> <version>1.0.4.Final</version> <scope>test</scope> </dependency> </dependencies> <build> <testResources> <testResource> <directory>src/test/resources</directory> </testResource> <testResource> <directory>src/test/resources-jbossas-remote</directory> </testResource> </testResources> </build> </profile> <profile> <id>jbossas-remote</id> <dependencies> <dependency> <groupId>org.jboss.arquillian.container</groupId> <artifactId>arquillian-jbossas-remote-6</artifactId> <version>${arquillian.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.jboss.spec</groupId> <artifactId>jboss-javaee-6.0</artifactId> <version>1.0.0.Final</version> <type>pom</type> <scope>provided</scope> </dependency> <dependency> <groupId>org.jboss.jbossas</groupId> <artifactId>jboss-as-client</artifactId> <version>6.0.0.Final</version> <type>pom</type> <scope>test</scope> </dependency> </dependencies> <build> <testResources> <testResource> <directory>src/test/resources</directory> </testResource> <testResource> <directory>src/test/resources-jbossas-remote</directory> </testResource> </testResources> </build> </profile> </profiles> <dependencies> <dependency> <!-- JPA 2 Annotation Processor: --> <groupId>org.hibernate</groupId> <artifactId>hibernate-jpamodelgen</artifactId> <version>1.1.1.Final</version> <scope>provided</scope> <exclusions> <exclusion> <groupId>org.hibernate.javax.persistence</groupId> <artifactId>hibernate-jpa-2.0-api</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.jboss.arquillian</groupId> <artifactId>arquillian-junit</artifactId> <version>${arquillian.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.8.2</version> <scope>test</scope> </dependency> </dependencies> <repositories> <repository> <id>jboss-public-repository</id> <name>JBoss Repository</name> <url>http://repository.jboss.org/nexus/content/groups/public</url> <releases> <updatePolicy>never</updatePolicy> </releases> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>jboss-public-repository</id> <name>JBoss Repository</name> <url>http://repository.jboss.org/nexus/content/groups/public</url> <releases> <updatePolicy>never</updatePolicy> </releases> <snapshots> <enabled>false</enabled> </snapshots> </pluginRepository> </pluginRepositories> </project>
Ersetzen Sie im Verzeichnis src\test\java\de\meinefirma\meinprojekt\dao den Inhalt der PersonTeamDaoTest.java durch:
package de.meinefirma.meinprojekt.dao; import java.util.List; import javax.inject.Inject; import javax.naming.InitialContext; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.transaction.UserTransaction; import org.jboss.arquillian.api.Deployment; import org.jboss.arquillian.junit.Arquillian; import org.jboss.shrinkwrap.api.Archive; import org.jboss.shrinkwrap.api.ShrinkWrap; import org.jboss.shrinkwrap.api.asset.EmptyAsset; import org.jboss.shrinkwrap.api.spec.WebArchive; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import de.meinefirma.meinprojekt.entities.*; @RunWith( Arquillian.class ) public class PersonTeamDaoTest { @PersistenceContext( unitName="MeineJpaPU", name="persistence/em" ) EntityManager em; @Inject UserTransaction utx; @Deployment public static Archive<?> createDeployment() { return ShrinkWrap.create( WebArchive.class, "test.war" ) .addPackage( Person.class.getPackage() ) .addPackage( PersonTeamDao.class.getPackage() ) .addManifestResource( "test-persistence.xml", "persistence.xml" ) .addWebResource( EmptyAsset.INSTANCE, "beans.xml" ); } @Test public void testEmTx() throws Exception { UserTransaction txJndi = (UserTransaction) (new InitialContext()).lookup( "java:comp/UserTransaction" ); EntityManager emJndi = (EntityManager) (new InitialContext()).lookup( "java:comp/env/persistence/em" ); Assert.assertNotNull( "UserTransaction", txJndi ); Assert.assertNotNull( "EntityManager", emJndi ); } @Test public void testPersonTeamDao() { // Konstruiere das zu testende DAO: PersonTeamDao dao = new PersonTeamDao(); // Loesche eventuell vorhandene Eintraege: dao.deleteAllEntities( Person.class ); dao.deleteAllEntities( Team.class ); // Teste createEntity() und queryEntities(): Person p1 = new Person(); Team t1 = new Team(); p1.setName( "p1" ); t1.setName( "t1" ); dao.createEntity( p1 ); dao.createEntity( t1 ); List<Person> p1Lst = dao.queryEntities( Person.QUERY_BY_NAME, "pname", "p1" ); List<Team> t1Lst = dao.queryEntities( Team.QUERY_BY_NAME, "tname", "t1" ); Assert.assertEquals( 1, p1Lst.size() ); Assert.assertEquals( 1, t1Lst.size() ); Assert.assertEquals( "p1", p1Lst.get( 0 ).getName() ); Assert.assertEquals( "t1", t1Lst.get( 0 ).getName() ); // Teste addDelPersonTeam() mit "add" (Zuordnung hinzufuegen): List<Person> pAll = dao.readAllEntities( Person.class ); List<Team> tAll = dao.readAllEntities( Team.class ); dao.addDelPersonTeam( "add", null, "p1", "t1", pAll, tAll ); pAll = dao.readAllEntities( Person.class ); tAll = dao.readAllEntities( Team.class ); Person p = pAll.get( 0 ); Team t = tAll.get( 0 ); Assert.assertEquals( "t1", p.getTeam().getName() ); Assert.assertTrue( t.getPersonen().contains( p ) ); // Teste addDelPersonTeam() mit "del" (Zuordnung entfernen): dao.addDelPersonTeam( null, "del", "p1", "t1", pAll, tAll ); pAll = dao.readAllEntities( Person.class ); tAll = dao.readAllEntities( Team.class ); p = pAll.get( 0 ); t = tAll.get( 0 ); Assert.assertNull( p.getTeam() ); Assert.assertTrue( t.getPersonen().isEmpty() ); System.out.println( "---- testPersonTeamDao() ok ----" ); } }
Erzeugen Sie im Verzeichnis src\test\resources die Arquillian-Konfigurationsdatei: arquillian.xml
<?xml version="1.0" encoding="UTF-8"?> <arquillian xmlns="http://jboss.com/arquillian" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:glassfish-embedded="urn:arq:org.jboss.arquillian.container.glassfish.embedded_3"> <glassfish-embedded:container> <glassfish-embedded:sunResourcesXml>src/test/resources-glassfish-embedded/sun-resources.xml</glassfish-embedded:sunResourcesXml> </glassfish-embedded:container> </arquillian>
Erzeugen Sie im Verzeichnis src\test\resources-glassfish-embedded zwei XML-Konfigurationsdateien:
sun-resources.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE resources PUBLIC "-//Sun Microsystems, Inc.//DTD Application Server 9.0 Resource Definitions //EN" "http://www.sun.com/software/appserver/dtds/sun-resources_1_4.dtd"> <resources> <jdbc-resource pool-name="ArquillianEmbeddedDerbyPool" jndi-name="jdbc/arquillian" /> <jdbc-connection-pool name="ArquillianEmbeddedDerbyPool" res-type="javax.sql.DataSource" datasource-classname="org.apache.derby.jdbc.EmbeddedDataSource" is-isolation-level-guaranteed="false"> <property name="databaseName" value="target/databases/derby" /> <property name="createDatabase" value="create" /> </jdbc-connection-pool> </resources>
test-persistence.xml
<?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="MeineJpaPU"> <jta-data-source>jdbc/arquillian</jta-data-source> <properties> <property name="eclipselink.ddl-generation" value="drop-and-create-tables" /> <property name="eclipselink.logging.level" value="FINE" /> <property name="hibernate.hbm2ddl.auto" value="create-drop" /> <property name="hibernate.show_sql" value="true" /> </properties> </persistence-unit> </persistence>
Bitte beachten: Es wird JPA 2.0 konfiguriert.
Erzeugen Sie im Verzeichnis resources-jbossas-remote zwei Konfigurationsdateien:
jndi.properties
java.naming.factory.initial=org.jnp.interfaces.NamingContextFactory java.naming.factory.url.pkgs=org.jboss.naming:org.jnp.interfaces java.naming.provider.url=jnp://localhost:1099
test-persistence.xml
<?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="MeineJpaPU"> <jta-data-source>java:/DefaultDS</jta-data-source> <properties> <property name="hibernate.hbm2ddl.auto" value="create-drop" /> <property name="hibernate.show_sql" value="true" /> </properties> </persistence-unit> </persistence>
Bitte beachten: Es wird JPA 2.0 konfiguriert.
Die Projektstruktur sieht jetzt so aus:
cd \MeinWorkspace\JpaJspOneToManyMitArquillianTest
tree /F
[\MeinWorkspace\JpaJspOneToManyMitArquillianTest] |- [src] | |- [main] | | |- [java] | | | '- [de] | | | '- [meinefirma] | | | '- [meinprojekt] | | | |- [dao] | | | | '- AbstractDao.java | | | | '- PersonTeamDao.java | | | '- [entities] | | | |- Person.java | | | '- Team.java | | |- [resources] | | | '- [META-INF] | | | '- persistence.xml | | '- [webapp] | | |- [WEB-INF] | | | '- web.xml | | '- index.jsp | '- [test] | |- [java] | | '- [de] | | '- [meinefirma] | | '- [meinprojekt] | | '- [dao] | | '- PersonTeamDaoTest.java | |- [resources] | | '- arquillian.xml | |- [resources-glassfish-embedded] | | |- sun-resources.xml | | '- test-persistence.xml | '- [resources-jbossas-remote] | |- jndi.properties | '- test-persistence.xml '- pom.xml
"embedded", "managed" und "remote":
Sehen Sie sich die Bedeutung der drei verschiedenen Modi "embedded", "managed" und "remote" an unter: http://docs.jboss.org/arquillian/reference/latest/en-US/html/containers.html#d0e657
GlassFish 3:
Sie benötigen für den Embedded-Test keine Installation von GlassFish. Download und Einbindung erfolgen automatisch.
Führen Sie die JUnit-Tests "embedded" mit GlassFish aus:
cd \MeinWorkspace\JpaJspOneToManyMitArquillianTest
mvn clean test
JBoss 6:
Falls Sie mit JBoss testen wollen und JBoss noch nicht installiert haben: Downloaden Sie jboss-as-distribution-6.0.0.Final.zip von http://www.jboss.org/jbossas und entzippen Sie den Inhalt zum Beispiel nach C:\JBoss.
Starten Sie den JBoss-Server und führen Sie den Test "remote" aus:
start C:\JBoss\bin\run.bat
mvn clean test -Pjbossas-remote
Beenden Sie JBoss mit "Strg + C".
Führen Sie (bei gestopptem JBoss-Server) den Test "managed" aus:
set JAVA_HOME=C:\PROGRA~1\Java\jdk1.6
set JBOSS_HOME=C:\JBoss
mvn clean test -Pjbossas-managed
Der JBoss-Server wird automatisch hoch- und runtergefahren.
Passen Sie die Java- und JBoss-Pfade an Ihre Installation an. Der JAVA_HOME-Pfad darf für diesen Test kein Leerzeichen enthalten. Falls Ihr Java in "C:\Program Files\Java\jdk1.6" installiert ist: Ermitteln Sie mit "dir C:\ /X" den leerzeichenfreien 8.3-Alias-Pfad. Untersuchen Sie bei Problemen die Server-Logs in C:\JBoss\server\default\log und das Testergebnis in target\surefire-reports\de.meinefirma.meinprojekt.dao.PersonTeamDaoTest.txt.
Selbstverständlich funktioniert auch die Webanwendung weiterhin, zum Beispiel im WebLogic:
Starten Sie WebLogic, bauen Sie die WAR-Datei, kopieren Sie sie ins Autodeploymentverzeichnis und rufen Sie die URL auf
(warten Sie lange genug auf den Server-Start und das Deployment):
start C:\WebLogic\user_projects\domains\MeineDomain\bin\startWebLogic.cmd
cd /D D:\MeinWorkspace\JpaJspOneToManyMitArquillianTest
mvn clean package
copy /Y target\JpaJspOneToMany.war C:\WebLogic\user_projects\domains\MeineDomain\autodeploy
start http://localhost:7001/JpaJspOneToMany
Führen Sie die oben genannten Tests durch.
Falls Sie das "mvn clean package"-Kommando häufiger ausführen müssen, und nicht jedesmal die Tests ausführen wollen, können Sie die Zeit verkürzen, indem Sie "mvn clean package -Dmaven.test.skip=true" aufrufen.
Bei Wechseln zwischen den Modi oder Servern genügt es nicht, "mvn test ..." aufzurufen, sondern Sie müssen "mvn clean test ..." aufrufen.
Falls Sie folgende Exception erhalten:
java.lang.NoClassDefFoundError: org/glassfish/api/embedded/Server$Builder
Caused by: java.lang.ClassNotFoundException: org.glassfish.api.embedded.Server$Builder
Dann haben Sie wahrscheinlich eine ungeeignete GlassFish-Embedded-Version in die pom.xml eingetragen. Die Versionen 3.0.1 und 3.1-b13 funktionieren, aber Version 3.1 nicht. Siehe hierzu: http://community.jboss.org/message/582758.
Falls Sie Java 8 verwenden und folgende Exception erhalten:
org.glassfish.deployment.common.DeploymentException:
Caused by: java.lang.VerifyError: (class: org/javassist/tmp/java/security/Principal_$$_javassist_1, method: _d2implies signature: (Ljavax/security/auth/Subject;)Z) Illegal use of nonvirtual function call
Die Methode Principal.implies() aus Java 8 verträgt sich nicht mit der in diesem Beispiel verwendeten GlassFish-Version. Siehe hierzu Glassfish Throws Exception when creating a domain with Java8.
Falls Sie folgende Exception erhalten:
SCHWERWIEGEND: Exception while loading the app
org.apache.jasper.JasperException: PWC6177: XML parsing error on file .../m2/repository/org/glassfish/extras/glassfish-embedded-all/3.0.1/glassfish-embedded-all-3.0.1.jar
Caused by: java.net.ConnectException: Connection timed out: connect
Dann befinden Sie sich vermutlich hinter einem Proxy. In diesem Fall müssen Sie im Maven-Kommando die URL und den Port des Proxies angeben. Im Falle einer Cntlm-Installation entsprechend der hier genannten Anleitung zum Beispiel folgendermaßen:
mvn -Dhttp.proxyHost=127.0.0.1 -Dhttp.proxyPort=3128 clean test
Weitere Informationen zu ShrinkWrap und Arquillian:
jboss.org/wiki/ShrinkWrap, jboss.org/Arquillian, community.jboss.org/Arquillian, Arquillian Reference Guide.
Beachten Sie die aufgelisteten Neuerungen in: http://howtojboss.com/2012/05/08/jboss-trading-java-ee-5-to-java-ee-6-migration-part-iv-deployment-testing.
Michael Schütz und Alphonse Bendt beschreiben im Javamagazin 2011.1, wie Tests innerhalb eines EJB-Containers mit Arquillian durchgeführt werden können. Siehe: entwickler.de und it-republik.de.
Marcel Birkner geht im Javamagazin 2011.5 im Artikel "Next Generation Application Development" kurz auch auf Arquillian ein.
Dan Allen beschreibt in The perfect recipe for testing JPA 2, wie insbesondere JPA 2 verwendende Tests mit Arquillian erstellt werden können.
Das folgende Beispiel demonstriert:
Sie können das Programmierbeispiel als Zipdatei downloaden oder die im Folgenden beschriebenen Schritte durchführen:
Installieren Sie GlassFish, zum Beispiel wie beschrieben unter jee-sunglassfish.htm#Installation.
Richten Sie eine Datasource zu einer beliebigen Datenbank ein, zum Beispiel wie beschrieben unter jee-sunglassfish.htm#DataSource-MySQL.
Dieses Beispiel basiert der Einfachheit halber auf dem Beispiel unter jee-sunglassfish.htm#Mini-EJB3 (enthalten in MeineJee5Apps.zip).
Führen Sie diese "Minimale EJB3-Anwendung" aus.
Kopieren Sie das EJB3-Beispiel und führen Sie die im Folgenden beschriebenen Erweiterungen durch:
cd \MeinWorkspace
xcopy MeineEjb3OhneMaven JpaEjb\ /S
cd JpaEjb
md src\META-INF
tree /F
Kopieren Sie in das src\meinpkg-Verzeichnis die Entity-Klasse
MeineDaten.java, die bereits weiter oben verwendet wurde.
Ersetzen Sie die "package entities;"-Zeile durch "package meinpkg;".
Ersetzen Sie im src\meinpkg-Verzeichnis den Inhalt der EjbImpl.java durch:
package meinpkg; import java.util.List; import javax.ejb.Stateless; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; @Stateless public class EjbImpl implements EjbIntf { @PersistenceContext private EntityManager em; public String echo( String s ) { MeineDaten dat = new MeineDaten(); dat.setMeinText( s ); em.persist( dat ); s = ""; List<MeineDaten> datAll = readAllEntities( em, MeineDaten.class ); for( MeineDaten d : datAll ) { s += "<br> Id: " + d.getId() + "; LastUpdate: " + d.getLastUpdate() + "; Text: " + d.getMeinText() + "; <br>\n"; } return s; } @SuppressWarnings("unchecked") public static <T> List<T> readAllEntities( EntityManager em, Class<T> clss ) { return em.createQuery( "Select d from " + clss.getSimpleName() + " d" ).getResultList(); } }
Da Session-EJBs zeitgleich nur von einzelnen Threads verwendet werden, wird keine EntityManagerFactory mehr explizit benötigt,
sondern der EntityManager wird direkt über die @PersistenceContext-Annotation injiziert.
Bitte beachten Sie, dass dies in Session-EJBs möglich ist, aber in anderen zeitgleich von mehreren Threads verwendeten Objekten nicht erlaubt ist,
weil EntityManager (anders als EntityManagerFactory) nicht "thread safe" ist.
Außerdem braucht kein Transaktionshandling implementiert zu werden, da dies der EJB-Container erledigt ("CMT").
Erstellen Sie im src\META-INF-Verzeichnis die JPA-Konfigurationsdatei: persistence.xml
<?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="MeineJpaPU" transaction-type="JTA"> <jta-data-source>jdbc/MeinDatasourceJndiName</jta-data-source> <properties> <!-- Nur falls Tabellen automatisch angelegt werden sollen: --> <property name="toplink.ddl-generation" value="create-tables" /> </properties> </persistence-unit> </persistence>
Die persistence.xml ist im JTA-Betrieb recht kurz:
Ersetzen Sie im Projektverzeichnis den Inhalt der run.bat durch (passen Sie die Pfade an):
set APPSRV_HOME=C:\GlassFish\glassfish set APPSRV_AUTODEPLOY=%APPSRV_HOME%\domains\domain1\autodeploy set APPSRV_RT_JAR=%APPSRV_HOME%\lib\appserv-rt.jar set JAVAEE_JAR=%APPSRV_HOME%\lib\javaee.jar rd bin /S /Q md bin\META-INF copy src\META-INF\persistence.xml bin\META-INF javac -cp bin;%JAVAEE_JAR% -d bin src\meinpkg\*.java cd bin jar cvf %APPSRV_AUTODEPLOY%\simple-ejb.jar meinpkg\EjbImpl.class meinpkg\EjbIntf.class meinpkg\MeineDaten.class META-INF\persistence.xml cd .. rd web\WEB-INF\classes /S /Q xcopy bin\meinpkg\*Intf.* web\WEB-INF\classes\meinpkg\ /Y xcopy bin\meinpkg\*Servlet.* web\WEB-INF\classes\meinpkg\ /Y cd web jar cvf %APPSRV_AUTODEPLOY%\simple-web.war *.* cd .. tree /F dir %APPSRV_AUTODEPLOY% pause ... Einen Moment warten, bis Deployment fertig ist ... dir %APPSRV_AUTODEPLOY% @echo. java -cp bin;%JAVAEE_JAR%;%APPSRV_RT_JAR% meinpkg.MeinClient _Mein_Text_1_ @echo. start http://localhost:8080/simple-web/MeinEjbServlet?name=_Mein_Text_2_
Ihre Projektstruktur sieht jetzt so aus:
[\MeinWorkspace\JpaEjb] |- [src] | |- [meinpkg] | | |- EjbImpl.java | | |- EjbIntf.java | | |- MeinClient.java | | |- MeineDaten.java | | '- MeinEjbServlet.java | '- [META-INF] | '- persistence.xml |- [web] | '- [WEB-INF] | '- web.xml '- run.bat
Starten Sie den Server, führen Sie die Build-Batchdatei aus und tragen Sie statt _Mein_Text_ andere Texte ein:
GlassFish: | call C:\GlassFish\glassfish\bin\asadmin start-domain domain1 |
Build + Run: | cd \MeinWorkspace\JpaEjb run.bat |
Ext. Client: | java -cp bin;%JAVAEE_JAR%;%APPSRV_RT_JAR% meinpkg.MeinClient _Mein_Text_ |
Web-Client: | start http://localhost:8080/simple-web/MeinEjbServlet?name=_Mein_Text_ |
Die Formatierung der Ausgabe ist nicht schön, aber dafür ist sie sowohl in Textform auf der Konsole als auch in HTML auf der Webseite lesbar.
Stoppen Sie GlassFish mit: call C:\GlassFish\glassfish\bin\asadmin stop-domain domain1
Falls Sie auf Probleme stoßen, sehen Sie sich die GlassFish-Logdatei C:\GlassFish\glassfish\domains\domain1\logs\server.log an.
Falls Sie in einer einzigen EJB mehrere Persistence Contexts (für verschiedene DataSourcen) verwenden wollen, müssen Sie beachten, dass dies mit dem container-managed EntityManager nicht möglich ist. Mögliche Lösungen finden Sie unter: Mehrere verschiedene Persistence Units und EntityManager in einer EJB.