Вопрос 19 · Раздел 16

Что такое @Version и зачем она нужна

@Version — аннотация JPA для реализации оптимистичной блокировки. Она автоматически отслеживает версию сущности и предотвращает конфликтующие обновления от нескольких транзакций.

Версии по языкам: English Russian Ukrainian

Обзор

@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)]]