На каком уровне (слое) приложения в Spring обычно объявляются транзакции и почему?

«На каком уровне (слое) приложения в Spring обычно объявляются транзакции и почему?» — вопрос из категории Spring, который задают на 10% собеседований Java Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

В Spring-приложениях, следующих общепринятым best practices, транзакции объявляются на уровне сервисов (Service layer), а не на уровне репозиториев (DAO/Repository layer).

Почему на уровне сервиса?

  1. Граница бизнес-транзакции: Сервисный слой инкапсулирует бизнес-логику, которая часто требует согласованного выполнения нескольких операций с БД в рамках одной транзакции. Если объявлять транзакции в репозитории, каждая операция будет в своей транзакции, что может нарушить целостность.
  2. Переиспользование: Методы репозитория могут вызываться из разных сервисов в разных транзакционных контекстах. Жесткая привязка транзакции к репозиторию ограничивает гибкость.
  3. Явность и контроль: Размещение @Transactional в сервисе делает границы транзакции явными и соответствующими границам бизнес-операции.

Типичная архитектура:

// Уровень РЕПОЗИТОРИЯ (БЕЗ @Transactional) - отвечает только за доступ к данным
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    // Spring Data JPA предоставляет реализации. Транзакции здесь не объявляем.
}

// Уровень СЕРВИСА (С @Transactional) - отвечает за бизнес-логику и границы транзакции
@Service
public class UserRegistrationService {

    @Autowired
    private UserRepository userRepository;
    @Autowired
    private AuditLogRepository auditLogRepository;

    // Одна бизнес-операция = одна транзакция
    @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
    public User registerUser(UserDto userDto) {
        // 1. Проверка бизнес-правил
        if (userRepository.existsByEmail(userDto.getEmail())) {
            throw new EmailAlreadyExistsException();
        }
        // 2. Создание сущности
        User user = new User(userDto);
        userRepository.save(user); // Вызов репозитория внутри транзакции сервиса

        // 3. Сопутствующая логическая операция (должна быть в той же транзакции)
        auditLogRepository.log(Action.USER_CREATED, user.getId());

        // Если здесь выбросится исключение, откатятся и save, и log.
        return user;
    }
}

Важные исключения и нюансы:

  • Spring Data JPA: Предоставляет встроенную транзакционность для своих CRUD-методов, но она обычно имеет настройки по умолчанию (Propagation.REQUIRED), что позволяет им участвовать в существующей транзакции сервиса.
  • Read-only операции: Для методов, которые только читают данные, можно использовать @Transactional(readOnly = true), что дает оптимизации на уровне Hibernate и БД.
  • Уровень контроллера: Объявление транзакций в контроллере считается антипаттерном, так как смешивает ответственность и может удерживать соединение с БД на время рендеринга представления.