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

Що робить анотація @Transactional?

4. Уникайте REQUIRES_NEW без причини 5. Не робіть довгі транзакції 6. TransactionalEventListener → для подій після комміту 7. Self-invocation → не працює!

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

🟢 Junior Level

@Transactional — анотація, яка автоматично керує транзакціями БД.

Проста аналогія: Банківський переказ. Або гроші списалися І надійшли, або нічого не сталося. Не можна, щоб гроші списалися, але не надійшли.

@Service
public class OrderService {

    @Transactional
    public void createOrder(Order order) {
        orderRepository.save(order);       // SQL INSERT
        paymentRepository.charge(order);   // SQL UPDATE
        // Якщо помилка → все відкотиться!
    }
}

Як працює:

Proxy → BEGIN TRANSACTION → викликає метод → COMMIT
                               ↓ (помилка)
                            ROLLBACK

🟡 Middle Level

Propagation (поширення)

Propagation Поведінка
REQUIRED (за замовч.) Використовує існуючу або створює нову
REQUIRES_NEW Завжди створює нову (призупиняє поточну)
NESTED Вкладена через Savepoint
MANDATORY Вимагає існуючу (інакше помилка)

Isolation (ізоляція)

Isolation Що запобігає
READ_COMMITTED Dirty Reads
REPEATABLE_READ Non-repeatable Reads
SERIALIZABLE Phantom Reads (але повільно!)

Rollback правила

// За замовчуванням: тільки RuntimeException та Error (unchecked exceptions). Checked exception (Exception і нижче) НЕ викликають rollback.
@Transactional
public void save() throws IOException { }
// IOException → НЕ відкотиться!

// ✅ Явне вказання
@Transactional(rollbackFor = Exception.class)
public void save() throws IOException { }
// IOException → відкотиться!

readOnly оптимізація

@Transactional(readOnly = true)
public List<User> findAll() {
    // → Hibernate вимикає Dirty Checking
    // → Економія CPU та пам'яті
}

🔴 Senior Level

REQUIRES_NEW ризик

@Transactional
public void outer() {
    inner();  // REQUIRES_NEW → нова транзакція!
}

@Transactional(propagation = REQUIRES_NEW)
public void inner() {
    // Призупиняє зовнішню транзакцію
    // Бере ІНШЕ з'єднання з пулу!
    // → Ризик deadlock при малому пулі
}

Connection Release проблема

@Transactional
public void process() {
    repository.save(order);     // З'єднання взято
    httpClient.callExternal();  // 5 секунд → з'єднання тримається!
    repository.save(payment);
}

// → З'єднання може утримуватися протягом усієї транзакції. Поведінка залежить від ConnectionReleaseMode: за замовчуванням у Spring + Hibernate з'єднання утримується до commit/rollback.

// Рішення: розбити на два методи

TransactionalEventListener

// Надіслати в Kafka ТІЛЬКИ після комміту!
@TransactionalEventListener(phase = AFTER_COMMIT)
public void onOrderCreated(OrderCreatedEvent event) {
    kafkaTemplate.send("orders", event);
}

// → Якщо транзакція відкотиться → подія НЕ надішлеться!
// → Якщо метод, що публікує подію, НЕ в транзакції — подія надішлеться негайно, як якби це був звичайний ApplicationEvent.

Production Experience

Реальний сценарій: довга транзакція

@Transactional метод:
  1. Зберегти замовлення (10 мс)
  2. HTTP виклик зовнішнього API (3 секунди)
  3. Зберегти платіж (10 мс)

→ З'єднання зайняте 3+ секунди!
→ При 100 RPS → 300 з'єднань потрібно!
→ Пул з 20 → всі зайняті → таймаути!

Рішення: розбити на два методи

Best Practices

  1. REQUIRED — за замовчуванням
  2. rollbackFor = Exception.class — завжди явно
  3. readOnly = true — для читання
  4. Уникайте REQUIRES_NEW без причини
  5. Не робіть довгі транзакції
  6. TransactionalEventListener → для подій після комміту
  7. Self-invocation → не працює!

Резюме для Senior

  • Proxy → TransactionInterceptor керує транзакцією
  • Propagation → REQUIRED/REQUIRES_NEW/NESTED
  • rollbackFor → явно вказуйте Exception.class
  • readOnly → вимикає Dirty Checking
  • Connection Release → не тримайте довго
  • TransactionalEventListener → AFTER_COMMIT
  • Self-invocation → не працює!

🎯 Шпаргалка для інтерв’ю

Обов’язково знати:

  • @Transactional = автоматичне керування транзакціями: BEGIN → метод → COMMIT / ROLLBACK
  • За замовчуванням: propagation = REQUIRED, rollback тільки для RuntimeException та Error
  • Propagation: REQUIRED (використовує/створює), REQUIRES_NEW (завжди нова), NESTED (через Savepoint)
  • Isolation: READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE — що запобігають
  • rollbackFor = Exception.class — завжди явно вказуйте для checked exceptions
  • readOnly = true — вимикає Hibernate Dirty Checking, економить CPU/пам’ять
  • REQUIRES_NEW бере ІНШЕ з’єднання з пулу → ризик deadlock при малому пулі
  • TransactionalEventListener(AFTER_COMMIT) — подія надішлеться тільки після комміту
  • Self-invocation НЕ працює — виклик через this повз проксі

Часті уточнюючі запитання:

  • Які виключення викликають rollback за замовчуванням? → Тільки unchecked (RuntimeException, Error). Checked Exception — НЕ відкочують
  • Чим REQUIRES_NEW відрізняється від NESTED? → REQUIRES_NEW = окрема транзакція, NESTED = вкладена через Savepoint в рамках тієї ж
  • Чому REQUIRES_NEW небезпечний? → Бере інше з’єднання, при малому пулі — deadlock/timeout
  • Що робити з довгими транзакціями? → Розбити на методи, прибрати HTTP-виклики з транзакції

Червоні прапорці (НЕ говорити):

  • «@Transactional відкочує всі виключення» — тільки unchecked за замовчуванням
  • «REQUIRES_NEW = вкладена транзакція» — це NESTED, REQUIRES_NEW — окрема
  • «readOnly = true забороняє запис» — це оптимізація Hibernate, не заборона
  • «Транзакція звільняє з’єднання між викликами» — утримує до commit/rollback

Пов’язані теми:

  • [[13. Що таке proxy в Spring]]
  • [[18. Чому @Transactional не працює при self-invocation]]
  • [[19. Як вирішити проблему з self-invocation]]
  • [[14. Коли Spring створює proxy]]