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, Oracle TopLink Essentials, Apache OpenJPA und Hibernate, aber es gibt noch einige weitere sehr leistungsfähige Implementierungen.

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, TopLink, EclipseLink, persistence.xml, MeineDaten.java, Main.java, Projektstruktur, Test
  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. Java-EE-Programmierbeispiel (mit EJB 3)
    GlassFish, EjbImpl.java, persistence.xml, bat, Projektstruktur, Ausführung
  7. 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
  8. JUnit-Test und App-Server beide mit JTA-UserTransaction, mit OpenEJB
    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
  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. 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), darunter die Verzeichnisse bin, lib und src, und unter letzterem entities, main und META-INF:

    [\MeinWorkspace\JpaJavaSE]
     |- [bin]
     |- [lib]
     '- [src]
         |- [entities]
         |- [main]
         '- [META-INF]
    
  3. Falls Sie OpenJPA verwenden wollen:
    Downloaden Sie apache-openjpa-1.2.3-binary.zip.
    Entzippen Sie das OpenJPA-Archiv in ein temporäres Verzeichnis.
    Sehen Sie sich die Doku im apache-openjpa-1.2.3/docs-Verzeichnis und die Beispiele im apache-openjpa-1.2.3/examples-Verzeichnis an.
    Kopieren Sie openjpa-1.2.3.jar aus dem apache-openjpa-1.2.3-Verzeichnis und alle Libs aus dem apache-openjpa-1.2.3/lib-Verzeichnis in das lib-Verzeichnis des JpaJavaSE-Projekts.

  4. Falls Sie TopLink verwenden wollen:
    Sie benötigen die toplink-essentials.jar-Lib (z.B. in der Version 2.1-60). Falls Sie GlassFish installiert haben, finden Sie sie am einfachsten im lib-Verzeichnis der GlassFish-Installation. Ansonsten können Sie TopLink hier, hier oder hier downloaden. Kopieren Sie die toplink-essentials...jar-Lib (und bei Bedarf auch die toplink-essentials-agent.jar) in das lib-Verzeichnis des JpaJavaSE-Projekts. Fügen Sie die Derby-Lib hinzu, z.B. aus dem OpenJPA-Download.

  5. Falls Sie EclipseLink verwenden wollen:
    Sie benötigen die EclipseLink-Lib. Falls Sie WebLogic 12.1.3 installiert haben, können Sie eclipselink.jar aus dem \WebLogic\oracle_common\modules\oracle.toplink_12.1.3-Verzeichnis der WebLogic-Installation verwenden. Ansonsten können Sie EclipseLink hier oder hier downloaden. Kopieren Sie die EclipseLink-Lib in das lib-Verzeichnis des JpaJavaSE-Projekts. Fügen Sie die geronimo-jpa_1.0_spec-1.1.2.jar und die Derby-Lib hinzu, z.B. aus dem OpenJPA-Download.

  6. Falls Sie OpenJPA verwenden wollen:
    Erzeugen 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="RESOURCE_LOCAL">
        <provider>org.apache.openjpa.persistence.PersistenceProviderImpl</provider>
        <class>entities.MeineDaten</class>
        <properties>
          <property name="openjpa.ConnectionDriverName"     value="org.apache.derby.jdbc.EmbeddedDriver" />
          <property name="openjpa.ConnectionURL"            value="jdbc:derby:/MeinWorkspace/Derby-DB/mydb;create=true" />
          <property name="openjpa.ConnectionUserName"       value="" />
          <property name="openjpa.ConnectionPassword"       value="" />
          <property name="openjpa.jdbc.SynchronizeMappings" value="buildSchema" />
        </properties>
      </persistence-unit>
    </persistence>
    

    Sehen Sie sich die Bedeutung der OpenJPA Properties, die allgemeinen Kommentare in PersistenceUnitInfo und die Persistence-Schema-XSD-Datei an.

  7. Falls Sie TopLink verwenden wollen:
    Erzeugen 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="RESOURCE_LOCAL">
        <provider>oracle.toplink.essentials.PersistenceProvider</provider>
        <exclude-unlisted-classes>false</exclude-unlisted-classes>
        <properties>
          <property name="toplink.jdbc.driver"    value="org.apache.derby.jdbc.EmbeddedDriver" />
          <property name="toplink.jdbc.url"       value="jdbc:derby:/MeinWorkspace/Derby-DB/mydb;create=true" />
          <property name="toplink.jdbc.user"      value="" />
          <property name="toplink.jdbc.password"  value="" />
          <property name="toplink.ddl-generation" value="create-tables" />
        </properties>
      </persistence-unit>
    </persistence>
    

    Sehen Sie sich die Bedeutung der TopLink-Properties in der TopLink Reference, die allgemeinen Kommentare in PersistenceUnitInfo und die Persistence-Schema-XSD-Datei an.

  8. Falls Sie EclipseLink verwenden wollen:
    Erzeugen 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="RESOURCE_LOCAL">
        <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
        <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" />
        </properties>
      </persistence-unit>
    </persistence>
    

    Sehen Sie sich die Bedeutung der EclipseLink-Properties im EclipseLink-UserGuide, die allgemeinen Kommentare in PersistenceUnitInfo und die Persistence-Schema-XSD-Datei an.

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

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

  11. Die Projektstruktur sieht jetzt so aus:

    cd \MeinWorkspace\JpaJavaSE

    tree /F

    [\MeinWorkspace]
     |- [Derby-DB]
     |   '- ...                                           -> [entsteht erst bei Ausführung]
     '- [JpaJavaSE]
         |- [bin]
         |- [lib]
         |   |- derby-10.11.1.1.jar                       -> [falls Derby-DB]
         |   |- commons-collections-3.2.jar               \
         |   |- commons-lang-2.1.jar                       |
         |   |- commons-logging-1.0.4.jar                  |
         |   |- commons-pool-1.3.jar                       } [falls OpenJPA]
         |   |- geronimo-jpa_1.0_spec-1.1.2.jar            |
         |   |- geronimo-jta_1.1_spec-1.1.1.jar            |
         |   |- openjpa-1.2.3.jar                          |
         |   |- serp-1.13.1.jar                           /
         |   |- geronimo-jpa_1.0_spec-1.1.2.jar           -> [falls EclipseLink]
         |   |- eclipselink-2.4.2.jar                     /
         |   |- toplink-essentials-2.1-60.jar             -> [falls TopLink]
         |   '- toplink-essentials-agent.jar              /
         '- [src]
             |- [entities]
             |   '- MeineDaten.java
             |- [main]
             |   '- Main.java
             '- [META-INF]
                 '- persistence.xml
    

    Anders als hier dargestellt, dürfen Sie die Libs verschiedener JPA-Implementierungen nicht gleichzeitig im lib-Verzeichnis speichern, sondern nur die benötigten, da die Libs teilweise dieselben Klassennamen verwenden.

  12. Öffnen Sie ein Kommandozeilenfenster ('Windows-Taste' + 'R', 'cmd'), bauen Sie das Projekt und starten Sie die Main-Klasse:

    cd \MeinWorkspace\JpaJavaSE

    md bin\META-INF

    copy src\META-INF\persistence.xml bin\META-INF

    javac -cp bin;lib/* -d bin src/entities/*.java

    javac -cp bin;lib/* -d bin src/main/*.java

    java  -cp bin;lib/* main.Main

    Sie erhalten:

    ---- MeineDaten: id=1, lastUpdate='<aktuelles Datum>', meinText='Hallo Welt' ----
    

    Bitte beachten Sie, dass id und lastUpdate automatisch gesetzt werden und id bei weiteren Aufrufen hochzählt.

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

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

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

  16. 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. Eine solche Einbindung beispielsweise für OpenJPA in der Maven-pom.xml wird im nächsten Beispiel gezeigt.
    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-7- oder -6-Installation, WebLogic-12.1.3-Installation oder GlassFish-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

    mvn archetype:generate -DinteractiveMode=false -DarchetypeArtifactId=maven-archetype-webapp -DgroupId=de.meinefirma.meinprojekt -DartifactId=JpaServlet

    cd JpaServlet

    tree /F

    md src\main\resources\META-INF

    md src\main\java\de\meinefirma\meinprojekt\entities

    md src\main\java\de\meinefirma\meinprojekt\servletfilter

    md src\main\java\de\meinefirma\meinprojekt\servletimpl

    md src\main\java\de\meinefirma\meinprojekt\test

    tree /F

  4. 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/maven-v4_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>
      <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
          <plugin>
            <groupId>org.codehaus.mojo</groupId>
            <artifactId>openjpa-maven-plugin</artifactId>
            <version>1.2</version>
            <configuration>
              <includes>**/entities/*.class</includes>
            </configuration>
            <executions>
              <execution>
                <id>enhancer</id>
                <phase>process-classes</phase>
                <goals>
                  <goal>enhance</goal>
                </goals>
              </execution>
            </executions>
          </plugin>
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.2</version>
            <configuration>
              <source>1.7</source>
              <target>1.7</target>
            </configuration>
          </plugin>
        </plugins>
      </build>
      <dependencies>
        <dependency>
          <groupId>org.apache.openjpa</groupId>
          <artifactId>openjpa</artifactId>
          <version>2.3.0</version>
          <!-- Tomcat:   Ohne die folgende Zeile,
               WebLogic: Eventuell mit folgender Zeile:
          <scope>provided</scope>
          -->
        </dependency>
        <dependency>
          <groupId>org.apache.derby</groupId>
          <artifactId>derby</artifactId>
          <version>10.11.1.1</version>
        </dependency>
        <dependency>
          <groupId>javaee</groupId>
          <artifactId>javaee-api</artifactId>
          <version>5</version>
          <scope>provided</scope>
        </dependency>
      </dependencies>
    </project>
    
  5. 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>
      <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. Ersetzen Sie im src\main\webapp-Verzeichnis den Inhalt der index.jsp durch:

    <jsp:forward page="MeinTestServlet"/>
    
  7. Kopieren Sie in das src\main\resources\META-INF-Verzeichnis die OpenJPA-Konfigurationsdatei persistence.xml, wie sie im obigen Beispiel verwendet wurde.
    Ersetzen Sie die "<class>entities.MeineDaten</class>"-Zeile durch "<class>de.meinefirma.meinprojekt.entities.MeineDaten</class>".

  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<EntityManager>();
       private EntityManagerFactory emf;
    
       @Override public void init( FilterConfig filterConfig ) throws ServletException
       {
          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" );
          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.size() > 0 ) {
                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>" );
          }
          out.close();
       }
    
       @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
    
    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
    

    Oder für GlassFish (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 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
    
  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-GlassFish.bat
     |- 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. 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. Wenn Sie das Projekt in Eclipse laden wollen, führen Sie vorher aus:

    cd \MeinWorkspace\JpaServlet

    mvn eclipse:eclipse

  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 auf Probleme stoßen, sehen Sie sich die entsprechende Logdatei D:\Tools\Tomcat\logs\localhost.*.log, C:\GlassFish\glassfish\domains\domain1\logs\server.log bzw. C:\WebLogic\user_projects\domains\MeineDomain\servers\AdminServer\logs\AdminServer.log an.

  20. 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 müssen Sie die persistence.xml auf JTA-Transaktionen umstellen:

<?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="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>

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<EntityManager>();
    
       @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 {}
       @Override public void destroy() {}
    }
    

    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<EntityManager>();
    
       @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 {}
       @Override public void destroy() {}
    }
    
  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>
    

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

  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.



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.



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.1.3-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/maven-v4_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>
      <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.2</version>
            <configuration>
              <source>1.7</source>
              <target>1.7</target>
            </configuration>
          </plugin>
        </plugins>
      </build>
      <dependencies>
        <dependency>
          <groupId>javaee</groupId>
          <artifactId>javaee-api</artifactId>
          <version>5</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"?>
    <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">
        <!-- 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>
    

    Einige Java EE Application Server beinhalten mehrere JPA-Implementierungen. Zum Beispiel WebLogic 12.1.1 und 10.3.5 enthalten 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 im obigen Beispiel verwendet 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.size() > 0 ) {
             out.println( "<table border='1' cellspacing='0' cellpadding='5'>" + Person.htmlTableHeader() );
             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.size() > 0 ) {
             out.println( "<table border='1' cellspacing='0' cellpadding='5'>" + Team.htmlTableHeader() );
             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";
    
       @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";
       }
    
       public static String htmlTableHeader() {
          return "<tr><th>Id</th><th>LastUpdate</th><th>Name</th><th>Team.Name</th></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";
    
       @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<Person>();
    
       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";
       }
    
       public static String htmlTableHeader() {
          return "<tr><th>Id</th><th>LastUpdate</th><th>Name</th><th>Person.Name</th></tr>\n";
       }
    
       private static String personenToString( Set<Person> personen ) {
          StringBuffer sb = new StringBuffer();
          if( personen != null && personen.size() > 0 )
             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

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/maven-v4_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>
      <build>
        <finalName>JpaJspOneToMany</finalName>
        <plugins>
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.2</version>
            <configuration>
              <source>1.7</source>
              <target>1.7</target>
            </configuration>
          </plugin>
        </plugins>
      </build>
      <dependencies>
        <!-- Falls fuer die JUnit-Tests nicht der JPA-Provider OpenJPA verwendet werden soll,
             sondern EclipseLink vom WebLogic 12.1.3:
             a) In der persistence.xml die Auskommentierung um die Zeile
                <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
                entfernen.
             b) Folgende Dependency hinzufuegen und EclipseLink im lokalen Maven-Repository "installieren" mit:
                mvn install:install-file -DgroupId=org.eclipse.persistence -DartifactId=eclipselink
                    -Dversion=2.5.2.v20140319 -Dpackaging=jar -Dfile=C:/WebLogic/oracle_common/modules/oracle.toplink_12.1.3/eclipselink.jar
                (eine alternative Maven-Einbindung ist beschrieben unter: http://wiki.eclipse.org/EclipseLink/Maven).
        <dependency>
          <groupId>org.eclipse.persistence</groupId>
          <artifactId>eclipselink</artifactId>
          <version>2.5.2.v20140319</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.11.1.1</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>ojdbc7</artifactId>
          <version>11.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>3.1.4</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 nicht javaee:javaee-api-5.jar, sondern org.apache.openejb:javaee-api-5.0-3.jar verwendet.
             Falls doch unbedingt javaee:javaee-api-5.jar 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 Zeile
             <classpathentry kind="var" path="M2_REPO/javaee/javaee-api/5/javaee-api-5.jar"/>
             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>5.0-3</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"?>
    <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">
        <!-- 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>
    

    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 ) {/* nothing todo */}
             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 ) {/* nothing todo */}
             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 ) {/* nothing todo */}
             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:

    cd \MeinWorkspace\JpaJspOneToManyMitOpenEJBTest

    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 MySQL-JDBC-Treiber mysql-connector-java-5.1.31-bin.jar so:

    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 Oracle-JDBC-Treiber ojdbc7-12.1.jar (z.B. Version 12.1.0.2 beim WebLogic 12.2.1 in /WebLogic/oracle_common/modules/oracle.jdbc sowie beim WebLogic 12.1.3 in /WebLogic/oracle_common/modules/oracle.jdbc_12.1.0):

    mvn install:install-file -DgroupId=com.oracle.jdbc -DartifactId=ojdbc7 -Dversion=12.1 -Dpackaging=jar -Dfile=ojdbc7.jar

    Oder für den JDBC-Treiber für DB2/400 auf AS/400 (iSeries), jt400-7.1.0.10.jar (aus JTOpen):

    mvn install:install-file -DgroupId=net.sf.jt400 -DartifactId=jt400 -Dversion=7.1.0.10 -Dpackaging=jar -Dfile=jt400.jar

    Oder für den JPA-Provider EclipseLink 2.6.0 von http://www.eclipse.org/eclipselink/downloads:

    mvn install:install-file -DgroupId=org.eclipse.persistence -DartifactId=eclipselink -Dversion=2.6.0 -Dpackaging=jar -Dfile=eclipselink-2.6.0.jar

    Oder für den JPA-Provider EclipseLink 2.5.2.v20140319 aus WebLogic 12.1.3 (siehe auch pom.xml):

    mvn install:install-file -DgroupId=org.eclipse.persistence -DartifactId=eclipselink -Dversion=2.5.2.v20140319 -Dpackaging=jar -Dfile=/WebLogic/oracle_common/modules/oracle.toplink_12.1.3/eclipselink.jar

    Oder für den JPA-Provider EclipseLink 2.4.2.v20130514 aus WebLogic 12.1.2:

    mvn install:install-file -DgroupId=org.eclipse.persistence -DartifactId=eclipselink -Dversion=2.4.2.v20130514 -Dpackaging=jar -Dfile=/WebLogic/oracle_common/modules/oracle.toplink_12.1.2/eclipselink.jar

    Falls Sie dem Dateinamen nicht die genaue Versionsnummer entnehmen können (wie z.B. bei ojdbc7.jar und jt400.jar), dann lohnt sich oft ein Blick in die meistens vorhandene Manifest-Datei, beispielsweise für ojdbc7.jar so:

    jar xf ojdbc7.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-5.0-3.jar die javaee:javaee-api-5.jar in der pom.xml eingetragen. Falls Sie nicht die org.apache.openejb:javaee-api-5.0-3.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:



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/maven-v4_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.1.1 und 10.3.5:

    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.



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