Go: GORM
Теория: Работа с gorm.Expr и сырыми SQL
GORM закрывает 90% повседневных задач: CRUD, связи, фильтры, транзакции. Но база данных умеет гораздо больше: оконные функции, сложные агрегаты, хитрые JOIN-ы, CTE, работу с JSONB и массивами. Не всё это удобно и вообще возможно выразить «чистым» API ORM. На этот случай у GORM есть два запасных пути: сырые запросы Raw()/Exec() и выражения gorm.Expr().
Сырые запросы позволяют писать SQL напрямую, но по-прежнему пользоваться параметрами и сканированием результатов в структуры. gorm.Expr() встраивает отдельные выражения в обычные вызовы GORM — для инкрементов, формул, CASE-условий, подзапросов и диалектных функций.
Сырые запросы: Raw и Exec
Raw()— дляSELECT-ов (чтение и сканирование данных);Exec()— дляINSERT/UPDATE/DELETEи любых запросов без результата.
Оба метода принимают SQL-строку с ? и список параметров. GORM передаёт их драйверу через database/sql, так что параметры не попадают в SQL как конкатенация строк — защита от инъекций сохраняется.
Чтение данных через Raw
Когда нужно выполнить произвольный SELECT и получить результат в структуры, Raw() + Scan() работают как обычный запрос GORM, только SQL вы пишете сами:
Под капотом будет обычный запрос:
Точно так же можно читать агрегаты в простые типы:
GORM берёт на себя только передачу параметров и маппинг результата в структуру/переменную. Всё, что касается текста запроса, — ваша ответственность.
Изменения через Exec
Exec() используют, когда запрос ничего не возвращает (или вас не интересует результат):
Точно так же — массовое удаление:
SQL при этом остаётся прямым:
INSERT с RETURNING
В PostgreSQL удобно сразу получить идентификатор свежесозданной записи через RETURNING. Это тоже можно сделать через Raw():
Здесь Raw() выполняет INSERT, а затем сканирует возвращённое значение в переменную.
Гибриды: сложные JOIN-ы и представления
Когда SELECT становится сложно выразить через Preload()/Joins(), проще честно написать SQL, а результат положить в нужный тип:
Вы задаёте нужный набор полей и JOIN-ов, база выполняет запрос, GORM просто раскладывает строки по структуре.
Процедуры и функции
Если база поддерживает хранимые процедуры или SQL-функции, Exec() подойдёт и для них:
Всё это происходит в том же контексте: можно вызывать tx.Exec внутри транзакции, добавлять WithContext() с тайм-аутами и видеть запросы в логгере GORM.
Сложные выражения с gorm.Expr
Иногда нужен не целый кастомный запрос, а точечная «умная» операция: инкремент счётчика без чтения, изменение поля по формуле, CASE-условие, работа с JSONB, массивами, NOW() и другими функциями базы. Для таких случаев у GORM есть gorm.Expr().
Expr() — это объект, который говорит: «запиши здесь вот это SQL-выражение с параметрами». Его используют в Update/Updates, Select(), Where(), Order() и других местах, где обычно передаётся простое значение.
Атомарный инкремент без чтения
Классический пример — счётчик просмотров. Читать значение, увеличивать в Go и писать обратно опасно: под нагрузкой инкременты теряются. Правильно делать это одной командой UPDATE:
В SQL получится:
Инкремент выполняется на стороне базы, операция атомарная, гонок нет.
Массовое обновление по формуле
Допустим, нужно выдать 10% скидку на все книги. Вместо того чтобы выгружать их в память и пересчитывать цену, проще отдать формулу базе:
БД сама умножит цену на 0.9, округлит и запишет обратно всем подходящим товарам.
CASE в UPDATE: защита от отрицательных остатков
Ещё один популярный сценарий — не уходить в минус при списании со склада:
Логика «если достаточно товара — списать, иначе оставить как есть» живёт в базе, а не в приложении. Даже если два процесса обновят одну строку, CASE отработает корректно.
Вычисляемые поля в Select и Order
Иногда удобнее сразу вернуть производное значение и сортировку по нему:
База посчитает возраст в месяцах и отсортирует по нему же, GORM просто заберёт результат.
Диалектные фишки: JSONB, массивы, функции
GORM не знает всех операторов PostgreSQL, но Expr() позволяет аккуратно вставить их в запрос.
Например, обновление поля в JSONB:
Или добавление тега в массив:
База применяет свои функции и операторы, параметры по-прежнему передаются через ?.
Подзапросы и upsert с выражениями
Подзапрос в условии:
Upsert с вычислениями при конфликте:
PostgreSQL использует EXCLUDED.*, GORM аккуратно подставляет выражения в DO UPDATE.
Когда пора выходить за рамки GORM
gorm.Expr() отлично работает, пока выражение укладывается в одну-две строки и ещё можно прочитать запрос целиком. Как только начинается:
- несколько CTE (
WITH/WITH RECURSIVE); - пачка оконных функций;
- сложные
HAVING, хинты оптимизатора, специфичные индексы; - многоуровневые
JOIN-ы с ветвящейся логикой, удобнее честно перейти на «чистый» SQL — черезRaw()дляSELECTиExec()для изменений. В больших проектах такие запросы часто выносят в отдельные файлы рядом с миграциями или генерируют через инструменты вродеsqlc, чтобы получить типобезопасные обёртки.



