Follow @vlad_mihalcea Imagine having a tool that can automatically detect if you are using JPA and Hibernate properly. Hypersistence Optimizer is that tool!

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.

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.

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.

Insert details about how the information is going to be processed DOWNLOAD NOW