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.
Optimistic locking version property with JPA and Hibernate@vlad_mihalcea https://t.co/mcESSRxB48 pic.twitter.com/oOHIgkhZDl
— Java (@java) June 13, 2019
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:
The flow of actions happens like this:
- Alice loads a
Product
having a quantity of 5. - Right after, a warehouse batch process updates the
Product
quantity to0
. - 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:
This time, the flow of actions happens like this:
- Alice loads a
Product
having a quantity of 5. - Right after, the warehouse batch process updates the
Product
quantity to0
. - Alice decides to buy the
Product
, so she decreases the quantity and tries to execute the UPDATE. - The UPDATE is prevented because the Product
version
is no longer1
, and Hibernate throws anOptimisticLockException
.
Cool, right?
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.
