С чем нужно быть аккуратным при использовании @Transactional
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
С чем нужно быть аккуратным при использовании @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 транзакции: минимален и сфокусирован