Beispiel: Einfache Container Managed Entity Bean


Inhalt:

Der Weg des Assistenten: Project Facet "Java Persistence API"
Der Weg für Harte: Anlegen der Entity Bean ohne JPA
Der Weg für Harte: persistence.xml
Der Weg des Assistenten: Anlegen der Entity Bean mit JPA
Anlegen der Session Bean
Application Client
Datenbank
Ausführen des Clients
Logging der SQL-Parameter
Ohne Annotations
Manueller Primary Key
Troubleshooting

Beispiel für eine Container Managed Entity Bean, auf die per Applicationclient zugegriffen wird.
Hier gibt es das Projekt zum Download (dies ist ein EAR-Export, die Importanleitung findet man im Stateless-Beispiel): KuchenSimple.ear

Falls mit der Project Facet "Java Persistence API" gearbeitet werden soll, muss diese nach jedem Import dem Projekt neu zugefügt werden (identische Schritte wie beim Projekt-Erstellen)

Aufbau des Beispieles


a) Entity Bean-Klasse
b) Zugriff auf die Entity-Bean erfolgt über eine Stateless Session Bean.
c) Ein Application Client greift auf die Session Bean zu.


Das Beispiel besteht aus einem "EAR Application Project" mit dem Namen "KuchenSimple" sowie einem EJB-Projekt und einem Anwendungsclientprojekt.


Der Weg des Assistenten: Project Facet "Java Persistence API"

Der folgende Abschnitt ist optional, man kann seine Entity Beans sowie den nötigen Deployment Deskriptor "persistence.xml" auch händisch erzeugen, und der Aufwand dürfte sogar geringer sein als beim Klicken über Assistenten ;-).

Schritt 1: Rechtsklick auf das EJB-Projekt, in den "Properties" den Punkt "Projekt Facets" auswählen.
Den Haken bei "Java Persistence" setzen, die Version bleibt auf "1.0".
Java Persistence 1.0 facet (1)
Anschließend auf "Further Configuration available" klicken.
Hier bleiben fast alle Einstellungen auf ihren Default-Werten (da wir keinen Zugriff auf eine vorhandene Datenbank brauchen, und uns darauf verlassen, dass der JBoss eine JPA-Implementation mitbringt). Wichtig ist nur, dass der Haken "Create orm.xml" entfernt wird. Diese Datei wird erst im Beispiel "Ohne Annotations" wichtig sein.
Java Persistence 1.0 facet (2)
Jetzt wird im Projekt eine Datei "META-INF\persistence.xml" erzeugt, und es taucht im "Project Explorer" ein Punkt "JPA Content" auf.
Java Persistence 1.0 facet (3)

Die Datei "persistence.xml" öffnen wir (per Doppelklick, oder Rechtsklick => "Open With" => "Persistence XML Editor"), und ändern auf dem Karteireiter "General" den Name auf "kuchenPersistenceUnit". Der einzige Grund hierfür ist allerdings, dass ich dies im Vorjahresbeispiel genauso gehalten hatte ;-).
Persistence Unit Name

Auf dem Karteireiter "Connection" ändern wir den "Transaction Type" auf "JTA" und können danach beim "JTA Data Source Name" den Wert java:/DefaultDS eintragen (Erklärung im Abschnitt
Der Weg für Harte: persistence.xml).
JTA Data Source Name

Auf dem Karteireiter "Properties" werden die Properties "hibernate.hbm2ddl.auto" mit dem Wert "create-drop" und "hibernate.show_sql" mit dem Wert "true" angelegt (Erklärung im Abschnitt Der Weg für Harte: persistence.xml).
Properties
Auf dem Karteireiter "Source" können wir uns das Ergebnis anschauen:
<?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="kuchenPersistenceUnit" transaction-type="JTA">
		<jta-data-source>java:/DefaultDS</jta-data-source>
		<properties>
			<property name="hibernate.hbm2ddl.auto" value="create-drop" />
			<property name="hibernate.show_sql" value="true"></property>
		</properties>
	</persistence-unit>
</persistence>
Anmerkung:
In meinen händisch erzeugten "persistence.xml" fehlt das Attribut "transaction-type". Zitat hierzu aus der Hibernate-Doku:

"transaction-type (attribute): Transaction type used. Either JTA or RESOURCE_LOCAL (default to JTA in a JavaEE environment and to RESOURCE_LOCAL in a JavaSE environment). When a jta-datasource is used, the default is JTA, if non-jta-datasource is used, RESOURCE_LOCAL is used."

Wir können hier also gemäß Doku den Defaultwert belassen, nur Eclipse scheint das anders zu sehen.

