Optimistic lock with JPA and 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
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!
Delete comment
Confirm delete comment
Pham Duc Minh
Da Nang, Vietnam