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

С чем нужно быть аккуратным при использовании @Transactional

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

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

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

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

С чем нужно быть аккуратным при использовании @Transactional

@Transactional — это один из самых удобных, но одновременно опасных аннотаций в Spring Framework. Неправильное использование приводит к сложным багам и проблемам производительности.

Основное правило

@Transactional
public void processOrder(Order order) {
    // При входе в метод: начинается транзакция
    order.setStatus("processing");
    save(order);
    // При выходе: commit или rollback
}

Spring создаёт прокси вокруг класса и управляет транзакцией.

Ловушка 1: Вызов @Transactional метода из другого метода того же класса

Это НЕ РАБОТАЕТ:

@Service
public class OrderService {
    
    @Transactional
    public void processPayment() {
        // Транзакция 1
        charge();
    }
    
    @Transactional
    public void charge() {
        // Это должна быть отдельная транзакция
        // Но она НЕ БУДЕТ отдельной!
        database.update(...);
    }
    
    public void process() {
        processPayment();  // Работает
        this.charge();     // БЕЗ НОВОЙ ТРАНЗАКЦИИ!
        // processPayment() вызывает this.charge()
        // Это обходит прокси Spring'а
    }
}

Почему? Spring создаёт прокси только на injection. Когда ты вызываешь this.charge(), ты вызываешь оригинальный объект, не прокси. Аннотация @Transactional игнорируется.

Правильно:

@Service
public class OrderService {
    
    @Autowired
    private PaymentService paymentService;  // Injection!
    
    @Transactional
    public void processOrder() {
        // Транзакция 1
        paymentService.charge();  // Через bean → прокси → транзакция!
    }
}

@Service
public class PaymentService {
    @Transactional
    public void charge() {
        // Транзакция 2 (отдельная)
        database.update(...);
    }
}

Ловушка 2: Rollback по умолчанию только для RuntimeException

@Transactional
public void process() {
    update();
    
    try {
        riskyOperation();  // throws IOException
    } catch (IOException e) {
        // Исключение обработано
    }
    
    update2();
    // Транзакция COMMITTTTTTTTTEDDDDD!
    // Потому что IOException это checked exception
}

По умолчанию Spring откатывает только unchecked exceptions (RuntimeException и потомки).

Правильно:

// Явно указать какие исключения вызывают rollback
@Transactional(rollbackFor = IOException.class)
public void process() {
    // Теперь IOException вызывает rollback
}

// Или не ловить исключение
@Transactional
public void process() throws IOException {
    riskyOperation();  // IOException пойдет выше
    // Spring перехватит и откатит
}

Ловушка 3: LazyInitializationException

Если у ентити есть lazy-loaded коллекция:

@Entity
public class User {
    @OneToMany(fetch = FetchType.LAZY)  // Ленивая загрузка!
    private List<Order> orders;  // Загружается только при обращении
}

@Service
public class UserService {
    @Transactional
    public User getUser(Long id) {
        return userRepository.findById(id).orElse(null);
    }
    
    public void printOrders(Long userId) {
        User user = getUser(userId);  // Транзакция закончилась
        user.getOrders().forEach(System.out::println);  
        // LazyInitializationException!
        // Session закрыт, orders не загружены
    }
}

Правильно:

// Вариант 1: Eager loading
@Entity
public class User {
    @OneToMany(fetch = FetchType.EAGER)  // Сразу загружается
    private List<Order> orders;
}

// Вариант 2: Загружаем в @Transactional методе
@Transactional
public User getUserWithOrders(Long id) {
    User user = userRepository.findById(id).orElse(null);
    user.getOrders().size();  // Инициализируем коллекцию ДО конца транзакции
    return user;
}

// Вариант 3: Явно жгружаем
@Transactional
public User getUserWithOrders(Long id) {
    return userRepository.findUserWithOrdersById(id);
    // Custom query с JOIN FETCH
}

Ловушка 4: Долгие транзакции

@Transactional
public void processAndEmail() {
    // Начало транзакции
    
    // 1. Обновляю БД (быстро)
    updateOrderStatus();
    
    // 2. Отправляю email (МЕДЛЕННО, 5 секунд)
    emailService.send("order@example.com");
    
    // 3. Обновляю логи (долгая операция)
    logService.writeToFile();
    
    // Конец транзакции (после email и логов)
}

Проблемы:

  • Блокируем БД долго
  • Много памяти потребляется на открытую сессию
  • Может быть deadlock если другой процесс ждет

Правильно:

@Service
public class OrderService {
    
    @Transactional
    public void updateOrderStatus(Order order) {
        // Только БД операции в транзакции
        order.setStatus("completed");
        orderRepository.save(order);
        // Транзакция СРАЗУ закончилась
    }
    
