В этой статье рассмотрим настройку propagation аннотации @Transactional. Аннотация @Transactional (без настроек) заставляет метод выполняться в рамках транзакции.
Но что если два метода аннотированы @Transactional, и один вызывается из другого? Будет ли создано две транзакции, или же одна? Будет ли внутренний метод выбрасывать исключение, если снаружи нет никакой транзакции? На эти вопросы и отвечает настройка propagation.
Ниже рассмотрим пример.
Пример
Пусть есть отчет Report, который куда-то отправляется и при успешной отправке помечается как published:
Класс Report:
@Data @AllArgsConstructor @NoArgsConstructor public class Report { @Id private long id; private boolean published; }
ReportRepository
Метод репозитория updatePublished() пусть будет @Transactional, так как иногда нам надо вызывать его отдельно (вне отправки отчета):
@Transactional @Repository public interface ReportRepository extends CrudRepository<Report, Long> { @Modifying @Query("update report set published = 'true' where id=:id") void updatePublished(long id); }
ReportService
Метод сервиса отправляет отчет и если все ок, вызывает метод репозитория.
Для простоты пусть отправка отчета заключается в выводе его в консоль.
Пометим метод тоже как @Transactional, потому что все должно быть либо выполнено, либо нет. Если при отправке возникнет исключение, метод должен откатиться.
@Service public class ReportService { @Autowired private ReportRepository reportRepository; @Transactional public void sendReport(long id) { System.out.println(id + " sent"); reportRepository.updatePublished(id); //исключение заставит транзакцию откатиться: // throw new RuntimeException(); } }
propagation=REQUIRED
Хотя propagation мы нигде не указали, по умолчанию подразумевается, что propagation=REQUIRED.
То есть объявление метода репозитория без propagation (как выше) равносильно следующему:
@Transactional(propagation = Propagation.REQUIRED) @Repository public interface ReportRepository extends CrudRepository<Report, Long> { @Modifying @Query("update report set published = 'true' where id=:id") void updatePublished(long id); }
И эта настойка нам подойдет. При ней для метода репозитория не создается отдельная транзакция, а метод выполняется в рамках транзакции вызывающего метода sendReport().
Вообще propagation=REQUIRED задает следующее поведение:
- Если метод updatePublished() вызывается вне транзакции, для него создается отдельная транзакция.
- Если же updatePublished() вызывается из метода сервиса, в котором уже есть транзакция (наш случай), то updatePublished() вызывается в рамках этой транзакции.
Сначала рассмотрим случай 2.
Чтобы отследить моменты создания и подтверждения/отката транзакций в консоли, включим в application.properties настройку:
logging.level.org.springframework.jdbc=trace
Далее вызовем метод sendReport() в тесте:
@Test void shouldSendReport() { reportService.sendReport(1l); }
В консоли мы увидим, что создается одна транзакция, update выполняется внутри нее. В конце транзакция подтверждается либо откатывается Откатывается, если раскомментировать строку:
throw new RuntimeException()
Ниже показан лог в случае, когда строка раскомментирована и транзакция откатывается:
Теперь уберем аннотацию@Transactional у метода sendReport(), чтобы получить случай из первого пункта 1 (метод вызывается вне транзакции). Мы увидим, что транзакция создается для updatePublished(). Но внешний метод как был, так и останется вне транзакции. Если в нем раскомментировать исключение, это не вызовет откат внутренней транзакции, потому что исключение будет вне транзакции.
Мы увидим лог:
По логу видно, что хотя, во внешнем методе выбрасывается исключение, транзакция метода updatePublished() все равно подтверждается.
В случае, когда sendReport() аннотирован @Transactional, транзакция создается вокруг метода sendReport() и она откатывается.
propagation=REQUIRED_NEW
В случае REQUIRED_NEW для внутреннего метода создается своя отдельная транзакция. Пока выполняется внутренний метод, внешняя транзакция приостанавливается. Но это две отдельных транзакции, и исключение во внешнем методе не повлияет на успешное подтверждение внутренней транзакции. Проверим это.
Делаем для репозитория REQUIRES_NEW:
@Transactional(propagation = Propagation.REQUIRES_NEW) @Repository public interface ReportRepository extends CrudRepository<Report, Long> { @Modifying @Query("update report set published = 'true' where id=:id") void updatePublished(long id); }
Ниже показан внешний метод со своей транзакцией, которая откатится из-за исключения. (Да, в этой внешней транзакции вообще нет sql-команд, но это неважно для теста; все равно для нее будет создаваться jdbc-соединение, создаваться, а затем из-за RuntimeExeption откатываться транзакция):
@Transactional(propagation = Propagation.REQUIRED) public void sendReport(long id) { System.out.println(id + " sent"); reportRepository.updatePublished(id); //исключение заставит транзакцию откатиться: throw new RuntimeException(); }
Если бы во внешнем методе были SQL-команды, они бы откатились, а метод updatePublished() все равно подтвердился в своей транзакции абсолютно независимо. Впрочем, можно попробовать. Пусть во внешней транзакции делается insert в таблицу Address — например, это адрес отправки отчета:
@Service public class ReportService { @Autowired private ReportRepository reportRepository; @Autowired private AddressRepository addressRepository; @Transactional(propagation = Propagation.REQUIRED) public void sendReport(long id) { addressRepository.addAddress(1, "addr1"); System.out.println(id + " sent"); reportRepository.updatePublished(id); //исключение заставит транзакцию откатиться: throw new RuntimeException(); } }
Метод addAddress() и репозиторий AddressRepository вообще не аннотированы @Transactional, так что для addAddress() транзакция не создается, добавление адреса расположено как бы непосредственно внутри sendReport().
В консоли увидим, что создается две транзакции, из них одна подтверждается (для метода updatePublished()), а вторая (для метода sendReport()) откатывается из-за исключения:
Получается, что в таблице report обновляется поле update, а в таблицу address ничего не вставляется из-за отката внешней транзакции.
Приостановку внешней транзакции можно представить следующим образом. Это как открыть две консоли Query Tool в pg_admin и последовательно отправлять в эти две консоли команды:
1. begin; -- 1 консоль 2. insert into address (id, name) values (1, 'addr1') -- 1 консоль 3. begin; -- 2 консоль 4. update report set published = 'true' where id=1; -- 2 консоль 5. commit; -- 2 консоль 6. rollback; -- 1 консоль
1 консоль относится к транзакции внешнего метода, а 2 консоль — внутреннего. Приостановка внешней транзакции заключается в том, что rollback (либо commit) для нее (пункт 6) делается после того, как мы на время переключились на 2 консоль внутреннего метода и выполнили в ней все команды ее транзакции (пункты 3 — 5).
Далее рассмотрим SUPPORTS и NOT_SUPPORTED, которые «прохладнее» относятся к транзакции.
propagation=SUPPORTS
SUPPORTS использует транзакцию во внешнем методе, если она есть. Но если нет, своя транзакция для внутреннего метода создаваться не будет. А без транзакции все команды внутреннего метода будут выполнены в режиме автофиксации (AUTOCOMMIT).
Попробуем поставить эту настройку.
Итак, ReportRepository:
@Transactional(propagation = Propagation.SUPPORTS) @Repository public interface ReportRepository extends CrudRepository<Report, Long> { @Modifying @Query("update report set published = 'true' where id=:id") void updatePublished(long id); }
Теперь если внешний метод аннотирован @Transactional, то создается одна транзакция, обрамляющая все команды.
@Transactional public void sendReport(long id) { addressRepository.addAddress(1, "addr1"); System.out.println(id + " sent"); reportRepository.updatePublished(id); //исключение заставит транзакцию откатиться: throw new RuntimeException(); }
Она откатывается из-за исключения:
Если же внешний метод не @Transactional, то транзакции никакой не создается, а все внутренние команды выполняются в режиме AUTOCOMMIT. То есть несмотря на выброшенное исключение и добавляется запись в address, и обновляется report:
public void sendReport(long id) { addressRepository.addAddress(1, "addr1"); System.out.println(id + " sent"); reportRepository.updatePublished(id); //исключение throw new RuntimeException(); }
Propagation.NOT_SUPPORTED
В отличие от SUPPORTS, здесь команды ускользают от транзакции, даже если вызываются в рамках ее. То есть если внешний метод аннотирован @Transactional:
@Transactional public void sendReport(long id) { addressRepository.addAddress(1, "addr1"); System.out.println(id + " sent"); reportRepository.updatePublished(id); //исключение заставит транзакцию откатиться: throw new RuntimeException(); }
А в методе репозитория стоит NOT_SUPPORTED:
@Transactional(propagation = Propagation.NOT_SUPPORTED) @Repository public interface ReportRepository extends CrudRepository<Report, Long> { @Modifying @Query("update report set published = 'true' where id=:id") void updatePublished(long id); }
То метод репозитория выполнится в режиме AUTOCOMMIT.
Это значит, при откате внешней транзакции наш updatePublished() все равно выполнится. А вот не аннотированный ничем addAddress() откатится вместе с внешней транзакцией. Проследим вывод в консоль:
Итог — адрес добавится, отчет не обновится.
Propagation.NEVER
Propagation.NEVER не терпит транзакции снаружи и выбрасывает исключение, если транзакция обнаружена. Проверим это.
Пусть внешний метод @Transactional:
@Transactional public void sendReport(long id) { addressRepository.addAddress(1, "addr1"); System.out.println(id + " sent"); reportRepository.updatePublished(id); //исключение заставит транзакцию откатиться: // throw new RuntimeException(); }
Заметьте, в методе sendReport() мы закомментировали исключение.
А внутренний Propagation.NEVER:
@Transactional(propagation = Propagation.NEVER) @Repository public interface ReportRepository extends CrudRepository<Report, Long> { @Modifying @Query("update report set published = 'true' where id=:id") void updatePublished(long id); }
Создается внешняя транзакция, из-за Propagation.NEVER внутренний метод выбрасывает исключение и откатывает внешнюю транзакцию. То есть откат тут инициализировало не RuntimeException из sendReport, как предыдущих случаях. А внутренний метод, который не стерпел вызова в рамках транзакции из-за своей настройки Propagation.NEVER.
Итог — address не добавится, report не обновится.
Вывод в консоль:
Propagation.MANDATORY
Наконец, Propagation.MANDATORY требует внешнюю транзакцию, а иначе выбрасывается исключение.
То есть если внешний метод @Transactional, то все ок, команды выполняются в рамках одно транзакции:
@Transactional public void sendReport(long id) { addressRepository.addAddress(1, "addr1"); System.out.println(id + " sent"); reportRepository.updatePublished(id); //исключение заставит транзакцию откатиться: // throw new RuntimeException(); }
ReportRepository:
@Transactional(propagation = Propagation.MANDATORY) @Repository public interface ReportRepository extends CrudRepository<Report, Long> { @Modifying @Query("update report set published = 'true' where id=:id") void updatePublished(long id); }
Если же с внешнего метода убрать @Transactional, то будет исключение, вызванное методом updatePublished():
public void sendReport(long id) { addressRepository.addAddress(1, "addr1"); System.out.println(id + " sent"); reportRepository.updatePublished(id); }
При этом addAddress() выполнится в режиме AUTOCOMMIT (как всегда бывает при вызове команд вне транзакции; он стоит раньше, так что выполнится до исключения).
Итог — address не добавится, report не обновится.
Вывод в консоль:
Итоги
Таким образом, метод updatePublished(), в зависимости от его настройки propagation и от того, откуда он вызывается, ведет себя по-разному. В таблице показано поведение в случаях вызова метода из транзакции (левая колонка) и вне транзакции (правая колонка).
Если вызывается из @Transactional sendReport() |
Если вызывается из sendReport() без @Transactional, либо вызывается отдельно. | |
REQUIRED | используется существующая транзакция | транзакция создается |
REQUIRED_NEW | создается отдельная вторая транзакция для внутреннего метода | транзакция создается |
SUPPORTS | используется существующая транзакция | транзакция не создается |
NOT_SUPPORTED | существующая транзакция не используется, код выполняется вне транзакции | транзакция не создается |
NEVER | выбрасывает исключение | транзакция не создается |
MANDATORY | используется существующая транзакция | выбрасывает исключение |
- код выполняется вне транзакции (режим Auto-Commit)
- одна транзакция
- две транзакции
- исключение
Код примера доступен на GitHub.
Про уровни изоляции тут.
Самое понятное объяснение propagation! В итогах п. 7 похоже опечатка, д.б. «Итог — address добавится, report не обновится»
а где NESTED?)
NESTED не каждой базой поддерживается…в общем, еще не разобрано.
В итогах п. 5 опечатка, д.б. «Итог — address не добавится, report обновится»
Отличная статья, очень доступно, спасибо