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

Будет ли выполнена транзакция, если нетранзакционный метод вызывает транзакционный метод в Spring?

2.0 Middle🔥 201 комментариев
#Spring Framework

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

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

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

Spring транзакции: правильное использование @Transactional

Это коварный вопрос, потому что ответ не интуитивен. Давайте разберемся в деталях как работает @Transactional в Spring.

Проблема: транзакция не срабатывает

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
    
    // Нетранзакционный метод
    public void createUserWithFriend(String name, String friendName) {
        saveUser(name);           // Транзакция?
        saveUser(friendName);     // Транзакция?
    }
    
    // Транзакционный метод
    @Transactional
    public void saveUser(String name) {
        User user = new User(name);
        userRepository.save(user);
    }
}

// Вызов:
UserService service = new UserService();
service.createUserWithFriend("John", "Jane");

Ответ: НЕТ, транзакция НЕ будет выполнена!

Почему? Как работает Spring AOP

Spring использует AOP (Aspect-Oriented Programming) для реализации @Transactional.

Когда ты вызываешь метод через Spring bean:
1. Spring создает PROXY вокруг объекта
2. Proxy перехватывает вызов
3. Proxy проверяет есть ли @Transactional
4. Если есть: открывает транзакцию
5. Если нет: просто вызывает метод

Spring AOP Proxy:
┌─────────────────────────────────┐
│  userService.saveUser("John")   │  Вызов через proxy
└──────────────┬──────────────────┘
               │
               ├─ Проверка: есть ли @Transactional? ДА
               ├─ Открыть транзакцию
               ├─ Выполнить saveUser
               └─ Закрыть транзакцию

КРИТИЧНО: это работает только для вызовов через Spring bean.

Проблемный сценарий

@Service
public class UserService {
    @Transactional
    public void saveUser(String name) {
        User user = new User(name);
        userRepository.save(user);
        // Транзакция открыта
    }
    
    public void createUserWithFriend(String name, String friendName) {
        // Вызов этого метода через proxy
        // Но когда этот метод вызывает saveUser...
        
        this.saveUser(name);        // ОШИБКА! это.saveUser - прямой вызов
        this.saveUser(friendName);  // ОШИБКА! это.saveUser - прямой вызов
    }
}

// Почему ошибка?
// this.saveUser - это вызов на оригинальный объект, НЕ на proxy!
// Proxy не перехватит вызов
// @Transactional не сработает

Диаграмма что происходит

Клиент вызывает:
userService.createUserWithFriend("John", "Jane")
                  │
                  ├─ Это Spring bean? ДА
                  ├─ Есть @Transactional? НЕТ на createUserWithFriend
                  ├─ Просто вызывай метод
                  │
                  └─ Внутри createUserWithFriend:
                     this.saveUser("John")  // this - оригинальный объект!
                     │
                     └─ Это Spring bean? НЕТ (это прямой вызов)
                        └─ Proxy НЕ перехватит
                           └─ @Transactional НЕ сработает

Решение 1: Правильный способ (inject самого себя)

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private UserService self;  // Inject себя! (это proxy)
    
    public void createUserWithFriend(String name, String friendName) {
        // Вызываем через self (который является proxy)
        self.saveUser(name);       // ✅ Транзакция сработает!
        self.saveUser(friendName); // ✅ Транзакция сработает!
    }
    
    @Transactional
    public void saveUser(String name) {
        User user = new User(name);
        userRepository.save(user);
    }
}

Решение 2: Правильный способ (все в одной транзакции)

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
    
    // Вся логика в одной транзакционной функции
    @Transactional
    public void createUserWithFriend(String name, String friendName) {
        User user1 = new User(name);
        userRepository.save(user1);
        
        User user2 = new User(friendName);
        userRepository.save(user2);
        // Одна транзакция для обоих
    }
}

Решение 3: Использовать ObjectProvider

@Service
public class UserService {
    @Autowired
    private ObjectProvider<UserService> userServiceProvider;
    
    public void createUserWithFriend(String name, String friendName) {
        // Получить proxy через provider
        UserService proxy = userServiceProvider.getIfAvailable();
        proxy.saveUser(name);       // ✅ Транзакция сработает!
        proxy.saveUser(friendName); // ✅ Транзакция сработает!
    }
    
    @Transactional
    public void saveUser(String name) {
        User user = new User(name);
        userRepository.save(user);
    }
}

