Вопрос 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). Но если есть нетранзакционные методы — 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

  1. Есть нетранзакционные методы — unnecessary 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 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

Проблемы:

  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]]