Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Optimistic locking

Anna and Oleg open the same post for editing at the same time. Anna saves first, then Oleg saves his version — and Anna's changes silently disappear. This is called a lost update. Optimistic locking prevents it without locking anything in the database: each row carries a version number, and an update succeeds only if the version has not changed since the row was read.

The version field

// src/main/java/com/example/blog/Post.java
package com.example.blog;
 
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Version;
 
@Entity
public class Post {
 
    @Id
    @GeneratedValue
    private Long id;
 
    private String title;
 
    @Version
    private int version;
 
    // constructors, getTitle(), setTitle(), getVersion()
}

That is all the configuration. Hibernate fills the field itself; your code never changes it.

Two editors

// src/main/java/com/example/blog/Main.java
emf.runInTransaction(em -> em.persist(new Post("Optimistic locking")));
 
EntityManager anna = emf.createEntityManager();
EntityManager oleg = emf.createEntityManager();
anna.getTransaction().begin();
oleg.getTransaction().begin();
 
Post annasCopy = anna.find(Post.class, 1L);
Post olegsCopy = oleg.find(Post.class, 1L);
IO.println("Both read version " + annasCopy.getVersion());
 
annasCopy.setTitle("Optimistic locking in Hibernate");
anna.getTransaction().commit();
IO.println("Anna saved, version is now " + annasCopy.getVersion());
 
olegsCopy.setTitle("Locking without locks");
try {
    oleg.getTransaction().commit();
} catch (RollbackException e) {
    if (e.getCause() instanceof OptimisticLockException) {
        IO.println("Oleg: the post was changed by someone else");
    }
} finally {
    anna.close();
    oleg.close();
}

Two EntityManager objects play the role of two users. Output:

Hibernate: create table Post (version integer not null, id bigint not null, title varchar(255), primary key (id))
Hibernate: insert into Post (title,version,id) values (?,?,?)
Hibernate: select p1_0.id,p1_0.title,p1_0.version from Post p1_0 where p1_0.id=?
Hibernate: select p1_0.id,p1_0.title,p1_0.version from Post p1_0 where p1_0.id=?
Both read version 0
Hibernate: update Post set title=?,version=? where id=? and version=?
Anna saved, version is now 1
Hibernate: update Post set title=?,version=? where id=? and version=?
Oleg: the post was changed by someone else

How it works

Look at the update: where id=? and version=?. Anna's update ran with version = 0, found the row and set the version to 1. Oleg's update also searched for version = 0, but the row already had 1, so zero rows were changed. Hibernate noticed this and threw an exception. The full message:

jakarta.persistence.OptimisticLockException: Unexpected row count (expected row count 1 but was 0) [update Post set title=?,version=? where id=? and version=?] for entity [com.example.blog.Post with id '1']

What to do with it is a business decision: show Oleg the current text and ask him to repeat the edit, or merge the changes.

Edits from a form

In a web application the version must travel with the form. If the edited object is detached and passed to merge, Hibernate compares its version with the database:

// src/main/java/com/example/blog/Main.java
Post form = emf.callInTransaction(em -> em.find(Post.class, 1L));
emf.runInTransaction(em -> em.find(Post.class, 1L).setTitle("Changed by Oleg"));
form.setTitle("Changed by Anna");
emf.runInTransaction(em -> em.merge(form));
Exception in thread "main" jakarta.persistence.OptimisticLockException: Row was already updated or deleted by another transaction for entity [com.example.blog.Post with id '1']

Common mistake

A popular way to save a form is to load a fresh entity with find and copy the form fields into it. The fresh entity has the current version, so the check always passes — and the lost update is back. If you copy fields, also send the version in a hidden form field and compare it with the entity's version yourself before copying.

Thanks for reading!

Donate to the site — we work hard for you.