← Назад к вопросам
Что будет, если не указать @Transactional при работе с репозиторием
2.0 Middle🔥 231 комментариев
#Spring Framework
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Отсутствие @Transactional при работе с репозиторием
Суть проблемы
Eсли не указать @Transactional, то каждый вызов метода репозитория выполняется в отдельной транзакции (auto-commit режим), что приводит к серьезным проблемам целостности данных и производительности.
Что происходит по умолчанию
// БЕЗ @Transactional
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public void createUserWithAccount(User user, Account account) {
// Каждый save() — отдельная транзакция (auto-commit)
User savedUser = userRepository.save(user); // COMMIT 1
account.setUserId(savedUser.getId());
accountRepository.save(account); // COMMIT 2
// Если между COMMIT 1 и 2 произойдет исключение,
// User создан, но Account нет! Неконсистентное состояние.
}
}
// С @Transactional
@Service
public class UserService {
@Transactional // Все операции в одной транзакции
public void createUserWithAccount(User user, Account account) {
User savedUser = userRepository.save(user); // В памяти
account.setUserId(savedUser.getId());
accountRepository.save(account); // В памяти
// COMMIT только в конце метода
// Или ROLLBACK если произойдет exception
}
}
Проблема 1: Потеря Atomicity (атомарности)
public void transferMoney(long fromAccountId, long toAccountId, BigDecimal amount) {
// БЕЗ @Transactional
Account from = accountRepository.findById(fromAccountId).orElseThrow();
Account to = accountRepository.findById(toAccountId).orElseThrow(); // COMMIT 1
from.setBalance(from.getBalance().subtract(amount));
accountRepository.save(from); // COMMIT 2
to.setBalance(to.getBalance().add(amount));
accountRepository.save(to); // COMMIT 3
// Если произойдет исключение между COMMIT 2 и 3:
// Деньги вычли, но не добавили!
}
Сценарий:
1. Читаем счет from (100$) ✓
2. Вычли 50$ → 50$ ✓ COMMIT
3. Читаем счет to (200$) ✓
4. Добавляем 50$ → 250$ ✗ EXCEPTION (сетевой сбой!)
5. Сохранить не получилось ✗
Результат: from = 50$, to = 200$ (деньги потеряны!)
С @Transactional:
1. Читаем счет from (100$) ✓
2. Вычли 50$ → 50$ (в памяти) ✓
3. Читаем счет to (200$) ✓
4. Добавляем 50$ → 250$ (в памяти) ✓
5. COMMIT обе операции ✓ или ROLLBACK if error
Результат: либо both успешно, либо обе откачены
Проблема 2: Dirty Reads (грязное чтение)
// Потоки A и B работают одновременно
// Поток A (БЕЗ @Transactional)
public void updateUser(long userId, String name) {
User user = userRepository.findById(userId); // COMMIT (видит текущее состояние)
user.setName(name);
userRepository.save(user); // COMMIT (новое состояние видно всем)
Thread.sleep(1000); // Долгая операция
}
// Поток B (БЕЗ @Transactional)
public void processUser(long userId) {
User user = userRepository.findById(userId); // Может прочитать измененное состояние
// Поток А уже закоммитил, но операция еще не завершена!
// user видит незавершённое изменение
}
Проблема 3: Lost Updates (потерянные обновления)
public void incrementCounter(long entityId) {
// БЕЗ @Transactional
Entity entity = repository.findById(entityId); // значение: 5
entity.setCounter(entity.getCounter() + 1); // 5 + 1 = 6
repository.save(entity); // COMMIT
}
// Два потока одновременно вызывают incrementCounter
Posted T1: read counter = 5
Thread T2: read counter = 5
Thread T1: write counter = 6 // COMMIT
Thread T2: write counter = 6 // COMMIT (должно быть 7!)
Результат: counter = 6 (потеряна одна инкрементация)
С @Transactional и SELECT FOR UPDATE:
@Transactional
public void incrementCounter(long entityId) {
@Query("SELECT e FROM Entity e WHERE e.id = :id FOR UPDATE")
Entity entity = repository.findById(entityId);
entity.setCounter(entity.getCounter() + 1);
repository.save(entity);
// Блокируем строку для других потоков
}
Result: counter = 7 ✓
Проблема 4: Cascade Delete/Save не работает
@Entity
public class User {
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Address> addresses;
}
// БЕЗ @Transactional
public void deleteUserAddresses(long userId) {
User user = userRepository.findById(userId);
user.getAddresses().clear(); // Удаляем из памяти
userRepository.save(user); // COMMIT - но orphanRemoval может не сработать!
// Hibernate может не заметить удаление в отдельной транзакции
}
// С @Transactional
@Transactional
public void deleteUserAddresses(long userId) {
User user = userRepository.findById(userId);
user.getAddresses().clear(); // Удаляем из памяти
userRepository.save(user); // В памяти
// При COMMIT Hibernate сгенерирует DELETE адресов
}
Проблема 5: Lazy Loading вне транзакции
@Entity
public class User {
@OneToMany(fetch = FetchType.LAZY)
private List<Order> orders;
}
// БЕЗ @Transactional
public void displayUserOrders(long userId) {
User user = userRepository.findById(userId); // SELECT user (COMMIT)
System.out.println(user.getOrders().size()); // SELECT orders (?)
// LazyInitializationException!
// Транзакция закончилась, session закрыт
}
// С @Transactional
@Transactional
public void displayUserOrders(long userId) {
User user = userRepository.findById(userId); // SELECT user
System.out.println(user.getOrders().size()); // SELECT orders (в одной транзакции)
}
Проблема 6: Performance (производительность)
// БЕЗ @Transactional
public void createBatch(List<User> users) {
for (User user : users) { // 1000 пользователей
userRepository.save(user); // 1000 COMMIT операций!
// Каждый COMMIT = запись на диск, очень медленно
}
// Время: ~5-10 секунд
}
// С @Transactional
@Transactional
public void createBatch(List<User> users) {
for (User user : users) { // 1000 пользователей
userRepository.save(user); // В памяти
}
// 1 COMMIT в конце
// Время: ~100 ms
}
// С batch insert
@Transactional
public void createBatchOptimized(List<User> users) {
for (int i = 0; i < users.size(); i++) {
userRepository.save(users.get(i));
if (i % 100 == 0 && i > 0) {
entityManager.flush(); // Batch flush каждые 100
entityManager.clear(); // Очищаем 1st level cache
}
}
}
Spring Data JPA behavior
// CrudRepository методы БЕЗ явной @Transactional
public interface UserRepository extends CrudRepository<User, Long> {
@Transactional(readOnly = true) // По умолчанию на findXxx
Optional<User> findById(Long id);
// save() имеет транзакцию по умолчанию (Spring Data)
User save(User user);
}
// Но если переопределить в service БЕЗ @Transactional,
// может быть проблема с несколькими операциями подряд
Правильное использование @Transactional
@Service
public class UserService {
@Transactional // Для операций с изменением данных
public User createUserWithAccount(User user, Account account) {
User savedUser = userRepository.save(user);
account.setUserId(savedUser.getId());
accountRepository.save(account);
return savedUser;
}
@Transactional(readOnly = true) // Для чтения (оптимизация)
public User getUserWithOrders(long userId) {
return userRepository.findById(userId) // Может загрузить lazy
.orElseThrow();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logOperation(Operation op) {
// Отдельная транзакция, не зависит от основной
operationLogRepository.save(op);
}
}
Уровни изоляции
@Transactional(isolation = Isolation.READ_COMMITTED) // Default
public void safeOperation() {
// READ_COMMITTED — видим только закоммиченные данные
// Защита от dirty reads
}
@Transactional(isolation = Isolation.SERIALIZABLE)
public void strictOperation() {
// SERIALIZABLE — полная изоляция
// Медленно, но максимально безопасно
}
Вывод
Без @Transactional:
- Потеря атомарности — частичные обновления
- Грязное чтение — видим незавершённые изменения
- Потерянные обновления — race conditions
- Cascade не работает — orphanRemoval не срабатывает
- Lazy loading fails — LazyInitializationException
- Медленно — 1000 COMMIT операций вместо 1
Правило: ВСЕГДА используй @Transactional для методов, которые:
- Изменяют данные (создание, обновление, удаление)
- Работают с несколькими сущностями
- Требуют целостности данных
Используй @Transactional(readOnly = true) для методов чтения (оптимизация).