Beispiel: Einfache Container Managed Entity Bean
Inhalt:
Anlegen der Enterprise Application
Anlegen der Entity Bean
persistence.xml
Logging der SQL-Parameter
Anlegen der Session Bean
Application Client
Datenbank
Ausführen des Clients
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
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.
Anlegen der Enterprise Application
Ein leeres "EAR Application Project" mit dem Namen "KuchenSimple" erstellen. Dabei die Option "Generate Deployment Descriptor" setzen.
Zu erzeugende Module zufügen. Dieses Beispiel benötigt ein EJB-Projekt und
ein Anwendungsclientprojekt. Dabei jeweils die Option "Generate Deployment Descriptor" setzen.
Anlegen der Entity Bean
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.swtvertiefung.knauf.kuchen" angelegt. Wichtig ist dass
diese Klasse das Interface "java.io.Serializable" implementiert !
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;
}
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"), wobei hier als Default-Wert eine "Sequence" für die ID-Generierung
verwendet werden soll.
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.
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.
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/session-configuration.html#configuration-logging).
In der Datei "\server\default\conf\jboss-log4j.xml" wird folgendes eingetragen (ACHTUNG: Datei als UTF-8 speichern !):
- Im Bereich "appender" wird ein neuer Appender namens "CONSOLE.HIBERNATE_PARAMETERS" angehängt.
<appender name="CONSOLE.HIBERNATE_PARAMETERS" class="org.apache.log4j.ConsoleAppender">
<errorHandler class="org.jboss.logging.util.OnlyOnceErrorHandler"/>
<param name="Target" value="System.out"/>
<param name="Threshold" value="TRACE"/>
<layout class="org.apache.log4j.PatternLayout">
<!-- The default pattern: Date Priority [Category] Message\n -->
<param name="ConversionPattern" value="%d{ABSOLUTE} %-5p [%c{1}] %m%n"/>
</layout>
</appender>
Es wird ein neuer Appender definiert, der auf die Konsole (System.out) loggt. Einziger Unterschied zum vorhandenen Konsolen-Appender: sein
"Threshold" also das Loglevel, ab dem Meldungen durchgelassen wurde, wurde auf die maximale Detailstufe "TRACE" erhöht.
Anmerkung: es würde eigentlich reichen, den Standard-Konsolenappender auf "TRACE" umzustellen, allerdings würden wir dann in JBoss-Meldungen ertrinken
- Im Bereich "Limit categorie" wird eine neue Kategorie definiert. Diese referenziert unseren Appender (dadurch werden NUR Logausgaben
der Kategorie "org.hibernate.type" an diesen Appender gegeben, und zwar mit TRACE-Level und höher).
<category name="org.hibernate.type">
<priority value="TRACE"/>
<appender-ref ref="CONSOLE.HIBERNATE_PARAMETERS"/>
</category>
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.
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".
Zuerst einmal legen wir das Remote Interface "KuchenWorker" an:
Das Interface wird als "Remote" markiert:
@Remote
public interface KuchenWorker
{
Es erhält drei Methoden:
public void saveKuchen (KuchenSimpleBean kuchen);
public void deleteKuchen (KuchenSimpleBean kuchen);
public List getKuchen();
Jetzt legen wir die Bean-Klasse "KuchenWorkerBean" an. Wichtig ist dass die Bean das Interface "KuchenWorker" implementiert !
Wir markieren die Bean-Klasse "KuchenWorkerBean" als "Stateless":
@Stateless
public class KuchenWorkerBean implements KuchenWorker
{
Der Entity-Manager für den Zugriff auf die Entity Bean wird als vom Container "injected" Variable deklariert:
@PersistenceContext(unitName="kuchenPersistenceUnit")
EntityManager entityManager = null;
Die Implementierung von "saveKuchen" sieht so aus:
this.entityManager.merge(kuchen);
Wichtig ist dass hier "merge" und nicht "persist" genommen wird, siehe Abschnitt Troubleshooting.
"deleteKuchen":
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:
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).
Die GUI wurde mit dem Eclipse-Plugin "Visual Edit" erzeugt, der allerdings scheinbar im aktuellen Eclipse-Zweig nicht mit voller Kraft weiterentwickelt wird.
(http://www.eclipse.org/vep). Deshalb können wir die GUI hier nicht weiter bearbeiten, sondern müssen sie
als gegeben hinnehmen, bis der Plugin irgendwann mal wieder up-to-date ist.
Folgende GUI-Elemente sind vorhanden:
-de.fhw.swtvertiefung.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 "KuchenApplicationClient" liegen, die den FrameKuchen erzeugt und anzeigt.
Die EJB-Referenzen müssen im Deployment-Deskriptor des Clients deklariert werden.
Dazu die Datei "application-client.xml" öffnen und folgendes einfügen:
<ejb-ref>
<ejb-ref-name>ejb/KuchenWorker</ejb-ref-name>
<ejb-ref-type>Session</ejb-ref-type>
<home>java.lang.Object</home>
<remote>de.fhw.swtvertiefung.knauf.kuchen.KuchenWorker</remote>
</ejb-ref>
Auch hier müssen wir wieder ein sinnloses "home"-Tag einfügen.
Damit die JNDI-Referenz funktioniert müssen wir außerdem den JBoss-spezifischen Deployment-Deskriptor "jboss-client.xml" anlegen:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE jboss-client PUBLIC "-//JBoss//DTD Application Client 4.0//EN" "http://www.jboss.org/j2ee/dtd/jboss-client_4_0.dtd" >
<jboss-client>
<jndi-name>KuchenSimpleClient</jndi-name>
<ejb-ref>
<ejb-ref-name>ejb/KuchenWorker</ejb-ref-name>
<jndi-name>KuchenSimple/KuchenWorkerBean/remote</jndi-name>
</ejb-ref>
</jboss-client>
Der Code für das JNDI-Lookup sieht so aus (im Konstruktor von "FrameKuchen"):
Hashtable props = new Hashtable();
props.put(Context.INITIAL_CONTEXT_FACTORY, "org.jnp.interfaces.NamingContextFactory");
props.put(Context.URL_PKG_PREFIXES, "org.jboss.naming.client");
props.put(Context.PROVIDER_URL, "jnp://localhost:1099");
props.put("j2ee.clientName", "KuchenSimpleClient");
InitialContext initialContext = new InitialContext(props);
Object objKuchen = initialContext.lookup("java:comp/env/ejb/KuchenWorker");
this.kuchenWorker = (KuchenWorker) PortableRemoteObject.narrow(objKuchen, KuchenWorker.class);
Wichtig: die Property j2ee.clientName
muss den Namen des Projekts haben !
Jetzt noch die Main class in "MANIFEST.MF" eintragen:
Manifest-Version: 1.0
Class-Path: KuchenSimpleEJB.jar
Main-Class: de.fhw.swtvertiefung.knauf.kuchen.KuchenApplicationClient
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):
2006-09-13 20:28:24,437 DEBUG [org.hibernate.persister.entity.AbstractEntityPersister] Static SQL for entity: de.fhw.swtvertiefung.knauf.kuchen.KuchenSimpleBean
2006-09-13 20:28:24,437 DEBUG [org.hibernate.persister.entity.AbstractEntityPersister] Version select: select id from KuchenSimpleBean where id =?
2006-09-13 20:28:24,437 DEBUG [org.hibernate.persister.entity.AbstractEntityPersister] Snapshot select: select kuchensimp_.id, kuchensimp_.name as name0_ from KuchenSimpleBean kuchensimp_ where kuchensimp_.id=?
2006-09-13 20:28:24,437 DEBUG [org.hibernate.persister.entity.AbstractEntityPersister] Insert 0: insert into KuchenSimpleBean (name, id) values (?, ?)
2006-09-13 20:28:24,437 DEBUG [org.hibernate.persister.entity.AbstractEntityPersister] Update 0: update KuchenSimpleBean set name=? where id=?
2006-09-13 20:28:24,437 DEBUG [org.hibernate.persister.entity.AbstractEntityPersister] Delete 0: delete from KuchenSimpleBean where id=?
2006-09-13 20:28:24,437 DEBUG [org.hibernate.persister.entity.AbstractEntityPersister] Identity insert: insert into KuchenSimpleBean (id, name) values (null, ?)
2006-09-13 20:28:24,453 DEBUG [org.hibernate.loader.entity.EntityLoader] Static select for entity de.fhw.swtvertiefung.knauf.kuchen.KuchenSimpleBean: select kuchensimp0_.id as id0_0_, kuchensimp0_.name as name0_0_ from KuchenSimpleBean kuchensimp0_ where kuchensimp0_.id=?
2006-09-13 20:28:24,453 DEBUG [org.hibernate.loader.entity.EntityLoader] Static select for entity de.fhw.swtvertiefung.knauf.kuchen.KuchenSimpleBean: select kuchensimp0_.id as id0_0_, kuchensimp0_.name as name0_0_ from KuchenSimpleBean kuchensimp0_ where kuchensimp0_.id=?
2006-09-13 20:28:24,453 DEBUG [org.hibernate.loader.entity.EntityLoader] Static select for entity de.fhw.swtvertiefung.knauf.kuchen.KuchenSimpleBean: select kuchensimp0_.id as id0_0_, kuchensimp0_.name as name0_0_ from KuchenSimpleBean kuchensimp0_ where kuchensimp0_.id=?
2006-09-13 20:28:24,453 DEBUG [org.hibernate.loader.entity.EntityLoader] Static select for entity de.fhw.swtvertiefung.knauf.kuchen.KuchenSimpleBean: select kuchensimp0_.id as id0_0_, kuchensimp0_.name as name0_0_ from KuchenSimpleBean kuchensimp0_ where kuchensimp0_.id=?
2006-09-13 20:28:24,468 DEBUG [org.hibernate.loader.entity.EntityLoader] Static select for action ACTION_MERGE on entity de.fhw.swtvertiefung.knauf.kuchen.KuchenSimpleBean: select kuchensimp0_.id as id0_0_, kuchensimp0_.name as name0_0_ from KuchenSimpleBean kuchensimp0_ where kuchensimp0_.id=?
2006-09-13 20:28:24,468 DEBUG [org.hibernate.loader.entity.EntityLoader] Static select for action ACTION_REFRESH on entity de.fhw.swtvertiefung.knauf.kuchen.KuchenSimpleBean: select kuchensimp0_.id as id0_0_, kuchensimp0_.name as name0_0_ from KuchenSimpleBean kuchensimp0_ where kuchensimp0_.id=?
2006-09-13 20:28:24,484 INFO [org.hibernate.tool.hbm2ddl.SchemaExport] Running hbm2ddl schema export
2006-09-13 20:28:24,484 DEBUG [org.hibernate.tool.hbm2ddl.SchemaExport] import file not found: /import.sql
2006-09-13 20:28:24,484 INFO [org.hibernate.tool.hbm2ddl.SchemaExport] exporting generated schema to database
2006-09-13 20:28:24,484 DEBUG [org.hibernate.tool.hbm2ddl.SchemaExport] drop table KuchenSimpleBean if exists
2006-09-13 20:28:24,484 DEBUG [org.hibernate.tool.hbm2ddl.SchemaExport] create table KuchenSimpleBean (id integer generated by default as identity (start with 1), name varchar(255), primary key (id))
2006-09-13 20:28:24,484 INFO [org.hibernate.tool.hbm2ddl.SchemaExport] 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.
Ab zur Funktion "startDatabaseManager" und auf "Invoke" klicken.
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
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:
Zu beachten ist, dass vor der Anwendung der EJB-Server gestartet sein muss.
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:
- Injection des Persistence Context in die Session Bean (wobei das Pflichtelement "persistence-context-ref-name"
für uns keine Bedeutung hat, das ist nur relevant wenn der Persistence Context per JNDI-Lookup geholt werden muss).
- Bei der EntityBean sind die oben markierten Angaben Pflicht. "persistence-type" gibt an ob der Container oder die Bean
selbst sich um die Datenbankzugriffe kümmert. Bei einer EntityBean gemäß EJB3-Spezifikation ist hier natürlich "Container"
die einzig sinnvolle Angabe. Die "prim-key-class" gibt den Datentyp des Primary Keys an, hier "java.lang.Integer".
"reentrant" gibt an ob man ausgehend von einer Methode der Bean zu einer anderen Bean und wieder zurück laufen kann. Für
unsere Bedürfnisse können wir hier immer "true" nehmen.
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">
<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>
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" ist sehr wichtig denn wenn wir es auf "true" setzen wird es beim Deploy seltsame Fehlermeldungen geben
(siehe Troubleshooting).
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.
Es klingt verführerisch hier auch ein Unterelement <id-class class="java.lang.Integer" />
anzugeben,
das ist allerdings ein dicker Fehler, denn eine "id-class" darf es nur geben wenn der Primary Key eine eigene Klasse
(z.B. in Form eines Composite Primary Key) ist.
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, dass 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 ;-)
-
Beim Speichern eines bereits in der Datenbank vorhandenen Kuchens gibt es folgende Fehlermeldung:
javax.ejb.EJBException: javax.persistence.PersistenceException: org.hibernate.PersistentObjectException: detached entity passed to persist: de.fhw.swtvertiefung.knauf.kuchen.KuchenSimpleBean
at org.jboss.ejb3.tx.Ejb3TxPolicy.handleExceptionInOurTx(Ejb3TxPolicy.java:69)
at org.jboss.aspects.tx.TxPolicy.invokeInOurTx(TxPolicy.java:83)
at org.jboss.aspects.tx.TxInterceptor$Required.invoke(TxInterceptor.java:197)
at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:101)
at org.jboss.aspects.tx.TxPropagationInterceptor.invoke(TxPropagationInterceptor.java:76)
at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:101)
at org.jboss.ejb3.stateless.StatelessInstanceInterceptor.invoke(StatelessInstanceInterceptor.java:62)
at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:101)
at org.jboss.aspects.security.AuthenticationInterceptor.invoke(AuthenticationInterceptor.java:78)
at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:101)
at org.jboss.ejb3.ENCPropagationInterceptor.invoke(ENCPropagationInterceptor.java:47)
at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:101)
at org.jboss.ejb3.asynchronous.AsynchronousInterceptor.invoke(AsynchronousInterceptor.java:106)
at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:101)
at org.jboss.ejb3.stateless.StatelessContainer.dynamicInvoke(StatelessContainer.java:225)
at org.jboss.aop.Dispatcher.invoke(Dispatcher.java:106)
at org.jboss.aspects.remoting.AOPRemotingInvocationHandler.invoke(AOPRemotingInvocationHandler.java:82)
at org.jboss.remoting.ServerInvoker.invoke(ServerInvoker.java:828)
at org.jboss.remoting.ServerInvoker.invoke(ServerInvoker.java:681)
at org.jboss.remoting.transport.socket.ServerThread.processInvocation(ServerThread.java:358)
at org.jboss.remoting.transport.socket.ServerThread.dorun(ServerThread.java:412)
at org.jboss.remoting.transport.socket.ServerThread.run(ServerThread.java:239)
Caused by: javax.persistence.PersistenceException: org.hibernate.PersistentObjectException: detached entity passed to persist: de.fhw.swtvertiefung.knauf.kuchen.KuchenSimpleBean
at org.hibernate.ejb.AbstractEntityManagerImpl.throwPersistenceException(AbstractEntityManagerImpl.java:567)
at org.hibernate.ejb.AbstractEntityManagerImpl.persist(AbstractEntityManagerImpl.java:192)
at org.jboss.ejb3.entity.TransactionScopedEntityManager.persist(TransactionScopedEntityManager.java:175)
at de.fhw.swtvertiefung.knauf.kuchen.KuchenWorkerBean.saveKuchen(KuchenWorkerBean.java:42)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
...
Lösung: die zu speichernde, bereits in der Datenbank vorhandene Entity wurde in der KuchenWorkerBean nicht mehr "entityManager.merge()"
gespeichert sondern mittels "entityManager.persist()". Dies klappt aber NUR bei neuen Objekten, nicht bei bereits vorhandenen Daten !
-
Beim Löschen einer Entity kommt es zu folgender Fehlermeldung:
javax.ejb.EJBException: java.lang.IllegalArgumentException: Removing a detached instance de.fhw.swtvertiefung.knauf.kuchen.KuchenSimpleBean#1
at org.jboss.ejb3.tx.Ejb3TxPolicy.handleExceptionInOurTx(Ejb3TxPolicy.java:69)
at org.jboss.aspects.tx.TxPolicy.invokeInOurTx(TxPolicy.java:83)
at org.jboss.aspects.tx.TxInterceptor$Required.invoke(TxInterceptor.java:197)
at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:101)
at org.jboss.aspects.tx.TxPropagationInterceptor.invoke(TxPropagationInterceptor.java:76)
at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:101)
at org.jboss.ejb3.stateless.StatelessInstanceInterceptor.invoke(StatelessInstanceInterceptor.java:62)
at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:101)
at org.jboss.aspects.security.AuthenticationInterceptor.invoke(AuthenticationInterceptor.java:78)
at org.jboss.ejb3.security.Ejb3AuthenticationInterceptor.invoke(Ejb3AuthenticationInterceptor.java:131)
at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:101)
at org.jboss.ejb3.ENCPropagationInterceptor.invoke(ENCPropagationInterceptor.java:47)
at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:101)
at org.jboss.ejb3.asynchronous.AsynchronousInterceptor.invoke(AsynchronousInterceptor.java:106)
at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:101)
at org.jboss.ejb3.stateless.StatelessContainer.dynamicInvoke(StatelessContainer.java:263)
at org.jboss.aop.Dispatcher.invoke(Dispatcher.java:106)
at org.jboss.aspects.remoting.AOPRemotingInvocationHandler.invoke(AOPRemotingInvocationHandler.java:82)
at org.jboss.remoting.ServerInvoker.invoke(ServerInvoker.java:828)
at org.jboss.remoting.ServerInvoker.invoke(ServerInvoker.java:681)
at org.jboss.remoting.transport.socket.ServerThread.processInvocation(ServerThread.java:358)
at org.jboss.remoting.transport.socket.ServerThread.dorun(ServerThread.java:412)
at org.jboss.remoting.transport.socket.ServerThread.run(ServerThread.java:239)
Caused by: java.lang.IllegalArgumentException: Removing a detached instance de.fhw.swtvertiefung.knauf.kuchen.KuchenSimpleBean#1
at org.hibernate.ejb.event.EJB3DeleteEventListener.performDetachedEntityDeletionCheck(EJB3DeleteEventListener.java:47)
at org.hibernate.event.def.DefaultDeleteEventListener.onDelete(DefaultDeleteEventListener.java:75)
at org.hibernate.event.def.DefaultDeleteEventListener.onDelete(DefaultDeleteEventListener.java:49)
at org.hibernate.impl.SessionImpl.fireDelete(SessionImpl.java:766)
at org.hibernate.impl.SessionImpl.delete(SessionImpl.java:744)
at org.hibernate.ejb.AbstractEntityManagerImpl.remove(AbstractEntityManagerImpl.java:245)
at org.jboss.ejb3.entity.TransactionScopedEntityManager.remove(TransactionScopedEntityManager.java:187)
at de.fhw.swtvertiefung.knauf.kuchen.KuchenWorkerBean.deleteKuchen(KuchenWorkerBean.java:36)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
...
Lösung: seit EJB3 RC9 müssen die Entities vorher "attached" werden, d.h. in eine vom Container verwaltete Entity umgewandelt
werden:
kuchen = this.entityManager.find (KuchenSimpleBean.class, kuchen.getId() );
this.entityManager.remove(kuchen);
Vor dem Aufruf von "remove" müßte eigentlich noch eine Prüfung erfolgen ob der Datensatz in der Datenbank gefunden wurde.
- Beim Aufrufen einer NamedQuery kommt diese Fehlermeldung:
javax.ejb.EJBException: javax.persistence.PersistenceException: org.hibernate.MappingException: Named query not known: findAllKuchen
at org.jboss.ejb3.tx.Ejb3TxPolicy.handleExceptionInOurTx(Ejb3TxPolicy.java:69)
at org.jboss.aspects.tx.TxPolicy.invokeInOurTx(TxPolicy.java:83)
at org.jboss.aspects.tx.TxInterceptor$Required.invoke(TxInterceptor.java:197)
at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:101)
at org.jboss.aspects.tx.TxPropagationInterceptor.invoke(TxPropagationInterceptor.java:76)
at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:101)
at org.jboss.ejb3.stateless.StatelessInstanceInterceptor.invoke(StatelessInstanceInterceptor.java:62)
at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:101)
at org.jboss.aspects.security.AuthenticationInterceptor.invoke(AuthenticationInterceptor.java:78)
at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:101)
at org.jboss.ejb3.ENCPropagationInterceptor.invoke(ENCPropagationInterceptor.java:47)
at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:101)
at org.jboss.ejb3.asynchronous.AsynchronousInterceptor.invoke(AsynchronousInterceptor.java:106)
at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:101)
at org.jboss.ejb3.stateless.StatelessContainer.dynamicInvoke(StatelessContainer.java:225)
at org.jboss.aop.Dispatcher.invoke(Dispatcher.java:106)
at org.jboss.aspects.remoting.AOPRemotingInvocationHandler.invoke(AOPRemotingInvocationHandler.java:82)
at org.jboss.remoting.ServerInvoker.invoke(ServerInvoker.java:828)
at org.jboss.remoting.ServerInvoker.invoke(ServerInvoker.java:681)
at org.jboss.remoting.transport.socket.ServerThread.processInvocation(ServerThread.java:358)
at org.jboss.remoting.transport.socket.ServerThread.dorun(ServerThread.java:398)
at org.jboss.remoting.transport.socket.ServerThread.run(ServerThread.java:239)
Caused by: javax.persistence.PersistenceException: org.hibernate.MappingException: Named query not known: findAllKuchen
at org.hibernate.ejb.AbstractEntityManagerImpl.throwPersistenceException(AbstractEntityManagerImpl.java:567)
at org.hibernate.ejb.AbstractEntityManagerImpl.createNamedQuery(AbstractEntityManagerImpl.java:90)
at org.jboss.ejb3.entity.TransactionScopedEntityManager.createNamedQuery(TransactionScopedEntityManager.java:134)
at de.fhw.swtvertiefung.knauf.kuchen.KuchenWorkerBean.getKuchen(KuchenWorkerBean.java:58)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
Die NamedQuery ist aber scheinbar korrekt deklariert.
Lösungsansatz: Ist die NamedQuery auf der Entity-Klasse deklariert deren Instanzen sie finden soll ? Bei mir trat
dieser Fehler auf als ich die NamedQuery in der SessionBean deklarierte die den EntityManager verwendete.
- Nur beim Deploy ohne Annotations:
org.hibernate.AnnotationException: Cannot override an property with it does not have an @Id already
at org.hibernate.reflection.java.EJB3OverridenAnnotationReader.getId(EJB3OverridenAnnotationReader.java:919)
at org.hibernate.reflection.java.EJB3OverridenAnnotationReader.initAnnotations(EJB3OverridenAnnotationReader.java:347)
at org.hibernate.reflection.java.EJB3OverridenAnnotationReader.isAnnotationPresent(EJB3OverridenAnnotationReader.java:257)
at org.hibernate.reflection.java.JavaXAnnotatedElement.isAnnotationPresent(JavaXAnnotatedElement.java:40)
at org.hibernate.cfg.AnnotationBinder.mustBeSkipped(AnnotationBinder.java:998)
at org.hibernate.cfg.AnnotationBinder.addProperty(AnnotationBinder.java:978)
at org.hibernate.cfg.AnnotationBinder.addElementsOfAClass(AnnotationBinder.java:942)
at org.hibernate.cfg.AnnotationBinder.getElementsToProcess(AnnotationBinder.java:788)
at org.hibernate.cfg.AnnotationBinder.bindClass(AnnotationBinder.java:614)
at org.hibernate.cfg.AnnotationConfiguration.processArtifactsOfType(AnnotationConfiguration.java:353)
at org.hibernate.cfg.AnnotationConfiguration.secondPassCompile(AnnotationConfiguration.java:265)
at org.hibernate.cfg.Configuration.buildMappings(Configuration.java:1034)
at org.hibernate.ejb.Ejb3Configuration.buildMappings(Ejb3Configuration.java:1015)
at org.hibernate.ejb.EventListenerConfigurator.configure(EventListenerConfigurator.java:154)
...
Diese Fehlermeldung bedeutet dass das Attribut metadata-complete="true"
in orm.xml nicht gesetzt ist !
Alternativ wäre hier wohl auch diese Angabe in "orm.xml" möglich gewesen:
<persistence-unit-metadata>
<xml-mapping-metadata-complete/>
</persistence-unit-metadata>
Quelle: http://forum.hibernate.org/viewtopic.php?p=2310540&sid=f14052739a96c418c3fb6ec5cde5283d
Stand 08.11.2007
Historie:
28.10.2007: Erstellt aus Vorjahresbeispiel, Visual-Edit-Beschreibungen entfernt.
08.11.2007: SQL-Parameter-Logging.