Однажды в компанию, где я работал, пришел новый тимлид. И «го уберем SQL запросы из кода» стало одной из самых популярных фраз на ревью. Так что посвящается ему :-)
Обращения к базе — одно из самых популярных действий бэкенд приложений, и чаще всего оно происходит с помощью SQL запросов. И есть несколько способов хранить запросы в коде: строка или константа непосредственно в качестве аргумента функции, билдер запросов или отдельно лежащий файл с SQL запросом, который эмбедится в Go код в момент сборки. Этот последний способ чаще всего можно найти по запросу типа «Golang embed SQL» и он действительно довольно хорош:
// Content of the repository.go
package repository
import (
_ "embed"
)
//go:embed user_list.sql
var userListQuery string
func GetUsersList() []User {
rows, err := db().Query(userListQuery)
//...
}
-- Content of user_list.sql
SELECT * FROM users;
При таком подходе запрос не засоряет Go код и удобно читается. Но есть некоторые минусы. Во-первых, лично мне не нравится, что переменная запроса остается изменяемой. Во-вторых, если запросов становится много, все эти переменные превращаются в плохо читаемую кашу:
// Content of the repository.go
package repository
import (
_ "embed"
)
//go:embed user_list.sql
var userListQuery string
//go:embed user_get_data.sql
var userGetDataQuery string
//go:embed user_activity_stat.sql
var userActivityStatQuery string
//go:embed user_update_status.sql
var userUdateStatusQuery string
// ... some other 100500 queries
С недавнего времени в своих проектах я использую подход, который положил в опенсорс и скромно хочу предложить читателю: хранение SQL запросов в отдельных файлах, из которых генерируется пакет Go файлов с помощью go:generate
.


