EJB 3.0 Persistence – using the @Version annotation for Optimistic Locking – in the GlassFish Reference Implementation

4

One of the annotations available to us when specifying the EJB 3.0 Persistence metadata for our Entities is the @Version annotation. The specification states:

"The Version annotation specifies the version property (optimistic lock value) of an entity class. The @Version is a marker annotation
that keeps track of the version property (optimistic lock value) of an
entity class. This is used to ensure integrity when reattaching and for
overall optimistic concurrency control. It can be used in conjunction with a @Column and/or @Id but may appear on its own. Only a single Version property/field should be used per class; applications that use more than one are not expected to be portable. The Version property should be mapped to the primary table for the entity class; applications that map the Version property to a table other than the primary table are not portable. Fields or properties that are specified with the Version annotation should not be updated by the application. The following types are supported for version properties: int, Integer, short, Short, long, Long, Timestamp. 
"

I
have a clue what @Version is about, but I can’t really get my head
round this. And Googling this time brought no comfort. I have to do it
the hard way… actually trying out the @Version annotation.

What
I do get from the description is that with @Version I indicate that a
specific Attribute – mapped to a database column – is to be used to
verify whether an Optimistic Lock can be acquired. So by comparing the
value of that Attribute to the current value of the underlying database
column, we – that this: the EntityManager – can find out whether the
record has been changed since we retrieved it and if not, we get our
optimistic lock. Sounds very useful! Of course we also require the
EntityManager to update the attribute value and the database column
when we update the Entity, to indicate to other users of the same
Entity that they have an old version of the Entity. And perhaps the
EntityManager can be smart enough – although that really is no concern
of mine – to use the Version Attribute when deciding whether a Refresh
must actually be done or can be ignored.

In this post we will take a brief look at what the GlassFish EntityManager does with the @Version annotation.

....

I
am a lazy programmer. So instead of creating a brand new version column
in my ALS_BOOKS table, I will abuse an existing NUMBER column that I do
not really need for other purposes. I designate the COMPLEXITY column
as Version Column. I lay down this design choice in the Entity Book in
the following way:

import javax.persistence.Version;<br />...<br />    @Version<br />    @Column(name=&quot;COMPLEXITY&quot;)<br />    public Long getVersion() {<br />        return version;<br />    }<br />    public void setVersion(Long version) {<br />        this.version = version;<br />    }<br /><br />

That
is all. I have told the EntityManager that I have an Attribute called
Version – by the way: the name can be anything, Version just seemed
appropriate – that is mapped to the database column COMPLEXITY. The
Attribute that is annotated with @Version must be a numeric
type, Long, Integer, perhaps but less likely Short. I have
seen some references to a @Timestamp annotation that would do roughly
the same as the @Version annotation. Unfortunately, I cannot get clear
confirmation and on my GlassFish installation, @Timestamp does not
exist. Note: I should try to just use @Version for an Attribute of
type Date, mapped to a DATE or TIMESTAMP type database column. I will do that at the end of this article.

Now
let’s see what the EntityManager makes of this @Version annotation. I
will retrieve a Book, make some changes and monitor what is happening
with the Version Attribute (or COMPLEXITY column). Here is the code:

        LibraryService libraryService = new LibraryService();<br />        Book jsf = libraryService.getEntityManager().find(Book.class, new Long(22));<br />        System.out.println(&quot;Version of JSF after querying - no changes applied: &quot;+jsf.getVersion());<br />        System.out.println(&quot;The title of JSF before any changes are made: &quot;+jsf.getTitle());<br />        EntityTransaction tx = libraryService.getEntityManager().getTransaction();<br />        tx.begin();<br />        jsf.setTitle(jsf.getTitle()+&quot;!&quot;); // change the title<br />        libraryService.getEntityManager().merge(jsf); // persist the change - merge will eventually result in an update<br />        libraryService.getEntityManager().flush(); // we enforce that the update is done at this point<br />        tx.commit(); // complete the transaction; the EntityManager can now decide to do the update any time it fancies<br />        System.out.println(&quot;Let's see what value the Version attribute of the JSF Entity has now, after the update before the 'refresh': &quot;+jsf.getVersion());<br />        jsf = libraryService.getEntityManager().find(Book.class, new Long(22)); // retrieve the updated JSF entity<br />        System.out.println(&quot;Let's see what value the Version attribute of the JSF Entity has now (after the 'refresh'): &quot;+jsf.getVersion());<br />        tx.begin();<br />        jsf.setTitle(jsf.getTitle()+&quot;?&quot;);<br />        libraryService.getEntityManager().merge(jsf);<br />        tx.commit();<br />        jsf = libraryService.getEntityManager().find(Book.class, new Long(22));<br />        System.out.println(&quot;Value of Version attribute of JSF entity after second update: &quot;+jsf.getVersion());<br />&nbsp;

