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

Как открыть новую транзакцию при вызове метода @Service

2.3 Middle🔥 181 комментариев
#Spring Boot и Spring Data#Spring Framework

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

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

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

# Управление транзакциями в Spring (@Transactional)

Основное понятие

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

Как работает @Transactional

Spring использует AOP прокси-паттерн: при вызове метода с аннотацией @Transactional, прокси перехватывает вызов, открывает транзакцию, выполняет метод, затем коммитит или откатывает транзакцию.

// Без @Transactional
public void createOrder(Order order) {  // Запросы выполняются без транзакции
    repository.save(order);
}

// С @Transactional
@Transactional
public void createOrder(Order order) {  // Все запросы в одной транзакции
    repository.save(order);
    // Если исключение — откат
    // Если успех — коммит
}

Базовый синтаксис

@Service
public class OrderService {
    
    @Autowired
    private OrderRepository orderRepository;
    
    @Autowired
    private UserRepository userRepository;
    
    // Простой случай — новая транзакция
    @Transactional
    public Order createOrder(Long userId, OrderRequest request) {
        User user = userRepository.findById(userId);
        Order order = new Order(user, request.getAmount());
        return orderRepository.save(order);
    }
}

Параметры @Transactional

1. readOnly

Оптимизация для операций только на чтение:

@Transactional(readOnly = true)
public List<Order> getAllOrders() {
    // Spring может применить оптимизации на уровне БД
    // Например, на уровне изоляции
    return orderRepository.findAll();
}

2. isolation

Уровень изоляции транзакции:

@Transactional(isolation = Isolation.SERIALIZABLE)
public void transferMoney(Long fromId, Long toId, double amount) {
    // Максимальная изоляция, предотвращает все аномалии
}

@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void fastRead() {
    // Быстрое чтение, возможны грязные чтения
}

Уровни (от слабого к сильному):

  • READ_UNCOMMITTED — видит незакоммиченные изменения
  • READ_COMMITTED — видит только закоммиченные
  • REPEATABLE_READ — повторяемое чтение
  • SERIALIZABLE — полная изоляция

3. propagation

Поведение при вложенных вызовах:

@Transactional
public void outerMethod() {
    innerMethod();  // Что произойдёт с транзакцией?
}

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

Все варианты propagation:

// REQUIRED (по умолчанию)
@Transactional(propagation = Propagation.REQUIRED)
public void required() {
    // Использует существующую или создаёт новую
}

// REQUIRES_NEW
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void requiresNew() {
    // ВСЕГДА создаёт новую транзакцию
    // Даже если уже есть, приостанавливает текущую
}

// NESTED
@Transactional(propagation = Propagation.NESTED)
public void nested() {
    // Создаёт nested транзакцию (savepoint)
    // Если ошибка — откат только вложенной
}

// NEVER
@Transactional(propagation = Propagation.NEVER)
public void never() {
    // Выбросит исключение, если есть активная транзакция
}

// NOT_SUPPORTED
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void notSupported() {
    // Приостанавливает текущую транзакцию
}

// SUPPORTS
@Transactional(propagation = Propagation.SUPPORTS)
public void supports() {
    // Использует существующую, если нет — выполняет без транзакции
}

// MANDATORY
@Transactional(propagation = Propagation.MANDATORY)
public void mandatory() {
    // Требует существующую транзакцию, иначе ошибка
}

4. rollbackFor и noRollbackFor

@Service
public class PaymentService {
    
    // По умолчанию откатывается на RuntimeException и Error
    @Transactional
    public void processPayment(Payment payment) {
        if (payment.getAmount() < 0) {
            throw new IllegalArgumentException("Invalid amount");
            // Откат
        }
    }
    
    // Откатывать на конкретных исключениях
    @Transactional(rollbackFor = {PaymentException.class, IOException.class})
    public void processWithSpecificRollback() {
        // Откатится на PaymentException или IOException
    }
    
    // НЕ откатывать на конкретных исключениях
    @Transactional(noRollbackFor = ClientException.class)
    public void processIgnoringClient() {
        // Не откатится на ClientException
    }
    
    // Откатывать на Checked исключениях
    @Transactional(rollbackFor = Exception.class)
    public void processWithCheckedException() throws Exception {
        // Откатится на любом Exception
    }
}

5. timeout

@Transactional(timeout = 30)  // 30 секунд
public void longOperation() {
    // Если выполняется дольше 30 сек — выбросит TimedOutException
}

Практические примеры

Пример 1: Базовая операция

@Service
public class OrderService {
    private final OrderRepository orderRepo;
    private final PaymentService paymentService;
    
    public OrderService(OrderRepository orderRepo, PaymentService paymentService) {
        this.orderRepo = orderRepo;
        this.paymentService = paymentService;
    }
    
