Питання 17 · Розділ 11

На якому рівні можна використовувати @Transactional?

Анотацію @Transactional можна ставити на метод, клас або інтерфейс. Але не всі варіанти однаково хороші.

Мовні версії: English Russian Ukrainian

🟢 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

  1. Є нетранзакційні методи — непотрібний overhead на кожен виклик
  2. Різні propagation/isolation — доведеться перевизначати на кожному методі, що збиває з пантелику
  3. Клас містить і 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

Проблеми:

  1. N+1 queries: Lazy loading під час JSON серіалізації тригерить додаткові запити
  2. Connection held: З’єднання зайняте поки рендериться view
  3. 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)

  1. Bridge methods: Java генерує bridge methods при generics. Spring повинен правильно резолвити @Transactional з bridge method на target method. В деяких версіях Spring були баги з @Transactional на generic методах.

  2. Наслідування з перевизначенням: Якщо BaseService.save() має @Transactional(timeout = 10), а DerivedService.save() — без анотації, поведінка залежить від proxy type. CGLIB може не знайти анотацію в superClass за певних умов.

  3. Multiple @Transactional на одному методі: Якщо метод одночасно успадкував @Transactional від інтерфейсу та від класу — тільки одна застосовується (порядок не визначений). Spring бере перше знайдене.

  4. @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

  1. Method-level granularity: Точний контроль — тільки write-методи з @Transactional, read-методи з readOnly = true.
  2. Disable OSIV в production: spring.jpa.open-in-view: false.
  3. Never put @Transactional on interfaces: CGLIB — silent failure.
  4. Avoid class-level @Transactional в великих сервісах — деякі методи можуть не потребувати транзакції.
  5. Consider connection pool sizing: @Transactional тримає connection на всю тривалість метода. Довгі методи = pool exhaustion.
  6. 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]]