Опубликовано: 5 декабря 2022 г. 7:21 | Автор: echodiv | Категория: Разработка | 👁 620

Организация SQL кода с использованием шаблонов Go

Как организовать хранение SQL кода внутри проекта? В интрнете можно найти несколько вариантов:

  • Использовать файлы
  • Использовать константы
  • Использовать хранимые процедуры и представления
  • Использовать порождающие шаблоны
  • Использовать ORM

Мне больше всего нравится второй вариант - хранить запросы в константах внутри кода. Но, честно говоря, необходимости в другом подходе у меня не было. А с использованием шаблонов Go хранение запросов в константах становится крайне удобным. Да, шаблоны можно хранить и в файлах. Но поставлять продукт вместе с файлами, достуаными для редактирования не лучшая идея. Шаблон может сильно отличаться от финального варианта запроса и редактирование его без полного понимания, как он будет обработан не лучшая идея.

Для работы с шаблонами в go нужно понимать следующие темы:

Инъекция данных в шаблон

Для того чтобы воспользоваться преимуществом шаблона перед готовой строкой, ему нужно как-то передать данные. Для каждого шаблона стоит использовать собственную структуру. В таком случае будет проще понять поведение шаблонизатора при заполнении собственно шаблона данными.

Создадим пару шаблонов для создания запроса поиска пользователя в базе по eMail и ID.

package main
 
import (
    "os"
    "text/template"
)
 
const selectUserBy = `{{ define "getUserByID" }}
SELECT * FROM users WHERE id = {{ .ID }};
{{ end }}
 
{{ define "getUserByEmail" }}
SELECT * FROM users WHERE email = '{{ .Email }}';
{{ end }}
`
 
func main() {
    type UserSearch struct {
        ID int
        Email string
    }

    user := UserSearch{1, "admin@xztvc.ru"}
    tmpl, _ := template.New("default").Parse(selectUserBy)
 
    if err := tmpl.ExecuteTemplate(os.Stdout, "getUserByID", user); err != nil {
        panic(err)
    }
    if err := tmpl.ExecuteTemplate(os.Stdout, "getUserByEmail", user); err != nil {
        panic(err)
    }
}

Внутри константы есть два шаблона. При желании можно включить один в другой, это делается достаточно просто и описано в godev. Тут после исполнения кода мы получим в стандартном выводе следующие строки:

SELECT * FROM users WHERE id = 1;

SELECT * FROM users WHERE email = 'admin@xztvc.ru';

Их стоит записать в переменную и отдать базе.

Условные операторы 

Шаблон хорош тем, что его можно обоготить логикой, самый верный способ это условные операторы if. Посмотрим предыдущий пример, но уже с условным оператором. Сильно сложнее или проще он не станет. Предположим в базе может быть много пользователей с одиним адресом электронной почты, что даст следующему шаблону право на жизнь.

package main
 
import (
    "os"
    "text/template"
)
 
const selectUserBy = `
{{ define "getUserByEmail" }}
SELECT * FROM users WHERE email = '{{ .Email }}' {{ if .Limit }}LIMIT {{ .Limit }} {{end}};
{{ end }}
`
 
func main() {
    type UserSearch struct {
        Email string
        Limit int
    }
    userWithLimit := UserSearch{"admin@xztvc.ru", 1}
    userWithoutLimit := UserSearch{"admin@xztvc.ru", 0}
 
    if err := tmpl.ExecuteTemplate(os.Stdout, "getUserByEmail", userWithLimit); err != nil {
        panic(err)
    }
    if err := tmpl.ExecuteTemplate(os.Stdout, "getUserByEmail", userWithoutLimit); err != nil {
        panic(err)
    }
}

В выводе будет шаблон с лимитом и без него соответственно. Но что если нужно добавить условие в условный оператор? Для этого уже нужны функции.

Циклы и функции 

Для проверки эквивалентности нужно использовать функцию eq. Она уже есть в шаблоне. И через пробел передать аргументы.

{{ if eq 1 1 }}

Вообще для сравнения  есть следующие функции возвращающие булево значение:

  • eq - эквивалентность двух значений arg1 == arg2
  • ne - проверка, что два значения не равны arg1 != arg2
  • lt - проверка, что первый аргумент меньше второго arg1 < arg2
  • le - проверка, что первый аргумент меньше либо равен второму arg1 <= arg2
  • gt - второй аргумент больше первого arg1 > arg2
  • ge - второй аргумент больше либо равен первому arg1 >= arg2
{{ define "getUsersByEmail" }}
SELECT * FROM users WHERE 1 = 1     {{range $item := .Emails}}         AND email = '{{$item}}'     {{end}} {{ end }}

Этот невероятный шаблон показывает как итерироваться по срезу. Всё достаточно просто. Так же доступны и обычные циклы. 

Пользовательские функции шаблонизатора

Основное, что дают шаблоны в Go - это пользовательские функции. Они позволяют расширить функционал до максимально комфортного уровня.

Рассмотрим простой пример с разворачиванием фильтра с использованием IN. Чтобы не писать, то, что в примере выше.

package main
 
import (
    "fmt"
    "os"
    "strings"
    "text/template"
)
 
const selectUserBy = `
{{ define "getUsersByEmail" }}
SELECT * FROM users WHERE 1 = 1
    {{stingsIn "email" .Emails}}
{{ end }}`
 
func stingsIn(field string, value []string) string {
    switch len(value) {
    case 0:
        return ""
    case 1:
        return fmt.Sprintf("AND %s = %s", field, value[0])
    }
    return fmt.Sprintf("AND %s IN ('%s')", field, strings.Join(value, "', '"))
}
 
func main() {
    type Users struct {
        Emails []string
    }
    users := Users{[]string{"a@a.a", "b@b.b", "c@c.c"}}

    tmpl, _ := template.New("default").Funcs(template.FuncMap{"stingsIn": stingsIn}).Parse(selectUserBy)
    if err := tmpl.ExecuteTemplate(os.Stdout, "getUsersByEmail", users); err != nil {         panic(err)     } }

Для начала нужно написать функцию, которую мы отдадим шаблонизатору. В данном случае она будет разворачивать срез строк в условие IN. После мы должны отдать эту функцию обработчику шаблона. FuncMap говорит сама за себя. Можно передать столько функций, сколько хочется. После вызываем нашу функцию в шаблоне и передаём аргументы. В выводе будет развернутый IN:

SELECT * FROM users WHERE 1 = 1 AND email IN ('a@a.a', 'b@b.b', 'c@c.c');

 

Так же шаблоны в Go можно использовать для сборки HTML кода. Его уже не стоит держать в константах, это теряет всякий смысл.

Представленные примеры вполне рабочие, но в реальном продукте лучше разбавить слоями абстракций. Иначе станет только хуже.