Transaction Propagation

В этой статье рассмотрим настройку 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 задает следующее поведение:

  1. Если метод updatePublished() вызывается вне транзакции, для него создается отдельная транзакция.
  2. Если же updatePublished() вызывается из метода сервиса, в котором уже есть  транзакция (наш случай), то updatePublished()  вызывается в рамках этой транзакции.

Сначала рассмотрим случай 2.

Чтобы отследить моменты создания и подтверждения/отката транзакций в консоли, включим в application.properties настройку:

logging.level.org.springframework.jdbc=trace

Далее вызовем метод sendReport() в тесте:

@Test
void shouldSendReport()
{
    reportService.sendReport(1l);
}

В консоли мы увидим, что создается одна транзакция, update выполняется внутри нее. В конце транзакция подтверждается либо откатывается Откатывается, если раскомментировать строку:

throw new RuntimeException()

Ниже показан лог в случае, когда строка раскомментирована и транзакция откатывается:

REQUIRED, вызов из @Transactional
REQUIRED, вызов из @Transactional

Теперь уберем аннотацию@Transactional  у метода sendReport(), чтобы получить случай из первого пункта 1 (метод вызывается вне транзакции). Мы увидим, что транзакция создается для updatePublished(). Но внешний метод как был, так и останется вне транзакции. Если в нем раскомментировать исключение, это не вызовет откат внутренней транзакции, потому что исключение будет вне транзакции.

Мы увидим лог:

REQUIRED, вызов из не @Transactional
REQUIRED, вызов из не @Transactional

 По логу видно, что хотя, во внешнем методе выбрасывается исключение, транзакция метода 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()) откатывается из-за исключения:

REQUIRED_NEW

Получается, что в таблице 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).

В режиме AUTOCOMMIT каждая команда автоматически подтверждается (как бы обрамляется своей отдельной транзакцией — commit-ом, происходит это на уровне базы данных). То есть если б в методе updatePublished() было несколько SQL-операторов update, то в режиме 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();
}

Она откатывается из-за исключения:

SUPPORTED
SUPPORTS

Если же внешний метод не @Transactional, то транзакции никакой не создается, а все внутренние команды выполняются в режиме AUTOCOMMIT. То есть несмотря на выброшенное исключение и добавляется запись в address, и обновляется report:

public void sendReport(long id) {
    addressRepository.addAddress(1, "addr1");

    System.out.println(id + " sent");
    reportRepository.updatePublished(id);
    //исключение 
    throw new RuntimeException();
}
SUPPORTS
SUPPORTS

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() откатится вместе с внешней транзакцией. Проследим вывод в консоль:

NOT_SUPPORTED
NOT_SUPPORTED

Итог — адрес добавится, отчет не обновится.

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 не обновится.

Вывод в консоль:

NEVER
NEVER

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 не обновится.

Вывод в консоль:

MANDATORY
MANDATORY

Итоги

Таким образом, метод updatePublished(), в зависимости от его настройки propagation и от того, откуда он вызывается, ведет себя по-разному. В таблице показано поведение в случаях вызова метода из транзакции (левая колонка) и вне транзакции (правая колонка).

Если вызывается из @Transactional
sendReport()
Если вызывается из sendReport() без @Transactional, либо вызывается отдельно.
REQUIRED используется существующая транзакция транзакция создается
REQUIRED_NEW создается отдельная вторая транзакция для внутреннего метода   транзакция создается
SUPPORTS используется существующая транзакция транзакция не создается
NOT_SUPPORTED  существующая транзакция не используется, код выполняется вне транзакции транзакция не создается
NEVER выбрасывает исключение транзакция не создается
MANDATORY используется существующая транзакция выбрасывает исключение
  • код выполняется вне транзакции (режим Auto-Commit)
  • одна транзакция
  • две транзакции
  • исключение

Код примера доступен на GitHub.

Про уровни изоляции тут.

 

Transaction Propagation: 5 комментариев

  1. Самое понятное объяснение propagation! В итогах п. 7 похоже опечатка, д.б. «Итог — address добавится, report не обновится»

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *