The best way to map an entity version property 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 what is the best way to map the entity version property with JPA and Hibernate.
Most often, we overlook the basic entity type mappings, focusing more on associations or querying options. However, basic types can also have a significant impact on application performance, especially if the type in question is used in many entity mappings.
The best way to map an entity version property with JPA and Hibernate with @vlad_mihalcea https://t.co/EoKKHozMf8 pic.twitter.com/kgL08pgtOB
— Java (@java) June 19, 2019
Version property and optimistic locking
As I previously explained, the entity version
property is used by the Hibernate optimistic locking mechanism to prevent lost updates.
However, a typical version
entity property mapping might look like this:
@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 }
Notice that the version
property is mapped as Java int
primitive. On the database side, the version
column is of the type integer
:
CREATE TABLE product ( id bigint NOT NULL, quantity integer NOT NULL, version integer NOT NULL, PRIMARY KEY (id) )
However, the integer
column takes 4 bytes, meaning that it can accommodate 4 294 967 295
values from the minimum value -2 147 483 648
to the maximum value of 2 147 483 647
.
Overflowing the version column value
The numeric column types act like circular buffers as, when overflowing, the transition is done from the maximum value to the minimum value.
For example, considering we have persisted the following Product
entity:
entityManager.persist( new Product() .setId(1L) .setQuantity(10) .setVersion(Integer.MAX_VALUE) );
Hibernate generates the following SQL INSERT statement:
INSERT INTO product ( quantity, version, id ) VALUES ( 10, 2147483647, 1 )
Now, when fetching and changing the Product
entity:
Product product = entityManager.find( Product.class, 1L ); assertEquals( Integer.MAX_VALUE, product.getVersion() ); product.setQuantity(9);
Hibernate increments the version
column value, so now the next value is -2147483648
:
SELECT p.id AS id1_0_0_, p.quantity AS quantity2_0_0_, p.version AS version3_0_0_ FROM product p WHERE p.id = 1 UPDATE product SET quantity = 9, version = -2147483648 WHERE id = 1 AND version = 2147483647
So, the value overflows when reaching the maximum value.
The best way to map the version property with JPA and Hibernate
However, using an integer
container for the version
property might be too much as the goal of the version
property is to ensure that, if the entity state changed, the never version
column value is different than the one we have loaded from the database upon fetching the entity.
If you want a detailed explanation of how the entity version property is used by the Hibernate optimistic locking mechanism when the read and write happen either in the scope of the same database transaction or in separate transactions and JPA Persistence Contextx, then you should definitely read this article.
Therefore, the version
column effectiveness lies in the probability of not finding the same entity version while the row itself has changed due to concurrent modifications.
So, theoretically, there can be so many changes in between the entity read and write so that, by the time we write the entity back to the database, the values have rotated to the very same value we read from the database.
In the case of an integer
column, we can accommodate 4294967294
changes in between the read and the write.
But, that’s way too much.
Even if we allow 100 concurrent database transactions and all these transactions modify the very same database record because only one transaction can modify a record at a time, the probability of reaching the same value due to overflowing is given by the average transaction time.
So, even if the average transaction time is just 10 ms, it will take 42,949,672.96
seconds to reach the same version
column value. That’s 497
days.
However, between the read and the write of a database record, there will surely be way less than 497
days. Even if the read and the write happen in separate database transactions like in the following example:
The user’s think-time might be limited by the HTTP session timeout period anyway, and even for batch processing tasks, the time between a read and a successive write will not take very long, even if retries are being employed.
Therefore, we can choose a short
or smallint
column type for the version
property:
@Version private short version;
The short
column takes 2 bytes and accommodates 65,536
possible values. So, with an average transaction of just 10 ms, it will take 655
seconds before we hit the very same value we read previously, but which has been changed 65,536
times ever since.
You must make sure that the Java
version
attribute and the database column type match perfectly. So, if you are using ashort
primitive orShort
Java object wrapper, then the database column must besmallint
.You should never use the
UNSIGNED
column type for theversion
column as this property is meant to operate like a circular buffer, and theUNSIGNED
column types might throw out-of-range and overflow errors, so don’t useUNSIGNED
columns for theversion
entity attribute.
However, in reality, the average transaction time is higher than 10 ms, and not every transaction wants to modify the same record we have previously read before. Therefore, the time it takes to modify an entity 65,536
times will take way more than 655
seconds.
I'm running an online workshop on the 11th of October about High-Performance SQL.If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.
Conclusion
Using a short
primitive type for the entity version
property and a smallint
column type on the database side is usually a much better choice than opting for an int
or a long
property.
By using a more compact column type, we can save up space both on disk and in memory. This way, the database server can accommodate more entity records per in-memory page, so more rows can be ultimately stored in the buffer pool.
So, the best way to map an entity version property with JPA and Hibernate is to choose the right column type based on how often the entity is modified. Most often, a smallint
type is sufficient. If the entity is rarely changed and the database supports a tinyint
column type (e.g. MySQL), then you should choose a byte
version type which will still allow you to benefit from the optimistic locking mechanism while only adding a one-byte overhead to the underlying row storage.
