JPA (Java Persistence API)

+ andere TechDocs
+ JPA und JSF in Webanwendung
+ JPA mit GAE
+ Hibernate
+ Vererbung in SQL
+


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.



Inhalt

  1. Verschiedene JPA-Basiskonfigurationen
  2. "Best Practice"-Empfehlungen und Besonderheiten
    Container-managed, Concurrency, Servlet-Filter, Relationen, Cascade, Eager/Lazy, em.find(), JPQL, Left join fetch, Bulk Updates, Named Queries, Optimistic Locking, Geteilte Transaktion, Explicit Locking, Embedded, Vererbung, equals() und hashCode(), Composite Primary Keys, Blueprints
  3. JPA-HelloWorld-Programmierbeispiel (Java SE)
    OpenJPA, persistence.xml, MeineDaten.java, Main.java, Projektstruktur, Test, EclipseLink
  4. JPA-Servlet-Filter mit "application-managed EntityManager" und "EntityTransaction"
    pom.xml, web.xml, index.jsp, persistence.xml, MeineDaten.java, JpaServletFilter.java, MeinTestServlet.java, TestMultithreading.java, bat, Projektstruktur, Ausführung
  5. JPA-Servlet-Filter mit "container-managed EntityManager" und "JTA-UserTransaction"
    persistence.xml für JTA, JTA-Transaktionen mit Injection-Annotationen, JpaServletFilter.java, JTA-Transaktionen ohne Injection-Annotationen, JpaServletFilter.java, web.xml, Anmerkungen
  6. JSP-Webanwendung mit geteilter Transaktion und bidirektionaler OneToMany-Relation
    pom.xml, web.xml, persistence.xml, JpaServletFilter.java, JSP, AbstractDao.java, PersonTeamDao.java, Person.java, Team.java, bat, Projektstruktur, Ausführung, Datenbanktabellen
  7. JUnit-Test und App-Server beide mit JTA-UserTransaction, mit OpenEJB 4.7.5 und JPA 2.0
    pom.xml, web.xml, persistence.xml, AbstractDao.java, PersonTeamDao.java, PersonTeamDaoTest.java, JpaTestUtil.java, application-client.xml, Projektstruktur, Ausführung, Hinweise zu Datenbanken und JPA-Providern, Probleme, Fehler
  8. JUnit-Test und App-Server beide mit JTA-UserTransaction, mit TomEE-OpenEJB 7.0.4 und JPA 2.1
    pom.xml, persistence.xml
  9. JUnit-Test und App-Server beide mit JTA-UserTransaction, mit Arquillian
    pom.xml, PersonTeamDaoTest.java, arquillian.xml, resources-glassfish-embedded, resources-jbossas-remote, Projektstruktur, Testausführung, Webanwendung, Exceptions, Arquillian
  10. Java-EE-Programmierbeispiel (mit EJB 3)
    GlassFish, EjbImpl.java, persistence.xml, bat, Projektstruktur, Ausführung
  11. Links auf weiterführende Informationen



Verschiedene JPA-Basiskonfigurationen

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


"Best Practice"-Empfehlungen und zu beachtende Besonderheiten



JPA-HelloWorld-Programmierbeispiel (Java SE)

Das folgende Beispiel demonstriert:

Sie können das Programmierbeispiel als Zipdatei downloaden oder die im Folgenden beschriebenen Schritte durchführen:

  1. Installieren Sie ein aktuelles Java SE JDK.
  2. 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]
    
  3. 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.

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

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

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

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

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

  9. 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' ----
    
  10. 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.

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

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

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



JPA-Servlet-Filter mit "application-managed EntityManager" und "EntityTransaction"

Das folgende Beispiel demonstriert:

Sie können das Programmierbeispiel als Zipdatei downloaden oder die im Folgenden beschriebenen Schritte durchführen:

  1. Sie benötigen einen installierten Webserver mit Servlet-Container. Führen Sie die Tomcat-9-Installation oder WebLogic-12.2.1-Installation durch.

  2. Falls noch nicht erfolgt, führen Sie die Maven-Installation durch.

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

  4. 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>
    
  5. 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>
    
  6. Erstellen Sie im src\main\webapp-Unterverzeichnis die JSP-Datei:
    index.jsp

    <jsp:forward page="MeinTestServlet" />
    
  7. 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>
    
  8. 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;".

  9. 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();
       }
    }
    
  10. 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();
       }
    }
    
  11. 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" );
       }
    }
    
  12. 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
    
  13. 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.

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

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

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

    start http://localhost:8080/JpaServlet/

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

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

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

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

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

  22. Falls Sie die Datenbank löschen wollen, löschen Sie einfach das gesamte /MeinWorkspace/Derby-DB-Verzeichnis.



JPA-Servlet-Filter mit "container-managed EntityManager" und "JTA-UserTransaction"

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.

JTA-Transaktionen mit den Injection-Annotationen "@PersistenceContext" und "@Resource UserTransaction"

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

JTA-Transaktionen ohne Injection-Annotationen

  1. 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 */ }
    }
    
  2. 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>
    
  3. Diese Variante wird weiter unten in JSP-Webanwendung mit geteilter Transaktion und bidirektionaler OneToMany-Relation in einer vollständigen Demo gezeigt.

Anmerkungen zu beiden Varianten

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

  2. 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" );

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

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

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



