Что такое транзакция?
Транзакция — это последовательность операций, которые выполняются как единое целое. Если одна из операций не выполнится, все изменения отменяются. Например:
- Вы создаёте заказ.
- Уменьшаете количество товара на складе.
Если на складе не хватает товара, заказ тоже не должен быть создан. В этом и заключается суть транзакций.
Как работает @Transactional?
Когда вы добавляете аннотацию @Transactional к методу, Spring создаёт прокси, который управляет транзакцией. Вот как это работает:
- Открытие транзакции: перед вызовом метода Spring открывает новую транзакцию.
- Выполнение метода: метод выполняется в контексте транзакции.
- Фиксация или откат: если метод завершился успешно, транзакция фиксируется. Если произошло исключение, транзакция откатывается.
Пример
Допустим, вы пишете сервис для создания пользователей:
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Transactional
public void createUser(String name, String email) {
User user = new User();
user.setName(name);
user.setEmail(email);
userRepository.save(user);
if (email.contains("@example.com")) {
throw new IllegalArgumentException("Email not allowed: " + email);
}
}
}
- Если email содержит @example.com, метод выбросит исключение, и данные в базе откатятся.
- Если ошибок нет, пользователь сохранится в базе.
Настройка @Transactional
Аннотация @Transactional поддерживает множество параметров:
1. Propagation
Определяет, как текущая транзакция взаимодействует с существующими транзакциями. Основные значения:
REQUIRED (по умолчанию) — если транзакция уже существует, метод использует её; если нет, создаётся новая.
REQUIRES_NEW — всегда создаёт новую транзакцию, независимо от существующих.
SUPPORTS — если транзакция существует, метод использует её; если нет, выполняется без транзакции.
2. Isolation
Задаёт уровень изоляции транзакции. Например:
READ_UNCOMMITTED — предотвращает чтение данных которые не были подтверждены другой транзакцией (данный уровень изоляции не поддерживается ORACLE и PostgreSQL).
READ_COMMITTED (по умолчанию) — предотвращает чтение неподтверждённых данных (существует фантомное и неповторяющееся чтение).
REPEATABLE_READ — в данном уровне изоляции существует только фантомное чтение.
SERIALIZABLE — обеспечивает максимальную изоляцию, но снижает производительность.
3. Timeout
Указывает максимальное время выполнения транзакции. Если время превышено, транзакция откатывается.
@Transactional(timeout = 5) // транзакция завершится, если выполнение превысит 5 секунд
public void performLongTask() {
// Долгое выполнение
}
4. RollbackFor
Указывает, при каких исключениях следует откатывать транзакцию.
@Transactional(rollbackFor = CustomException.class)
public void performTask() throws CustomException {
// Код
}
5. ReadOnly
Используется для оптимизации операций только для чтения. Например, при чтении данных из базы:
@Transactional(readOnly = true)
public List getAllUsers() {
return userRepository.findAll();
}
Частые ошибки и почему они происходят
1. Вызов транзакционного метода изнутри класса
Spring использует прокси для управления транзакциями, и вызов метода с @Transactional изнутри того же класса обходит этот механизм. В результате транзакция не создаётся.
Пример:
@Service
public class UserService {
@Transactional
public void methodA() {
// транзакция создаётся
}
public void methodB() {
methodA(); // транзакция НЕ активируется
}
}
Решение:
Вынесите транзакционный метод в отдельный компонент или вызовите его через Spring-контекст.
2. Откат работает только для unchecked исключений
По умолчанию транзакция откатывается только при исключениях, наследующихся от RuntimeException. Если вы выбрасываете checked-исключение (например, IOException), транзакция не откатится.
Пример:
@Transactional
public void performTask() throws IOException {
// код
throw new IOException("Ошибка");
}
Почему это происходит: Spring настроен откатывать только unchecked исключения для совместимости с JPA.
Решение:
@Transactional(rollbackFor = IOException.class)
public void performTask() throws IOException {
// код
}
3. Долгие операции в транзакции
Если транзакция открыта слишком долго (например, во время работы с внешним API), это может вызвать блокировки базы данных.
Пример:
@Transactional
public void longOperation() {
userRepository.save(new User());
externalApiCall(); // долгий вызов
}
Почему это происходит: Транзакция блокирует ресурсы базы данных до её завершения.
Решение:
public void longOperation() {
userRepository.save(new User());
callExternalApi(); // без транзакции
}
Полезные настройки
- Только для чтения: добавьте
readOnly = true, чтобы ускорить выполнение методов, которые только читают данные. - Таймаут: укажите максимальное время выполнения транзакции с помощью параметра
timeout. - Откат: используйте параметр
rollbackFor, чтобы откатывать транзакцию при checked исключениях.
Заключение
Аннотация @Transactional — это удобный инструмент для управления транзакциями в Spring Boot. Она помогает избежать ошибок в работе с базой данных и сохраняет код чистым. Но важно знать, как она работает, и избегать частых ошибок.
Если вам понравилась статья, подписывайтесь на Telegram-канал JavaCraft.ru. Там вы найдёте ещё больше простых и полезных материалов!