Что такое вложенные транзакции и как их реализовать в Go?

Ответ

Вложенная транзакция — это транзакция, которая начинается внутри уже существующей (родительской) транзакции. Это позволяет создавать изолированные точки для отката части операций, не затрагивая всю родительскую транзакцию.

Стандартный пакет database/sql в Go не поддерживает вложенные транзакции напрямую, так как он предоставляет общий интерфейс, а не все СУБД имеют такую функциональность.

Эмуляция с помощью точек сохранения (Savepoints)

Наиболее распространенный способ эмулировать вложенные транзакции — использовать точки сохранения (savepoints), которые поддерживаются многими СУБД (например, PostgreSQL, MySQL, SQLite).

SAVEPOINT создает именованную точку внутри транзакции, к которой можно позже откатиться с помощью ROLLBACK TO SAVEPOINT.

Пример реализации

// tx - это уже начатая транзакция *sql.Tx

// Начало "вложенной" транзакции
_, err := tx.Exec("SAVEPOINT my_savepoint")
if err != nil {
    log.Fatalf("Failed to create savepoint: %v", err)
}

// Выполняем какие-то операции внутри "вложенной" транзакции
_, err = tx.Exec("UPDATE accounts SET balance = balance - 100 WHERE id = 1")
if err != nil {
    // Если произошла ошибка, откатываемся к точке сохранения
    log.Printf("Operation failed, rolling back to savepoint: %v", err)
    _, rollbackErr := tx.Exec("ROLLBACK TO SAVEPOINT my_savepoint")
    if rollbackErr != nil {
        log.Fatalf("Failed to rollback to savepoint: %v", rollbackErr)
    }
} else {
    // Если все успешно, можно "закоммитить" изменения, удалив точку сохранения
    // В PostgreSQL это не обязательно, но в некоторых СУБД может быть полезно
    _, releaseErr := tx.Exec("RELEASE SAVEPOINT my_savepoint")
    if releaseErr != nil {
        log.Printf("Failed to release savepoint: %v", releaseErr)
    }
}

// В конце родительская транзакция коммитится или откатывается целиком
err = tx.Commit()
if err != nil {
    log.Fatalf("Failed to commit transaction: %v", err)
}

Важно

  • Некоторые драйверы (например, pgx для PostgreSQL) предоставляют более удобные абстракции для работы с точками сохранения.
  • Синтаксис SAVEPOINT является стандартным для SQL, но всегда лучше сверяться с документацией вашей СУБД.

Ответ 18+ 🔞

Да ты посмотри, что эти умники придумали — вложенные транзакции! Это когда ты внутри одной большой транзакции, типа родительской, начинаешь ещё одну, маленькую. Ну, чтобы если в этой мелкой что-то пошло не так, откатить только её, а не всю конструкцию до основания, блядь.

А вот стандартный пакет database/sql в Go на это смотрит и говорит: «Да похуй, не поддерживаю». Ну а как поддерживать, если не все базы данных это умеют? Он же общий интерфейс, ему со всеми надо уживаться.

Как обычно выкручиваются? Точки сохранения!

Да, вот эта штука — savepoints. Многие базы (PostgreSQL, MySQL) её понимают. Это как зарубка на дереве: поставил — и если накосячил дальше, можно откатиться обратно к этой зарубке, а не валить всё дерево.

// tx - это уже начатая транзакция *sql.Tx

// Ставим зарубку, типа "начало вложенной"
_, err := tx.Exec("SAVEPOINT my_savepoint")
if err != nil {
    log.Fatalf("Не удалось поставить зарубку: %v", err)
}

// Пробуем что-то сделать внутри
_, err = tx.Exec("UPDATE accounts SET balance = balance - 100 WHERE id = 1")
if err != nil {
    // Ой, всё, пиздец! Катимся назад к зарубке
    log.Printf("Всё пошло по пизде, откатываемся к savepoint: %v", err)
    _, rollbackErr := tx.Exec("ROLLBACK TO SAVEPOINT my_savepoint")
    if rollbackErr != nil {
        log.Fatalf("И откатиться-то не смогли, ёпта: %v", rollbackErr)
    }
} else {
    // Всё чики-пуки, можно зарубку стереть
    _, releaseErr := tx.Exec("RELEASE SAVEPOINT my_savepoint")
    if releaseErr != nil {
        log.Printf("Не удалось стереть зарубку, ну и хуй с ней: %v", releaseErr)
    }
}

// А вот тут уже решаем — коммитить всю родительскую транзакцию или нет
err = tx.Commit()
if err != nil {
    log.Fatalf("Финальный коммит провалился, пиздец: %v", err)
}

Важный момент, блядь

Некоторые драйверы, тот же pgx для PostgreSQL, могут дать тебе готовые обёртки, чтобы не писать эти SAVEPOINT руками. Но в целом идея одна — ставишь зарубку, работаешь, а если наебнулся, откатываешься к ней. Главное, в документацию своей СУБД глянь, а то вдруг она эту хуйню не понимает!