Go: GORM
Теория: Транзакции
Когда приложение меняет данные в базе, почти никогда не обходится одной операцией. Создать заказ — это сразу несколько шагов: записать самого заказчика, сам заказ, позиции заказа, списать деньги, обновить остатки. Ошибка в любом месте не должна оставлять базу «наполовину обновлённой». Для этого и существуют транзакции: набор операций, который либо выполняется целиком, либо откатывается целиком.
В GORM транзакции живут поверх стандартного механизма базы данных. Под капотом всегда тот же сценарий: BEGIN, набор INSERT/UPDATE/DELETE, а в конце COMMIT или ROLLBACK. Разница в том, как вы этим управляете из кода.
Транзакции через db.Transaction
Самый безопасный и «правильный по умолчанию» способ — обернуть набор операций в db.Transaction(). Вы передаёте функцию, GORM сам открывает транзакцию, передаёт в неё tx *gorm.DB и ждёт результат. Вернули nil — делается COMMIT. Вернули ошибку — ROLLBACK. При панике GORM тоже откатит транзакцию.
Пример: создаём пользователя и его заказ как одну операцию.
В логах при включённом logger.Info вы увидите последовательность:
Если вторая вставка упадёт, GORM вместо COMMIT выполнит ROLLBACK, и пользователь не останется «висеть» в базе без заказа.
Важно не забывать внутри транзакции везде использовать tx, а не исходный db. Любой вызов через db пойдёт мимо транзакции.
Через WithContext() транзакцию можно привязать к контексту с тайм-аутом. Если время вышло, запросы отменятся, транзакция откатится:
Ручное управление: Begin, Commit, Rollback
Иногда одной функции db.Transaction() недостаточно. Например, границы транзакции шире одной функции, или момент COMMIT должен зависеть от нескольких независимых веток логики. В таких случаях можно управлять транзакцией вручную.
Транзакция начинается с db.Begin(). Этот вызов возвращает новый объект *gorm.DB, привязанный к активной транзакции. Далее все операции выполняются через него. В конце — Commit() или Rollback().
Базовый сценарий:
SQL-последовательность такая же: BEGIN → несколько INSERT → COMMIT. Если что-то идёт не так, вы сами вызываете Rollback(), и база возвращается в исходное состояние.
Чтобы не размазывать Rollback() по коду, удобно повесить его в defer:
Если Commit() отработал успешно, повторный Rollback() ничего не испортит, но гарантирует, что при раннем выходе или панике транзакция не «зависнет».
Ключевое правило остаётся тем же: все функции, которые должны работать внутри одной транзакции, должны получать именно tx, а не db. Это касается и отдельных хелперов:



