Что такое @Version и зачем она нужна
@Version — аннотация JPA для реализации оптимистичной блокировки. Она автоматически отслеживает версию сущности и предотвращает конфликтующие обновления от нескольких транзакций.
Обзор
@Version — аннотация JPA для реализации оптимистичной блокировки. Она автоматически отслеживает версию сущности и предотвращает конфликтующие обновления от нескольких транзакций.
🟢 Junior Level
Что такое @Version
@Version — аннотация для оптимистичной блокировки. Автоматически отслеживает версию сущности и предотвращает конфликтующие обновления.
@Entity
public class Order {
@Id
private Long id;
@Version
private Integer version; // увеличивается автоматически
private String status;
}
Зачем нужна
Когда две транзакции обновляют один объект, @Version гарантирует что одна из них получит ошибку:
Initial: Order(id=1, version=1, status="new")
Транзакция 1: читает (version=1)
Транзакция 2: читает (version=1)
Транзакция 2: обновляет → UPDATE WHERE version=1 → version=2 ✅
Транзакция 1: обновляет → UPDATE WHERE version=1 → 0 rows → ❌ OptimisticLockException
Как это работает — просто
1. При INSERT: version = 0. При каждом UPDATE: version = version + 1.
2. При каждом UPDATE: version = version + 1
3. WHERE clause: WHERE id = ? AND version = ?
4. Если version не совпал → 0 rows updated → OptimisticLockException
🟡 Middle Level
Типы полей version
@Version
private Integer version; // int — увеличивается на 1
@Version
private Long version; // long — увеличивается на 1
@Version
private Timestamp version; // timestamp — обновляется текущим временем
Детальный пример
@Entity
public class Product {
@Id
private Long id;
@Version
private Integer version;
private String name;
private int stock;
}
// Транзакция A
Product pA = em.find(Product.class, 1L); // version=1, stock=10
// Транзакция B
Product pB = em.find(Product.class, 1L); // version=1, stock=10
pB.setStock(8);
em.flush(); // UPDATE SET stock=8, version=2 WHERE id=1 AND version=1 ✅
// Транзакция A
pA.setStock(5);
em.flush(); // UPDATE SET stock=5, version=2 WHERE id=1 AND version=1
// → 0 rows → OptimisticLockException!
// stock=8 (от B) не перезаписан
Типичные ошибки
// ❌ Ручное изменение version
order.setVersion(0); // ❌ сломает механизм блокировки
// ❌ Нет @Version для важных данных
@Entity
public class Account {
// нет @Version → два потока могут одновременно изменить баланс
}
// ❌ Игнорирование OptimisticLockException
try {
em.flush();
} catch (OptimisticLockException e) {
// молча проигнорировали → данные потеряны
}
🔴 Senior Level
Внутренняя реализация
Hibernate:
- При INSERT: version = 1 (или 0 для некоторых типов)
- При UPDATE: version = version + 1
- WHERE clause: WHERE id = ? AND version = ?
- Если 0 rows → OptimisticLockException
Для Timestamp:
- version = CURRENT_TIMESTAMP
- WHERE clause: WHERE id = ? AND version = ?
- Timestamp-версия опасна: если две транзакции обновят сущность в одну миллисекунду, conflict НЕ обнаружится. В production используйте Integer/Long, а не Timestamp.
OPTIMISTIC_FORCE_INCREMENT
// Увеличить version без изменения entity
em.lock(order, LockModeType.OPTIMISTIC_FORCE_INCREMENT);
// Когда нужно:
// - При изменении дочерней коллекции
// - Для принудительной инвалидации кэша
// - Когда entity не меняется, но нужно зафиксировать изменение
Audit с @Version
@Entity
@EntityListeners(AuditingEntityListener.class)
public class Order {
@Id
private Long id;
@Version
private Integer version;
@LastModifiedDate
private LocalDateTime updatedAt;
// version для optimistic locking
// updatedAt для аудита (когда было последнее изменение)
}
Performance implications
@Version overhead:
- +1 column в таблице
- +1 condition в WHERE при UPDATE
- Практически negligible
Benefits:
- Предотвращение lost updates
- Нет блокировок (в отличие от pessimistic)
- Автоматическое управление
Production паттерны
// Pattern 1: Retry с @Version
@Retryable(value = OptimisticLockException.class, maxAttempts = 3)
@Transactional
public Order updateOrder(OrderDto dto) {
Order order = entityManager.find(Order.class, dto.id());
order.setStatus(dto.status());
return order;
}
// Pattern 2: Refresh and retry
@Transactional
public Order updateWithRefresh(OrderDto dto) {
Order order = entityManager.find(Order.class, dto.id());
try {
order.setStatus(dto.status());
entityManager.flush();
return order;
} catch (OptimisticLockException e) {
entityManager.refresh(order); // получить актуальные данные
order.setStatus(dto.status()); // применить к актуальным
entityManager.flush();
return order;
}
}
Best Practices
✅ @Version на всех mutable сущностях
✅ Retry при OptimisticLockException
✅ Не менять version вручную
✅ Мониторинг частоты конфликтов
✅ OPTIMISTIC_FORCE_INCREMENT когда нужно
❌ Без @Version для критических данных
❌ Ручное управление version
❌ Игнорирование OptimisticLockException
❌ Пессимистичная блокировка без причины
🎯 Шпаргалка для интервью
Обязательно знать:
- @Version — аннотация для оптимистичной блокировки, автоматически увеличивает version при UPDATE
- WHERE clause: WHERE id = ? AND version = ? — если 0 rows → OptimisticLockException
- Типы: Integer/Long (увеличиваются на 1), Timestamp (обновляется текущим временем, опасен)
- При INSERT: version = 0, при первом UPDATE: version = 1
- OPTIMISTIC_FORCE_INCREMENT — увеличивает version без изменения entity
- @Version на всех mutable сущностях — предотвращает lost updates
Частые уточняющие вопросы:
- Почему Timestamp опасен для @Version? Два обновления в одну миллисекунду — конфликт не обнаружится
- Когда OPTIMISTIC_FORCE_INCREMENT? Изменение дочерней коллекции, инвалидация кэша, entity не меняется но нужно зафиксировать
- Performance overhead @Version? +1 column, +1 condition в WHERE — практически negligible
- Можно ли менять version вручную? Нет — сломает механизм блокировки
Красные флаги (НЕ говорить):
- «Timestamp для @Version в production» — конфликт в одну миллисекунду не обнаружится
- «Ручное изменение version» — сломает optimistic locking
- «Без @Version для Account/Balance» — lost updates возможны
- «Игнорирую OptimisticLockException» — данные потеряны молча
Связанные темы:
- [[17. Как реализовать оптимистичную блокировку в JPA]]
- [[18. Как реализовать пессимистичную блокировку в JPA]]
- [[15. Что делает метод refresh()]]
- [[20. Как работают каскадные операции (Cascade)]]