← Назад к вопросам
Что произойдет при вызове одного метода 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
}
}
Выводы
- @Transactional работает через proxy — прямые вызовы его игнорируют
- По умолчанию REQUIRED — методы работают в одной транзакции
- REQUIRES_NEW для независимого выполнения — отдельная транзакция
- NESTED для сохранения прогресса — откатит только часть
- Всегда через proxy — используй autowired self или другой bean
Возможно, самый важный момент: многие ошибки "странного" поведения транзакций происходят именно потому, что забывают про proxy. Это частая ошибка даже в production коде!