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

Что произойдет при вызове одного метода c @Transactional другим c @Transactional?

2.3 Middle🔥 171 комментариев
#Spring Framework#Базы данных и SQL

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

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

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

# Что произойдет при вызове одного метода @Transactional другим с @Transactional?

Это очень частый вопрос, потому что поведение транзакций в Spring может быть неинтуитивным. Ответ зависит от propagation strategy (Propagation).

Основная проблема: Proxy не работает на прямом вызове

Прежде всего, важно понять, что @Transactional работает через proxy. Если ты вызываешь метод напрямую (без прохода через proxy), транзакция не создаётся вообще!

@Service
public class UserService {
    
    @Transactional
    public void methodA() {
        System.out.println("MethodA");
        methodB();  // ❌ Это ПРЯМОЙ ВЫЗОВ! @Transactional на B игнорируется!
    }
    
    @Transactional
    public void methodB() {
        System.out.println("MethodB");
    }
}

// Визуально:
public void methodA() {          // No proxy wrapper
    System.out.println("MethodA");
    this.methodB();              // Прямой вызов через 'this'
}

ДеБез proxy:

  • Transaction от methodA создана
  • methodB выполняется внутри той же транзакции
  • @Transactional от methodB игнорируется

Решение: использовать autowired ссылку

@Service
public class UserService {
    
    @Autowired
    private UserService self;  // Инъецируем себя (это proxy!)
    
    @Transactional
    public void methodA() {
        System.out.println("MethodA");
        self.methodB();  // ✅ Вызов через proxy! Теперь @Transactional на B сработает
    }
    
    @Transactional
    public void methodB() {
        System.out.println("MethodB");
    }
}

Propagation strategies

Если вызов через proxy работает, что происходит? Это зависит от Propagation:

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

@Transactional(propagation = Propagation.REQUIRED)  // Это дефолт
public void methodA() {
    userRepository.save(new User("Alice"));
    self.methodB();  // Войдёт в ТУ ЖЕ транзакцию
}

@Transactional(propagation = Propagation.REQUIRED)
public void methodB() {
    userRepository.save(new User("Bob"));
}

// Результат:
// 1 транзакция создана для methodA
// methodB работает внутри неё
// Если methodB выбросит исключение — оба save отменятся (ROLLBACK)

2. REQUIRES_NEW

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodA() {
    userRepository.save(new User("Alice"));
    try {
        self.methodB();  // Создаст НОВУЮ независимую транзакцию
    } catch (Exception e) {
        logger.error("methodB failed");
        // methodA продолжит работу!
    }
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
    userRepository.save(new User("Bob"));
    throw new RuntimeException("Ошибка!");
}

// Результат:
// 2 транзакции (methodA и methodB независимо)
// methodB откатится (ROLLBACK)
// methodA продолжит работу и закоммитится
// Пользователь "Alice" будет сохранён, "Bob" — нет

3. NESTED

@Transactional(propagation = Propagation.NESTED)
public void methodA() {
    userRepository.save(new User("Alice"));
    try {
        self.methodB();  // Создаст SAVEPOINT внутри транзакции methodA
    } catch (Exception e) {
        logger.error("methodB failed but methodA continues");
        // Откатится только до savepoint, не вся транзакция
    }
}

@Transactional(propagation = Propagation.NESTED)
public void methodB() {
    userRepository.save(new User("Bob"));
    throw new RuntimeException("Ошибка!");
}

// Результат:
// 1 транзакция с SAVEPOINT
// Если methodB выбросит исключение — откатится только её часть
// methodA может продолжить работу
// "Alice" остаётся, "Bob" не сохраняется

4. NEVER

@Transactional(propagation = Propagation.NEVER)
public void methodA() {
    userRepository.save(new User("Alice"));
    self.methodB();  // ❌ Выбросит исключение!
}

@Transactional(propagation = Propagation.NEVER)
public void methodB() {
    // Если мы здесь, транзакция не должна быть создана
    // Но она есть (от methodA) → IllegalTransactionStateException
}

