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 elseHow 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.