← Назад к вопросам
Возможны ли вложенные транзакции в БД
1.8 Middle🔥 201 комментариев
#Базы данных (SQL)
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
# Вложенные транзакции в БД (Savepoints)
Краткий ответ
Классические вложенные транзакции невозможны, но большинство БД поддерживают Savepoints (точки сохранения), которые предоставляют похожую функциональность.
Почему вложенные транзакции невозможны?
Транзакция — это атомарная единица работы: либо всё выполняется, либо ничего. Стандартная модель SQL не поддерживает вложенные транзакции, потому что:
- Один уровень коммита/откатов — В момент времени может быть только одна активная транзакция
- ACID свойства несовместимы — Вложенные транзакции усложнили бы гарантирование изоляции
- Стандарт SQL не определяет вложенные транзакции
-- ❌ Классический подход (НЕ РАБОТАЕТ)
BEGIN TRANSACTION; -- Начало транзакции 1
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
BEGIN TRANSACTION; -- ❌ Ошибка! Уже в транзакции
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT; -- Откатит ВСЮ транзакцию, не только вложенную
COMMIT; -- Финально коммитит
Решение: Savepoints
Savepoint — это точка внутри транзакции, к которой можно откатиться без отката всей транзакции.
Это стандартный SQL механизм (SQL:2003), поддерживаемый большинством современных БД.
Синтаксис SQL
-- PostgreSQL, MySQL, SQLite, Oracle и т.д.
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- Создаём savepoint (точка сохранения)
SAVEPOINT sp1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- Если ошибка — откатываемся к savepoint
IF error_occurred THEN
ROLLBACK TO SAVEPOINT sp1; -- Откатывает только до sp1
END IF;
-- Другой код
UPDATE accounts SET balance = balance + 50 WHERE id = 3;
COMMIT; -- Коммитим всю транзакцию
Python пример (SQLAlchemy)
Без вложенности (текущий подход)
from sqlalchemy import create_engine, text
from sqlalchemy.orm import Session
engine = create_engine('postgresql://...')
# Попытка 1: Просто откат
with Session(engine) as session:
try:
account1 = session.query(Account).get(1)
account1.balance -= 100
account2 = session.query(Account).get(2)
account2.balance += 100
# Если здесь ошибка — откатывается ВСЕ
result = process_some_logic() # Может выбросить исключение
session.commit()
except Exception as e:
session.rollback() # Откатывает всю транзакцию
print(f"Failed: {e}")
С savepoints (вложенная логика)
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
engine = create_engine('postgresql://...')
with Session(engine) as session:
# Основная транзакция
account1 = session.query(Account).get(1)
account1.balance -= 100
session.flush() # Применить изменения (без коммита)
# SAVEPOINT — точка сохранения
savepoint = session.begin_nested() # Создаём savepoint
try:
account2 = session.query(Account).get(2)
account2.balance += 100
session.flush()
# Опасная операция
result = process_some_logic()
if not result:
raise ValueError("Logic failed")
savepoint.commit() # Коммитим savepoint
except Exception as e:
savepoint.rollback() # Откатываем только до savepoint
print(f"Savepoint rolled back: {e}")
# Логика восстановления
account2.balance = account2.original_balance
session.flush()
# Основная транзакция продолжается
account3 = session.query(Account).get(3)
account3.balance += 50
session.commit() # Коммитим всё
Практический пример: Система с откатом
from sqlalchemy import create_engine, Column, Integer, String, Float
from sqlalchemy.orm import Session, declarative_base
Base = declarative_base()
class Account(Base):
__tablename__ = 'accounts'
id = Column(Integer, primary_key=True)
name = Column(String(100))
balance = Column(Float)
engine = create_engine('postgresql://user:password@localhost/mydb')
Base.metadata.create_all(engine)
def transfer_money_with_savepoints(
from_account_id: int,
to_account_id: int,
amount: float,
fee_percent: float = 0.02
) -> dict:
"""Трансфер денег с комиссией и откатом при ошибке"""
with Session(engine) as session:
# Основная транзакция начинается здесь
# 1. Снять деньги со счёта отправителя
from_account = session.query(Account).filter_by(id=from_account_id).with_for_update().first()
if not from_account:
raise ValueError(f"Account {from_account_id} not found")
if from_account.balance < amount:
raise ValueError("Insufficient funds")
fee = amount * fee_percent
total_debit = amount + fee
from_account.balance -= total_debit
session.flush()
print(f"✓ Debited {total_debit} from account {from_account_id}")
# SAVEPOINT 1: После дебита
sp1 = session.begin_nested()
try:
# 2. Добавить деньги на счёт получателя
to_account = session.query(Account).filter_by(id=to_account_id).with_for_update().first()
if not to_account:
raise ValueError(f"Account {to_account_id} not found")
to_account.balance += amount
session.flush()
print(f"✓ Credited {amount} to account {to_account_id}")
# 3. Проверка (например, fraud detection)
if to_account.balance > 1_000_000: # Лимит
raise ValueError("Balance limit exceeded")
sp1.commit() # Коммитим этот savepoint
print(f"✓ Savepoint 1 committed")
except Exception as e:
sp1.rollback() # Откатываем credit к account
print(f"✗ Savepoint 1 rolled back: {e}")
# Откатить дебит тоже
from_account.balance += total_debit
session.flush()
raise
# SAVEPOINT 2: Логирование (не критичное)
sp2 = session.begin_nested()
try:
# 4. Логирование транзакции (может быть в отдельной таблице)
log_entry = create_transaction_log(
from_id=from_account_id,
to_id=to_account_id,
amount=amount,
fee=fee,
status="completed"
)
session.add(log_entry)
session.flush()
print(f"✓ Transaction logged")
sp2.commit()
except Exception as e:
sp2.rollback() # Откатываем только логирование
print(f"⚠ Logging failed, but transfer completed: {e}")
# Транзакция всё равно будет коммичена (логирование не критично)
# ФИНАЛЬНЫЙ КОММИТ
session.commit()
print(f"✓ Transaction committed")
return {
"status": "success",
"from_balance": from_account.balance,
"to_balance": to_account.balance,
"amount_transferred": amount,
"fee": fee
}
# Использование:
try:
result = transfer_money_with_savepoints(
from_account_id=1,
to_account_id=2,
amount=100
)
print(result)
except Exception as e:
print(f"Transfer failed: {e}")
Niveled Transactions (вложенные транзакции в Python)
Некоторые ORM предоставляют удобный API для вложенности:
# SQLAlchemy — begin_nested()
with session.begin():
# Основная транзакция
session.add(user1)
# Вложенная (savepoint)
with session.begin_nested():
session.add(user2)
if error:
raise # Откатывает только savepoint
session.add(user3)
# После with блока savepoint автоматически коммитится
# Django ORM — transaction.atomic()
from django.db import transaction
with transaction.atomic():
# Основная транзакция
User.objects.create(name="Alice")
try:
with transaction.atomic():
# Вложенная (savepoint в Django)
User.objects.create(name="Bob")
if error:
raise
except Exception:
# Откатывает только savepoint
pass
User.objects.create(name="Charlie")
Важные различия: Транзакции vs Savepoints
| Аспект | Транзакция | Savepoint |
|---|---|---|
| Вложенность | Нет (одна на раз) | Да (можно множество) |
| Откат | Откатывает ВСЕ | Откатывает только до точки |
| Коммит | Коммитит всё | Коммитит только сохранённые изменения |
| ACID | Гарантирует на уровне БД | Гарантирует частичный откат |
| Изоляция | Уровень БД | Внутри транзакции |
Когда использовать Savepoints?
# 1. Опциональные операции
with session.begin() as trans:
create_user(user) # Обязательно
with trans.begin_nested():
send_welcome_email(user) # Опционально, не откатывать user если ошибка
# 2. Попробовать несколько вариантов
with session.begin() as trans:
user = create_user("Alice")
# Вариант 1
with trans.begin_nested() as sp1:
try:
auto_recommend_plan(user, "premium")
sp1.commit()
except:
sp1.rollback()
# Вариант 2
auto_recommend_plan(user, "free")
# 3. Независимые операции в цикле
with session.begin() as trans:
for item in items:
with trans.begin_nested():
try:
process_item(item)
except Exception as e:
print(f"Failed to process {item}: {e}")
# Продолжаем со следующего, не откатываем весь цикл
Заключение
- Вложенные транзакции в классическом смысле невозможны
- Savepoints — это стандартный способ имитировать вложенность
- Поддерживается всеми основными БД (PostgreSQL, MySQL, SQLite, Oracle)
- SQLAlchemy и Django облегчают работу с savepoints
- Используй когда нужен частичный откат без откатывания всей транзакции