Das Hinzufügen der "Java Persistence"-Facet führt zu einer Warnung "No connection specified for project. No database-specific validation will be performed.".
Der Warnung zu folgen und eine Connection anzulegen, ist nicht sinnvoll, denn wir definieren durch unsere EJBs schließlich die Struktur der Datenbank, nicht umgekehrt. Ein Vergleich der Entity-Beans und ihrer Felder mit einer Datenbankstruktur wäre also nicht sinnvoll.
Außerdem hindert uns die Hypersonic-Datenbank des JBoss effektiv daran, sie zu konfigurieren, da bei laufendem JBoss kein Zugriff eines zweiten Clients (also z.B. Eclipse) auf die Datenbank möglich ist.
Deshalb empfehle ich, die JPA-Validierung abzuschalten. Dies geht in den Preferences in der Rubrik "Validation": die Haken in der Zeile "JPA Validator" entfernen.
JPA Validation


Der Weg für Harte: Anlegen der Entity Bean ohne JPA

Wir fügen eine Klasse "KuchenSimpleBean" zu. Der Namenszusatz "Simple" kommt daher dass es noch weitere Beispiele kommen in denen Kuchen-Entity-Beans enthalten sind. Deshalb muss in jedem Beispiel ein eindeutiger Name für das Objekt "Kuchen" vergeben sein damit es keine Konflikte beim JNDI-Namen oder bei Tabellennamen gibt.


Es wird eine neue Klasse "KuchenSimpleBean" im Package "de.fhw.komponentenarchitekturen.knauf.kuchen" angelegt. Wichtig ist dass diese Klasse das Interface "java.io.Serializable" implementiert !
Neue Entity Bean (1)

Die Bean-Klasse bekommt die Annotation "@javax.persistence.Entity". Da wir die EJB-QL-Strings zum Finden der Instanzen nicht hartcodiert in der Session-Bean haben wollen deklarieren wir bei der Bean-Klasse eine "@javax.persistence.NamedQuery".
@NamedQuery (name="findAllKuchen", query="select o from KuchenSimpleBean o")
@Entity()
public class KuchenSimple implements Serializable
{
Anmerkung 1:
Wenn mehr als eine Query nötig sind muss man diese in eine Annotation "@NamedQueries" packen:
@NamedQueries({
  @NamedQuery (name="findAllKuchen", query="select o from KuchenSimpleBean o"),
  @NamedQuery (name="findByName", query="select o from KuchenSimpleBean o where o.name like ?1")
  })
Anmerkung 2:
In EJB3 RC8 hätte die Query noch kürzer dargestellt werden können (so steht es auch im EJB3-Buch von Monson-Haefel/Bill Burke):
@NamedQuery (name="findAllKuchen", query="from KuchenSimpleBean") 
In EJB3 RC9 (ist in JBoss 4.2 enthalten) führt dies allerdings zu einer Fehlermeldung und ist wohl FALSCH.

Zwei Felder sowie die zugehörigen Getter und Setter werden zugefügt:
  private Integer intId;
  private String strName;
  
  @Column()
  @Id ()
  @GeneratedValue () 
  public Integer getId()
  {
    return this.intId;
  }

  public void setId(Integer int_Id)
  {
    this.intId = int_Id;
  }
  
  @Column()
  public String getName()
  {
    return this.strName;
  }

  public void setName(String str_Name)
  {
    this.strName = str_Name;
  }
  
