Сервисный слой и транзакции (@Transactional)
Сервис — дом бизнес-логики. А @Transactional гарантирует, что группа операций с базой выполнится целиком или не выполнится вовсе.
Суть: @Transactional оборачивает метод в транзакцию. Если внутри вылетает исключение — все изменения откатываются. Это защищает данные от половинчатых обновлений.
Бизнес-операция часто состоит из нескольких шагов. Перевод денег: списать у одного, зачислить другому. Если после списания случится сбой, а зачисление не пройдёт — деньги исчезнут. Транзакция превращает группу операций в неделимое целое: либо все шаги, либо ни одного.
Сервис с транзакцией
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class TransferService {
private final AccountRepository accounts;
public TransferService(AccountRepository accounts) {
this.accounts = accounts;
}
@Transactional
public void transfer(Long fromId, Long toId, BigDecimal amount) {
Account from = accounts.findById(fromId).orElseThrow();
Account to = accounts.findById(toId).orElseThrow();
from.withdraw(amount); // шаг 1
to.deposit(amount); // шаг 2
// если между шагами вылетит исключение -> откат обоих
}
}
@Transactional на методе создаёт границу транзакции: она открывается на входе и фиксируется (commit) на выходе. Если вылетает непроверяемое исключение (RuntimeException), Spring делает откат (rollback).
Как работает под капотом
Spring оборачивает бин в прокси. Когда вызывается @Transactional-метод, прокси сначала открывает транзакцию, затем вызывает ваш код, и в конце — фиксирует или откатывает. Важное следствие: вызов @Transactional-метода изнутри того же класса идёт мимо прокси и транзакция не стартует.
Вызов transfer(...)
|
v
ПРОКСИ Spring
┌──────────────────────────┐
│ BEGIN TRANSACTION │
│ твой метод │
│ ok? -> COMMIT │
│ исключение? -> ROLLBACK │
└──────────────────────────┘
|
v
база в согласованном состоянии
Смоделируем транзакцию «всё или ничего»:
# Транзакция: применяем изменения только если все шаги прошли
accounts = {"A": 100, "B": 50}
def transfer(from_acc, to_acc, amount):
snapshot = dict(accounts) # запоминаем состояние (для отката)
try:
if accounts[from_acc] < amount:
raise ValueError("Недостаточно средств")
accounts[from_acc] -= amount # шаг 1
accounts[to_acc] += amount # шаг 2
print("COMMIT:", accounts)
except Exception as e:
accounts.clear()
accounts.update(snapshot) # ROLLBACK
print("ROLLBACK (", e, "):", accounts)
transfer("A", "B", 30) # успех
transfer("A", "B", 999) # откат, состояние не изменилось
Нажмите «Попробуй сам ▶»: при ошибке состояние откатывается к снимку — это и есть атомарность транзакции.
Частые ошибки
- Самовызов @Transactional-метода. Вызов через
this.method()идёт мимо прокси — транзакция не откроется. - Ожидать откат на checked-исключении. По умолчанию Spring откатывает только на RuntimeException; для проверяемых нужен
rollbackFor. - @Transactional на контроллере. Транзакции — забота сервисного слоя, не веба.
Best practices
- Ставьте
@Transactionalна методы сервиса, объединяющие несколько операций с БД. - Для чтения используйте
@Transactional(readOnly = true)— это подсказка оптимизатору. - Держите транзакции короткими: не зовите внутри них долгие внешние вызовы (HTTP).
Итог: сервис содержит бизнес-логику, а @Transactional делает группу операций атомарной. Помните про прокси (самовызов не работает) и про правила отката (по умолчанию — RuntimeException).
Закрепим главное
Транзакция превращает набор операций в неделимое целое: либо применяются все изменения, либо ни одного. Это свойство атомарности защищает данные от половинчатых состояний, которые иначе пришлось бы вычищать вручную. Ставьте @Transactional там, где бизнес-операция объединяет несколько шагов с базой, — именно сервисный слой, а не контроллер, отвечает за границы транзакций.
Две ловушки заслуживают того, чтобы запомнить их наизусть. Первая — самовызов: транзакциями управляет прокси вокруг бина, и вызов this.method() внутри того же класса идёт мимо прокси, поэтому транзакция не откроется. Если нужен транзакционный внутренний вызов — выносите метод в отдельный бин. Вторая — правила отката: по умолчанию Spring откатывает транзакцию только при непроверяемых исключениях (RuntimeException), а для проверяемых нужно явно указать rollbackFor. Не зная этого, легко получить «зафиксированную наполовину» операцию там, где ждали отката.