Go: GORM
Теория: Кастомизация и хуки
Когда вы только начинаете работать с GORM, кажется, что ORM просто превращает структуры в SQL и обратно. Но модели могут быть «умнее»: проверять себя перед сохранением, автоматически считать derived-поля, вести аудит, запрещать опасные операции. Всё это делается через хуки — специальные методы, которые GORM вызывает в ключевые моменты жизненного цикла записи.
Хуки позволяют перенести часть инвариантов и бизнес-правил ближе к данным. Модель перестаёт быть «тупым мешком полей» и начинает сама следить за своей целостностью: нормализует email, хэширует пароль, не даёт удалить оплачённый заказ и т. д.
BeforeCreate, AfterFind и другие хуки
GORM ищет на типе модели методы с особыми именами и вызывает их автоматически. Каждый такой метод получает *gorm.DB, работает в той же транзакции, что и основная операция, и может вернуть ошибку, чтобы эту операцию отменить.
Основные хуки:
BeforeCreate()/AfterCreate()— до и после вставки новой записи;BeforeSave()/AfterSave()— до и после любого сохранения (и создания, и обновления);BeforeUpdate()/AfterUpdate()— до и после обновления;BeforeDelete()/AfterDelete()— до и после удаления;AfterFind()— сразу после выборки записи из базы.
Хук всегда объявляется на указателе:
Если метод вернул error, GORM:
- прерывает текущую операцию (
Create(),Save(),Delete()и т. д.); - откатывает транзакцию, если операция была внутри транзакции;
- возвращает ошибку вызывающему коду.
Это удобно: вся критичная валидация и подготовка данных может жить прямо в модели, а не размазываться по сервисам.
Изменение поведения операций через хуки
Хуки — не только про проверки. Они позволяют менять поведение операций: автоматически дополнять запись, вычислять derived-поля, писать аудит, подготавливать данные для чтения.
Пример: продукт, который сам считает цену с НДС перед любым сохранением:
Как только где-то в коде выполнится:
GORM сначала вызовет BeforeSave(), модель проверит цену и пересчитает PriceWithVAT, и только затем ORM отправит UPDATE в базу. Если цена некорректная, BeforeSave() вернёт ошибку, запрос не уйдёт в базу, а транзакция откатится.
Пример: обогащение данных после чтения через AfterFind(). Пусть у пользователя в профиле хранится часовой пояс, а в коде нам удобно иметь «текущее локальное время» в этом поясе, но хранить его в базе бессмысленно.
При выборке:
GORM:
- выполнит
SELECTпо таблице profiles; - распакует поля в структуру;
- вызовет
AfterFind(), который заполнитLocalNow.
В итоге в коде сразу доступна удобная, «готовая к использованию» структура, а в базе по-прежнему хранятся только необходимые поля.
Пример: запрет опасного удаления через BeforeDelete(). Заказ со статусом paid вы хотите только отменять или рефандить, но не удалять физически:
Вызов:
для оплаченного заказа закончится ошибкой, а строка в базе останется на месте. Правило живёт рядом с моделью и сработает независимо от того, из какого участка кода попытались удалить заказ.
Ещё типичный сценарий — аудит. После создания пользователя можно записать событие в отдельную таблицу в рамках той же транзакции:
Если по какой-то причине запись аудита не сохранилась, AfterCreate() вернёт ошибку, и вся операция создания пользователя откатится.
Автоматическое хэширование пароля
Самый показательный и практически полезный пример кастомизации — работа с паролями. Хранить пароль в базе в открытом виде нельзя ни при каких условиях. Даже в тестовой базе. Даже «на пару дней».
Правильный вариант — всегда сохранять только криптографический хэш (bcrypt, Argon2, scrypt). В GORM этим удобно заниматься на уровне модели.
Опишем пользователя:
Задача: при любой вставке:
- нормализовать email (обрезать пробелы, привести к нижнему регистру);
- проверить длину пароля;
- заменить пароль на bcrypt-хэш.
Реализуем BeforeCreate():
Теперь любая попытка создать пользователя:
пройдёт через BeforeCreate(). В базу уйдёт строка вроде:
а не «secret123». Если email пустой или пароль слишком короткий, хук вернёт ошибку, INSERT не выполнится, и пользователь не появится в таблице. Программист при этом не обязан помнить про хэширование — защита встроена в модель.
Осталось покрыть обновление пароля. Для этого используют BeforeSave() и проверку изменённых полей:
Теперь при любой операции вроде:
GORM:
- Увидит изменение поля
Password. - Вызовет
BeforeSave(). - Захэширует новый пароль.
- Сохранит в базе только хэш.
Если пароль не менялся, tx.Statement.Changed("Password") вернёт false, и хук не будет трогать существующее значение.
Практические нюансы хуков
Несколько важных моментов, о которых стоит помнить:
- Хуки работают внутри той же транзакции. Если вы вызываете
db.Transaction(), всеBefore*/After*выполняются в рамках одногоBEGIN/COMMIT. - Любая ошибка из хука откатывает операцию. Это удобно для валидации, но опасно, если держать внутри долгие операции.
- Тяжёлую работу (HTTP-запросы, запись в внешние очереди) лучше не делать непосредственно в хуках. Правильный паттерн — писать событие в outbox-таблицу в
AfterCreate()и обрабатывать его асинхронным воркером.
Если по какой-то причине нужно явно отключить хуки для конкретного запроса, можно использовать сессию: db.Session(&gorm.Session{SkipHooks: true}).Create(&user), но это редкий сценарий; по умолчанию лучше считать, что хуки всегда включены.



