← Назад к вопросам
Можно ли делать 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
-
Всегда оборачивай логику в @Transactional
@Transactional public void businessOperation() { ... } -
Проверяй условия ДО изменений
if (balance < amount) throw new Exception(); // Потом изменяй -
Явно обрабатывай исключения
@Transactional(rollbackFor = CustomException.class) -
Не делай долгих операций в транзакции
// ❌ 10 минут блокировки @Transactional public void slowMethod() { ... } // ✅ Кэшируй до транзакции Data cached = expensiveCompute(); updateDB(cached); -
Мониторь блокировки в БД
SELECT * FROM pg_locks; -- PostgreSQL SHOW ENGINE INNODB STATUS; -- MySQL