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

Зачем нужна @Version в JPA?

2.0 Middle🔥 181 комментариев
#ORM и Hibernate

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

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

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

Ответ: Аннотация @Version в JPA и оптимистичная блокировка

@Version — это аннотация в JPA (Java Persistence API), которая реализует механизм оптимистичной блокировки (optimistic locking) для предотвращения конфликтов при одновременном обновлении одного и того же объекта несколькими потоками или транзакциями.

Проблема: конфликты при одновременном обновлении

Представьте ситуацию:

// Поток 1 и Поток 2 одновременно обновляют один User
User user = userRepository.findById(1);
// user имеет баланс = 1000

// Поток 1                      Поток 2
user.setBalance(900);           user.setBalance(950);
userRepository.save(user);      userRepository.save(user);
// Сохранён: баланс 900         Сохранён: баланс 950
// Результат: потеря данных!

Оба потока прочитали одно значение (1000), изменили его по-своему, и второе обновление перезаписало первое. Это называется lost update (потеря обновления).

Решение: @Version

import javax.persistence.*;

@Entity
@Table(name = "users")
public class User {
    @Id
    private Long id;
    
    private String name;
    private BigDecimal balance;
    
    // Версия для оптимистичной блокировки
    @Version
    private Long version; // или Integer, или Long
    
    // getters и setters
}

Как это работает

Шаг 1: Чтение
Поток 1: SELECT id, name, balance, version FROM users WHERE id = 1
Результат: version = 1

Шаг 2: Обновление
Поток 1: UPDATE users 
         SET balance = 900, version = 2 
         WHERE id = 1 AND version = 1
Результат: успешно (1 строка обновлена)

Шаг 3: Если другой поток пытался обновить
Поток 2: UPDATE users 
         SET balance = 950, version = 2 
         WHERE id = 1 AND version = 1
Результат: ОШИБКА! (0 строк обновлено - версия уже 2)
Исключение: OptimisticLockException

Пример кода

// Entity
@Entity
@Table(name = "bank_accounts")
public class BankAccount {
    @Id
    private Long id;
    
    private String accountNumber;
    private BigDecimal balance;
    
    @Version
    private Integer version; // Автоматически управляется JPA
    
    public void withdraw(BigDecimal amount) {
        this.balance = balance.subtract(amount);
        // version будет автоматически увеличена при save()
    }
}

// Service
@Service
public class BankAccountService {
    @Autowired
    private BankAccountRepository repository;
    
    @Transactional
    public void withdrawMoney(Long accountId, BigDecimal amount) {
        // Читаем счёт
        BankAccount account = repository.findById(accountId)
            .orElseThrow(() -> new RuntimeException("Account not found"));
        
        // Проверяем баланс
        if (account.getBalance().compareTo(amount) < 0) {
            throw new RuntimeException("Insufficient funds");
        }
        
        // Снимаем деньги
        account.withdraw(amount);
        
        // Сохраняем
        try {
            repository.save(account);
        } catch (OptimisticLockingFailureException e) {
            // Версия не совпадает - кто-то другой обновил счёт!
            throw new RuntimeException("Account was updated by another user, please try again");
        }
    }
}

// Repository
public interface BankAccountRepository extends JpaRepository<BankAccount, Long> {
}

// Использование
class Example {
    public static void main(String[] args) {
        // Транзакция 1
        new Thread(() -> {
            try {
                bankAccountService.withdrawMoney(1L, new BigDecimal("100"));
                System.out.println("Транзакция 1: успешно");
            } catch (RuntimeException e) {
                System.out.println("Транзакция 1: ошибка - " + e.getMessage());
            }
        }).start();
        
        // Транзакция 2 (одновременно)
        new Thread(() -> {
            try {
                bankAccountService.withdrawMoney(1L, new BigDecimal("150"));
                System.out.println("Транзакция 2: успешно");
            } catch (RuntimeException e) {
                System.out.println("Транзакция 2: ошибка - " + e.getMessage());
            }
        }).start();
    }
}

SQL под капотом

-- При чтении
SELECT id, account_number, balance, version FROM bank_accounts WHERE id = 1;
-- Результат: version = 5

-- При сохранении
UPDATE bank_accounts 
SET balance = 900.00, version = 6 
WHERE id = 1 AND version = 5;
-- Если версия не 5 → 0 строк обновлено → OptimisticLockException

Тип версии

// Все эти типы поддерживаются JPA:

@Version
private int version; // int

@Version
private Integer version; // Integer

@Version
private long version; // long

@Version
private Long version; // Long

@Version
private short version; // short

@Version
private Short version; // Short

@Version
private Timestamp version; // Timestamp (редко)

Сравнение: оптимистичная vs пессимистичная блокировка

// ❌ Пессимистичная блокировка (плохо в большинстве случаев)
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT u FROM User u WHERE u.id = :id")
User findByIdWithLock(@Param("id") Long id);
// SELECT ... FOR UPDATE (база блокирует строку)
// Медленнее, но гарантирует эксклюзивный доступ

// ✅ Оптимистичная блокировка (правильный выбор обычно)
// С @Version, никакой явной блокировки на уровне БД
// Быстрее, работает только если конфликты редки

Когда использовать @Version

Используйте @Version когда:

  • Конфликты обновления редкие
  • Нужна высокая пропускная способность (high throughput)
  • Возможна retry логика
  • Работаете с веб-приложениями (REST API)

Используйте пессимистичную блокировку когда:

  • Конфликты частые
  • Нужна абсолютная гарантия актуальности данных
  • Работаете с критичными операциями (платежи)

Обработка OptimisticLockException

@Transactional
public void updateWithRetry(Long id, Consumer<Entity> updater) {
    int maxRetries = 3;
    int retries = 0;
    
    while (retries < maxRetries) {
        try {
            Entity entity = repository.findById(id).orElseThrow();
            updater.accept(entity);
            repository.save(entity);
            return; // Успех
        } catch (OptimisticLockingFailureException e) {
            retries++;
            if (retries >= maxRetries) {
                throw new RuntimeException("Failed after " + maxRetries + " retries", e);
            }
            // Попытаемся снова
            try {
                Thread.sleep(100 * retries); // Экспоненциальная задержка
            } catch (InterruptedException ie) {
                Thread.currentThread().interrupt();
                throw new RuntimeException(ie);
            }
        }
    }
}

Заключение

@Version предоставляет элегантный механизм для обработки одновременных обновлений без пессимистичной блокировки. JPA автоматически управляет версией, увеличивая её при каждом обновлении, и проверяет её при сохранении. Это предотвращает конфликты обновления и гарантирует целостность данных в многопоточной среде.

Зачем нужна @Version в JPA? | PrepBro