The best way to map an entity version property with JPA and Hibernate

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.

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:

Application-level transaction prevents lost update using a version column

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.

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.

FREE EBOOK

5 Comments on “The best way to map an entity version property with JPA and Hibernate

  1. The negative version of a version field is counterintuitive.

    What is the version of this entity?
    It is… -3.
    What? Negative? It is some bug in a rollback procedure?

    Disk space is rather cheap. A bug caused by counterintuitive code, however, may result in significant costs. So, IMO you should avoid overflowing a version column value. Short is still fine if you expect that none entity will be updated more than 32766 times (which seems to be a resonable limit for many cases). But if you expect more updates would, then int looks like a better option, even if a number of a concurent transaction still couldn’t generate problems with optimistic locking.

    • The optimistic locking version has absolutely no meaning outside of preventing lost updates. If you need entity versioning, then you can use audit logging via Debezium or Envers. Those show you the version of an entity with its data snapshot.

      • If you have the field named version, then most of the code readers expect that there is never decremented after commit, even if it is used currently only by optimistic currency control. By the way, the regular version number is a nice piece of information to have. Even if you don’t use it is not used by app directly, then it still could help to analyze logs, etc. If you still want to save the few bytes, IMHO versionStamp (or something better) looks like a better name to me.

      • The SQL log entry already contain the timestamp if you use datasource-proxy. Anyway, you might think that savings 2 bytes is negligible, but if you have billions of rows, you can save GB of space.

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.