5. NOT_SUPPORTED

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodA() {
    userRepository.save(new User("Alice"));
    self.methodB();  // Выполнится БЕЗ транзакции (текущая будет приостановлена)
}

@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void methodB() {
    // Выполняется вне транзакции
    // Если здесь ошибка, methodA может продолжить
}

6. SUPPORTS

@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
    userRepository.save(new User("Alice"));
    self.methodB();  // Войдёт в транзакцию от methodA
}

@Transactional(propagation = Propagation.SUPPORTS)
public void methodB() {
    // Если есть транзакция (от methodA) — используется
    // Если нет — работает без неё
}

7. MANDATORY

@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
    userRepository.save(new User("Alice"));
    self.methodB();  // Войдёт в транзакцию от methodA
}

@Transactional(propagation = Propagation.MANDATORY)
public void methodB() {
    // ТРЕБУЕТ, чтобы была активная транзакция
    // Без неё → IllegalTransactionStateException
}

Практический пример: когда это важно

Сценарий 1: Логирование должно коммититься независимо

@Service
public class OrderService {
    @Autowired
    private AuditService auditService;
    
    @Transactional
    public void processOrder(Order order) {
        try {
            orderRepository.save(order);
            auditService.log("Order created");  // Должно коммититься
        } catch (Exception e) {
            auditService.log("Order failed: " + e.getMessage());
            throw e;
        }
    }
}

@Service
public class AuditService {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void log(String message) {
        // Логируется в отдельной транзакции
        // Даже если processOrder откатится, логирование останется
        auditRepository.save(new AuditLog(message));
    }
}

Сценарий 2: Вложенная логика с откатом

@Service
public class UserService {
    @Autowired
    private NotificationService notificationService;
    
    @Transactional
    public void createUserWithNotification(User user) {
        userRepository.save(user);
        
        try {
            notificationService.sendWelcomeEmail(user);
        } catch (MailException e) {
            // Email ошибка не должна отменять создание пользователя
            logger.warn("Failed to send email", e);
            // Но мы хотим залогировать это
        }
    }
}

@Service
public class NotificationService {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void sendWelcomeEmail(User user) {
        NotificationLog log = new NotificationLog(user, "welcome");
        notificationRepository.save(log);
        
        // Отправляем email через внешний сервис
        mailService.send(user.getEmail(), template);
    }
}

Таблица Propagation

PropagationТекущая TXСоздаёт новую TXПоведение при ошибке
REQUIREDДаНет (использует)Откатывает всё
REQUIREDНетДаОткатывает только себя
REQUIRES_NEWЛюбаяДаНезависимо
NESTEDДаSavepointОткатывает до savepoint
NESTEDНетОшибкаIllegalTransactionStateException
NEVERДаОшибкаIllegalTransactionStateException
NOT_SUPPORTEDЛюбаяНетВыполняется без TX
SUPPORTSЛюбаяНетСледует текущему контексту
MANDATORYДаНетИспользует
MANDATORYНетОшибкаIllegalTransactionStateException

Лучшие практики

✅ Правильно

@Service
public class UserService {
    @Autowired
    private UserService self;
    
    @Transactional
    public void method() {
        self.anotherMethod();  // Через proxy
    }
}

❌ Неправильно

@Service
public class UserService {
    @Transactional
    public void method() {
        this.anotherMethod();  // Прямой вызов, proxy не работает
    }
}

📌 Альтернативное решение: ApplicationContext

@Service
public class UserService implements ApplicationContextAware {
    private ApplicationContext applicationContext;
    
    public void method() {
        UserService proxy = applicationContext.getBean(UserService.class);
        proxy.anotherMethod();  // Вызов через proxy
    }
}

Выводы

  1. @Transactional работает через proxy — прямые вызовы его игнорируют
  2. По умолчанию REQUIRED — методы работают в одной транзакции
  3. REQUIRES_NEW для независимого выполнения — отдельная транзакция
  4. NESTED для сохранения прогресса — откатит только часть
  5. Всегда через proxy — используй autowired self или другой bean

Возможно, самый важный момент: многие ошибки "странного" поведения транзакций происходят именно потому, что забывают про proxy. Это частая ошибка даже в production коде!