  @Override
  public String toString()
  {
    return this.strName;
  }
  
Die Properties werden mit der Annotation @javax.persistence.Column als persistente Bean-Felder markiert. Die Property "ID" wird mit der Annotation @javax.persistence.Id als Primary-Key-Feld markiert. Der Wert soll vom Container automatisch generiert werden (@javax.persistence.GeneratedValue). Das Verfahren der Generierung bleibt dem Server überlassen, beim JBoss und der Hypersonic-Datenbank wird daraus eine Auto-ID-Spalte (sprich: beim Insert wird die ID erzeugt).

Die Methode toString wird überladen und gibt den Namen des Kuchens zurück. Das hat den Vorteil, dass wir die Kuchen im Client als Objekt in eine Listbox packen und auch wieder auslesen können.


Der Weg für Harte: persistence.xml

Für Entity Beans muss eine Persistence Unit deklariert werden. Dies geschieht über eine Datei "persistence.xml" im Unterverzeichnis "META-INF" des EJB-Projekts.
Sie hat diesen Inhalt:
	<?xml version="1.0" encoding="UTF-8"?>
	<persistence 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"
		version="1.0">
		<persistence-unit name="kuchenPersistenceUnit">
			<jta-data-source>java:/DefaultDS</jta-data-source>
			<properties>
				<!-- Setzen dieser Property aktiviert das automatische Tabellen-Generieren und Löschen beim Deploy! -->
				<property name="hibernate.hbm2ddl.auto" value="create-drop" />
				<!-- SQL-Logging einschalten: -->
				<property name="hibernate.show_sql" value="true"></property>
			</properties>
		</persistence-unit>
	</persistence> 
Es wird eine "persistence-unit" namens "kuchenPersistenceUnit" deklariert. Sie ist verbunden mit einer JDBC-Datenquelle des Servers die im JNDI unter dem Namen "java:/DefaultDS" abgelegt ist und auf die JBoss-interne Hypersonic-Datenbank zeigt.
DefaultDS

Die Property "hibernate.hbm2ddl.auto" ist JBoss-spezifisch und legt fest dass Datenbanktabelle beim Deploy einer Bean erzeugt und beim Undeploy wieder gelöscht werden sollen. Ohne diesen Parameter müssten wir die Datenbanktabellen von Hand anlegen.
Die Property "hibernate.show_sql" gibt an dass SQL-Befehle ins Server-Log geschrieben werden sollen, und als netter Nebeneffekt auch auf die Server-Console in Eclipse. Damit haben wir eine gute Diagnosemöglichkeit falls Datenbankzugriffe Probleme machen.


Der Weg des Assistenten: Anlegen der Entity Bean mit JPA

Im "Project Explorer" das EJB-Projekt wählen, Rechtsklick, "New" => "Other" wählen. In der Rubrik "JPA" den Punkt "Entity" wählen.
JPA Entity (1)
Name ("KuchenSimpleBean") und Package ("de.fhw.komponentenarchitekturen.knauf.kuchen") werden angegeben.
JPA Entity (2)
Im nächsten Dialog werden zwei Felder "id" (vom Typ java.lang.Integer, nicht in der Auswahlliste) und "name" (vom Typ "string") ) angelegt.
Am Ende sollte der Dialog so aussehen:
-Das Feld "id" ist als Primary Key markiert.
-Der "Access Type" wird von "Field-based" (Annotations werden an die Feldvariablen gesetzt) auf "Property-based" geändert (Zugriff und Annotations nur über getter/setter). Entity Properties
Hier der Dialog für das Hinzufügen des Felds "id":
Felder
Die so erzeugte Entity taucht jetzt auch im "Project Explorer" auf:
Project Explorer

