Допустимо ли в Go создавать вложенные транзакции с помощью пакета `database/sql`?

Ответ

Нет, стандартный пакет database/sql в Go не поддерживает концепцию вложенных транзакций напрямую. Если вы находитесь внутри объекта *sql.Tx и вызовете у него метод Begin(), вы получите ошибку, так как этот метод существует только у *sql.DB.

Однако, некоторые СУБД (например, PostgreSQL, MySQL, MS SQL Server) поддерживают механизм SAVEPOINT (точек сохранения), который позволяет эмулировать поведение вложенных транзакций.

Работать с ними нужно вручную, выполняя SQL-команды:

  • SAVEPOINT <имя_точки>: создает точку сохранения.
  • ROLLBACK TO SAVEPOINT <имя_точки>: откатывает транзакцию до указанной точки.
  • RELEASE SAVEPOINT <имя_точки>: удаляет точку сохранения, оставляя изменения.

Пример эмуляции для PostgreSQL:

tx, err := db.Begin()
if err != nil {
    log.Fatal(err)
}
// Гарантируем откат в случае паники или раннего выхода
defer tx.Rollback()

// Основная часть транзакции
_, err = tx.Exec(`INSERT INTO users (name) VALUES ('Alice')`)
if err != nil { ... }

// Эмуляция "вложенной" транзакции
_, err = tx.Exec("SAVEPOINT sp1")
if err != nil { ... }

_, err = tx.Exec(`UPDATE users SET balance = balance - 100 WHERE name = 'Alice'`)
if err != nil {
    // "Откатываем" вложенную транзакцию
    tx.Exec("ROLLBACK TO SAVEPOINT sp1")
} else {
    // "Коммитим" вложенную транзакцию
    tx.Exec("RELEASE SAVEPOINT sp1")
}

// Коммитим основную транзакцию
if err = tx.Commit(); err != nil {
    log.Fatal(err)
}

Важно: Это не настоящие вложенные транзакции. Если вы откатите внешнюю транзакцию (tx.Rollback()), все изменения, включая те, что были сделаны внутри SAVEPOINT, будут отменены. Использование SAVEPOINT усложняет код и зависит от конкретной СУБД, поэтому часто лучшим решением является пересмотр логики для избежания необходимости во вложенных транзакциях.

Ответ 18+ 🔞

Ага, слушай, вот сидишь ты такой, пишешь на Go, думаешь: "Ща сделаю транзакцию внутри транзакции, будет красиво, как в сказке про матрёшку". А потом — бац! — и оказывается, что стандартный пакет database/sql про эту твою матрёшку нихуя не знает. Прям как Герасим из того рассказа, только вместо "Муму" он тебе выдаст ошибку, если у *sql.Tx метод Begin() вызвать. Вообще, блядь, не существует такого метода, он только у *sql.DB водится. Ёперный театр!

Но не всё так грустно, как кажется. Некоторые базы данных, эти хитрожопые системы, вроде PostgreSQL или MySQL, поддерживают штуку под названием SAVEPOINT — это типа точки сохранения. Через них можно сымитировать вложенные транзакции, хоть и с приседаниями. Работает это так: создал точку, накосячил внутри — откатился к ней, всё прошло гладко — удалил её. Всё руками, блядь, как в старину, SQL-командами:

  • SAVEPOINT <имя_точки> — воткнул флажок, типа "здесь был Вася".
  • ROLLBACK TO SAVEPOINT <имя_точки> — отмотал плёнку назад до этого флажка, если накосячил.
  • RELEASE SAVEPOINT <имя_точки> — всё ок, флажок убираем, изменения остаются.

Вот тебе живой пример для PostgreSQL, смотри:

tx, err := db.Begin()
if err != nil {
    log.Fatal(err)
}
// На всякий пожарный, чтобы не остаться с незакрытой транзакцией, как дурак
defer tx.Rollback()

// Делаем что-то основное
_, err = tx.Exec(`INSERT INTO users (name) VALUES ('Alice')`)
if err != nil { ... }

// А теперь — внимание! — эмуляция "вложенности", ёбушки-воробушки!
_, err = tx.Exec("SAVEPOINT sp1")
if err != nil { ... }

_, err = tx.Exec(`UPDATE users SET balance = balance - 100 WHERE name = 'Alice'`)
if err != nil {
    // Ой, всё пошло по пизде! Откатываем только эту часть
    tx.Exec("ROLLBACK TO SAVEPOINT sp1")
} else {
    // Фух, пронесло. Закрываем "вложенную" часть
    tx.Exec("RELEASE SAVEPOINT sp1")
}

// И если всё супер — коммитим основную транзакцию
if err = tx.Commit(); err != nil {
    log.Fatal(err)
}

Но держи в голове, чувак: это не волшебство. Это просто точки отката внутри одной большой транзакции. Если ты потом сделаешь tx.Rollback() на основной транзакции — всё полетит к чёрту, включая все эти твои SAVEPOINT. Всё откатится, как будто ничего и не было. Пиздец, да?

И ещё: код становится сложнее, и для каждой СУБД свои пляски. Поэтому, прежде чем городить эту хуйню с точками сохранения, подумай — а может, проще переделать логику, чтобы обойтись без этих вложенных транзакций? Часто так и есть. А то получится, как в том анекдоте: "зачем просто, если можно сложно, с матом и точками сохранения".