← Назад к вопросам

Что будет, если не указать @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) для методов чтения (оптимизация).

Что будет, если не указать @Transactional при работе с репозиторием | PrepBro