Optimistic lock with JPA and Hibernate

java hibernate

Dec 31 2023 14:51

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

It sound confusing 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 times the database's record has been updated

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

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

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;
    }
}

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;

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();
                }
            });
        }
    }
}
  • 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

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**

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: Github

I use maven, hibernate 4.3 and postgres sql

Thank you for your reading!

me

Pham Duc Minh

Da Nang, Vietnam