Note:
For the complete source code of Book, Author, LibraryService and
persistence.xml, see my previous post on EJB 3.0 Persistence:EJB 3.0 Persistence – ManyToMany relations or the objectification of the intersection table

The results when running this code are:

[TopLink Fine]: 2006.01.03 08:03:05.995--ServerSession(25657668)--Connection(5863106)--Thread(Thread[main,5,main])--SELECT ID, PUBLISH_MONTH, PUBLISH_YEAR, ISBN, TITLE, COMPLEXITY FROM ALS_BOOKS WHERE (ID = 22)<br />Version of JSF after querying - no changes applied: 10<br />The title of JSF before any changes are made: JavaServer Faces Programming!!!?!?!!?!!!?!?!?<br />[TopLink Fine]: 2006.01.03 08:03:06.215--UnitOfWork(1677625)--Connection(24435002)--Thread(Thread[main,5,main])--UPDATE ALS_BOOKS SET TITLE = 'JavaServer Faces Programming!!!?!?!!?!!!?!?!?!', COMPLEXITY = 11 WHERE ((ID = 22) AND (COMPLEXITY = 10))<br />Let's see what value the Version attribute of the JSF Entity has now, after the update before the 'refresh': 10<br />Let's see what value the Version attribute of the JSF Entity has now (after the 'refresh'): 11<br />[TopLink Fine]: 2006.01.03 08:03:06.446--UnitOfWork(23293518)--Connection(3115866)--Thread(Thread[main,5,main])--UPDATE ALS_BOOKS SET TITLE = 'JavaServer Faces Programming!!!?!?!!?!!!?!?!?!?', COMPLEXITY = 12 WHERE ((ID = 22) AND (COMPLEXITY = 11))<br />Value of Version attribute of JSF entity after second update: 12<br />&nbsp;

So
what does the EntityManager do: when an Entity is updated (merge()),
the EntityManager includes the Version attribute in the update: the
value of the underlying database column is increased by one. It also
checks during the update if the value of the column underlying the
Version attribute is equal to the value currently set in the Entity. If
that is not the case, the update will do nothing because no Optimistic
Lock was acquired:

UPDATE ALS_BOOKS <br />SET    TITLE = 'JavaServer Faces Programming!!!?!?!!?!!!?!?!?!'<br />,      COMPLEXITY = 21 <br />WHERE ((ID = 22) AND (COMPLEXITY = 20))<br /><br />

Now
the same fragment a little extended – see the last 9 lines of code:
after performing the second update, I deliberately pause the execution
for 30 seconds. During that break, I make a change to the database: I
change the value in the COMPLEXITY column – the column that my EJB 3.0
EntityManager uses for optimistic locking. When the execution resumes
after 30 seconds, another update will be attempted – setTitle() and
merge() – and will fail:

        Book jsf = libraryService.getEntityManager().find(Book.class, new Long(22));<br />        System.out.println(&quot;Version of JSF after querying - no changes applied: &quot;+jsf.getVersion());<br />        System.out.println(&quot;The title of JSF before any changes are made: &quot;+jsf.getTitle());<br />        EntityTransaction tx = libraryService.getEntityManager().getTransaction();<br />        tx.begin();<br />        jsf.setTitle(jsf.getTitle()+&quot;!&quot;); // change the title<br />        libraryService.getEntityManager().merge(jsf); // persist the change - merge will eventually result in an update<br />        libraryService.getEntityManager().flush(); // we enforce that the update is done at this point<br />        tx.commit(); // complete the transaction; the EntityManager can now decide to do the update any time it fancies<br />        System.out.println(&quot;Let's see what value the Version attribute of the JSF Entity has now, after the update before the 'refresh': &quot;+jsf.getVersion());<br />        jsf = libraryService.getEntityManager().find(Book.class, new Long(22)); // retrieve the updated JSF entity<br />        System.out.println(&quot;Let's see what value the Version attribute of the JSF Entity has now (after the 'refresh'): &quot;+jsf.getVersion());<br />        tx.begin();<br />        jsf.setTitle(jsf.getTitle()+&quot;?&quot;);<br />        libraryService.getEntityManager().merge(jsf);<br />        tx.commit();<br />        jsf = libraryService.getEntityManager().find(Book.class, new Long(22));<br />        System.out.println(&quot;Value of Version attribute of JSF entity after second update: &quot;+jsf.getVersion());<br />        System.out.println(&quot;Sleep now for 30 seconds so you can do your update at this time; do something like: update als_books set complexity = complexity + 1 where id = 22 and commmit&quot;);<br />        try {<br />          Thread.currentThread().sleep(30000);<br />        } catch (Exception e){}<br />        tx.begin();<br />        jsf.setTitle(jsf.getTitle()+&quot;?&quot;);<br />        libraryService.getEntityManager().merge(jsf);<br />        tx.commit();<br />        jsf = libraryService.getEntityManager().find(Book.class, new Long(22));<br />        System.out.println(&quot;Value of Version attribute of JSF entity after second update: &quot;+jsf.getVersion());&nbsp;<br />

