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

В каких случаях аннотация Transactional не сработает

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

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

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

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

# Случаи, когда @Transactional не сработает

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

1. Внутренний вызов метода (Self-invocation)

Самая частая проблема. @Transactional работает через AOP (создание прокси), но внутренний вызов обходит прокси:

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
    
    public void createUser(String name) {  // Нет @Transactional
        this.saveUser(name);  // Обходит прокси!
    }
    
    @Transactional
    public void saveUser(String name) {
        userRepository.save(new User(name));
        throw new RuntimeException("Ошибка!");
        // Не откатится — @Transactional не сработала
    }
}

Почему происходит: когда вызываешь this.saveUser(), Spring прокси не вмешивается. Решение — внедрить Self:

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private UserService self;  // Внедрить сам сервис
    
    public void createUser(String name) {
        self.saveUser(name);  // Теперь через прокси
    }
    
    @Transactional
    public void saveUser(String name) {
        userRepository.save(new User(name));
        throw new RuntimeException("Откатится корректно");
    }
}

Или просто вынести в отдельный сервис:

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private UserCreationService creationService;
    
    public void createUser(String name) {
        creationService.saveUser(name);  // Разные классы = новый прокси
    }
}

@Service
public class UserCreationService {
    @Autowired
    private UserRepository userRepository;
    
    @Transactional
    public void saveUser(String name) {
        userRepository.save(new User(name));
    }
}

2. Вызов из методов без @Transactional контекста

Если основной метод не имеет @Transactional, то и вложенные @Transactional методы могут работать странно:

@Service
public class OrderService {
    @Autowired
    private OrderRepository orderRepository;
    @Autowired
    private PaymentService paymentService;
    
    public void processOrder(Order order) {  // Нет @Transactional!
        orderRepository.save(order);
        paymentService.processPayment(order);  // @Transactional здесь
    }
}

@Service
public class PaymentService {
    @Transactional
    public void processPayment(Order order) {
        // Работает, но без внешней транзакции
        // Если processOrder падёт после этого, платёж останется
    }
}

Решение — добавить @Transactional к основному методу:

@Transactional
public void processOrder(Order order) {
    orderRepository.save(order);
    paymentService.processPayment(order);  // Теперь в одной транзакции
}

3. Приватные методы

@Transactional на приватных методах не работает:

@Service
public class DataService {
    @Transactional
    private void internalSave() {  // ❌ Private!
        // @Transactional не сработает
    }
    
    public void publicSave() {
        internalSave();  // Нет транзакции
    }
}

Причина: Spring создаёт прокси на основе public интерфейса. Решение — сделать public или protected:

@Transactional
protected void internalSave() {  // ✅ Protected
    // Теперь сработает
}

4. Контроль исключений (Try-Catch)

Если поймаешь исключение, транзакция не откатится:

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
    
    @Transactional
    public void createUser(String name) {
        userRepository.save(new User(name));
        try {
            riskyOperation();
        } catch (Exception e) {
            // ❌ Исключение поймано — откат не произойдёт
            log.error("Ошибка", e);
        }
    }
}

Решение — либо переподнять исключение, либо использовать rollbackFor:

@Transactional(rollbackFor = Exception.class)
public void createUser(String name) {
    userRepository.save(new User(name));
    try {
        riskyOperation();
    } catch (Exception e) {
        throw new RuntimeException(e);  // Переподнять
    }
}

5. Проверяемые исключения (Checked Exceptions)

По умолчанию @Transactional откатывает только runtime исключения:

@Transactional  // ❌ IOException не откатит
public void saveToFile(User user) throws IOException {
    userRepository.save(user);
    fileWriter.write(user.toString());  // IOException — транзакция не откатится
}

Решение — явно указать rollbackFor:

@Transactional(rollbackFor = IOException.class)
public void saveToFile(User user) throws IOException {
    userRepository.save(user);
    fileWriter.write(user.toString());  // Теперь откатится
}

6. Неправильный isolation level

Если задаёшь неподходящий уровень изоляции, транзакция может не работать как ожидается:

@Service
public class ConcurrentService {
    @Transactional(isolation = Isolation.READ_UNCOMMITTED)
    public void unsafeUpdate() {
        // Очень низкий уровень изоляции = dirty reads возможны
    }
}

Стандартно используй READ_COMMITTED или REPEATABLE_READ:

@Transactional(isolation = Isolation.READ_COMMITTED)
public void safeUpdate() {
    // Защита от dirty reads
}

7. Отключённая поддержка транзакций

Если DataSource не настроен или @EnableTransactionManagement отсутствует:

// ❌ В конфигурации забыли
@Configuration
public class AppConfig {
    // Нет @EnableTransactionManagement
}

Решение:

@Configuration
@EnableTransactionManagement  // ✅ Обязательно
public class AppConfig {
    // ...
}

8. Вызов из другого потока

@Transactional работает с ThreadLocal контекстом. Если вызвать асинхронный метод без настройки, транзакция теряется:

@Service
public class EmailService {
    @Async  // Новый поток
    @Transactional  // ❌ Работать не будет корректно
    public void sendEmail(String email) {
        // Потеря транзакционного контекста
    }
}

Решение — использовать правильный порядок аннотаций и конфигурацию:

@Transactional
@Async
public void sendEmail(String email) {
    // @Transactional должна быть внешней
}

Чеклист отладки @Transactional

  1. Проверить, что метод public (не private)
  2. Убедиться, что это не self-invocation
  3. Проверить наличие @EnableTransactionManagement в конфигурации
  4. Убедиться, что не ловишь исключение
  5. Проверить isolation level
  6. Посмотреть логи — Spring должен вывести информацию о транзакции
  7. Включить debug логирование Spring:
logging.level.org.springframework.transaction=DEBUG
logging.level.org.hibernate.engine.transaction.internal.TransactionImpl=DEBUG

Стоит потратить время на понимание этих граничных случаев — это спасит множество часов отладки в боевых условиях.