golang

SQLSet — отделяем GO код от SQL-запросов

  • среда, 17 декабря 2025 г. в 00:00:11
https://habr.com/ru/articles/977046/

Предисловие

Однажды мне пришлось участвовать в переводе большого старого проекта на новую СУБД. Это заняло несколько месяцев тогда. И этот урок я запомнил на всю жизнь. В проекте повсеместно код приложения был перемешан с кодом SQL-запросов. При этом они во многих местах еще �� генерировались динамически из фрагментов текста. С тех пор я являюсь ярым сторонником отделения SQL-кода от непосредственно кода программы и патологически не перевариваю динамическую генерацию запросов.

Мухи - отдельно, котлеты - отдельно

Очевидным и хорошим способом отделения SQL от кода является тотальное использование хранимых процедур. Так я первое время и делал - весь SQL-код хранился непосредственно в БД. Тем не менее, такой подход имеет ряд недостатков:

  • Необходимость в дополнительных миграциях и контроле за ними

  • Меньшая портируемость

  • Более сложный синтаксис

  • Перенесение низкоуровневой логики проекта в слой данных

Кроме того, есть СУБД, которые не поддерживают использование хранимых процедур, такие как SQLite, DuckDB, до недавнего времени CockroachDB.
В связи с этим напрашивается решение - добавить в проект функционал, аналогичный хранимым процедурам - именованный набор текстов SQL-запросов.

SQLSet

Хочу представить вам простую библиотеку sqlset для удобного хранения и использования SQL-запросов в проектах Golang. При старте приложения мы натравливаем её на каталог с файлами наборов SQL-запросов (можно использовать embedded filesystem). Она их загружает в мапу, чтобы потом можно было получать из неё текст запроса по его имени. Каждый sql-файл может содержать произвольное количество запросов и загружается в мапу как отдельный набор. Таким образом, наборы запросов разбиты на категории (например - "user", "payment", "order" и т. д). Собственно, это и всё практически.

Пример использования

// Create a new SQLSet from the embedded filesystem.
sqlSet, err := sqlset.New(queriesFS)
if err != nil {
	log.Fatalf("Failed to create SQL set: %v", err)
}

...

// Get a specific query
query, err := sqlSet.Get("users", "GetUserByID")
if err != nil {
	fmt.Errorf("failed to get query: %w", err)
}

Постскриптум

Буду безмерно благодарен за советы, здоровую аргументированную критику!