Що таке @Version і навіщо вона потрібна
Анотація @jakarta.persistence.Version (у застарілих версіях javax.persistence.Version) — це стандартна анотація специфікації JPA, призначена для автоматичної реалізації оптиміст...
Анотація @jakarta.persistence.Version (у застарілих версіях javax.persistence.Version) — це стандартна анотація специфікації JPA, призначена для автоматичної реалізації оптимістичного блокування (Optimistic Locking). Вона захищає дані від критичної аномалії паралелізму — втрати оновлень (Lost Update), гарантуючи, що жодна транзакція не зможе перезаписати зміни, внесені іншим користувачем, без явного виявлення конфлікту.
🟢 Junior Level
Навіщо потрібна анотація @Version
Уявімо систему бронювання авіаквитків або банківських переказів без контролю версій:
- Користувач A читає баланс рахунку:
$100. - Користувач B паралельно читає баланс рахунку:
$100. - Користувач B знімає
$20і зберігає новий баланс:$80. - Користувач A додає
$50і зберігає свій розрахунок ($100+$50=$150). - Підсумок: Запис користувача A повністю затер списання користувача B!
$20безслідно зникли (аномалія Lost Update).
Анотація @Version усуває цю проблему на рівні ORM:
@Entity
@Table(name = "accounts")
public class Account {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private BigDecimal balance;
@Version
private Long version; // Керується Hibernate автоматично
}
Як Hibernate керує полем @Version
- При первинній вставці (
INSERT): Hibernate автоматично ініціалізує полеversionпочатковим значенням0(або1залежно від діалекту та версії). - При кожному оновленні (
UPDATE): Hibernate інкрементує версію на одиницю і додає перевірку в секціюWHERE:UPDATE accounts SET balance = ?, version = version + 1 WHERE id = ? AND version = ?; - При конфлікті версій: Якщо інша транзакція вже збільшила версію в таблиці, СУБД оновить
0рядків. Hibernate фіксує це і негайно викидає винятокjakarta.persistence.OptimisticLockException.
🟡 Middle Level
Дозволені типи даних для @Version
Специфікація JPA суворо регламентує типи даних, які можуть бути позначені анотацією @Version:
| Тип Java | Тип у БД | Особливості | Рекомендація |
|---|---|---|---|
int / Integer |
INTEGER |
Автоінкремент на +1 при кожному оновленні. Переповнення малоймовірне (до 2 млрд версій). | ✅ Рекомендується |
long / Long |
BIGINT |
Автоінкремент на +1. Виключає переповнення за будь-яких навантажень. | ✅ Найкращий вибір |
short / Short |
SMALLINT |
Обмежений 32 767 версіями. Швидко переповнюється при частих оновленнях. | ⚠️ Не рекомендується |
java.sql.Timestamp / Instant |
TIMESTAMP |
При оновленні записується системний час. | ❌ Небезпечно в Production (ризик колізій в одну мілісекунду) |
Різниця між @Version та @LastModifiedDate
Розробники часто плутають версіонування з аудитом:
@Entity
@EntityListeners(AuditingEntityListener.class)
public class Document {
@Id
private Long id;
// 1. Для оптимістичного блокування та ізоляції транзакцій
@Version
private Long version;
// 2. Для аудиту та бізнес-логіки (Spring Data Auditing)
@LastModifiedDate
private Instant updatedAt;
}
@Version: Призначений суворо для СУБД та ORM. Ручна мутація поля в бізнес-коді категорично заборонена (account.setVersion(5)зламає механізм перевірки).@LastModifiedDate: Служить для фіксації часу для аналітики, інтерфейсу та логів. Він не бере участі в умовіWHERE ... AND version = ?.
🔴 Senior Level
Вплив @Version на визначення статусу «New Entity» у Spring Data JPA
При використанні ручного призначення первинних ключів (assigned ID, наприклад, природні ключі або клієнтські UUID) виникає відома проблема:
- Spring Data JPA
SimpleJpaRepository.save()перевіряєentityInformation.isNew(entity). - Якщо у сутності немає поля
@Version, Spring перевіряєid == null. Оскільки ID задано вручну,isNew()повертаєfalse, і Spring викликає неефективнийentityManager.merge()(виконуючи зайвий штрафнийSELECTпередINSERT). - Senior-інсайт: Якщо в сутності оголошено поле
@Versionз об’єктним типом (LongабоInteger), Spring Data JPA змінює стратегію:// Всередині AbstractEntityInformation: if (versionAttribute != null) { return versionAttribute.get(entity) == null; // Якщо version == null, сутність нова! }Оскільки в нового об’єкта поле
versionдорівнюєnull, Spring Data безпомилково визначає сутність як нову і викликає швидкийentityManager.persist()без жодного зайвого читання!
ВИЗНАЧЕННЯ НОВОЇ СУТНОСТІ В SPRING DATA JPA
[ save(entity) ]
│
Чи є анотація @Version?
/ \
ТАК / \ НІ
/ \
version == null? id == null?
/ \ / \
ТАК / \ НІ ТАК / \ НІ
▼ ▼ ▼ ▼
persist() merge() persist() merge()
(Без SELECT) (З SELECT!) (Без SELECT) (З SELECT!)
@Version та масові HQL/JPQL оновлення (Bulk Operations)
Критична пастка для сеньйорів:
// Масова зміна цін через JPQL:
@Modifying
@Query("UPDATE Product p SET p.price = p.price * 1.1 WHERE p.category = :category")
int increasePrices(@Param("category") String category);
Що відбувається всередині:
- Масові запити
UPDATEтаDELETEтранслюються безпосередньо в одну SQL-інструкцію в базу даних. - Механізм Dirty Checking та життєвий цикл JPA повністю ігноруються.
- Hibernate НЕ БУДЕ автоматично інкрементувати колонку
versionу БД! - Наслідок: Поле
versionу базі даних залишиться старим, а якщо ці сутності вже були завантажені в L1/L2 кеш, застосунок продовжить працювати із застарілими версіями. - Правило: При виконанні кастомних
@Modifyingзапитів колонка версії зобов’язана інкрементуватися вручну прямо в тексті JPQL:@Modifying @Query("UPDATE Product p SET p.price = p.price * 1.1, p.version = p.version + 1 WHERE p.category = :category") int increasePricesManuallyIncrementingVersion(@Param("category") String category);
4 Tricky Questions
1. Чи можна оголосити в одній JPA-сутності дві різні анотації @Version (наприклад, числову для блокувань і Timestamp для контролю часу)?
Відповідь:
- Категорично ні. Специфікація JPA (розділ 4.4.4.2 Optimistic Locking) прямо забороняє наявність більше ніж одного поля з анотацією
@Versionу класі сутності. - При спробі розгорнути застосунок із двома
@VersionHibernate завершить ініціалізацію метамоделі фатальним винятком:org.hibernate.AnnotationException: A factory cannot support multiple version properties: com.example.MyEntity. - Для збереження часу зміни слід використовувати стандартне поле з
@LastModifiedDateабо Hibernate@UpdateTimestamp.
2. Що станеться, якщо передати detached-об’єкт в entityManager.merge(), але на клієнті перед відправкою поле version було змінено вручну або обнулено (version = null)?
Відповідь:
- Якщо
version = null: Hibernate може порахувати сутність новою і спробувати виконати SQLINSERT. Якщо первинний ключ уже зайнятий, СУБД викине помилку порушення унікальності первинного ключа (Unique Constraint Violation). - Якщо
versionпередано зі значенням, яке не збігається з базою даних (наприклад, клієнт передавversion = 1, а в БД ужеversion = 2): методmerge()завантажить актуальний рядок із БД, виявить розбіжність версій і негайно викинеOptimisticLockExceptionще до виконання будь-яких мутацій.
3. Чи інкрементує Hibernate версію сутності, якщо транзакція виконала лише читання без виклику сетерів, але при цьому було вказано LockModeType.OPTIMISTIC?
Відповідь:
- Ні, при
LockModeType.OPTIMISTIC(ранішеREAD) Hibernate не інкрементує версію. - Під час фази скидання контексту (
flush) перед комітом Hibernate виконує додатковий перевірочний запит:SELECT version FROM table WHERE id = ?. - Якщо версія в БД, як і раніше, збігається з прочитаною раніше, транзакція успішно фіксується. Версія залишається незмінною.
- Щоб версія інкрементувалася за відсутності змін полів, необхідно явно запросити режим
LockModeType.OPTIMISTIC_FORCE_INCREMENT.
4. Чому при використанні оптимістичного блокування в кластері мікросервісів поле @Version не рятує від повторної обробки однієї й тієї ж події з Kafka за відсутності ідемпотентності?
Відповідь:
- Якщо два мікросервіси одночасно отримають однакову подію створення нової сутності, поле
versionв обох спочатку будеnull(або0при вставці). - Оптимістичне блокування захищає лише операцію
UPDATEіснуючих рядків (WHERE version = ?). - При паралельному
INSERTобидві транзакції спробують вставити новий запис з однаковим бізнес-ключем. Захистити від цього може лише унікальний індекс на рівні бази даних (UNIQUE constraint), який призведе доDataIntegrityViolationException, але не@Version.
🎯 Шпаргалка для інтерв’ю
30-секундна відповідь (Elevator Pitch)
«Анотація
@Versionу JPA активує механізм оптимістичного блокування для захисту від аномалії Lost Update. Полем версії має бутиLongабоInteger. При кожному оновленні сутності Hibernate автоматично інкрементує значення і додає умовуWHERE id = ? AND version = ?. Якщо запис було змінено конкурентною транзакцією, запит оновлює 0 рядків і викидаєтьсяOptimisticLockException. Важливий senior-нюанс: у Spring Data JPA при ручному присвоєнні ID наявність@Version == nullдозволяє фреймворку розпізнати об’єкт як новий і виконати швидкийpersist()замістьmerge(). При масових HQL-запитах (@Modifying) версію необхідно інкрементувати вручну прямо в запиті».
Ключові метрики для Production-моніторингу
- Кількість OptimisticLockException: сигналізує про точки гарячої конкуренції в бізнес-процесах.
- Розподіл версій за сутностями: аномально високі версії (> 10 000) вказують на занадто часте оновлення одного й того ж об’єкта.
Червоні прапорці (Чого говорити НЕ МОЖНА)
- ❌ «Використовую Timestamp як @Version для надійності» — Ризик колізій у межах однієї мілісекунди; використовувати суворо
LongабоInteger. - ❌ «Я можу вручну викликати entity.setVersion(v + 1) у сервісі» — Ручне керування версією ламає внутрішній цикл синхронізації ORM.
- ❌ «Масовий запит UPDATE через @Modifying автоматично збільшує @Version» — Ні, bulk-запити йдуть в обхід контексту, версію потрібно інкрементувати вручну.
- ❌ «Можна повісити дві анотації @Version на різні поля» — Заборонено специфікацією JPA, впаде з
AnnotationException.