Optimistic locking with JPA and Hibernate

Imagine having a tool that can automatically detect JPA and Hibernate performance issues. Wouldn’t that be just awesome?

Well, Hypersistence Optimizer is that tool! And it works with Spring Boot, Spring Framework, Jakarta EE, Java EE, Quarkus, or Play Framework.

So, enjoy spending your time on the things you love rather than fixing performance issues in your production system on a Saturday night!

Introduction

In this article, we are going to see how optimistic locking version property works when using JPA and Hibernate.

Most often, we overlook basic concepts and focus only on more advanced topics such as associations or queries, without realizing that basic mappings can also have a significant impact when it comes to persistence effectiveness and efficiency.

Lost updates anomaly

Relational database systems have emerged in the time of mainframe systems when the client was interacting with the database from a terminal using a dedicated connection. Back then, when the client needed to operate on the database, a session was opened and all the reads and write happened in the scope of the same database transaction.

This way, the concurrency control mechanism could ensure that reads and writes coming from multiple concurrent clients can be properly interleaved so that they don’t break Serializability.

However, this session-based database interaction is no longer the norm, especially when accessing services over the Internet. This is because you can no longer hold onto a database connection across multiple HTTP requests. For this reason, the ACID guarantees no longer hold when your application-level transaction spans over multiple HTTP requests as well as physical database transactions.

One example of an anomaly that can happen because reads and writes happen in different database transactions is the lost update phenomenon.

To understand the lost update anomaly, consider the following example:

Application-level transaction lost update anomaly

The flow of actions happens like this:

  1. Alice loads a Product having a quantity of 5.
  2. Right after, a warehouse batch process updates the Product quantity to 0.
  3. Alice decides to buy the Product, so when the quantity is decreased, we get a negative quantity value.

The lost update happens because Alice thinks there are still products available while, in reality, there’s no product left for purchase.

Optimistic locking version property

To address this issue, a @Version column can be used:

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

    @Id
    private Long id;

    private int quantity;

    @Version
    private int version;

    //Getters and setters omitted for brevity
}

Now, when the Product entity is fetched:

Product product = entityManager.find(
    Product.class, 
    1L
);

The version property is set to the value found in the associated product table record at the moment the entity was loaded.

Now, when the Product entity is changed:

product.setQuantity(0);

Hibernate is going to include the version column in the row filtering criteria:

UPDATE 
    product 
SET 
    quantity = 0, version = 2 
WHERE 
    id = 1 AND version = 1

Notice that the version column is set to the next value while the previous value that was read when the entity was fetched from the database is used to filter the record.

So, if the version column value has changed, the UPDATE will not take place and a value of 0 will be returned by the executeUpdate JDBC PreparedStatement method.

When Hibernate reads an update count value of 0, and a javax.persistence.OptimisticLockException will be thrown.

The version property is also used for the DELETE SQL statement, so, if we remove the Product entity:

Product product = entityManager.getReference(
    Product.class, 
    1L
);

entityManager.remove(product);

Hibernate executes the following DELETE statement:

DELETE FROM 
    product 
WHERE 
    id = 1 AND 
    version = 1

Notice that the version column is used in the WHERE clause to make sure that we are deleting the very same entity snapshot that we have previously read from the database.

Now, back to our previous example, if we are now using a version column, we can prevent the lost update as illustrated by the following diagram:

Application-level transaction prevents lost update using a version column

This time, the flow of actions happens like this:

  1. Alice loads a Product having a quantity of 5.
  2. Right after, the warehouse batch process updates the Product quantity to 0.
  3. Alice decides to buy the Product, so she decreases the quantity and tries to execute the UPDATE.
  4. The UPDATE is prevented because the Product version is no longer 1, and Hibernate throws an OptimisticLockException.

Cool, right?

I'm running an online workshop on the 20-21 and 23-24 of November about High-Performance Java Persistence.

If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.

Conclusion

Knowing how the optimistic locking version property works is very important when using JPA and Hibernate, as it allows you to prevent the lost update anomaly when a given entity is being modified by multiple concurrent users.

For more details about the lost update phenomenon, and how the database prevents it in the scope of a single database transaction, check out the following article.

Transactions and Concurrency Control eBook

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.