JSP-Webanwendung mit geteilter Transaktion und bidirektionaler OneToMany-Relation

Das folgende Beispiel demonstriert:

Sie können das Programmierbeispiel als Zipdatei downloaden oder die im Folgenden beschriebenen Schritte durchführen:

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

  2. Im Java EE Application Server muss eine DataSource eingerichtet sein, zum Beispiel wie beschrieben für WebLogic und GlassFish.

  3. Falls noch nicht erfolgt, führen Sie die Maven-Installation durch.

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

  5. 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>
    
  6. 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>
    
  7. 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.

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

  9. 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>
    
  10. 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();
       }
    }
    
  11. 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 "";
       }
    }
    
  12. 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() );
       }
    }
    
  13. 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().

  14. 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
    
  15. 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.

  16. Bitte beachten Sie, dass für GlassFish in der persistence.xml die Zeile "<provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>" auskommentiert werden muss.

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

  18. Legen Sie auf der Webseite mehrere Personen, mehrere Teams und mehrere Zuordnungen an. Die Webseite zeigt alle Personen, Teams und Zuordnungen.

  19. Sehen Sie sich das Ergebnis in der Datenbank an (z.B. mit SQuirreL):

    Person
    IdLastUpdate    PersonName    Team_ID
    12010-05-15 23:24:31.0Anton5
    22010-05-15 23:24:44.0Berta5
    32010-05-15 23:24:12.0Caesar<null>
    Team
    IdLastUpdate     TeamName     
    42010-05-15 23:24:19.0Extreme
    52010-05-15 23:24:23.0SuperDuper

    Beachten Sie, dass nur die Person-Tabelle eine zusätzliche Spalte (Team_ID) für die Relation hat.

  20. Die Webseite sieht für dieses Beispiel so aus:

  21. Testen Sie die Reaktion auf Fehleingaben:

    In reellen Anwendungen müssten natürlich die Exceptions abgefangen und als verständliche Fehlermeldung präsentiert werden.

  22. 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;

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



JUnit-Test und App-Server beide mit JTA-UserTransaction, mit OpenEJB 4.7.5 und JPA 2.0

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.

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

  2. Löschen Sie das servletfilter-Package-Verzeichnis inklusive des JpaServletFilter:

    rd src\main\java\de\meinefirma\meinprojekt\servletfilter /S /Q

  3. 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>
    
  4. 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>
    
  5. 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.

  6. 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 );
          }
       }
    }
    
  7. 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 );
          }
       }
    }
    
  8. 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() );
       }
    }
    
  9. 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".

  10. 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/>
    
  11. 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
    

Ausführung

  1. Führen Sie die JUnit-Tests aus (mit Java 8):

    cd \MeinWorkspace\JpaJspOneToManyMitOpenEJBTest

    java -version

    mvn test

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

Hinweise zu Datenbanken und JPA-Providern

  1. 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;

  2. Zur verwendeten Test-Datenbank gibt es verschiedene Optionen (siehe auch JpaTestUtil.java):

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

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

Probleme, Fehler und Lösungen

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

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

  3. 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:

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

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

  6. 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:

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



JUnit-Test und App-Server beide mit JTA-UserTransaction, mit TomEE-OpenEJB 7.0.4 und JPA 2.1

Das folgende Beispiel demonstriert:

  1. Kopieren Sie das JpaJspOneToManyMitOpenEJBTest-JPA-2.0-Beispiel in ein neues Projekt:

    cd \MeinWorkspace

    xcopy JpaJspOneToManyMitOpenEJBTest JpaJspOneToManyMitTomEEOpenEJBTest\ /S

    cd JpaJspOneToManyMitTomEEOpenEJBTest

  2. 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>
    
  3. 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>
    
  4. Führen Sie die JUnit-Tests aus (mit Java 8):

    cd \MeinWorkspace\JpaJspOneToManyMitTomEEOpenEJBTest

    java -version

    mvn test

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



JUnit-Test und App-Server beide mit JTA-UserTransaction, mit Arquillian

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.

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

  2. 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>
    
  3. 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 ----" );
       }
    }
    
  4. 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>
    
  5. 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.

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

  7. 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
    
  8. "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

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

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

  11. WebLogic 12.2.1:

    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.

  12. Probleme:

    Bei Wechseln zwischen den Modi oder Servern genügt es nicht, "mvn test ..." aufzurufen, sondern Sie müssen "mvn clean test ..." aufrufen.

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

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

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

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



Java-EE-Programmierbeispiel (mit EJB 3)

Das folgende Beispiel demonstriert:

Sie können das Programmierbeispiel als Zipdatei downloaden oder die im Folgenden beschriebenen Schritte durchführen:

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

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

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

  4. 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;".

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

  6. 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:

  7. 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_
    
  8. 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
    
  9. 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.

  10. Stoppen Sie GlassFish mit: call C:\GlassFish\glassfish\bin\asadmin stop-domain domain1

  11. Falls Sie auf Probleme stoßen, sehen Sie sich die GlassFish-Logdatei C:\GlassFish\glassfish\domains\domain1\logs\server.log an.

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



Links auf weiterführende Informationen




Weitere Themen: andere TechDocs | JPA und JSF in Webanwendung | JPA mit GAE | Hibernate | Vererbung in SQL
© 2010 Torsten Horn, Aachen