Nachbearbeitung: Es muss eine Named Query zugefügt werden:
@NamedQuery (name="findAllKuchen", query="select o from KuchenSimpleBean o")
@Entity()
public class KuchenSimple implements Serializable
{
Die Property "id" muss außerdem mit der Annotation javax.persistence.GeneratedValue versehen werden:
	@Id    
	@GeneratedValue()
	public Integer getId()
	{
		return this.id;
	}
Anschließend wird die toString überladen (siehe Abschnitt
Der Weg für Harte: Anlegen der Entity Bean ohne JPA).

Anlegen der Session Bean

Da der Entity-Manager für den Zugriff auf die Entity-Bean nicht in einem Application Client verwendet werden kann, müssen wir alle Zugriffe auf die Bean kapseln. Dazu verwenden wir eine Session Bean "KuchenWorkerBean" mit einem Remote Interface "KuchenWorkerRemote".
KuchenWorkerBean

Der Entity-Manager für den Zugriff auf die Entity Bean wird als vom Container "injected" Variable deklariert:
  @PersistenceContext(unitName="kuchenPersistenceUnit")
  private EntityManager entityManager = null;
Die Implementierung von "saveKuchen" sieht so aus:
  public void saveKuchen (KuchenSimpleBean kuchen)
  {
    this.entityManager.merge(kuchen);
  }
Wichtig ist dass hier "merge" und nicht "persist" genommen wird, siehe Abschnitt
Troubleshooting.

"deleteKuchen":
  public void deleteKuchen(KuchenSimpleBean kuchen)
  {
    kuchen = this.entityManager.find (KuchenSimpleBean.class, kuchen.getId() );
    this.entityManager.remove(kuchen);
  } 
Wichtig ist dass das zu löschende Objekt eventuell "detached" ist und deshalb vorher unter Container-Verwaltung gestellt werden muss, siehe Abschnitt Troubleshooting.

"getKuchen": Diese Methode verwendet die NamedQuery die wir in der Entity-Bean deklariert haben:
  public List<KuchenSimpleBean> getKuchen()
  {
    Query query = this.entityManager.createNamedQuery("findAllKuchen");
    List<KuchenSimpleBean> listKuchen = query.getResultList();
    
    return listKuchen;
  } 
Anmerkung: wir hätten die Query hier auch direkt erzeugen können.

Query query = this.entityManager.createQuery("select o from KuchenSimpleBean o");

Dadurch hätten wir allerdings eine Abhängigkeit vom Namen "KuchenSimpleBean" gebaut die weit entfernt von der eigentlichen Klasse ist!

Anmerkung: die Zuweisung der ResultList an "List<KuchenSimpleBean>" führt zu einer Compilerwarnung (Type safety: The expression of type List needs unchecked conversion to conform to List<KuchenSimpleBean>). Dies könnten wir umgehen indem wir vor die entsprechende Zeile folgende Annotation eintragen:
    @SuppressWarnings("unchecked")
    List<KuchenSimpleBean> listKuchen = query.getResultList(); 


Application Client

Zuallerest einmal werfen die die Klasse "Main" im Default-Package weg.

Der Client muss die EJB-Jars referenzieren (siehe dazu Anleitung in den vorherigen Beispielen).

Für die GUI empfehle ich den Eclipse-Plugin "Jigloo" erzeugt. Da er nicht Open Source ist, ist er nicht in meinem Eclipse-Paket enthalten. Man findet ihn auf
http://www.cloudgarden.com/jigloo/ (für nicht-kommerzielle Verwendung frei).
Die Installation ist einfach: Jigloo ins Eclipse-Verzeichnis entpacken. Danach die Dialoge mit dem Designer öffenen per Rechtsklick => "Open With" => "Form Editor".

Folgende GUI-Elemente sind vorhanden:
-de.fhw.komponentenarchitekturen.knauf.kuchen.FrameKuchen ist das Hauptfenster der Anwendung. Er besteht aus einer JList auf einem JScrollPane und drei Buttons "Neu", "Bearbeiten" und "Löschen".
-Ein JDialog KuchenDialog zum Bearbeiten eines Kuchens (Textfeld für den Namen und OK/Abbrechen-Buttons)

Die Main-Methode soll in einer Klasse FrameKuchen liegen, die gleichzeitig das Hauptfenster der Anwendung ist.

Die SessionBean wird per Injecton in den FrameKuchen gepackt:
public class FrameKuchen extends JFrame
{
  @EJB
  private static KuchenWorkerRemote kuchenWorker;
Trotzdem sind zwei Deployment-Deskriptoren nötig:
Die Datei "application-client.xml" sieht so aus:
<?xml version="1.0" encoding="UTF-8"?>
<application-client version="5"
	xmlns="http://java.sun.com/xml/ns/javaee"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/application-client_5.xsd">
	<display-name>KuchenSimpleClient</display-name>
</application-client>
Außerdem benötigen wir den JBoss-spezifischen Deployment-Deskriptor "jboss-client.xml":
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE jboss-client PUBLIC
		"-//JBoss//DTD Application Client 5.0//EN"
		"http://www.jboss.org/j2ee/dtd/jboss-client_5_0.dtd">

<jboss-client>
	<jndi-name>KuchenSimpleClient</jndi-name>
</jboss-client>

Jetzt noch die Main class in "MANIFEST.MF" eintragen:
	Manifest-Version: 1.0
	Class-Path: KuchenSimpleEJB.jar
	Main-Class: de.fhw.komponentenarchitekturen.knauf.kuchen.FrameKuchen


Jetzt können wir die Bean auf den Server stellen. Dieser Prozess erzeugt die Datenbanktabelle. Im Server-Log finden sich unter anderem diese Einträge, an der wir erkennen können wie die Tabelle erzeugt wird und mit welchem Statements ein Einfügen oder Löschen erfolgt (diese werden scheinbar beim Deploy vorbereitet):
2008-11-05 20:15:12,578 DEBUG [org.hibernate.persister.entity.AbstractEntityPersister] (RMI TCP Connection(23)-127.0.0.1) Static SQL for entity: de.fhw.komponentenarchitekturen.knauf.kuchen.KuchenSimpleBean
2008-11-05 20:15:12,578 DEBUG [org.hibernate.persister.entity.AbstractEntityPersister] (RMI TCP Connection(23)-127.0.0.1)  Version select: select id from KuchenSimpleBean where id =?
2008-11-05 20:15:12,578 DEBUG [org.hibernate.persister.entity.AbstractEntityPersister] (RMI TCP Connection(23)-127.0.0.1)  Snapshot select: select kuchensimp_.id, kuchensimp_.name as name10_ from KuchenSimpleBean kuchensimp_ where kuchensimp_.id=?
2008-11-05 20:15:12,578 DEBUG [org.hibernate.persister.entity.AbstractEntityPersister] (RMI TCP Connection(23)-127.0.0.1)  Insert 0: insert into KuchenSimpleBean (name, id) values (?, ?)
2008-11-05 20:15:12,578 DEBUG [org.hibernate.persister.entity.AbstractEntityPersister] (RMI TCP Connection(23)-127.0.0.1)  Update 0: update KuchenSimpleBean set name=? where id=?
2008-11-05 20:15:12,578 DEBUG [org.hibernate.persister.entity.AbstractEntityPersister] (RMI TCP Connection(23)-127.0.0.1)  Delete 0: delete from KuchenSimpleBean where id=?
2008-11-05 20:15:12,578 DEBUG [org.hibernate.persister.entity.AbstractEntityPersister] (RMI TCP Connection(23)-127.0.0.1)  Identity insert: insert into KuchenSimpleBean (id, name) values (null, ?)
2008-11-05 20:15:12,578 DEBUG [org.hibernate.loader.entity.EntityLoader] (RMI TCP Connection(23)-127.0.0.1) Static select for entity de.fhw.komponentenarchitekturen.knauf.kuchen.KuchenSimpleBean: select kuchensimp0_.id as id10_0_, kuchensimp0_.name as name10_0_ from KuchenSimpleBean kuchensimp0_ where kuchensimp0_.id=?
2008-11-05 20:15:12,578 DEBUG [org.hibernate.loader.entity.EntityLoader] (RMI TCP Connection(23)-127.0.0.1) Static select for entity de.fhw.komponentenarchitekturen.knauf.kuchen.KuchenSimpleBean: select kuchensimp0_.id as id10_0_, kuchensimp0_.name as name10_0_ from KuchenSimpleBean kuchensimp0_ where kuchensimp0_.id=?
2008-11-05 20:15:12,578 DEBUG [org.hibernate.loader.entity.EntityLoader] (RMI TCP Connection(23)-127.0.0.1) Static select for entity de.fhw.komponentenarchitekturen.knauf.kuchen.KuchenSimpleBean: select kuchensimp0_.id as id10_0_, kuchensimp0_.name as name10_0_ from KuchenSimpleBean kuchensimp0_ where kuchensimp0_.id=?
2008-11-05 20:15:12,578 DEBUG [org.hibernate.loader.entity.EntityLoader] (RMI TCP Connection(23)-127.0.0.1) Static select for entity de.fhw.komponentenarchitekturen.knauf.kuchen.KuchenSimpleBean: select kuchensimp0_.id as id10_0_, kuchensimp0_.name as name10_0_ from KuchenSimpleBean kuchensimp0_ where kuchensimp0_.id=?
2008-11-05 20:15:12,578 DEBUG [org.hibernate.loader.entity.EntityLoader] (RMI TCP Connection(23)-127.0.0.1) Static select for entity de.fhw.komponentenarchitekturen.knauf.kuchen.KuchenSimpleBean: select kuchensimp0_.id as id10_0_, kuchensimp0_.name as name10_0_ from KuchenSimpleBean kuchensimp0_ where kuchensimp0_.id=?
2008-11-05 20:15:12,578 DEBUG [org.hibernate.loader.entity.EntityLoader] (RMI TCP Connection(23)-127.0.0.1) Static select for action ACTION_MERGE on entity de.fhw.komponentenarchitekturen.knauf.kuchen.KuchenSimpleBean: select kuchensimp0_.id as id10_0_, kuchensimp0_.name as name10_0_ from KuchenSimpleBean kuchensimp0_ where kuchensimp0_.id=?
2008-11-05 20:15:12,578 DEBUG [org.hibernate.loader.entity.EntityLoader] (RMI TCP Connection(23)-127.0.0.1) Static select for action ACTION_REFRESH on entity de.fhw.komponentenarchitekturen.knauf.kuchen.KuchenSimpleBean: select kuchensimp0_.id as id10_0_, kuchensimp0_.name as name10_0_ from KuchenSimpleBean kuchensimp0_ where kuchensimp0_.id=?
...
2008-11-05 20:15:12,593 INFO  [org.hibernate.tool.hbm2ddl.SchemaExport] (RMI TCP Connection(23)-127.0.0.1) Running hbm2ddl schema export
2008-11-05 20:15:12,593 DEBUG [org.hibernate.tool.hbm2ddl.SchemaExport] (RMI TCP Connection(23)-127.0.0.1) import file not found: /import.sql
2008-11-05 20:15:12,593 INFO  [org.hibernate.tool.hbm2ddl.SchemaExport] (RMI TCP Connection(23)-127.0.0.1) exporting generated schema to database
2008-11-05 20:15:12,593 DEBUG [org.hibernate.tool.hbm2ddl.SchemaExport] (RMI TCP Connection(23)-127.0.0.1) drop table KuchenSimpleBean if exists
2008-11-05 20:15:12,593 DEBUG [org.hibernate.tool.hbm2ddl.SchemaExport] (RMI TCP Connection(23)-127.0.0.1) create table KuchenSimpleBean (id integer generated by default as identity (start with 1), name varchar(255), primary key (id))
2008-11-05 20:15:12,593 INFO  [org.hibernate.tool.hbm2ddl.SchemaExport] (RMI TCP Connection(23)-127.0.0.1) schema export complete


Datenbank-Adminstrations-Tool

Die Hypersonic-Datenbank bringt ein rudimentäres Administrationstool mit. Dieses erreichen wir, indem wir bei laufendem Server auf die JMX-Konsole gehen (http://localhost:8080/jmx-console) und in der Kategorie "jboss" die MBean "database=localDB,service=Hypersonic" auswählen.
Hypersonic Database Manager (1)
Ab zur Funktion "startDatabaseManager" und auf "Invoke" klicken.
Hypersonic Database Manager (2)
Es erscheint nur eine Seite mit einer schnöden Erfolgsmeldung, aber nach kurzer Zeit sollte sich eine Java-Anwendung, der "HSQL Database Manager" öffnen. Hier sehen wir auf der linken Seite die Tabelle unserer Bean. Im Fenster rechts oben können wir SQL-Statements abfeuern, im Beispiel ist das ein Select auf die "KuchenSimpleBean"-Tabelle
Hypersonic Database Manager (3)


Ausführen des Clients

Im Menü "Run" den Punkt "Open Run Dialog..." wählen. In der Auswahl "Configurations" unter "Java Application" eine neue Konfiguration erstellen. Die Einstellungen sollten so aussehen:
MainClass:
org.jboss.client.AppClientMain
Anwendungsclient ausführen (1)

Program Arguments:
-jbossclient de.fhw.komponentenarchitekturen.knauf.kuchen.FrameKuchen -launchers org.jboss.ejb3.client.ClientLauncher -j2ee.clientName KuchenSimpleClient


VM Arguments:
-Djava.naming.factory.initial=org.jnp.interfaces.NamingContextFactory -Djava.naming.provider.url=jnp://localhost:1099


Anwendungsclient ausführen (2)


Logging der SQL-Parameter

Leider aktiviert die Property "hibernate.show_sql" in persistence.xml nur das Logging der SQL-Statements. Die Parameter der dabei benutzten Prepared Statements werden leider nicht ausgegeben. Wir erhalten nur solche Ausgaben:
insert into KuchenSimpleBean (id, name) values (null, ?)

Es gibt allerdings eine Lösung: mittels Log4J können wir die Ausgabe der Parameter konfigurieren.
Dies geht so: (Quelle:
http://www.javalobby.org/java/forums/t44119.html und http://www.hibernate.org/hib_docs/reference/en/html/configuration-logging.html).

In der Datei "\server\default\conf\jboss-log4j.xml" wird folgendes eingetragen (ACHTUNG: Datei als UTF-8 speichern !):
Das Ergebnis eines Inserts sieht jetzt so aus:
21:37:51,593 INFO  [STDOUT] Hibernate: insert into KuchenBean (id, name) values (null, ?)
21:37:51,593 TRACE [StringType] binding 'Mohnkuchen' to parameter: 1

Nachteil: jetzt werden auch bei einem Select alle zurückgelieferten Spalten ausgegeben:
21:37:51,593 INFO  [STDOUT] Hibernate: select kuchenbean0_.id as id0_, kuchenbean0_.name as name0_ from KuchenBean kuchenbean0_
21:37:51,593 TRACE [IntegerType] returning '2' as column: id0_
21:37:51,593 TRACE [StringType] returning 'Käsekuchen' as column: name0_
21:37:51,593 TRACE [IntegerType] returning '3' as column: id0_
21:37:51,593 TRACE [StringType] returning 'Mohnkuchen' as column: name0_

Hier gibt es eine modifizierte jboss-log4j.xml zum Download.


Ohne Annotations

Die Deklaration ohne Annotations erfordert eine ganze Reihe Arbeit.

ejb-jar.xml
<?xml version="1.0" encoding="UTF-8"?>
<ejb-jar version="3.0" xmlns="http://java.sun.com/xml/ns/javaee"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
                           http://java.sun.com/xml/ns/javaee/ejb-jar_3_0.xsd">
	<display-name>KuchenSimpleEJB</display-name>
	<enterprise-beans>
		<session>
			<description>
				<![CDATA[Stateless Session Bean für den Zugriff auf die Entitiy Bean
 * "Kuchen". Enthält Methoden zum Speichern und Löschen eines einzelnen
 * Kuchens sowie zum Holen einer Liste aller Kuchen.]]>
			</description>
			<display-name>KuchenWorkerBean</display-name>
			<ejb-name>KuchenWorkerBean</ejb-name>
			<remote>de.fhw.swtvertiefung.knauf.kuchen.KuchenWorker</remote>
			<ejb-class>de.fhw.swtvertiefung.knauf.kuchen.KuchenWorkerBean</ejb-class>
			<session-type>Stateless</session-type>
			<!--EntityManager-Injection -->
			<persistence-context-ref>
				<persistence-context-ref-name>KuchenPersistenceUnitRef</persistence-context-ref-name>
				<persistence-unit-name>kuchenPersistenceUnit</persistence-unit-name>
				<injection-target>
					<injection-target-class>
						de.fhw.swtvertiefung.knauf.kuchen.KuchenWorkerBean
					</injection-target-class>
					<injection-target-name>entityManager</injection-target-name>
				</injection-target>
			</persistence-context-ref>
		</session>
		<entity>
			<description>
				<![CDATA[Entity Bean für einen einzelnen Kuchen.]]>
			</description>
			<display-name>KuchenSimpleBean</display-name>
			<ejb-name>KuchenSimpleBean</ejb-name>
			<ejb-class>de.fhw.swtvertiefung.knauf.kuchen.KuchenSimpleBean</ejb-class>
			<persistence-type>Container</persistence-type>
			<prim-key-class>java.lang.Integer</prim-key-class>
			<reentrant>false</reentrant>
		</entity>
	</enterprise-beans>
</ejb-jar> 
Neu in diesem Beispiel:
Anmerkung:
In "ejb-jar.xml" dürfen KEINE EJB3.0-Entity-Beans (durch das Element "entity") eingetragen sein, wie ich das noch im Vorjahresbeispiel für JBoss 4.2 gemacht hatte! Dies führt zu folgender merkwürdiger Fehlermeldung:

21:54:06,312 ERROR [AbstractKernelController] Error installing to Real: name=vfszip:/C:/temp/jboss-5.0.0.CR2/server/default/deploy/KuchenSimpleNoAnnotation.ear state=PreReal mode=Manual requiredState=Real
org.jboss.deployers.spi.DeploymentException: java.lang.NullPointerException: name cannot be null
	at org.jboss.wsf.container.jboss50.deployer.WebServiceDeployerEJB.internalDeploy(WebServiceDeployerEJB.java:103)
	at org.jboss.deployers.spi.deployer.helpers.AbstractRealDeployer.deploy(AbstractRealDeployer.java:50)
	at org.jboss.deployers.plugins.deployers.DeployerWrapper.deploy(DeployerWrapper.java:169)
	at org.jboss.deployers.plugins.deployers.DeployersImpl.doDeploy(DeployersImpl.java:1285)
	at org.jboss.deployers.plugins.deployers.DeployersImpl.doInstallParentFirst(DeployersImpl.java:1003)
	at org.jboss.deployers.plugins.deployers.DeployersImpl.doInstallParentFirst(DeployersImpl.java:1056)
	at org.jboss.deployers.plugins.deployers.DeployersImpl.install(DeployersImpl.java:944)
	at org.jboss.dependency.plugins.AbstractControllerContext.install(AbstractControllerContext.java:348)
	at org.jboss.dependency.plugins.AbstractController.install(AbstractController.java:1598)
	at org.jboss.dependency.plugins.AbstractController.incrementState(AbstractController.java:934)
	at org.jboss.dependency.plugins.AbstractController.resolveContexts(AbstractController.java:1062)
	at org.jboss.dependency.plugins.AbstractController.resolveContexts(AbstractController.java:984)
	at org.jboss.dependency.plugins.AbstractController.change(AbstractController.java:822)
	at org.jboss.dependency.plugins.AbstractController.change(AbstractController.java:553)
	at org.jboss.deployers.plugins.deployers.DeployersImpl.process(DeployersImpl.java:627)
	at org.jboss.deployers.plugins.main.MainDeployerImpl.process(MainDeployerImpl.java:541)
	at org.jboss.system.server.profileservice.hotdeploy.HDScanner.scan(HDScanner.java:290)
	at org.jboss.system.server.profileservice.hotdeploy.HDScanner.run(HDScanner.java:221)
	at java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)
	at java.util.concurrent.FutureTask$Sync.innerRunAndReset(Unknown Source)
	at java.util.concurrent.FutureTask.runAndReset(Unknown Source)
	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$101(Unknown Source)
	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.runPeriodic(Unknown Source)
	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(Unknown Source)
	at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(Unknown Source)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
	at java.lang.Thread.run(Unknown Source)
Caused by: java.lang.NullPointerException: name cannot be null
	at javax.management.ObjectName.construct(Unknown Source)
	at javax.management.ObjectName.(Unknown Source)
	at org.jboss.wsf.container.jboss50.deployer.WebServiceDeployerEJB.internalDeploy(WebServiceDeployerEJB.java:99)
	... 26 more

Neu in diesem Beispiel ist ein weiterer Deployment-Deskriptor: "orm.xml". Hier wird das gesamte Mapping von Bean auf Datenbank erledigt.
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm http://java.sun.com/xml/ns/persistence/orm_1_0.xsd"
	version="1.0">
	<named-query name="findAllKuchen">
		<query>select o from KuchenSimpleBean o</query>
	</named-query>

	<entity class="de.fhw.swtvertiefung.knauf.kuchen.KuchenSimpleBean" access="PROPERTY"
		metadata-complete="true">
		<attributes>
			<id name="id">
				<generated-value/>
			</id>
			<basic name="name">
			</basic>
		</attributes>
	</entity>
</entity-mappings> 
Das Element "entity" hat keine in der Schema-Definition angegebenen Pflicht-Unterelemente, trotzdem sind einige nötig damit unser Beispiel funktioniert.

Das Attribut "access" gibt an ob die Fehler per set-Property vom Container befüllt werden sollen, oder ob er sie direkt in die (privaten) Membervariablen schreiben soll.

"metadata-complete" war zumindest in JBoss 4.2 sehr wichtig: dort gab es beim Deploy seltsame Fehlermeldungen, wenn es fehlte. Im JBoss 5.0 scheint es nicht mehr nötig zu sein, ich belasse es trotzdem.

Innerhalb der Entity werden die Properties definiert. Für die ID-Spalte geben wir an dass der Wert automatisch generiert werden soll.

Anmerkung:
Eine erweiterte Version, in der z.B. Tabellen- und Spaltennamen angegeben werden, sieht so aus:
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm http://java.sun.com/xml/ns/persistence/orm_1_0.xsd"
	version="1.0">
	<named-query name="findAllKuchen">
		<query>select o from KuchenSimpleBean o</query>
	</named-query>

	<entity class="de.fhw.swtvertiefung.knauf.kuchen.KuchenSimpleBean" access="PROPERTY"
		metadata-complete="true">
		<table name="KUCHENSIMPLEBEAN"></table>
		<attributes>
			<id name="id">
				<column name="ID" />
				<generated-value/>
			</id>
			<basic name="name">
				<column name="NAME" />
			</basic>
		</attributes>
	</entity>
</entity-mappings> 
Wir geben hier den Tabellennamen an ("KUCHENSIMPLEBEAN"), außerdem die Attribute und die zugehörigen Datenbankspalten. Für die ID-Spalte geben wir an dass der Wert automatisch generiert werden soll.

Die modifizierte Version des Projekts gibt es hier:
KuchenSimpleNoAnnotation.ear.
ACHTUNG: Dieses Projekt kann nicht neben dem obigen KuchenSimple-Beispiel existieren !


Manueller Primary Key


Jetzt wollen wir den Primary Key manuell generieren. Das Verfahren hier ist einfach, aber seeehr inperformant: bei jedem Neuanlegen eines Datensatzes wird eine Query "select max(id) from KuchenSimpleBean" ausgeführt. Das Ergebnis dieser Query + 1 wird als ID für den neuen Datensatz verwendet.
Im Code ist folgendes zu ändern:

In KuchenSimpleBean.getId wird die Annotation "@GeneratedValue" entfernt:
  @Column()
  @Id()
  public Integer getId()
  {
    return this.intId;
  } 
KuchenSimpleBean erhält eine weitere @NamedQuery-Annotation (die wir jetzt in eine Annotation @NamedQueries verpacken müssen:
@NamedQueries( { 
  @NamedQuery(name = "findAllKuchen", query = "select o from KuchenSimpleBean o"),
  @NamedQuery(name = "getMaxKuchenId", query = "select max (o.id) from KuchenSimpleBean o")
  })
public class KuchenSimpleBean implements Serializable
{
  ...
KuchenWorkerBean.saveKuchen sieht so aus:
  public void saveKuchen (KuchenSimpleBean kuchen)
  {
    if (kuchen.getId() == null)
    {
      Query query = this.entityManager.createNamedQuery("getMaxKuchenId");
      Integer intMaxId = (Integer) query.getSingleResult();
      //Beim ersten Aufruf kommt hier NULL zurück weil keine ID da ist.
      if (intMaxId == null)
      {
        //Wir starten mit der ID "1"
        kuchen.setId(1);
      }
      else
      {
        //MaxID + 1 verwenden:
        kuchen.setId(intMaxId + 1);
      }
    }
    this.entityManager.merge(kuchen);
  } 
Neu ist der fette Code: wenn der übergebene Kuchen keine ID enthält, dann nehmen wir an dass er neu ist. In diesem Fall führen wir unsere Query aus. Sie liefert beim ersten Aufruf NULL zurück, in diesem Fall setzen wir die ID auf "1". Finden wir eine ID erhöhen wir diese um 1.

Kleiner Kurs in Java: Vor Java5 hätten die Aufrufe zum ID-Setzen so aussehen müssen:
	kuchen.setId( new Integer (intMaxId.intValue() + 1));
Neu in Java 5 kam "Auto-Boxing" hinzu, das es erlaubt einen Basisdatentyp bei Bedarf in die entsprechende Klasse zu konvertieren und zurück (hier: int und java.lang.Integer).

Die modifizierte Version des Projekts gibt es hier:
KuchenSimpleManuellerPK.ear.
ACHTUNG: Dieses Projekt kann nicht neben dem obigen KuchenSimple-Beispiel existieren !



Troubleshooting

Hier werden ein paar Fehler beschrieben in die ich beim Programmieren gelaufen bin ;-)

Stand 11.05.2009
Historie:
05.11.2008: Erstellt aus Vorjahresbeispiel, angepaßt an Eclipse 3.4 und JBoss 5, JPA-Facet beschrieben.
07.11.2008: kleine Aufräumarbeiten in Webseite.
12.11.2008: Code aufgeräumt, "orm.xml" enthält keine <table>- und <column>-Elemente mehr.
21.11.2008: Füllen der JList optimiert, kleine Korrekturen auf Webseite
08.03.2009: Client-Start: VM Argument Context.URL_PKG_PREFIXES auf "org.jboss.naming.client" geändert
11.05.2009: Client-Start mit Injection: VM-Argument "Context.URL_PKG_PREFIXES" entfernt, weil unnötig.