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

Можно ли делать commit при зафиксированных данных в первой таблице и ошибке во второй таблице?

1.0 Junior🔥 171 комментариев
#Базы данных и SQL

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

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

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

# Можно ли делать commit при зафиксированных данных в первой таблице и ошибке во второй таблице?

Это вопрос о транзакциях и ACID принципах. Короткий ответ: НЕТ, нельзя. Вот почему и как правильно это сделать.

Теория: ACID принципы

A - Atomicity (Атомарность)

Транзакция - это или ВСЁ выполняется, или НИЧЕГО не выполняется. Промежуточное состояние недопустимо.

❌ НЕПРАВИЛЬНО
Транзакция T:
- INSERT в таблицу orders → OK
- INSERT в таблицу order_items → ОШИБКА
- Сделан COMMIT

Результат: заказ создан, но товары не добавлены. Данные в несогласованном состоянии!

✅ ПРАВИЛЬНО
Транзакция T:
- INSERT в таблицу orders → OK
- INSERT в таблицу order_items → ОШИБКА
- Сделан ROLLBACK

Результат: всё откачено, заказ не создан, данные согласованы.

Практическое объяснение

Сценарий: перевод денег между счётами

-- ❌ НЕПРАВИЛЬНЫЙ КОД (Может привести к потере денег!)
BEGIN TRANSACTION;

UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- ✓ На счёте 1 теперь на 100 меньше

UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- Ошибка: счёт 2 не существует!
-- ✗ Деньги пропали!

COMMIT;  -- Данные зафиксированы, откатить нельзя
-- ✅ ПРАВИЛЬНЫЙ КОД
BEGIN TRANSACTION;

UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;

IF ошибка_произошла THEN
    ROLLBACK;  -- Откачиваем ВСЁ
ELSE
    COMMIT;    -- Применяем ВСЁ
END IF;

В Java с Spring

Без управления транзакциями (опасно!)

// ❌ ПЛОХО
@Service
public class OrderService {
    
    @Autowired
    private OrderRepository orderRepository;
    
    @Autowired
    private OrderItemRepository itemRepository;
    
    public void createOrder(Order order, List<OrderItem> items) {
        // 1. Сохраняем заказ
        Order savedOrder = orderRepository.save(order);
        // Данные уже в БД! Если следующая строка упадёт, заказ останется
        
        // 2. Пытаемся добавить товары
        for (OrderItem item : items) {
            item.setOrderId(savedOrder.getId());
            itemRepository.save(item);
        }
        
        // Если здесь Exception, заказ остаётся в БД без товаров!
    }
}

С @Transactional (правильно!)

// ✅ ХОРОШО
@Service
public class OrderService {
    
    @Autowired
    private OrderRepository orderRepository;
    
    @Autowired
    private OrderItemRepository itemRepository;
    
    @Transactional  // Весь метод в одной транзакции
    public void createOrder(Order order, List<OrderItem> items) {
        // Транзакция НАЧИНАЕТСЯ здесь
        
        Order savedOrder = orderRepository.save(order);
        System.out.println("Order saved");
        
        for (OrderItem item : items) {
            item.setOrderId(savedOrder.getId());
            itemRepository.save(item);
        }
        
        // Если Exception произойдёт ЗДЕСЬ
        if (items.size() > 100) {
            throw new BusinessException("Too many items");
        }
        
        // Если исключение → ROLLBACK (откатываются ВСЕ change'и)
        // Если успех → COMMIT (фиксируются ВСЕ change'и)
    }
}

С явным Rollback

@Service
public class TransferService {
    
    @Autowired
    private TransactionTemplate transactionTemplate;
    
    @Autowired
    private AccountRepository accountRepository;
    
    public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
        // Явное управление транзакциями
        transactionTemplate.execute(status -> {
            try {
                Account from = accountRepository.findById(fromId)
                    .orElseThrow(() -> new AccountNotFoundException());
                Account to = accountRepository.findById(toId)
                    .orElseThrow(() -> new AccountNotFoundException());
                
                // Проверяем условия ДО изменений
                if (from.getBalance().compareTo(amount) < 0) {
                    throw new InsufficientFundsException();
                }
                
                // Выполняем изменения
                from.setBalance(from.getBalance().subtract(amount));
                to.setBalance(to.getBalance().add(amount));
                
                accountRepository.save(from);
                accountRepository.save(to);
                
                return null;  // OK
                
            } catch (Exception e) {
                status.setRollbackOnly();  // Явный откат
                throw new RuntimeException("Transfer failed", e);
            }
        });
    }
}