И тогда код из примера выше меняется на такой:
// Content of the repository.go
package repository
import (
"queries"
)
func GetUsersList() []User {
rows, err := db().Query(queries.Users().GetListQuery())
//...
}
Запросы по-прежнему не засоряют Go код и лежат отдельными SQL файлами, а кроме того становятся read-only и логически структурированными. Кроме того, у такого подхода есть преимущество по сравнению с эмбедом всей директории: вся логика по обработке файлов запросов выполняется только во время генерации и никак не затрагивается во время исполнения кода приложения.
Небольшой туториал по применению
Минимальная версия go для sqlamble — это 1.24, так что рекомендуемый способ установки — это go tool:
go get -tool github.com/kukymbr/sqlamble/cmd/sqlamble@latest
Затем нам понадобится директория с нашими SQL файлами, например, sql/
. В нее нужно положить .sql
файл запроса (или не один, или много в разных директориях), а также .go
файл с вызовом go:generate
внутри:
// Content of the sql/generate.go file
package sql
//go:generate go tool sqlamble --target=../internal/queries
И потом вызвать go generate
:
go generate ./sql
Sqlamble создаст директорию internal/queries
с go пакетом queries
, в котором директории и запросы из папки sql
распределены по следующей логике:
поддиректории становятся экспортируемыми функциями, например,
sql/users/
будет доступна какqueries.Users()
, аsql/users/single-user/
— какqueries.Users().SingleUser()
;запросы доступны по имени с суффиксом «Query», например, запрос из файла
sql/users/get-list.sql
будет доступен какqueries.Users().GetListQuery()
.
Изменить пути к SQL файлам, к папке для генерируемых файлов и имя пакета можно с помощью аргументов к команде sqlamble:
--source
: путь к папке с SQL файлами, по умолчанию это.
;--target
: путь к папке для go файлов, по умолчаниюinternal/queries
;--package
: имя пакета, по умолчаниюqueries
;--ext
: список расширений имен (разделитель — запятая) для фильтрации файлов в исходной директории, по умолчанию.sql
;--fmt
: форматер для сгенерированного кода, можноgofmt
, можно выключить (none
), по умолчаниюgofmt
;--silent
или-s
: не выводить сообщения в stdout, по умолчаниюfalse
;--query-suffix
: суффикс имен функций-геттеров запросов, по умолчаниюQuery
.
И, конечно, есть встроенная справка:
go tool sqlamble --help
Ссылки
P.S.
Тулза свежая, ревью, комментарии, ПРы и прочее приветствуется.
Комментарии (14)
evgeniy_kudinov
05.07.2025 07:29Спасибо за идею и её реализацию.
Я тоже пришёл к тому, чтобы хранить SQL-код отдельно в файлах с embed. Этот подход позволяет подключать различные инструменты для анализа и проверки SQL, а также использовать современные технологии, такие как LLM. Лично мне нравится работать с LLM, так как она действительно помогает выявлять потенциальные проблемы и уведомляет о них в MR (PR).
Конечно, есть сложности с переменными, но, на мой взгляд, их можно размещать в зависимости от контекста в более подходящих местах.
Идея кодогенерации пока кажется мне избыточной и сложной для понимания — это как магия. Но надо будет попробовать и поглядеть, как это будет выглядеть в процессах.
Sly_tom_cat
05.07.2025 07:29В целом тоже примерно так делаю. И с вами почти согласен.
Однако у автора есть здравое зерно: избавится от ручного прописывания и дальнейшей поддержки всех этих эмбедов.
Возможно попробую сделать что-то похожее или возьму то, что автор предложил. Надо попробовать.
Тем более что в кодогенерацию уже пробовал и никакой магии в ней не вижу.
Free_ze
05.07.2025 07:29При таком подходе запрос не засоряет Go код и удобно читается.
Но зачем? Запрос - это полноценная часть логики, в чем выигрыш вынесения в отдельный файл? Тут упомянали анализ инструментами, но ведь тот же GoLand умеет проверять SQL в литералах.
kukymbr Автор
05.07.2025 07:29Я выношу в отдельные файлы в случае больших цельных (не собираемых при разных условиях) запросов. Так их все же проще и удобнее обслуживать.
gohrytt
05.07.2025 07:29Опять решаем софт проблемы хард методами. Если приходит новый тимлид и резко требует "убрать SQL запросы из кода" должен стоять вопрос о профпригодности этого тимлида и целесообразности его требований, а не срочное исполнение всех хотелок странного извращенца.
kukymbr Автор
05.07.2025 07:29Мы
подрались иостановились на использовании билдера (goqu), ибо на том проекте было много мест, где запросы собираются с большим количеством разных фильтров, параметров и тд.evgeniy_kudinov
05.07.2025 07:29Спасибо за наводку на doug-martin/goqu, но с этими всеми «билдерами» на любом ЯП надо изучать особенности их DSL и особенно, если там есть неочевидные вещи. Если надо миграцию с версии 1 на 2 сделать, тоже дает проблемы. Пока пробуем github.com/VauntDev/tqla, и шаблон используется стандартный text/template.
QtRoS
05.07.2025 07:29Показалось похоже на sqlc, о нём писали на Хабре. Есть ли значимые различия?
kukymbr Автор
05.07.2025 07:29Отличие в более ограниченной зоне ответственности у sqlamble: никакого парсинга самих файлов, ни врапперов для отправки запросов, только внедрение содержимого файлов запросов. Чисто некий эмбеддер, так-то можно подключать им любые текстовые файлы.
gudvinr
Это можно сделать и без кодогенерации, если помнить что
embed
позволяет использоватьembed.FS
Тогда враппер может делать
Query("user_select.sql", 1, 2, "abc")
. У этого есть свои минусы, но минусы есть и в кодогенерации.kukymbr Автор
Про embed.FS помним, однако мне не нравится оверхед на чтение всего этого дела в момент выполнения уже непосредственно кода. Кодогенерация позволяет сделать это всё один раз на машине девелопера или в CI и дальше спокойно пользоваться константными значениями.
gudvinr
А этот оверхед он здесь с нами, в одной комнате?
Нравится/не нравится - это аргумент, который имеет место быть, но если бенчмарки показывают что разница исчезающе мала, то смысла в кодогенерации никакой.
Если приложение пишется один раз и никогда не изменяется, то действительно один раз сгенерировал и забыл.
Но если код меняется, не должно быть такого, что SQL поменялся у вас на машине, а результат кодогенерации меняется где-то там в CI, потому что это кошмар для отладки.
Значит, всем разработчикам нужен будет хук. Или проверки на MR. И решение внезапно перестает быть простым.
kukymbr Автор
Тут кроме временного оверхеда есть еще тот, который момент выявления ошибки. При кодогенерации и аналогичных подходах (причем не важно, говорим мы про интеграцию SQL или о чем-то еще) проблемы чтения, парсинга и тд выявляются на этапе сборки, а не потом где-нибудь на проде, задеплоеном в далекую галактику. Особенно актуально, если прод — это не веб-сервис, а системная приложуха, развернутая на какой-нибудь железке на заводе города N.
А если наши гипотетические разработчики меняют SQL и потом не проверяют ожидаемый результат тестами или хотя бы руками — то тут уже ни хуки, ни embed не поможет.
Ну а в целом, я не вижу причин, почему какой-либо из этих методов интеграции SQL не мог бы существовать. И так норм и этак неплохо, просто предлагаю тот, что симпатизирует мне во вполне ограниченном числе кейсов.