The output of this last code fragment:

[TopLink Fine]: 2006.01.02 06:06:12.848--ServerSession(25657668)--Connection(5863106)--Thread(Thread[main,5,main])--SELECT ID, PUBLISH_MONTH, PUBLISH_YEAR, ISBN, TITLE, COMPLEXITY FROM ALS_BOOKS WHERE (ID = 22)<br />Version of JSF after querying - no changes applied: 17<br />The title of JSF before any changes are made: JavaServer Faces Programming!!!?!?!!?!!!?!?<br />[TopLink Fine]: 2006.01.02 06:06:13.008--UnitOfWork(1677625)--Connection(24435002)--Thread(Thread[main,5,main])--UPDATE ALS_BOOKS SET TITLE = 'JavaServer Faces Programming!!!?!?!!?!!!?!?!', COMPLEXITY = 18 WHERE ((ID = 22) AND (COMPLEXITY = 17))<br />Let's see what value the Version attribute of the JSF Entity has now, after the update before the 'refresh': 17<br />Let's see what value the Version attribute of the JSF Entity has now (after the 'refresh'): 18<br />[TopLink Fine]: 2006.01.02 06:06:13.028--UnitOfWork(23293518)--Connection(3115866)--Thread(Thread[main,5,main])--UPDATE ALS_BOOKS SET TITLE = 'JavaServer Faces Programming!!!?!?!!?!!!?!?!?', COMPLEXITY = 19 WHERE ((ID = 22) AND (COMPLEXITY = 18))<br />Value of Version attribute of JSF entity after second update: 19<br />Sleep now for 30 seconds so you can do your update at this time; do something like: update als_books set complexity = complexity + 1 where id = 22 and commmit<br />[TopLink Fine]: 2006.01.02 06:06:43.051--UnitOfWork(14434757)--Connection(7912507)--Thread(Thread[main,5,main])--UPDATE ALS_BOOKS SET TITLE = 'JavaServer Faces Programming!!!?!?!!?!!!?!?!??', COMPLEXITY = 20 WHERE ((ID = 22) AND (COMPLEXITY = 19))<br />[TopLink Warning]: 2006.01.02 06:06:43.071--UnitOfWork(14434757)--Thread(Thread[main,5,main])--Exception [TOPLINK-5006] (Oracle TopLink Essentials - 10g release 4 (10.1.4.0.0) (Build 051215Dev)): oracle.toplink.essentials.exceptions.OptimisticLockException<br />Exception Description: The object [nl.amis.als.ejb30.Book@988707] cannot be updated because it has changed or been deleted since it was last read. <br />Class&gt; nl.amis.als.ejb30.Book Primary Key&gt; [22]<br />Exception in thread &quot;main&quot; javax.persistence.PersistenceException: Exception [TOPLINK-5006] (Oracle TopLink Essentials - 10g release 4 (10.1.4.0.0) (Build 051215Dev)): oracle.toplink.essentials.exceptions.OptimisticLockException<br />Exception Description: The object [nl.amis.als.ejb30.Book@988707] cannot be updated because it has changed or been deleted since it was last read. <br />Class&gt; nl.amis.als.ejb30.Book Primary Key&gt; [22]<br />	at oracle.toplink.essentials.internal.ejb.cmp3.transaction.EntityTransactionImpl.commit(EntityTransactionImpl.java:48)<br />	at nl.amis.als.ejb30.LibraryService.main(LibraryService.java:86)<br />Caused by: Exception [TOPLINK-5006] (Oracle TopLink Essentials - 10g release 4 (10.1.4.0.0) (Build 051215Dev)): oracle.toplink.essentials.exceptions.OptimisticLockException<br />Exception Description: The object [nl.amis.als.ejb30.Book@988707] cannot be updated because it has changed or been deleted since it was last read. <br />Class&gt; nl.amis.als.ejb30.Book Primary Key&gt; [22]<br />	at oracle.toplink.essentials.exceptions.OptimisticLockException.objectChangedSinceLastReadWhenUpdating(OptimisticLockException.java:136)<br />	at oracle.toplink.essentials.descriptors.VersionLockingPolicy.validateUpdate(VersionLockingPolicy.java:680)<br />&nbsp;