Типы транзакций в Spring

READ_COMMITTED (по умолчанию)

@Transactional(isolation = Isolation.READ_COMMITTED)
public void method() {
    // Видим только зафиксированные данные
    // Быстро, но есть dirty read risk в других сценариях
}

SERIALIZABLE (самый строгий)

@Transactional(isolation = Isolation.SERIALIZABLE)
public void criticalOperation() {
    // Исключает все race conditions
    // Но медленнее
}

Savepoint'ы (частичный откат)

В некоторых СУБД можно откатить часть транзакции:

@Service
public class PartialRollbackService {
    
    @Autowired
    private PlatformTransactionManager transactionManager;
    
    public void partialRollback() {
        TransactionStatus status = transactionManager.getTransaction(
            new DefaultTransactionDefinition()
        );
        
        try {
            // Операция 1
            saveUserData();
            
            // Savepoint - точка сохранения
            Object savepoint = status.createSavepoint();
            
            // Операция 2
            try {
                saveOrderData();
            } catch (Exception e) {
                // Откатываем только до savepoint
                status.rollbackToSavepoint(savepoint);
                // Пользовательские данные остаются!
            }
            
            // Коммитим всё
            transactionManager.commit(status);
            
        } catch (Exception e) {
            // Полный откат
            transactionManager.rollback(status);
        }
    }
}

Обработка исключений

Checked vs Unchecked

@Service
public class ExceptionHandlingService {
    
    @Transactional
    public void method1() {
        // Unchecked exception → автоматический ROLLBACK
        if (error) {
            throw new RuntimeException("Error");  // ROLLBACK
        }
    }
    
    @Transactional(rollbackFor = Exception.class)
    public void method2() throws Exception {
        // Checked exception НЕ откатывает по умолчанию!
        // Нужно явно указать rollbackFor
        if (error) {
            throw new Exception("Error");  // Теперь ROLLBACK
        }
    }
    
    @Transactional(noRollbackFor = BusinessException.class)
    public void method3() {
        // Для некоторых исключений НЕ откатываем
        if (expectedError) {
            throw new BusinessException();  // Нет ROLLBACK, но COMMIT!
        }
    }
}

Уровни изоляции и проблемы

Уровень              | Dirty Read | Non-Repeatable | Phantom
─────────────────────────────────────────────────────────────
READ_UNCOMMITTED     | ✓         | ✓             | ✓
READ_COMMITTED       | ✗         | ✓             | ✓
REPEATABLE_READ      | ✗         | ✗             | ✓
SERIALIZABLE         | ✗         | ✗             | ✗

Dirty Read: другая транзакция видит незафиксированные данные Non-Repeatable Read: одно значение меняется в середине транзакции Phantom: новые строки появляются в результате запроса

Ошибка 1: Вложенные @Transactional

@Service
public class Parent {
    
    @Transactional
    public void parentMethod() {
        childService.childMethod();
    }
}

@Service
public class Child {
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void childMethod() {
        // Создаёт НОВУЮ транзакцию
        // Если childMethod упадёт, parentMethod НЕ откатится
    }
    
    @Transactional(propagation = Propagation.NESTED)
    public void nestedMethod() {
        // Вложенная транзакция (если СУБД поддерживает)
        // Использует savepoint
    }
}

Итоговое правило

НИКОГДА не делайте partial COMMIT!

Либо:
- ВСЁ коммитится
- НИЧЕГО не коммитится (полный откат)

Это залог консистентности данных.

Best Practices

  1. Всегда оборачивай логику в @Transactional

    @Transactional
    public void businessOperation() { ... }
    
  2. Проверяй условия ДО изменений

    if (balance < amount) throw new Exception();
    // Потом изменяй
    
  3. Явно обрабатывай исключения

    @Transactional(rollbackFor = CustomException.class)
    
  4. Не делай долгих операций в транзакции

    // ❌ 10 минут блокировки
    @Transactional
    public void slowMethod() { ... }
    
    // ✅ Кэшируй до транзакции
    Data cached = expensiveCompute();
    updateDB(cached);
    
  5. Мониторь блокировки в БД

    SELECT * FROM pg_locks;  -- PostgreSQL
    SHOW ENGINE INNODB STATUS;  -- MySQL
    
Можно ли делать commit при зафиксированных данных в первой таблице и ошибке во второй таблице? | PrepBro