    @Transactional
    public Order placeOrder(Long userId, OrderRequest request) {
        // 1. Создаём заказ
        Order order = new Order(userId, request.getAmount());
        orderRepo.save(order);
        
        // 2. Обрабатываем платёж
        // Если платёж не удался — откат ВСЕГО (включая save заказа)
        paymentService.processPayment(order);
        
        return order;  // Успех — коммит транзакции
    }
}

Пример 2: Вложенные транзакции

@Service
public class OrderService {
    private final OrderRepository orderRepo;
    private final NotificationService notificationService;
    
    @Transactional
    public Order createOrder(OrderRequest request) {
        Order order = new Order(request);
        orderRepo.save(order);
        
        // Отправляем уведомление в отдельной транзакции
        notificationService.sendOrderConfirmation(order);
        
        return order;
    }
}

@Service
public class NotificationService {
    private final NotificationRepo notifRepo;
    
    // REQUIRES_NEW: даже если основная транзакция откатится,
    // уведомление будет сохранено
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void sendOrderConfirmation(Order order) {
        Notification notification = new Notification(order);
        notifRepo.save(notification);
        // Отправляем email/SMS
    }
}

Пример 3: Откат при исключении

@Service
public class TransferService {
    private final AccountRepository accountRepo;
    
    @Transactional
    public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
        Account from = accountRepo.findById(fromId);
        Account to = accountRepo.findById(toId);
        
        if (from.getBalance().compareTo(amount) < 0) {
            throw new InsufficientFundsException("Not enough money");
            // Откат — от и до остаются в исходном состоянии
        }
        
        from.withdraw(amount);
        to.deposit(amount);
        
        accountRepo.save(from);
        accountRepo.save(to);
        // Коммит обеих операций атомарно
    }
}

Пример 4: readOnly оптимизация

@Service
public class ReportService {
    private final OrderRepository orderRepo;
    private final UserRepository userRepo;
    
    @Transactional(readOnly = true)
    public ReportDto generateReport(LocalDate from, LocalDate to) {
        // Spring может применить оптимизации для чтения
        List<Order> orders = orderRepo.findByDateRange(from, to);
        List<User> users = userRepo.findAll();
        
        return new ReportDto(orders, users);
    }
    
    // БЕЗ readOnly для изменения
    @Transactional
    public void updateReport(Report report) {
        reportRepo.save(report);
    }
}

Пример 5: Обработка исключений

@Service
public class UserService {
    private final UserRepository userRepo;
    
    // Откатывать на BusinessException
    @Transactional(rollbackFor = BusinessException.class)
    public User createUser(UserRequest request) throws BusinessException {
        if (userRepo.existsByEmail(request.getEmail())) {
            throw new BusinessException("Email already exists");
            // Откат
        }
        
        User user = new User(request);
        return userRepo.save(user);
    }
    
    // НЕ откатывать на ClientException
    @Transactional(noRollbackFor = ClientException.class)
    public void processUserData(UserData data) throws ClientException {
        if (data.isInvalid()) {
            throw new ClientException("Invalid data");  // НЕ откат
        }
        // Обработка
    }
}

Частые ошибки

Ошибка 1: @Transactional на классе вместо методов

// ❌ Плохо
@Service
@Transactional
public class UserService {
    public void readUser() { }  // Тоже получит @Transactional
    public void writeUser() { }
}

// ✅ Хорошо
@Service
public class UserService {
    @Transactional(readOnly = true)
    public void readUser() { }
    
    @Transactional
    public void writeUser() { }
}

Ошибка 2: Вызов из того же класса

@Service
public class OrderService {
    
    @Transactional
    public void createOrder() {
        // ...
        processPayment();  // @Transactional НЕ сработает!
    }
    
    @Transactional
    public void processPayment() { }
}

// Правильно: через另инъецированный сервис
@Service
public class OrderService {
    private final PaymentService paymentService;
    
    public OrderService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }
    
    @Transactional
    public void createOrder() {
        paymentService.processPayment();  // Сработает @Transactional
    }
}

Ошибка 3: Неправильный propagation

// ❌ Если нужна отдельная транзакция
@Transactional(propagation = Propagation.REQUIRED)
public void audit() { }

// ✅ Правильно
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void audit() {
    // Создаст новую транзакцию, даже если есть существующая
}

Конфигурация TransactionManager

@Configuration
public class TransactionConfig {
    
    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory emf) {
        return new JpaTransactionManager(emf);
    }
}

Лучшие практики

  1. Используй @Transactional только на сервисах
  2. Указывай readOnly для операций чтения — оптимизация
  3. Используй конструктор вместо field injection — явность
  4. Откатывай на ошибках — используй rollbackFor
  5. Избегай больших транзакций — разбивай на меньшие
  6. Используй REQUIRES_NEW для audit логов — они сохранятся даже при откате
  7. Не вызывай @Transactional методы из того же класса — используй инъекцию

@Transactional — это один из самых важных инструментов в Spring для гарантии целостности данных.