Как работать с транзакциями в Go с использованием пакета `database/sql`?

Ответ

Для работы с транзакциями в Go используется объект *sql.Tx, который создается из *sql.DB.

Основной флоу работы с транзакцией:

  1. Начать транзакцию с помощью db.Begin() или db.BeginTx().
  2. Выполнять все запросы (SELECT, INSERT, UPDATE, DELETE) через объект транзакции tx, а не через db.
  3. Если любая из операций завершилась ошибкой, откатить транзакцию с помощью tx.Rollback().
  4. Если все операции успешны, зафиксировать изменения с помощью tx.Commit().

Пример с использованием defer для надежности:

Этот паттерн гарантирует, что транзакция будет отменена, если что-то пойдет не так.

db, err := sql.Open("postgres", "user=... password=... dbname=... sslmode=disable")
if err != nil {
    log.Fatal(err)
}
defer db.Close()

// 1. Начинаем транзакцию
ctx := context.Background()
tx, err := db.BeginTx(ctx, nil) // Используем BeginTx для передачи контекста
if err != nil {
    log.Fatal(err)
}
// 2. Откладываем Rollback. Если Commit выполнится успешно, 
// Rollback вернет ошибку sql.ErrTxDone, которую можно безопасно игнорировать.
defer tx.Rollback()

// 3. Выполняем операции внутри транзакции
_, err = tx.ExecContext(ctx, "UPDATE accounts SET balance = balance - 100 WHERE id = 1")
if err != nil {
    // Ошибка уже обработается в defer tx.Rollback()
    log.Printf("Failed to debit account: %v", err)
    return
}

_, err = tx.ExecContext(ctx, "UPDATE accounts SET balance = balance + 100 WHERE id = 2")
if err != nil {
    log.Printf("Failed to credit account: %v", err)
    return
}

// 4. Если все хорошо, коммитим транзакцию
if err = tx.Commit(); err != nil {
    log.Fatal(err)
}

fmt.Println("Transaction committed successfully")

Важные моменты:

  • Изоляция: Для установки уровня изоляции транзакции (например, sql.LevelSerializable) используйте db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable}).
  • Контекст: Передавайте context.Context в BeginTx и ExecContext/QueryRowContext для управления временем выполнения и отменой запросов.
  • Всегда используйте tx: После db.Begin(), все операции должны выполняться через объект tx, чтобы они были частью одной транзакции.