Скрэмбл для тех, кто ненавидит SQL в Go коде
- воскресенье, 6 июля 2025 г. в 00:00:07
Однажды в компанию, где я работал, пришел новый тимлид. И «го уберем 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
Тулза свежая, ревью, комментарии, ПРы и прочее приветствуется.