Как организовать хранение 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 кода. Его уже не стоит держать в константах, это теряет всякий смысл.
Представленные примеры вполне рабочие, но в реальном продукте лучше разбавить слоями абстракций. Иначе станет только хуже.