← Назад к вопросам

Как Spring обрабатывает вложенные транзакции внутри одного класса

3.0 Senior🔥 131 комментариев
#Spring Framework

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

# Spring Транзакции: вложенные вызовы в одном классе

Проблема Self-Invocation

Вот основная проблема, которую нужно понимать для интервью:

@Service
public class OrderService {
    
    @Transactional
    public void createOrder() {
        // Начало транзакции TX1
        saveOrder();
        // ...
    }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveOrder() {
        // Проблема! @Transactional НЕ будет работать!
        // Потому что это self-invocation (вызов из того же класса)
    }
}

Когда вы вызываете saveOrder() из createOrder() в одном и том же классе, Spring AOP proxy не работает. Аннотация @Transactional игнорируется.

Почему это происходит

Spring использует AOP (Aspect-Oriented Programming) для обработки @Transactional. Когда вы инъектируете OrderService в контроллер, вы получаете не сам сервис, а прокси объект:

Коды с Proxy:
Controller → @Autowired OrderService (это прокси!) → реальный OrderService
           ↓
           Spring перехватывает вызовы и управляет транзакциями

Self-invocation без Proxy:
реальный OrderService.createOrder()
    ↓
    this.saveOrder() ← это вызов на реальный объект, не на прокси!

Решение 1: Self-Injection (Рекомендуется)

Инъектируйте сам сервис и используйте для внутренних вызовов:

@Service
public class OrderService {
    
    @Autowired
    private OrderService self; // Self-injection
    
    @Transactional
    public void createOrder() {
        // TX1 начало
        self.saveOrder(); // Использует прокси!
        // TX1 завершение
    }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveOrder() {
        // TX2 - новая транзакция! (из-за REQUIRES_NEW)
        // TX1 будет suspended, TX2 выполнится независимо
    }
}

Решение 2: Извлечение в отдельный класс (Лучший подход)

Разделите бизнес-логику на несколько сервисов:

@Service
public class OrderService {
    
    @Autowired
    private OrderPersistenceService persistenceService;
    
    @Transactional
    public void createOrder(Order order) {
        // TX1 начало - главная транзакция
        validateOrder(order);
        persistenceService.save(order); // TX2 - отдельная транзакция
        // TX1 завершение
    }
    
    private void validateOrder(Order order) {
        // Без @Transactional, валидация не требует транзакции
    }
}

@Service
public class OrderPersistenceService {
    
    @Autowired
    private OrderRepository orderRepository;
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void save(Order order) {
        // TX2 - новая независимая транзакция
        orderRepository.save(order);
    }
}

Этот подход лучше потому что:

  • Нет циклических зависимостей
  • Код более чистый и тестируемый
  • Явное разделение ответственности

Решение 3: ObjectProvider (Альтернатива)

@Service
public class OrderService {
    
    @Autowired
    private ObjectProvider<OrderService> serviceProvider;
    
    @Transactional
    public void createOrder() {
        OrderService proxy = serviceProvider.getIfAvailable();
        proxy.saveOrder();
    }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveOrder() {
        // Новая транзакция
    }
}

Propagation: как Spring управляет вложенными транзакциями

REQUIRED (по умолчанию)

@Transactional(propagation = Propagation.REQUIRED)
public void outer() {
    // TX1 начало
    inner(); // Присоединяется к TX1
    // TX1 завершение
}

@Transactional(propagation = Propagation.REQUIRED)
public void inner() {
    // Не создаёт новую транзакцию, использует TX1
}

REQUIRES_NEW

@Transactional(propagation = Propagation.REQUIRED)
public void outer() {
    // TX1 начало
    try {
        inner(); // TX1 SUSPENDED, TX2 начало
    } catch (Exception e) {
        // TX1 всё ещё может быть committed!
    }
    // TX1 продолжение
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void inner() {
    // TX2 - полностью независимая транзакция
    // Если TX2 откатится, TX1 не откатится
}

NESTED

@Transactional(propagation = Propagation.REQUIRED)
public void outer() {
    // TX начало
    try {
        inner(); // Savepoint создаётся
    } catch (Exception e) {
        // Откат только до savepoint, TX продолжает работать
    }
    // TX завершение
}

@Transactional(propagation = Propagation.NESTED)
public void inner() {
    // Работает в savepoint TX, не в новой транзакции
}

Практический пример: правильная обработка вложенных операций

@Service
public class PaymentService {
    
    @Autowired
    private PaymentRepository paymentRepository;
    
    @Autowired
    private AuditService auditService;
    
    @Transactional
    public PaymentResult processPayment(Payment payment) {
        // TX1 - основная транзакция
        
        // 1. Сохраняем платёж
        Payment saved = paymentRepository.save(payment);
        
        // 2. Логируем в отдельной транзакции
        // Если аудит упадёт, платёж сохранится
        try {
            auditService.logPayment(saved);
        } catch (Exception e) {
            // Логируем ошибку, но платёж успешен
            log.error("Audit failed", e);
        }
        
        return new PaymentResult(saved.getId());
    }
}

@Service
public class AuditService {
    
    @Autowired
    private AuditRepository auditRepository;
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void logPayment(Payment payment) {
        // TX2 - независимая транзакция для логирования
        AuditLog log = new AuditLog();
        log.setPaymentId(payment.getId());
        log.setTimestamp(Instant.now());
        auditRepository.save(log);
    }
}

Как тестировать

@SpringBootTest
public class TransactionTest {
    
    @Autowired
    private OrderService orderService;
    
    @Test
    @Transactional
    public void testNestedTransaction() {
        // Test framework автоматически откатит все изменения
        orderService.createOrder();
        
        // Проверяем результаты
        // Не используйте @Transactional в тестах, если нужны реальные транзакции
    }
}

Ошибки, которые нужно избегать

// ❌ ПЛОХО: Self-invocation не работает
@Service
public class Service1 {
    @Transactional
    public void method1() {
        this.method2(); // @Transactional на method2 не сработает!
    }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void method2() {}
}

// ✅ ХОРОШО: Используйте injection
@Service
public class Service1 {
    @Autowired
    private Service2 service2; // Отдельный класс!
    
    @Transactional
    public void method1() {
        service2.method2(); // Сработает правильно
    }
}

@Service
public class Service2 {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void method2() {}
}

Итого

  1. Self-invocation проблема: Spring AOP не работает при вызове метода внутри того же класса
  2. Решения:
    • Self-injection (работает, но не идеально)
    • Разделение на отдельные сервисы (лучший выбор)
    • ObjectProvider (редко используется)
  3. Propagation типы: REQUIRED, REQUIRES_NEW, NESTED имеют разное поведение
  4. Best practice: избегайте вложенных @Transactional в одном классе, разделяйте логику
Как Spring обрабатывает вложенные транзакции внутри одного класса | PrepBro