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

Что будет, если не поставить аннотацию Transactional над методом, в котором создается одна запись

2.0 Middle🔥 191 комментариев
#Основы Java

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Что произойдёт без @Transactional при сохранении записи

Этот вопрос проверяет понимание транзакций и управления сессией Hibernate. Ответ зависит от конфигурации и контекста.

Сценарий 1: Автоматический commit (по умолчанию в Spring)

В большинстве случаев это работает:

@Service
public class UserService {
    
    private final UserRepository repository;
    
    // ❌ БЕЗ @Transactional
    public User createUser(CreateUserRequest request) {
        User user = new User();
        user.setEmail(request.getEmail());
        user.setName(request.getName());
        
        // repository.save() сам создаёт и коммитит транзакцию
        return repository.save(user);  // Работает!
    }
}

Почему работает:

  • Spring Data JPA и Hibernate автоматически создают транзакцию для save()
  • Данные коммитятся сразу
  • Новые идентификаторы возвращаются

Сценарий 2: Проблемы БЕЗ @Transactional

Множественные операции в одном методе:

@Service
public class OrderService {
    
    private final OrderRepository orderRepository;
    private final InventoryRepository inventoryRepository;
    
    // ❌ БЕЗ @Transactional — ОПАСНО для сложных операций!
    public Order placeOrder(OrderRequest request) {
        // Операция 1: создание заказа
        Order order = new Order();
        order.setTotal(request.getTotal());
        Order savedOrder = orderRepository.save(order);  // КОММИТ 1
        
        // Операция 2: резервирование товара
        Inventory inventory = inventoryRepository.findById(request.getInventoryId()).orElseThrow();
        inventory.decrementQuantity(request.getQuantity());
        inventoryRepository.save(inventory);  // КОММИТ 2
        
        // ⚠️ ПРОБЛЕМА: если здесь ошибка, заказ уже создан, но товар не зарезервирован!
        if (inventory.getQuantity() < 0) {
            throw new InsufficientInventoryException();
        }
        
        return savedOrder;
    }
    
    // ❌ Результат: ДАННЫЕ В НЕСОГЛАСОВАННОМ СОСТОЯНИИ
    // Заказ есть, товара нет
}

Сценарий 3: С @Transactional (ПРАВИЛЬНО)

@Service
public class OrderService {
    
    private final OrderRepository orderRepository;
    private final InventoryRepository inventoryRepository;
    
    // ✅ С @Transactional — все операции как единое целое
    @Transactional
    public Order placeOrder(OrderRequest request) {
        // Операция 1: создание заказа
        Order order = new Order();
        order.setTotal(request.getTotal());
        Order savedOrder = orderRepository.save(order);
        
        // Операция 2: резервирование товара
        Inventory inventory = inventoryRepository.findById(request.getInventoryId())
            .orElseThrow();
        inventory.decrementQuantity(request.getQuantity());
        inventoryRepository.save(inventory);
        
        // Проверка
        if (inventory.getQuantity() < 0) {
            throw new InsufficientInventoryException();  // Откат всех операций!
        }
        
        return savedOrder;
        
        // ✅ При успехе: ОДИН КОММИТ обеих операций
        // ✅ При ошибке: ОТКАТ обеих операций (ACID свойства)
    }
}

Основные различия

БЕЗ @Transactional

public void demo() {
    // Каждый save() создаёт собственную транзакцию
    repository.save(entity1);  // Отдельная транзакция
    repository.save(entity2);  // Отдельная транзакция
    repository.save(entity3);  // Отдельная транзакция
    
    // Если entity3 вызвал ошибку:
    // entity1 и entity2 уже закоммичены (потеря данных)
    if (someCondition) {
        throw new RuntimeException();
    }
}

С @Transactional

@Transactional
public void demo() {
    // Одна транзакция для всех операций
    repository.save(entity1);  // В памяти
    repository.save(entity2);  // В памяти
    repository.save(entity3);  // В памяти
    
    if (someCondition) {
        throw new RuntimeException();  // ВСЕ откатятся!
    }
    
    // После выхода из метода: ОДИН КОММИТ всех операций
}

Реальный пример: перевод денег

@Service
public class BankService {
    
    private final AccountRepository accountRepository;
    
    // ❌ БЕЗ @Transactional — КРИТИЧНАЯ ОШИБКА!
    public void transferMoney(String fromId, String toId, BigDecimal amount) {
        Account from = accountRepository.findById(fromId).orElseThrow();
        Account to = accountRepository.findById(toId).orElseThrow();
        
        from.setBalance(from.getBalance().subtract(amount));
        accountRepository.save(from);  // КОММИТ 1: деньги списаны
        
        to.setBalance(to.getBalance().add(amount));
        accountRepository.save(to);  // КОММИТ 2: деньги зачислены
        
        // ⚠️ РИСК: сбой после КОММИТ 1 и до КОММИТ 2
        // Деньги потеряны!
    }
    
