Поради та помилки у Spring Transactional - Блог Анатолія Корсакова

У цьому пості я зберу найкращі практики використання Spring Transactional

Service чи DAO?

  • Встановлюйте інструкцію @Transactional у шарі сервісу (Service layer), а не в шарі DAO. Сервісні біни можуть використовувати кілька DAO, дотримуючись ACID під однією транзакцією. Інакше якщо лише у DAO визначено механізм транзакцій, то сервісні біни збільшать витрати на створення множинних транзакцій для концептуально згрупованих операцій, не кажучи вже про неконсистентний стан даних, який ми ризикуємо отримати.

  • У цьому пункті, продовжуємо думку з попереднього, ми можемо встановити @Transactional(propagation = Propagation.MANDATORY) на рівні класу в наших DAO, таким чином змушуючи споживачів наших DAO ініціювати управління транзакціями.
  • Необхідно знати дефолтну поведінку анотації @Transactional і не використовувати її наосліп. А саме, якщо не зазначено явно, рівень розповсюдження (propagation) встановиться в Propagation.REQUIRED, що означає використання поточної транзакції, інакше створити нову; ізоляція встановиться значення Isolation.DEFAULT, це значення визначається нижчележачої БД (за умовчанням в багатьох БД це значення одно Isolation.READ_COMMITED) ; readOnly прапор вимкнений за замовчуванням; rollbackFor може бути задано з Throwable класом, але будьте обережні: за замовчуванням відкат (rollback) відбувається тільки коли викидається RuntimeException (якщо не встановлено цей параметр).
  • Будьте обережні з прапором readOnly . Хоча @Transactional(readOnly = true, propagation=Propagation.REQUIRED) викине виняток при спробі виконатися команді вставки/оновлення через JDBC у транзакції, вона може мати несподівану поведінку при спробі вставки/оновлення через ORM,де операція потай виконується і успішно виконається коміт транзакції. В ORM оточенні необхідно використовувати цей прапор разом із Propagation.SUPPORTS. У цьому випадку ми не платитимемо вартість створення нової транзакції у простій операції вибірки. Або навіть розгляньте можливість позбутися управління транзакціями для операцій SELECT.
  • Будьте обережні під час використання Propagation.REQUIRES_NEW не на найвищому рівні транзакцій. Це може спричинити численні проблеми. Щоразу, як нова нижченалежна транзакція обертається цим аспектом, у випадках коли множинні транзакції з REQUIRES_NEW s включені в межах одного транзакційного сервісного методу можуть виникнути проблеми зі втратою даних. З іншого боку, можна використовувати аннтацію на транзакційному методі вищого рівня, що насправді аналогічно поведінці дефолтного Propagation.REQUIRED

Spring автоматично відкочує транзакції для unchecked (Runtime) виключень

Коли виникає Runtime exception, Spring позначає поточну транзакцію для відкату.

Можливо найбільш заплутаною частиною таких відкатів є спостереження такого повідомлення "UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only" .

Це очікувана поведінка, але може бути заплутаною, якщо не розуміти, що сталося насправді. Якщо один транзакційний метод викликає інший транзакційний метод в іншому класі і цей внутрішній виклик викидає Runtime exception, то вся транзакція повністю буде скасована. Це буде відбуватися, якщо Ви не ловите Exception у класі, що викликає. Якщо відкат зовнішнього класу не бажаний, Ви можете виконати внутрішню транзакцію в новій транзакції (використовуючи інші propagation) або використовувати атрибутnoRollBackFor анотації @Transactional .

Не використовуйте анотацію @Transactional на приватних, protected чи default методах.

Цей пункт походить з першого пункту про те, як Spring управляє транзакціями. Додавання інструкції до способів з private, protected або default модифікаторами доступу не викине виняток. Але анотація буде проігнорована

Логування транзакцій

Дуже важливо у разі виникнення непередбачених ситуацій дивитися та розуміти логи. Для включення логів необхідно додати до log4j.properties

Це дозволить Вам відстежити активності, що відбуваються всередині транзакційних процесів. Логи забезпечують детальну інформацію про нові, перевикористані та активні транзакції, режими транзакцій, коміти транзакцій. Це найкращий спосіб зрозуміти як транзакції керують Вашим додатком та вирішити проблему.

Невелика довідка щодо атрибуту Transactional Propagation

@Transactional(propagation=Propagation.REQUIRED)

Якщо не вказано, то стратегія поширення дефолту це REQUIRED.

Інші опції це REQUIRES_NEW, MANDATORY, SUPPORTS, NOT_SUPPORTED, NEVER, та NESTED.

REQUIRED

  • Це означає, що цільовий метод не може бути запущений без активної транзакції. Якщо транзакція вже стартувала до виклику цього методу, тоді методи виконається в ній, або інакше при викликі методу нова транзакція буде створена.

REQUIRES_NEW

  • Означає, що нова транзакція повинна починатися щоразу, як цільовий метод буде викликатися. Якщо вже є розпочата транзакція, то її буде припинено, до початку нової.

MANDATORY

  • Означає, що цільовий метод вимагає активної транзакції для старту. Якщо її немає, товиконання не буде зроблено, і викинеться виняток.

SUPPORTS

  • Означає, що цільовий метод може бути виконаний поза транзакцією. Якщо є розпочата транзакція, то метод запуститься у ній. Якщо немає запущеної транзакції, то метод виконається однаково, лише над транзакціонному контексті.
  • Методи, які виконують вибірку даних, найкращі кандидати для цієї опції.

NOT_SUPPORTED

  • Це означає, що цільовий метод не вимагає транзакційного контексту для виконання. Якщо є розпочата транзакція, то її буде припинено.

NEVER

  • Означає, що цільовий метод викине виняток, якщо виконається в транзакційному процесі.
  • Не раджу використати цю опцію.

@Transactional (rollbackFor=Exception.class)

  • За замовчуванням відкат відбувається за rollbackFor=RunTimeException.class
  • У спрингу, всі API класи викидають RuntimeException, що означає, що якщо будь-який метод впав, то контейнер завжди здійснить відкат поточної транзакції.
  • Проблема є тільки з checked exceptions. Так що ця опція може використовуватися для декларативного відкату транзакції, якщо викинеться Checked Exception.

@Transactional (noRollbackFor=IllegalStateException.class)

  • Означає, що відкат не повинен відбуватися, якщо цільовий метод викинув цей виняток.