Более сложный сценарий: rollback

@Service
public class UserService {
    @Transactional
    public void saveUser(String name) {
        User user = new User(name);
        userRepository.save(user);
    }
    
    public void createUserWithFriend(String name, String friendName) {
        this.saveUser(name);        // БЕЗ транзакции (this вызов)
        this.saveUser(friendName);  // БЕЗ транзакции (this вызов)
        
        // Если здесь выбросим исключение, что произойдет?
        throw new RuntimeException("Error!");
    }
}

// Результат:
// - Оба user'а созданы в БД (каждый в своей separate transaction)
// - Исключение выброшено
// - Rollback НЕ произойдет (каждый saveUser закомитился сразу)

Правильный способ (с откатом)

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private UserService self;  // Proxy
    
    public void createUserWithFriend(String name, String friendName) {
        try {
            self.createUserWithFriendTransactional(name, friendName);
        } catch (RuntimeException e) {
            log.error("Failed to create users", e);
            throw e;
        }
    }
    
    @Transactional  // Одна транзакция для всего
    public void createUserWithFriendTransactional(String name, String friendName) {
        User user1 = new User(name);
        userRepository.save(user1);
        
        User user2 = new User(friendName);
        userRepository.save(user2);
        
        // Если здесь ошибка - ВСЁ откатится
        if (shouldFail()) {
            throw new RuntimeException("Error!");
        }
    }
}

// Результат:
// - Если ошибка - оба user'а НЕ будут созданы (полный rollback)
// - Если успех - оба user'а созданы в одной транзакции

Тестирование

@Service
public class ProblematicService {
    @Autowired
    private UserRepository userRepository;
    
    public void publicMethod() {
        this.transactionalMethod();  // ❌ Транзакция НЕ сработает
    }
    
    @Transactional
    public void transactionalMethod() {
        userRepository.save(new User("John"));
    }
}

@Test
public void shouldFail_transactionNotWorking() {
    // Создаем реальный объект (БЕЗ proxy)
    ProblematicService service = new ProblematicService();
    service.publicMethod();
    
    // Транзакция НЕ работала, БЕЗ rollback
    // User создан в БД (если база поддерживает autocommit)
}

@Test
public void shouldWork_usingBean(@Autowired ProblematicService service) {
    service.publicMethod();
    // Через Spring bean (@Autowired), это работает
}

Когда еще НЕ работает @Transactional

// 1. Private метод
@Transactional
private void privateMethod() {  // ❌ Spring AOP не может proxy private
    // Транзакция НЕ сработает
}

// 2. Final метод
@Transactional
public final void finalMethod() {  // ❌ AOP не может переопределить final
    // Транзакция НЕ сработает
}

// 3. Final класс
@Transactional
public final class FinalService {  // ❌ AOP не может создать proxy final класса
    public void method() {
        // Транзакция НЕ сработает
    }
}

// 4. Вызов БЕЗ Spring container
ProblematicService service = new ProblematicService();  // Без @Autowired
service.publicMethod();  // ❌ Нет proxy

Best practices

// ✅ Правильно: все логику в одну транзакцию
@Service
public class GoodService {
    @Transactional
    public void createUserWithFriend(String name, String friendName) {
        // Весь код в одной транзакции
    }
}

// ✅ Правильно: разные уровни транзакций (если нужно)
@Service
public class BetterService {
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private BetterService self;  // Inject proxy
    
    @Transactional(propagation = Propagation.REQUIRED)  // По умолчанию
    public void method1() {
        // Транзакция 1
    }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)  // Новая транзакция
    public void method2() {
        // Транзакция 2 (независима от 1)
    }
    
    public void caller() {
        self.method1();
        self.method2();
    }
}

Вывод

Ответ на вопрос: НЕТ, транзакция НЕ будет выполнена, если нетранзакционный метод вызывает транзакционный метод через this.

Почему:

  1. Spring использует AOP Proxy для @Transactional
  2. Proxy работает только для вызовов через Spring bean
  3. Вызов this.method() — это вызов на оригинальный объект, НЕ на proxy
  4. Proxy НЕ перехватит вызов
  5. @Transactional НЕ сработает

Как исправить:

  1. Инъект себя в самого себя
  2. Вся логика в одном транзакционном методе
  3. Использовать ObjectProvider
  4. Или вызывать через Spring bean

Это частая ошибка даже опытных разработчиков!