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

Возможны ли вложенные транзакции в БД

1.8 Middle🔥 201 комментариев
#Базы данных (SQL)

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

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

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

# Вложенные транзакции в БД (Savepoints)

Краткий ответ

Классические вложенные транзакции невозможны, но большинство БД поддерживают Savepoints (точки сохранения), которые предоставляют похожую функциональность.

Почему вложенные транзакции невозможны?

Транзакция — это атомарная единица работы: либо всё выполняется, либо ничего. Стандартная модель SQL не поддерживает вложенные транзакции, потому что:

  1. Один уровень коммита/откатов — В момент времени может быть только одна активная транзакция
  2. ACID свойства несовместимы — Вложенные транзакции усложнили бы гарантирование изоляции
  3. Стандарт 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
  • Используй когда нужен частичный откат без откатывания всей транзакции