    @Async  // Асинхронно, вне транзакции
    public void sendEmail(String email) {
        emailService.send(email);
    }
    
    public void processOrder(Order order) {
        updateOrderStatus(order);  // Быстро
        sendEmail(order.getCustomerEmail());  // Асинхронно
    }
}

Ловушка 5: Readonly транзакции

@Transactional(readOnly = true)
public List<Order> getAllOrders() {
    return orderRepository.findAll();
}

Spring оптимизирует для чтения:

  • Не запускает flush
  • Может использовать только-читаемую реплику БД
  • Запрещает изменения
// ОШИБКА
@Transactional(readOnly = true)
public void updateOrder(Order order) {
    order.setStatus("paid");  // Изменение
    orderRepository.save(order);  // Может быть проигнорировано!
}

Ловушка 6: Propagation неправильно

@Service
public class OrderService {
    @Autowired
    private PaymentService paymentService;
    
    @Transactional
    public void processOrder() {
        createOrder();
        paymentService.charge();  // Разная propagation?
    }
}

@Service  
public class PaymentService {
    // Какой propagation по умолчанию?
    // PROPAGATION_REQUIRED (по умолчанию)
    // Означает: используй текущую транзакцию если есть
    @Transactional
    public void charge() {
        chargeCard();
    }
}

Важные propagation значения:

// REQUIRED (по умолчанию): используй текущую или создай новую
@Transactional(propagation = Propagation.REQUIRED)
public void method() { }  // Если транзакция есть, используй

// REQUIRES_NEW: создай НОВУЮ транзакцию
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logAudit() { }  // Всегда новая транзакция
// Если processOrder откатится, logAudit остается

// SUPPORTS: если есть транзакция, используй, нет - без неё
@Transactional(propagation = Propagation.SUPPORTS)
public void readData() { }  // Может быть с или без транзакции

// NOT_SUPPORTED: НИКОГДА не используй транзакцию
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void sendEmail() { }  // Даже если вне транзакции

// NEVER: если есть транзакция, выброс исключения
@Transactional(propagation = Propagation.NEVER)
public void externalApi() {  // Запрет на транзакцию
    externalService.call();  // Бросит exception если вызвана из @Transactional
}

Ловушка 7: Изменение после commit'а

@Transactional
public Order getAndUpdateOrder(Long id) {
    Order order = orderRepository.findById(id).get();
    order.setStatus("processed");
    return order;
    // Транзакция здесь commit'ится
    // Hibernate сохраняет изменения
}

public void use() {
    Order order = getAndUpdateOrder(1);
    order.setStatus("shipped");  // Изменяем ПОСЛЕ commit'а
    // Это изменение ПОТЕРЯЕТСЯ!
    // Нужно вызвать save() явно
}

Правильно:

@Transactional
public Order getAndUpdateOrder(Long id) {
    Order order = orderRepository.findById(id).get();
    order.setStatus("processed");
    return order;
}

public void use() {
    Order order = getAndUpdateOrder(1);
    if (someCondition) {
        updateOrderAgain(order);  // Отдельный @Transactional
    }
}

@Transactional
public void updateOrderAgain(Order order) {
    order.setStatus("shipped");
    orderRepository.save(order);  // Явно сохраняем
}

Ловушка 8: Тестирование с @Transactional

@SpringBootTest
public class OrderServiceTest {
    
    @Test
    @Transactional  // Откатывает все после теста
    public void testCreateOrder() {
        Order order = orderService.createOrder(...);
        assertThat(order).isNotNull();
        // После теста: автоматический ROLLBACK
        // В БД ничего не остается
    }
    
    // Проблема: твой код работает в production,
    // но не видит lazy-loaded fields,
    // потому что сессия открыта в тесте
}

Общие рекомендации

// ✅ Правильно:
1. Minimal транзакции - только БД операции
2. Долгие операции (email, API) вне @Transactional
3. Explicitly указывать rollbackFor если нужно
4. Внимателен к propagation
5. Спать о lazy loading
6. Не вызывать @Transactional методы через this
7. Использовать @Transactional(readOnly=true) где можно
8. Проверять LazyInitializationException при тестировании

Заключение

@Transactional требует осторожности:

  • По умолчанию не откатывает checked exceptions
  • Не работает при вызове через this
  • Может вызвать LazyInitializationException
  • Долгие транзакции блокируют ресурсы
  • Different propagation levels важны
  • Readable транзакции нужны для оптимизации
  • Тестирование может скрывать problems
  • Всегда думай о scope транзакции: минимален и сфокусирован