Beispiel: @Version
-Annotation
Inhalt:
EJB-Projekt
Web-Projekt
Ohne Annotations
Für WildFly 31 und JakartaEE 10: Beispiel für eine Entity Bean die ein "optimistic locking" mittels @Version
-Annotation implementiert.
Hier gibt es das Projekt zum Download (dies ist ein EAR-Export, die Importanleitung findet
man im Stateless-Beispiel): KuchenVersion.ear
Aufbau des Beispieles
a) Entity Bean-Klasse KuchenVersionBean
b) Session Bean KuchenVersionBeanWorker
sowie Local Interface.
d) Web Client
EJB-Projekt
In der Entity Bean de.hsrm.jakartaee.knauf.version.KuchenVersionBean
wird ein neues Feld zugefügt:
private Integer intVersion;
@Version()
public Integer getVersion()
{
return this.intVersion;
}
public void setVersion(Integer int_Version)
{
this.intVersion = int_Version;
}
In der Session-Bean ergeben sich durch das Versions-Feld keine Änderungen, da ihren Speichern/Löschen-Methoden jeweils eine komplette
Entity Bean übergeben wird.
Im Projekt sollten allerdings auf SessionBean-Ebene die Exceptions, die sich durch eine zwischenzeitliche Änderung ergeben,
abgefangen und in eine eigene Exceptionklasse verpackt werden.
In der Datenbank sieht das so aus:
Im Screenshot wurde der "Käsekuchen" schon zweimal geändert und hat deshalb eine Version von "2".
Web-Projekt
Die Webanwendung ist unter dieser URL erreichbar: http://localhost:8080/KuchenVersionWeb/
Im Web-Projekt gibt es eine Änderung: wenn eine Entity Bean in einem Formular bearbeitet wird, dann muss man nicht nur die ID mitführen,
sondern auch den beim Öffnen des Formulars gültigen Wert des Version-Felds.
Grund: Das Version-Feld kann nur anschlagen, wenn beim Speichern einer Bean ein Wert im Version-Feld steht, der nicht mehr dem aktuellen
Datenbankwert entspricht. Würde man nur die ID ins Formular geben und beim Submit die Bean laden, dann könnte man auch eine zwischenzeitlich
geänderte Entity-Bean speichern, denn durch das Laden würde man ja schon die aktuellste Version erwischen.
Codeausschnitt aus "kuchenEdit.jsp" (Aufbau und Auswerten des Bearbeiten-Formulars):
String strSubmitType = request.getParameter("submit");
...
if ("Bearbeiten".equals(strSubmitType) == true)
{
//Kuchen bearbeiten: Den Kuchen holen (ID steckt im Request !) und den Namen im Textfeld vorgeben.
String strID = request.getParameter("kuchenid");
KuchenVersionBean kuchenBearbeiten = kuchenVersionWorker.findKuchenById( Integer.valueOf(strID) );
%>
<FORM method="post">
Kuchen-Bezeichnung: <input type="text" name="name" value="<%=kuchenBearbeiten.getName()%>"> <br>
<INPUT TYPE="SUBMIT" NAME="submit" VALUE="BearbeitenOK"> <br>
<!--Die Kuchen-ID als Hidden Field mitführen -->
<input type="hidden" name="kuchenid" value="<%=kuchenBearbeiten.getId()%>">
<!--Auch die Version muss mitgeführt werden ! -->
<input type="hidden" name="version" value="<%=kuchenBearbeiten.getVersion()%>">
</FORM>
<%
}
else if ("BearbeitenOK".equals(strSubmitType) == true)
{
//Im Bearbeiten-Formular auf "OK" geklickt: Kuchen updaten.
//Den Kuchen holen (ID steckt im Request !) und den Namen im Textfeld vorgeben.
String strID = request.getParameter("kuchenid");
KuchenVersionBean kuchenBearbeiten = kuchenVersionWorker.findKuchenById( Integer.valueOf(strID) );
kuchenBearbeiten.setName( request.getParameter("name") );
//Auch die Version wird hier mitgeführt !
Integer intVersion = Integer.valueOf(request.getParameter ("version") );
kuchenBearbeiten.setVersion (intVersion);
kuchenVersionWorker.saveKuchen(kuchenBearbeiten);
//Request weiterleiten an Übersichtsseite:
request.getRequestDispatcher ("index.jsp").forward(request, response);
}
Zum Test kann man zwei Browserfenster öffnen, und in jedem Fenster die gleiche Entity Bean zum Bearbeiten öffnen. Dann im ersten Fenster
etwas ändern und Speichern, danach das gleiche im zweiten Browserfenster versuchen. Das führt zu dieser Meldung:
20:18:09,169 ERROR [io.undertow.request] (default task-5) UT005023: Exception handling request to /KuchenVersionWeb/kuchenEdit.jsp: org.apache.jasper.JasperException: jakarta.ejb.EJBException: jakarta.persistence.OptimisticLockException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [de.hsrm.jakartaee.knauf.version.KuchenVersionBean#1]
at io.undertow.jsp@2.2.7.Final//org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:461)
at io.undertow.jsp@2.2.7.Final//org.apache.jasper.servlet.JspServlet.serviceJspFile(JspServlet.java:403)
at io.undertow.jsp@2.2.7.Final//org.apache.jasper.servlet.JspServlet.service(JspServlet.java:347)
at jakarta.servlet.api@6.0.0//jakarta.servlet.http.HttpServlet.service(HttpServlet.java:614)
at io.undertow.servlet@2.3.10.Final//io.undertow.servlet.handlers.ServletHandler.handleRequest(ServletHandler.java:74)
...
at io.undertow.core@2.3.10.Final//io.undertow.server.HttpServerExchange$1.run(HttpServerExchange.java:859)
at org.jboss.threads@2.4.0.Final//org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35)
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:1990)
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1486)
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1377)
at org.jboss.xnio@3.8.12.Final//org.xnio.XnioWorker$WorkerThreadFactory$1$1.run(XnioWorker.java:1282)
at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: jakarta.ejb.EJBException: jakarta.persistence.OptimisticLockException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [de.hsrm.jakartaee.knauf.version.KuchenVersionBean#1]
at org.jboss.as.ejb3@31.0.0.Final//org.jboss.as.ejb3.tx.CMTTxInterceptor.invokeInOurTx(CMTTxInterceptor.java:251)
at org.jboss.as.ejb3@31.0.0.Final//org.jboss.as.ejb3.tx.CMTTxInterceptor.required(CMTTxInterceptor.java:373)
at org.jboss.as.ejb3@31.0.0.Final//org.jboss.as.ejb3.tx.CMTTxInterceptor.processInvocation(CMTTxInterceptor.java:143)
...
at org.jboss.as.ee@31.0.0.Final//org.jboss.as.ee.component.ProxyInvocationHandler.invoke(ProxyInvocationHandler.java:64)
at deployment.KuchenVersion.ear.KuchenVersionEJB.jar//de.hsrm.jakartaee.knauf.version.KuchenVersionWorkerLocal$$$view1.saveKuchen(Unknown Source)
at org.apache.jsp.kuchenEdit_jsp._jspService(kuchenEdit_jsp.java:186)
at io.undertow.jsp@2.2.7.Final//org.apache.jasper.runtime.HttpJspBase.service(HttpJspBase.java:70)
at jakarta.servlet.api@6.0.0//jakarta.servlet.http.HttpServlet.service(HttpServlet.java:614)
at io.undertow.jsp@2.2.7.Final//org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:422)
... 47 more
Caused by: jakarta.persistence.OptimisticLockException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [de.hsrm.jakartaee.knauf.version.KuchenVersionBean#1]
at org.hibernate@6.4.2.Final//org.hibernate.internal.ExceptionConverterImpl.wrapStaleStateException(ExceptionConverterImpl.java:206)
at org.hibernate@6.4.2.Final//org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:95)
at org.hibernate@6.4.2.Final//org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:167)
at org.hibernate@6.4.2.Final//org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:173)
at org.hibernate@6.4.2.Final//org.hibernate.internal.SessionImpl.fireMerge(SessionImpl.java:858)
at org.hibernate@6.4.2.Final//org.hibernate.internal.SessionImpl.merge(SessionImpl.java:833)
at org.jboss.as.jpa@31.0.0.Final//org.jboss.as.jpa.container.AbstractEntityManager.merge(AbstractEntityManager.java:551)
at deployment.KuchenVersion.ear.KuchenVersionEJB.jar//de.hsrm.jakartaee.knauf.version.KuchenVersionWorkerBean.saveKuchen(KuchenVersionWorkerBean.java:49)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
...
at org.jboss.invocation@2.0.0.Final//org.jboss.invocation.InterceptorContext.proceed(InterceptorContext.java:422)
at org.jboss.as.ejb3@31.0.0.Final//org.jboss.as.ejb3.tx.CMTTxInterceptor.invokeInOurTx(CMTTxInterceptor.java:237)
... 93 more
Caused by: org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [de.hsrm.jakartaee.knauf.version.KuchenVersionBean#1]
at org.hibernate@6.4.2.Final//org.hibernate.event.internal.DefaultMergeEventListener.targetEntity(DefaultMergeEventListener.java:382)
at org.hibernate@6.4.2.Final//org.hibernate.event.internal.DefaultMergeEventListener.entityIsDetached(DefaultMergeEventListener.java:352)
at org.hibernate@6.4.2.Final//org.hibernate.event.internal.DefaultMergeEventListener.merge(DefaultMergeEventListener.java:150)
at org.hibernate@6.4.2.Final//org.hibernate.event.internal.DefaultMergeEventListener.doMerge(DefaultMergeEventListener.java:143)
at org.hibernate@6.4.2.Final//org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:127)
at org.hibernate@6.4.2.Final//org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:81)
at org.hibernate@6.4.2.Final//org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:127)
at org.hibernate@6.4.2.Final//org.hibernate.internal.SessionImpl.fireMerge(SessionImpl.java:847)
... 124 more
....
Sauberer Code sollte also im Client bei jeder Speicherung die jakarta.persistence.OptimisticLockException
fangen.
Ohne Annotations
"ejb-jar.xml" sieht so aus:
<?xml version="1.0" encoding="UTF-8"?>
<ejb-jar xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/ejb-jar_4_0.xsd"
version="4.0">
<enterprise-beans>
<session>
<description>
<![CDATA[Stateless Session Bean für das Arbeiten mit Kuchen]]>
</description>
<display-name>KuchenVersionWorkerBean</display-name>
<ejb-name>KuchenVersionWorkerBean</ejb-name>
<business-local>de.hsrm.jakartaee.knauf.version.KuchenVersionWorkerLocal</business-local>
<ejb-class>de.hsrm.jakartaee.knauf.version.KuchenVersionWorkerBean</ejb-class>
<session-type>Stateless</session-type>
<persistence-context-ref>
<persistence-context-ref-name>KuchenVersionPersistenceUnitRef</persistence-context-ref-name>
<persistence-unit-name>kuchenVersionPersistenceUnit</persistence-unit-name>
<injection-target>
<injection-target-class>
de.hsrm.jakartaee.knauf.version.KuchenVersionWorkerBean
</injection-target-class>
<injection-target-name>entityManager</injection-target-name>
</injection-target>
</persistence-context-ref>
</session>
</enterprise-beans>
</ejb-jar>
Es gibt keine Neuerungen im Vergleich zu den bisherigen Beispielen.
"orm.xml" enthält die Deklaration der Versions-Spalte:
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="https://jakarta.ee/xml/ns/persistence/orm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence/orm https://jakarta.ee/xml/ns/persistence/orm/orm_3_1.xsd"
version="3.1">
<named-query name="findAllKuchen">
<query>select o from KuchenVersionBean o</query>
</named-query>
<entity class="de.hsrm.jakartaee.knauf.version.KuchenVersionBean" access="PROPERTY"
metadata-complete="true">
<attributes>
<id name="id">
<generated-value />
</id>
<basic name="name">
</basic>
<version name="version"></version>
</attributes>
</entity>
</entity-mappings>
Die modifizierte Version des Projekts gibt es hier: KuchenVersionNoAnnotation.ear.
ACHTUNG: Dieses Projekt kann nicht neben dem obigen KuchenVersion-Beispiel existieren !
Stand 14.02.2024
Historie:
14.02.2024: Erstellt aus JavaEE6-Beispiel, angepasst an JakartaEE10/WildFly 31.