JPA entity versioning (@Version and Optimistic Locking)

29 July 2015
By Gonçalo Marques
This article covers database optimistic locking in general and also demonstrates how to implement optimistic locking in Java Persistence API (JPA) entities by using the @Version annotation

Introduction

Optimistic locking is a mechanism that prevents an application from being affected by the "lost update" phenomenon in a concurrent environment while allowing some high degree of concurrency at the same time. This is achieved without actually resorting to any locks: Optimistic locking relies on checks that are made against the existing data on the data store by transactions during update time.

The main concept is to check if another existing transaction has made any concurrent changes against the same data that a given transaction may be trying to change at a given time. If that concurrent changes happen to exist, the current transaction aborts because there is the possibility of the updates made by the other transaction being lost (or not taken into account).

This article covers optimistic locking in general and also describes how to use it in Java Persistence API (JPA).

Database locking at row level

In order to properly understand optimistic locking it's essential to know how a relational database locks rows during update execution. Considering the following table:


PRODUCT:

ID DESCRIPTION PRICE
1 Book 11.00
2 Television 99.00

If two distinct transactions try to update the same record, the transaction that lastly executes the modifying update statement will become locked in that same statement until the first transaction finishes its work (commit or rollback). Practically speaking:

TRANSACTION A:
UPDATE PRODUCT
SET PRICE = 12.00
WHERE ID = 1;

TRANSACTION B:
UPDATE PRODUCT
SET PRICE = 14.00
WHERE ID = 1;

If transactions A and B happen to execute the above respective statements concurrently during their execution time, one of them will certainly become locked on that update statement until the other transaction completes (commit or rollback). This is because they are updating the same record (ID = 1). The database will lock all modifications on that record until the transaction that holds the lock finishes its work.

Optimistic locking

Now that we understand how relational databases lock records being updated during transaction execution time, we are ready to proceed to optimistic locking mechanism description. As we will see right away, another important concept that is used in optimistic locking is the updated rows count that databases return on every update statement execution.

We mentioned in the introduction that optimistic locking relies on data comparison during update execution. This means that we will need a field (or set of fields) that may be used for this, and in this example we will use an additional field called VERSION:


PRODUCT:

ID DESCRIPTION PRICE VERSION
1 Book 11.00 3
2 Television 99.00 7

In order for optimistic locking to work correctly, any transaction that decides to update a given record must read that record first. This is because the transaction must know what is the current VERSION field value in order to use it later in the update statement.

Supposing that a given transaction needs to update the PRODUCT which ID is equal to 1 in our example. First, it will read the record in order to know the current VERSION value, and then it will use the value in the update statement in a couple of ways: it will increment the VERSION to the next value and will also check if the current VERSION is still the same that it has read in the first place:

UPDATE PRODUCT
SET PRICE = 14.00, VERSION = 4
WHERE ID = 1
AND VERSION = 3;

Since the database will return the updated rows count, if some other transaction happened to change (and commit) the record in the meanwhile, and obviously also incremented the VERSION field, the WHERE clause will not match and our update will return zero rows updated because the VERSION field value will not be 3 any more, but instead the value that the other transaction has set. This means that a concurrent update has taken place and the application must abort the current transaction, because this would result in a lost update: the one made by the other transaction.

This only applies if our system is not tolerant to the lost update phenomenon because of some business requirement, of course. There are many systems where the lost update is simply not a problem, so optimistic locking does not apply.

Going back to our example, if no other transaction has changed the record in the meanwhile, the VERSION value will still be 3, so the updated row count returned by the database will be the expected: 1. This way the application knows that no other concurrent update has taken place and may proceed to safely commit the changes.

Keep in mind that, as we have seen previously in this article, if a couple of distinct transactions try to update the same record, one of them will become locked in the update statement until the other transaction finishes its work, so when the locked transaction is finally able to proceed, it will see the updated VERSION field by the other transaction in order to perform the optimistic lock check.

Optimistic Locking in JPA

Implementing the optimistic locking in JPA is quite simple. You just have to configure your entities with a field annotated with @Version. The JPA provider will take care of issuing update statements in the form we have seen previously (incrementing and checking the version field in each update of a versioned entity) and will also automatically check for the updated row count in order to detect concurrent updates.

If a conflict is detected the JPA provider will throw a javax.persistence.OptimisticLockException.

There are other types of locking available in JPA (both optimistic and pessimistic). You can find a detailed description of those locking mechanisms in the following article: Locking in JPA (LockModeType).

A possible configuration of an entity that models our versioned PRODUCT table could look like the following:

Product entity

@Entity
@Table(name = "PRODUCT")
public class Product {

  @Id
  @Column(name = "ID")
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @NotNull
  @Column(name = "DESCRIPTION")
  private String description;

  @NotNull
  @Column(name = "PRICE", precision = 9, scale = 2)
  private BigDecimal price;

  @Version
  @Column(name = "VERSION")
  private Integer version;

}

Depending on your JPA provider, you may also use a Version field of type Date, but don't forget to actually map it into a TIMESTAMP and actually check if your database is correctly storing the timestamp with milliseconds precision. You could configure the version field as a timestamp like the following:

Timestamp version field

@Version
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "VERSION")
private Date version;
  

Sometimes the Version field in the timestamp format is also used to store the last update time of a given record (or entity) in a column named, for example, LAST_UPDATED (instead of a column named VERSION for obvious semantic reasons). It's really up to one's needs.

Additional considerations

Under some circumstances, problems may occur when versioned updates are used together with batch updates. It has happened to me in the past with a given version of Oracle 11g and Hibernate, for example. The Oracle JDBC driver was not able to extract the correct number of updated rows count in JDBC batch statements execution.

If you are also facing this problem, you may check if you have set the Hibernate property hibernate.jdbc.batch_versioned_data to true. When this setting is true, Hibernate will use batch updates even for updates that are made against versioned data, ie. updates that need to use the updated rows count in order to check for concurrent updates. Since the JDBC driver may not return the correct updated rows count, Hibernate will not be able to detect if concurrent updates actually happened. The default value for this setting in Hibernate is false, so it will not use batch updates when it detects that versioned data updates are going to be executed in a given flush operation.

This is of course a specific scenario with Oracle 11g that is easily worked around, as we have just seen.

Related Articles

Comments

About the author
Gonçalo Marques is a Software Engineer with several years of experience in software development and architecture definition. During this period his main focus was delivering software solutions in banking, telecommunications and governmental areas. He created the Bytes Lounge website with one ultimate goal: share his knowledge with the software development community. His main area of expertise is Java and open source.

GitHub profile: https://github.com/gonmarques

He is also the author of the WiFi File Browser Android application: