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