Prevent database concurrency with @Version JPA and Hibernate

docvominh

Pham Duc Minh

Posted on May 29, 2022

Prevent database concurrency with @Version JPA and Hibernate

When working on a project of big business company, sometime someone will ask "What happens when 2 users update the same record in the database?"

It sound confuse for the new developer, but very easy to avoid it.

In Java Persistence API (JPA), there's @version annotation that will help you to check how many time the database's record has been updated

Let's have a look at these simple table and entity

create table device
(
    id      integer not null constraint device_pk primary key,
    serial  integer,
    name    varchar(255),
    version integer
)

Enter fullscreen mode Exit fullscreen mode

And the entity

package com.vominh.example.entity;

import javax.persistence.*;

@Entity
@Table(name = "device")
public class DeviceWithVersionEntity {
    @Id
    @Column(name = "id")
    private Integer id;
    @Column(name = "serial")
    private Integer serial;
    @Column(name = "name")
    private String name;
    @Version
    @Column(name = "version")
    private int version;

    public Integer getId() {
        return id;
    }
    public void setId(Integer id) {
        this.id = id;
    }

    public Integer getSerial() {
        return serial;
    }
    public void setSerial(Integer serial) {
        this.serial = serial;
    }

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }

    public int getVersion() {
        return version;
    }

    public void setVersion(int version) {
        this.version = version;
    }
}
Enter fullscreen mode Exit fullscreen mode

The part can understand like "This field will increase by 1 when someone perform update action on it"

@Version
@Column(name = "version")
private int version;
Enter fullscreen mode Exit fullscreen mode

Main class

package com.vominh.example;

import com.vominh.example.entity.DeviceWithVersionEntity;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.cfg.Configuration;
import org.hibernate.service.ServiceRegistry;

import java.util.Random;
import java.util.concurrent.CompletableFuture;

public class ConcurrencyControl {
    private static final SessionFactory sessionFactory;
    private static final ServiceRegistry serviceRegistry;

    static {
        Configuration configuration = new Configuration();
        configuration.configure("hibernate.cfg.xml");

        // Since Hibernate Version 4.x, ServiceRegistry Is Being Used
        serviceRegistry = new StandardServiceRegistryBuilder().applySettings(configuration.getProperties()).build();
        sessionFactory = configuration.buildSessionFactory(serviceRegistry);
    }

    public static void main(String[] args) {
        Session session = sessionFactory.openSession();
        Query deleteAllDevice = session.createQuery("delete from DeviceWithVersionEntity");

        try {
            session.beginTransaction();
            deleteAllDevice.executeUpdate();

            DeviceWithVersionEntity device = new DeviceWithVersionEntity();
            device.setId(1);
            device.setSerial(8888);
            device.setName("Dell xps 99");

            session.save(device);

            session.getTransaction().commit();
        } catch (Exception e) {
            e.printStackTrace();
            session.getTransaction().rollback();
        } finally {
            session.close();
        }

        // open 50 session in 50 thread to update one record
        for (int i = 0; i < 50; i++) {
            CompletableFuture.runAsync(() -> {
                var s = sessionFactory.openSession();
                try {
                    s.beginTransaction();

                    DeviceWithVersionEntity d = (DeviceWithVersionEntity) s.load(DeviceWithVersionEntity.class, 1);
                    d.setName((new Random()).nextInt(500) + "");
                    s.save(d);

                    s.getTransaction().commit();
                } catch (Exception e) {
                    e.printStackTrace();
                    s.getTransaction().rollback();
                } finally {
                    s.close();
                }
            });
        }
    }
}

Enter fullscreen mode Exit fullscreen mode
  • First part save a record to database
  • Second part create 50 thread & hibernate session then try to update the inserted record

Execute result will throw a lot of org.hibernate.StaleObjectStateException

Execute result
The exception message already explain what happens, but how and when?

Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect)

If @version add to a field of Entity (number), when the create/update operation execute, Hibernate will set/increase value of @version field.

UPDATE device set name = 'new name', version = version + 1
Enter fullscreen mode Exit fullscreen mode

When the next UPDATE happens, Hibernate will check if the version is matched by add a condition to WHERE clause

UPDATE device SET name = 'new name', version = version + 1
WHERE id = ? AND version = **CURRENT_VERSION**
Enter fullscreen mode Exit fullscreen mode

if another session already update the record and version had increased, WHERE clause won't match and exception throw

This method also called Optimistic Locking and it's easy to implement with JPA and Hibernate (which handle the heavy logic)

Source code available at: https://github.com/docvominh/java-example/tree/master/hibernate-jpa
I use maven, hibernate 4.3 and postgres sql

Thank you for your reading!

💖 💪 🙅 🚩
docvominh
Pham Duc Minh

Posted on May 29, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related