This
clearly shows that the EntityManager uses the @Version attribute to
make sure it only performs the requested updated if it can acquire the
‘optimistic lock’; note the where clause for the update statement: …
WHERE ((ID = 22) AND (COMPLEXITY = 19)). In the example we see here, we
had changed the underlying column (Complexity) from 19 to 20 prior to
the update-statement, so this update did not actually update any
records. The EntityManager checks the number of records updated and
when that is zero, it throws the OptimisticLockException. By the way:
this is uncannily similar to behavior of Oracle Forms when it attempts
to update a record.

  • Using @Version therefore brings two great benefits:
  • The
    EJB 3.0 EntityManager will maintain the value of the database column,
    updating it to the next highest integer value upon each update

The
EJB 3.0 EntityManager will verify that it will only update the database
record if the value of the version column is still the same as it was
when the entity was last retrieved from the database. If that is not
the case, the EntityManager assumes that someone else has changed the
database since the last retrieval and therefore the optimistic lock
could not be acquired. Clearly, we have to catch this exception in our
code – probably by refreshing the entity, reapplying our changes when
still relevant and reattempting the merge operation.

Timestamp 

As promised I will take a look at using a Version attribute of type Timestamp rather than a Long/Integer/Short attribute.

First I add a column last_changed to the table that contains the books: 

alter table als_books add (last_changed date);

I made these changes in Entity Book (file Book.java) to specify that I want to have an attribute called version mapped to the LAST_CHANGED column: 

    private Timestamp version;<br />    @Version<br />    @Column(name=&quot;LAST_CHANGED&quot;)<br />    public Timestamp getVersion() {<br />        return version;<br />    }<br />    public void setVersion(Timestamp version) {<br />        this.version = version;<br />    }<br /><br />

After the merge operation:

        libraryService.getEntityManager().merge(jsf); // persist the change – merge will eventually result in an update

Exception in thread "main" Local Exception Stack:
Exception [TOPLINK-3002] (Oracle TopLink Essentials – 10g release 4 (10.1.4.0.0) (Build 051215Dev)): oracle.toplink.essentials.exceptions.ConversionException
Exception Description: The object [1/3/06 8:43 PM], of class [class java.sql.Timestamp], from mapping [oracle.toplink.essentials.mappings.DirectToFieldMapping[version-->ALS_BOOKS.LAST_CHANGED]] with descriptor [RelationalDescriptor(nl.amis.als.ejb30.Book --> [DatabaseTable(ALS_BOOKS)])], could not be converted to [class java.math.BigDecimal].
    at oracle.toplink.essentials.exceptions.ConversionException.couldNotBeConverted(ConversionException.java:51)
    at oracle.toplink.essentials.internal.helper.ConversionManager.convertObjectToBigDecimal(ConversionManager.java:182)
    at oracle.toplink.essentials.internal.helper.ConversionManager.convertObject(ConversionManager.java:137)

I have tried changing the column’s data type to TIMESTAMP, which did not help. I also tried changing the type of the version attribute to java.sql.Date and java.util.Date. I kept on getting the Exception. So while we should be able to use a TIMESTAMP version attribute, in the current build of GlassFish it does not seem to be working. 

Note: I have edited this article late on Tuesday 3th January after having read some more on @Version, especially on the use of the Timestamp Data Type. The Public Draft of the EJB 3.0 Specification proved very helpful. See: http://jcp.org/aboutJava/communityprocess/pr/jsr220/index.html

Share.

About Author

Lucas Jellema, active in IT (and with Oracle) since 1994. Oracle ACE Director for Fusion Middleware. Consultant, trainer and instructor on diverse areas including Oracle Database (SQL & PLSQL), Service Oriented Architecture, BPM, ADF, Java in various shapes and forms and many other things. Author of the Oracle Press book: Oracle SOA Suite 11g Handbook. Frequent presenter on conferences such as JavaOne, Oracle OpenWorld, ODTUG Kaleidoscope, Devoxx and OBUG. Presenter for Oracle University Celebrity specials.

4 Comments

  1. Rod Macpherson on

    They have an @Timestamp annotation for optimistic locking. Specification states that
    @Version has to be numeric.