Оптимистическая блокировка
Анна и Олег одновременно открыли одну статью для редактирования. Анна сохраняет первой, потом Олег сохраняет свой вариант — и правки Анны молча пропадают. Это называется потерянным обновлением (lost update). Оптимистическая блокировка предотвращает его, ничего не блокируя в базе: у каждой строки есть номер версии, и изменение проходит, только если версия не поменялась с момента чтения.
Поле версии
// 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;
// конструкторы, getTitle(), setTitle(), getVersion()
}Это вся настройка. Поле заполняет Hibernate, ваш код его никогда не меняет.
Два редактора
// src/main/java/com/example/blog/Main.java
emf.runInTransaction(em -> em.persist(new Post("Оптимистическая блокировка")));
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("Оба прочитали версию " + annasCopy.getVersion());
annasCopy.setTitle("Оптимистическая блокировка в Hibernate");
anna.getTransaction().commit();
IO.println("Анна сохранила, теперь версия " + annasCopy.getVersion());
olegsCopy.setTitle("Блокировка без блокировок");
try {
oleg.getTransaction().commit();
} catch (RollbackException e) {
if (e.getCause() instanceof OptimisticLockException) {
IO.println("Олег: статью уже изменил кто-то другой");
}
} finally {
anna.close();
oleg.close();
}Два объекта EntityManager играют роль двух пользователей. Вывод:
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=?
Оба прочитали версию 0
Hibernate: update Post set title=?,version=? where id=? and version=?
Анна сохранила, теперь версия 1
Hibernate: update Post set title=?,version=? where id=? and version=?
Олег: статью уже изменил кто-то другойКак это работает
Посмотрите на update: where id=? and version=?. Изменение Анны выполнилось с version = 0, нашло строку и записало версию 1. Изменение Олега тоже искало version = 0, но в строке уже была 1, поэтому изменилось ноль строк. Hibernate это заметил и бросил исключение. Полный текст:
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']Что с ним делать — решение предметной области: показать Олегу текущий текст и попросить повторить правку или объединить изменения.
Правки из формы
В веб-приложении версия должна путешествовать вместе с формой. Если отредактированный объект отсоединён и передан в merge, Hibernate сравнит его версию с базой:
// 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("Изменил Олег"));
form.setTitle("Изменила Анна");
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']Частая ошибка
Популярный способ сохранить форму — загрузить свежую сущность через find и скопировать в неё поля формы. У свежей сущности текущая версия, поэтому проверка всегда проходит — и потерянное обновление возвращается. Если копируете поля, передавайте версию в скрытом поле формы и перед копированием сами сравнивайте её с версией сущности.