    // ✅ С @Transactional — БЕЗОПАСНО
    @Transactional
    public void transferMoneyCorrect(String fromId, String toId, BigDecimal amount) {
        Account from = accountRepository.findById(fromId).orElseThrow();
        Account to = accountRepository.findById(toId).orElseThrow();
        
        from.setBalance(from.getBalance().subtract(amount));
        accountRepository.save(from);
        
        to.setBalance(to.getBalance().add(amount));
        accountRepository.save(to);
        
        // Гарантированно: либо обе операции, либо откат
    }
}

Поведение при различных конфигурациях

Spring Boot (по умолчанию — autocommit включен)

spring:
  jpa:
    hibernate:
      ddl-auto: update
    # autocommit по умолчанию TRUE в большинстве драйверов
// Результат: БЕЗ @Transactional будет работать для простых save()
public User createUser(User user) {
    return userRepository.save(user);  // Каждый save() — отдельная транзакция
}

Spring Boot с отключенным autocommit

spring:
  datasource:
    hikari:
      auto-commit: false  # Отключили autocommit
// ❌ БЕЗ @Transactional — может не сохраниться!
public User createUser(User user) {
    return userRepository.save(user);  // Может виснуть или не коммититься
}

// ✅ С @Transactional — гарантированно сохранится
@Transactional
public User createUserCorrect(User user) {
    return userRepository.save(user);
}

Настройка @Transactional

// По умолчанию: read-write, уровень изоляции по умолчанию
@Transactional
public Order createOrder(OrderRequest request) { }

// Только для чтения (оптимизация)
@Transactional(readOnly = true)
public List<Order> getOrders() { }

// Пропаганда транзакций
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void criticalOperation() {  // Новая транзакция, даже внутри другой
}

// Откат при исключении
@Transactional(rollbackFor = Exception.class)  // Откат при любом Exception
public void operation() { }

// Таймаут
@Transactional(timeout = 30)  // Откат если метод выполняется > 30 сек
public void longOperation() { }

// Уровень изоляции
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void isolatedOperation() { }

Типичные ошибки

1. @Transactional на service, но вызов из другого места

@Service
public class UserService {
    
    @Transactional
    public void updateUser(User user) {
        userRepository.save(user);
    }
    
    public void createAndUpdate(User newUser, String updateId) {
        create(newUser);
        update(updateId);  // ❌ Если update() в другом сервисе без @Transactional
    }
}

2. LazyInitializationException

@Transactional
public User getUser(String id) {
    return userRepository.findById(id).orElseThrow();
}

// БЕЗ @Transactional при доступе к lazy полям
public void demo() {
    User user = getUser("1");
    user.getOrders().size();  // ❌ LazyInitializationException!
}

// ✅ С @Transactional
@Transactional
public void demo() {
    User user = getUser("1");
    user.getOrders().size();  // Работает!
}

3. Изменения без save()

@Transactional
public void updateUser(String id, String newName) {
    User user = userRepository.findById(id).orElseThrow();
    user.setName(newName);
    // БЕЗ save() — но с @Transactional Hibernate отследит изменение!
    // При коммите будет UPDATE
}

Когда @Transactional НЕ нужна

// 1. Когда только читаем
public List<User> getUsers() {
    return userRepository.findAll();
}

// 2. Когда один простой save()
public User createUser(User user) {
    return userRepository.save(user);
}

// 3. Когда update/delete ведутся через repository методы
public void deleteUser(String id) {
    userRepository.deleteById(id);
}

Заключение

Для одного простого save():

  • @Transactional НЕ обязателен, Spring Data JPA сам создаст транзакцию
  • Но рекомендуется добавлять @Transactional для явности

Для множественных операций:

  • @Transactional КРИТИЧЕН
  • Без него данные могут остаться в несогласованном состоянии
  • ACID свойства будут нарушены

Best Practice:

@Service
@Transactional  // На уровне класса
public class UserService {
    
    public User createUser(User user) {
        // Уже в транзакции
    }
    
    @Transactional(readOnly = true)  // Переопределяем для методов только чтения
    public User getUser(String id) {
    }
}

Здоровый скепсис: всегда думайте о том, на что полагаетесь, и явно указывайте @Transactional, когда нужна консистентность данных.

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