На якому рівні можна використовувати @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). Але якщо є нетранзакційні методи — непотрібний 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
- Є нетранзакційні методи — непотрібний 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 override 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]]