Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Что такое @Version
@Version — это аннотация из Hibernate/JPA, которая используется для реализации оптимистичной блокировки (optimistic locking) при работе с базой данных. Это важный инструмент для предотвращения race conditions и конфликтов при одновременном редактировании одного и того же объекта несколькими потоками или процессами.
Как это работает
Аннотация @Version добавляет в таблицу специальное поле, которое автоматически увеличивается при каждом обновлении записи:
import javax.persistence.*;
@Entity
@Table(name = "accounts")
public class Account {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private BigDecimal balance;
@Version
private Long version;
// getters и setters
}
Когда Hibernate сохраняет сущность обратно в БД, он проверяет текущую версию и выбрасывает исключение OptimisticLockException, если версия не совпадает.
Пример с race condition
Представь, что два потока одновременно читают и меняют один счёт:
@Transactional
public void transferMoney(Long accountId, BigDecimal amount) {
// Поток 1: читает счёт (version = 1)
Account account = accountRepository.findById(accountId);
// version = 1, balance = 1000
// Поток 2: тоже читает счёт (version = 1)
// version = 1, balance = 1000
// Поток 1: обновляет счёт (balance = 900)
account.setBalance(account.getBalance().subtract(amount));
accountRepository.save(account);
// version становится 2 в БД
// Поток 2: пытается сохранить (balance = 900)
account.setBalance(account.getBalance().subtract(amount));
accountRepository.save(account);
// OptimisticLockException! version в БД = 2, а в объекте = 1
}
Обработка OptimisticLockException
Нужно ловить исключение и обрабатывать конфликт:
@Transactional
public void transferMoneyWithRetry(Long accountId, BigDecimal amount) {
int maxRetries = 3;
int retryCount = 0;
while (retryCount < maxRetries) {
try {
Account account = accountRepository.findById(accountId);
account.setBalance(account.getBalance().subtract(amount));
accountRepository.save(account);
return; // Успех
} catch (OptimisticLockException e) {
retryCount++;
if (retryCount >= maxRetries) {
throw new TransferFailedException(
"Failed to transfer money after " + maxRetries + " attempts"
);
}
// Ждём немного перед повтором
try {
Thread.sleep(100 * retryCount);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new TransferFailedException("Interrupted during retry");
}
}
}
}
Типы версий
1. Числовой тип (Integer, Long):
@Version
private Long version;
Это самый распространённый и рекомендуемый подход. Версия увеличивается на 1 при каждом обновлении.
2. Временной тип (Timestamp):
@Version
private Timestamp version;
Используется редко, может быть проблемнее из-за precision.
Как Hibernate использует @Version
При обновлении Hibernate генерирует SQL типа:
UPDATE accounts
SET balance = 900, version = 2
WHERE id = 1 AND version = 1;
Этот SQL выполняет atomic check-and-update операцию:
- Если версия в БД всё ещё 1 — обновляет и увеличивает версию на 1
- Если версия отличается — ничего не обновляет (0 rows affected)
- Hibernate видит, что 0 строк обновлено, и выбрасывает исключение
Плюсы и минусы
Плюсы:
- Не требует pessimistic locks (блокировок БД)
- Позволяет лучше масштабировать (много читающих потоков)
- Подходит для сценариев, где конфликты редкие
- Простая реализация
Минусы:
- Если конфликты частые — много retry-ев
- Требует обработки OptimisticLockException в коде
- Не подходит для критичных операций, требующих гарантированного завершения
Когда использовать @Version
✅ Используй @Version когда:
- Одновременные обновления возможны, но редкие
- Чтение намного чаще, чем запись
- Приложение может обработать retry-механизм
- Не нужна гарантированная последовательность операций
❌ Не используй @Version когда:
- Конфликты очень частые
- Нужна strong consistency
- Критичная финансовая операция без retry-механизма
Лучшие практики
1. Всегда обрабатывай исключение:
@Transactional
public void updateEntity(Long id, String newValue) {
try {
Entity entity = repository.findById(id).orElseThrow();
entity.setValue(newValue);
repository.save(entity);
} catch (OptimisticLockException e) {
logger.warn("Update conflict, retrying...");
// Retry logic
}
}
2. Не пытайся повторно использовать старый объект:
// Плохо
Entity entity = repository.findById(id);
entity.setValue("new");
repository.save(entity); // Может выбросить OptimisticLockException
repository.save(entity); // Используешь тот же объект
// Хорошо
Entity entity = repository.findById(id);
entity.setValue("new");
repository.save(entity);
// Если конфликт - перечитай заново
3. Используй с трансакциями:
@Transactional // ОБЯЗАТЕЛЬНО!
public void update(Long id, String newValue) {
Entity entity = repository.findById(id);
entity.setValue(newValue);
repository.save(entity);
}
@Version — это элегантное решение для обработки конкурентности без блокировок на уровне БД. Это одна из главных фич Hibernate для масштабируемости.