Как реализовать оптимистическую блокировку (Optimistic Locking) в Spring Data JPA?

Ответ

Оптимистическая блокировка в Spring Data JPA реализуется через механизм версионирования сущностей. Она предотвращает "потерю обновлений" (lost update) в многопользовательских сценариях без физических блокировок в БД.

1. Базовая реализация: Добавьте поле с аннотацией @Version в сущность.

@Entity
public class Account {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private BigDecimal balance;

    @Version // Ключевая аннотация
    private Long version; // Тип может быть Long, Integer, Timestamp

    // Геттеры и сеттеры
}

2. Как это работает:

  1. При загрузке сущности JPA фиксирует значение version.
  2. При обновлении в UPDATE-запрос добавляется условие WHERE id=? AND version=?.
  3. Если версия в БД изменилась (обновил другой поток/транзакция), количество обновлённых строк будет 0.
  4. JPA выбрасывает OptimisticLockException (обёрнутое в JpaSystemException или ObjectOptimisticLockingFailureException в Spring).

3. Пример использования и обработки конфликта:

@Service
public class AccountService {
    @Transactional
    public void transferMoney(Long accountId, BigDecimal amount) {
        try {
            Account account = accountRepository.findById(accountId).orElseThrow();
            account.setBalance(account.getBalance().add(amount));
            // При вызове save() выполнится проверка версии
            accountRepository.save(account);
        } catch (ObjectOptimisticLockingFailureException ex) {
            // Стратегия обработки: повторить операцию, уведомить пользователя
            throw new RetryableException("Конфликт версий. Попробуйте ещё раз.", ex);
        }
    }
}

Ключевые моменты:

  • Тип поля: Должен быть Long, Integer, java.sql.Timestamp или java.time.Instant.
  • Изменение: Поле обновляется автоматически JPA, не меняйте его вручную.
  • Когда использовать: Идеально для read-heavy сценариев с редкими конфликтующими обновлениями.
  • Альтернатива: Для write-heavy сценариев с частыми конфликтами рассмотрите пессимистическую блокировку (@Lock(LockModeType.PESSIMISTIC_WRITE)).

Ответ 18+ 🔞

А, оптимистичная блокировка в Spring Data JPA, ну это же классика, блядь! Чисто чтобы несколько юзеров не нассали друг другу в данные одновременно, понимаешь? Потеря обновлений называется, ага. Делается всё через версии, без этих ваших тяжёлых замков в базе.

1. Как прикрутить, сука: Просто тыкаешь волшебную аннотацию @Version на поле в своей сущности. Всё, пиздец, готово.

@Entity
public class Account {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private BigDecimal balance;

    @Version // Вот она, палочка-выручалочка, ёпта!
    private Long version; // Long, Integer — не важно, главное чтоб был

    // Геттеры и сеттеры
}

2. Магия под капотом, блядь:

  1. Загружаешь сущность — JPA запоминает, какая там версия была.
  2. Пытаешься обновить — а она в запрос подсовывает WHERE id=? AND version=?. Хитро, сука!
  3. Если пока ты думал, кто-то другой уже всё проебал и обновил, то версия в базе поменялась. Твой UPDATE нихуя не обновит, строки-то не найдёт.
  4. JPA, такая вся из себя принципиальная, выкинет тебе OptimisticLockException. Spring её ещё в свою обёрточку завернёт, чтоб красивее было.

3. Ну и как с этим жить, бля?

@Service
public class AccountService {
    @Transactional
    public void transferMoney(Long accountId, BigDecimal amount) {
        try {
            Account account = accountRepository.findById(accountId).orElseThrow();
            account.setBalance(account.getBalance().add(amount));
            // Вот тут, при вызове save(), и начнётся вся движуха с проверкой версии
            accountRepository.save(account);
        } catch (ObjectOptimisticLockingFailureException ex) {
            // Лови конфликт! Либо ретрай делай, либо пользователю кричи "ПИЗДЕЦ, КТО-ТО УЖЕ ВСЁ ИСПОРТИЛ!"
            throw new RetryableException("Конфликт версий, ёпта. Давай по-новой, Миша, всё хуйня!", ex);
        }
    }
}

Важные нюансы, чтоб не обосраться:

  • Тип поля: Бери Long, Integer и не выёбывайся. Timestamp тоже можно, но зачем усложнять?
  • Не лезь руками: Это поле меняет сама JPA, автоматически. Ты его даже не трожь, а то нахуяришь делов, блядь.
  • Где пахнет: Идеально, когда много читают, но редко пишут. Если у тесятка потоков, а обновления раз в час — самое то.
  • Если всё плохо: Когда все лезут в одну запись как тараканы на кухню ночью, и конфликты — каждый второй запрос, то это пиздец. Тут уже надо @Lock(LockModeType.PESSIMISTIC_WRITE) юзать, пессимистичную блокировку. Но это уже другая история, с настоящими замками в базе и слезами админов.