Разобраться с транзакциями при self-invocation

14. Разобраться с транзакциями при self-invocation

Условие задачи:
Дан сервис с двумя транзакционными методами:

@Service
class SomeService {

    @Transactional(
            propagation = Propagation.REQUIRES_NEW
    )
    public void a() {
    }

    @Transactional
    public void b() {
        a();
    }
}

Метод b() вызывается через Spring-бин:

someService.b();

Необходимо ответить:

  1. Сколько транзакций будет создано?

  2. Почему REQUIRES_NEW на методе a() не срабатывает?

  3. Как сделать так, чтобы метод a() выполнялся в новой транзакции?


Спойлеры к решению

Подсказки
💡 @Transactional обычно работает через Spring AOP-прокси.
💡 Внешний вызов метода бина проходит через прокси.
💡 Внутренний вызов метода того же объекта называется self-invocation.
💡 Вызов a() внутри b() фактически выполняется как this.a().
💡 Надёжнее всего вынести a() в отдельный Spring-бин.

Решение

При вызове b() будет создана одна транзакция.

Внешний вызов b() проходит через Spring-прокси, поэтому аннотация @Transactional срабатывает и открывает транзакцию.

Вызов a() происходит внутри того же объекта:

public void b() {
    this.a();
}

Такой вызов не проходит через прокси. Поэтому настройки Propagation.REQUIRES_NEW не применяются, а метод a() выполняется внутри транзакции метода b().

Предпочтительный вариант — вынести метод a() в отдельный сервис:

@Service
public class AService {

    @Transactional(
            propagation = Propagation.REQUIRES_NEW
    )
    public void a() {
        // код
    }
}
@Service
public class SomeService {

    private final AService aService;

    public SomeService(AService aService) {
        this.aService = aService;
    }

    @Transactional
    public void b() {
        aService.a();
    }
}

Теперь вызов aService.a() проходит через прокси другого Spring-бина.

В результате:

  1. для b() создаётся первая транзакция;

  2. перед выполнением a() первая транзакция приостанавливается;

  3. для a() создаётся новая независимая транзакция;

  4. после завершения a() продолжается транзакция метода b().

Альтернативный вариант — вызвать метод через прокси текущего сервиса:

@Service
public class SomeService {

    private final SomeService self;

    public SomeService(
            @Lazy SomeService self
    ) {
        this.self = self;
    }

    @Transactional(
            propagation = Propagation.REQUIRES_NEW
    )
    public void a() {
        // код
    }

    @Transactional
    public void b() {
        self.a();
    }
}

Такой подход работает, но усложняет структуру класса. Обычно отдельный сервис является более понятным решением.