На каком уровне можно использовать @Transactional?
Аннотацию @Transactional можно ставить на метод, класс или интерфейс. Но не все варианты одинаково хороши.
🟢 Junior Level
Аннотацию @Transactional можно ставить на метод, класс или интерфейс. Но не все варианты одинаково хороши.
Уровень метода (рекомендуется)
Самый точный подход. Вы контролируете каждый метод отдельно. Почему: каждый метод может иметь свою propagation, isolation, readOnly. Это даёт максимальную гибкость без побочных эффектов на соседние методы.
@Service
public class UserService {
@Transactional
public void createUser(User user) {
userRepository.save(user);
}
@Transactional(readOnly = true)
public User findById(Long id) {
return userRepository.findById(id).orElse(null);
}
}
Уровень класса
Все public методы класса становятся транзакционными. Почему: удобно, когда весь класс — сервис с одной зоной ответственности (только CRUD). Но если есть нетранзакционные методы — unnecessary overhead на каждый вызов.
@Service
@Transactional // Все public методы — транзакционны
public class UserService {
public void createUser(User user) { ... }
public User findById(Long id) { ... }
}
Уровень интерфейса (не рекомендуется)
@Transactional на интерфейсе работает только с JDK Dynamic Proxy. Если Spring использует CGLIB (по умолчанию для классов без интерфейсов) — аннотация на интерфейсе игнорируется. Поэтому best practice — ставить @Transactional на класс или метод.
public interface UserService {
@Transactional // Работает только с JDK Dynamic Proxy
User findById(Long id);
}
Где НЕ стоит использовать
На контроллере — антипаттерн. Контроллер отвечает за HTTP, а транзакция — за целостность данных. Смешивание приводит к удержанию соединения с БД во время сериализации JSON.
Золотое правило
@Transactional — на сервисном слое (Business Logic Layer). Не на Controller, не на Repository (кроме кастомных модифицирующих запросов).
Когда НЕ ставить @Transactional на class level
- Есть нетранзакционные методы — unnecessary overhead на каждый вызов
- Разные propagation/isolation — придётся переопределять на каждом методе, что сбивает с толку
- Класс содержит и read, и write методы — readOnly придётся ставить на каждый read-метод
🟡 Middle Level
Приоритет аннотаций
Если аннотация стоит и на классе, и на методе — метод побеждает.
@Service
@Transactional(readOnly = true) // Default для всех методов
public class UserService {
public User findById(Long id) { ... } // readOnly = true (наследует)
@Transactional // Переопределяет — readOnly = false, REQUIRED
public void updateUser(User user) { ... }
}
Уровень репозитория
Spring Data репозитории уже размечены @Transactional(readOnly = true).
public interface UserRepository extends JpaRepository<User, Long> {
// findById, findAll — уже транзакционны (readOnly)
@Modifying
@Transactional // Обязательно для UPDATE/DELETE
@Query("UPDATE User u SET u.active = false WHERE u.id = :id")
void deactivate(@Param("id") Long id);
}
Важно: Если вызвать @Modifying метод из сервиса с @Transactional(readOnly = true) — получите TransactionRequiredException, т.к. параметр сервиса переопределяет параметры репозитория.
Почему @Transactional на Controller — антипаттерн
@RestController
@Transactional // ПЛОХО!
public class UserController {
@PostMapping("/users")
public User create(@RequestBody User user) {
User saved = userService.create(user);
return saved; // JSON serialization внутри транзакции!
}
}
Проблемы:
- Соединение с БД удерживается во время сериализации JSON
- Lazy-loading триггерит N+1 запросы в слое контроллера
- Под нагрузкой → exhaustion пула соединений
Сравнение уровней
| Уровень | Точность | Гибкость | Когда использовать |
|---|---|---|---|
| Метод | Высокая | Высокая | Всегда рекомендуется |
| Класс | Средняя | Средняя | Когда все методы одного типа (только CRUD) |
| Интерфейс | Низкая | Низкая | ❌ Не рекомендуется |
| Контроллер | — | — | ❌ Антипаттерн |
Распространённые ошибки
| Ошибка | Что происходит | Как исправить |
|---|---|---|
@Transactional на интерфейсе + CGLIB |
Аннотация не видна, нет транзакции | Ставить на implementation класс |
@Modifying внутри readOnly сервиса |
TransactionRequiredException |
Переопределить readOnly = false |
Классовая readOnly = true без переопределения |
Write-методы тоже readOnly | Явно аннотировать write-методы |
@Transactional на final методе |
CGLIB не может переопределить — аннотация игнорируется | Убрать final |
Когда НЕ использовать
| Ситуация | Почему | Альтернатива |
|---|---|---|
| Простой SELECT по ID | Избыточно | Без @Transactional или readOnly = true |
| HTTP-вызов в методе | Держит DB connection при сетевом I/O | Вынести HTTP за транзакцию |
| Фоновая обработка | Может быть асинхронной | @Async с отдельной транзакцией |
| Кеширование | Не требует атомарности | Без транзакции |
🔴 Senior Level
Annotation Resolution Order
// AbstractFallbackTransactionAttributeSource
protected TransactionAttribute findTransactionAttribute(Method method, Class<?> targetClass) {
// 1. Check method first (highest priority)
TransactionAttribute attr = determineTransactionAttribute(method);
if (attr != null) return attr;
// 2. Check method's declaring class
attr = determineTransactionAttribute(method.getDeclaringClass());
if (attr != null) return attr;
// 3. Check bridge methods (interface vs implementation)
if (ClassUtils.isUserLevelMethod(method, targetClass)) {
Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
attr = determineTransactionAttribute(specificMethod);
if (attr != null) return attr;
attr = determineTransactionAttribute(specificMethod.getDeclaringClass());
}
return null;
}
Ключевой принцип: Method-level annotation ALWAYS overrides class-level. Наиболее специфичное совпадение побеждает.
Proxy Type Impact on Annotation Detection
JDK Dynamic Proxy (interface-based)
public interface UserService {
@Transactional
User findById(Long id);
}
@Service
public class UserServiceImpl implements UserService {
// No @Transactional — РАБОТАЕТ, proxy на интерфейсе
public User findById(Long id) { ... }
}
CGLIB Proxy (class-based)
public interface UserService {
@Transactional // ИГНОРИРУЕТСЯ с CGLIB!
User findById(Long id);
}
@Service
public class UserServiceImpl implements UserService {
// No @Transactional — НЕ РАБОТАЕТ с CGLIB
public User findById(Long id) { ... }
}
CGLIB создаёт подкласс UserServiceImpl, а не proxy интерфейса. Аннотация интерфейса не видна.
Решение: Всегда ставьте @Transactional на implementation class/method, никогда на интерфейсы.
OSIV (Open Session in View) — Deep Analysis
# application.yml — по умолчанию true!
spring:
jpa:
open-in-view: true
С OSIV:
Request → Filter opens Session → Controller → Service → View render → Filter closes Session
Проблемы:
- N+1 queries: Lazy loading во время JSON сериализации триггерит дополнительные запросы
- Connection held: Соединение занято пока рендерится view
- Hidden performance issues: Lazy loading маскирует неэффекный fetching
Решение — отключить OSIV:
spring:
jpa:
open-in-view: false
@Transactional(readOnly = true)
public UserDTO getUserWithDetails(Long id) {
User user = userRepository.findByIdWithDetails(id); // Eager fetch
return UserDTO.from(user); // Полностью загруженная сущность
}
Edge Cases (минимум 3)
-
Bridge methods: Java генерирует bridge methods при generics. Spring должен правильно резолвить
@Transactionalс bridge method на target method. В некоторых версиях Spring были баги с@Transactionalна generic методах. -
Наследование с переопределением: Если
BaseService.save()имеет@Transactional(timeout = 10), аDerivedService.save()— без аннотации, поведение зависит от proxy type. CGLIB может не найти аннотацию в superClass при определённых условиях. -
Multiple
@Transactionalна одном методе: Если метод одновременно унаследовал@Transactionalот интерфейса и от класса — только одна применяется (порядок не определён). Spring берёт первое найденное. -
@Transactionalна@Configurationклассах: Bean creation methods внутри@Configurationклассов не оборачиваются в транзакции автоматически.@Transactionalна@Beanметоде не имеет эффекта — бин создаётся вне transaction context.
Layer Architecture
┌─────────────────────────────────────────┐
│ Controller Layer │
│ - HTTP request/response │
│ - Validation │
│ - NO @Transactional │
├─────────────────────────────────────────┤
│ Service Layer │
│ - Business logic │
│ - @Transactional HERE ✓ │
│ - Transaction boundaries defined here │
├─────────────────────────────────────────┤
│ Repository Layer │
│ - Data access │
│ - Spring Data auto-transactional │
│ - Only @Transactional for @Modifying │
└─────────────────────────────────────────┘
Rule: Transaction boundaries должны совпадать с бизнес-use-case boundaries, не с техническими операциями.
Performance Numbers
| Операция | Время |
|---|---|
| Annotation resolution (Spring cache) | ~0.1 μs |
| Proxy invocation | ~1-2 μs |
| Transaction setup | ~5-10 μs |
| Connection acquisition (HikariCP) | ~50-200 μs |
Memory Implications
- Классовая
@Transactionalсоздаёт proxy для всех public методов - Каждый метод резолвит
TransactionAttribute(кешируется) - ThreadLocal entries: ~1 KB per active transaction
- Hibernate L1 cache: растёт линейно с количеством загруженных entities
Thread Safety
Аннотация сама по себе immutable и thread-safe. Состояние транзакции хранится в ThreadLocal — каждый поток изолирован. Singleton бин безопасен при конкурентных вызовах.
Production War Story
Проект с микросервисами: @Transactional стоял на интерфейсе UserService. В dev-среде работал JDK Dynamic Proxy (был интерфейс). В production — CGLIB (Spring Boot auto-config). Результат: в production транзакции не открывались, данные коммитились без rollback при ошибках. Баг проявился только под нагрузкой. Fix: перенести @Transactional на implementation класс.
Monitoring
Debug logging:
logging:
level:
org.springframework.transaction: DEBUG
org.springframework.aop: TRACE
Actuator:
GET /actuator/beans # Видно какие бины обернуты в proxy
Programmatically:
AopUtils.isAopProxy(bean) // true если proxy создан
AopUtils.isCglibProxy(bean) // true если CGLIB
Highload Best Practices
- Method-level granularity: Точный контроль — только write-методы с
@Transactional, read-методы сreadOnly = true. - Disable OSIV в production:
spring.jpa.open-in-view: false. - Never put
@Transactionalon interfaces: CGLIB — silent failure. - Avoid class-level
@Transactionalв больших сервисах — некоторые методы могут не нуждаться в транзакции. - Consider connection pool sizing:
@Transactionalдержит connection на всю длительность метода. Долгие методы = pool exhaustion. - Use
@Transactional(readOnly = true)на query-сервисах: Может роутить на read replica черезAbstractRoutingDataSource.
🎯 Шпаргалка для интервью
Обязательно знать:
- @Transactional ставится на метод (рекомендуется), класс или интерфейс
- Метод побеждает класс — method-level annotation всегда override class-level
- Золотое правило: @Transactional на сервисном слое, НЕ на Controller или Repository
- @Transactional на интерфейсе работает только с JDK Dynamic Proxy, игнорируется с CGLIB
- Spring Data репозитории уже имеют @Transactional(readOnly = true) — @Modifying требует override
- OSIV (Open Session in View) по умолчанию true — вызывает N+1 queries, рекомендуется отключить
Частые уточняющие вопросы:
- Почему @Transactional на Controller — антипаттерн? — Connection held during JSON serialization, lazy loading N+1
- Что такое OSIV и почему его отключают? — Session открыт до render view, маскирует N+1, держит connection
- Когда class-level @Transactional оправдан? — Когда все методы одного типа (только CRUD)
- Что будет если @Modifying метод внутри readOnly сервиса? — TransactionRequiredException
Красные флаги (НЕ говорить):
- “@Transactional на интерфейсе всегда работает” — CGLIB silently ignores
- “Controller с @Transactional — нормальная практика” — connection pool exhaustion risk
- “OSIV = хорошо” — маскирует performance issues, N+1 queries
Связанные темы:
- [[16. Что такое аннотация @Transactional]]
- [[21. Что такое readonly транзакция]]
- [[22. Что произойдёт при вызове @Transactional метода из другого метода того же класса]]
- [[13. Что такое Propagation в Spring]]