Ответ
В Spring-приложениях, следующих общепринятым best practices, транзакции объявляются на уровне сервисов (Service layer), а не на уровне репозиториев (DAO/Repository layer).
Почему на уровне сервиса?
- Граница бизнес-транзакции: Сервисный слой инкапсулирует бизнес-логику, которая часто требует согласованного выполнения нескольких операций с БД в рамках одной транзакции. Если объявлять транзакции в репозитории, каждая операция будет в своей транзакции, что может нарушить целостность.
- Переиспользование: Методы репозитория могут вызываться из разных сервисов в разных транзакционных контекстах. Жесткая привязка транзакции к репозиторию ограничивает гибкость.
- Явность и контроль: Размещение
@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 и БД. - Уровень контроллера: Объявление транзакций в контроллере считается антипаттерном, так как смешивает ответственность и может удерживать соединение с БД на время рендеринга представления.