← Назад к вопросам

Что такое @Version?

1.7 Middle🔥 161 комментариев
#ORM и Hibernate#Базы данных и SQL

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Что такое @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 для масштабируемости.