Большинство современных сайтов реализуют некий MV*-фреймворк как формально, так и неформально. Если вы пишете много кода, скорее всего, вы пишете много моделей снова и снова. Они в основном похожи по структуре и отличаются только деталями схемы. Вы определяете SQL-схему, создаете структуры и соединяете некоторые базовые CRUD API. Затем вы подправляете всё это по мере развития логики приложения. Разве не здорово было бы автоматизировать что-то из этого, чтобы сократить и время, и количество ошибок? В этой статье мы сделаем именно это. Мы рассмотрим метапрограммирование в Go для автоматического создания простого CRUD API, основанного на определениях таблиц SQL.
Введение
Метапрограммирование — это, в сущности, написание программы для написания программы. Таким образом, вы на один уровень абстракции выше конкретной программы и решаете задачу для класса программ. Известный пример этого — генераторы Rails (Ruby on Rails). Запустив new test rails
, вы создадите полнофункциональный проект под названием test
. Метапрограммирование обычно занимает немного больше времени, но в конце концов у вас появляется инструмент, который вы можете повторно использовать для решения схожих проблем.
Обычно я использую Postgres, поэтому хотел бы автоматически преобразовать набор определений таблиц SQL Create Table в код на Go. Синтаксис Postgres для CREATE TABLE невероятно богат, хотя это всего лишь одно выражение. К счастью, нам не нужно разбирать весь синтаксис, чтобы достичь цели. Чтобы построить CRUD API, нам нужно распознавать:
- Существующее выражение.
- Имя таблицы.
- Имена колонок.
- Типы данных.
Кажется, много? На самом деле это позволит игнорировать большинство необязательных частей синтаксиса CREATE TABLE
. Мы можем искать, что хотим, игнорируя всё остальное.
Создадим простой компилятор для просеивания определений таблиц SQL, сохраняя необходимые части, чтобы генерировать наш новый код из найденных выражений. Компилятор будет состоять из трех основных частей: лексера, парсера и генератора. Грамматика основана на документации Postgres и выглядит так:
CREATE [some_stuff]* TABLE [IF NOT EXISTS] table_name (
column_name data_type [some_stuff]*
[, ...]
) [some_stuff]*;
Наш код должен пропускать некоторый код, но игнорировать остальной синтаксис SQL: например, мы должны разобрать опцию [IF NOT EXISTS]
. Если мы будем рассматривать ее как произвольный текст, то можем пропустить table_name. Кроме того, для простоты мы не будем поддерживать table_constraints и подобные операторы в списке столбцов: это вынудит нас к гораздо более сложному синтаксическому анализу. Выражение должно заканчиваться на ;
. Мы будем поддерживать все основные типы данных и переводить их в Go следующим образом:
SQL GO
----------------|-------
BOOLEAN bool
BOOL bool
CHAR(n) string
VARCHAR(n) string
TEXT string
SMALLINT int16
INT int32
INTEGER int32
BIGINT int64
SMALLSERIAL int16
SERIAL int32
BIGSERIAL int64
FLOAT(n) float64
REAL float32
FLOAT8 float32
DECIMAL float64
NUMERIC float64
NUMERIC(p,s) float64
DOUBLE PRECISION float64
DATE time.Time
TIME time.Time
TIMESTAMPTZ time.Time
TIMESTAMP time.Time
INTERVAL time.Time
JSON []byte
JSONB []byte
UUID string
func CreateTableFoo(db *sql.DB) (err error){}
type Foo struct{}
func (foo *Foo) CreateFoo(db *sql.DB) (result Foo, err error){}
func (foo *Foo) RetrieveFoo(db *sql.DB) (result Foo, err error){}
func (foo *Foo) RetrieveAllFoo(db *sql.DB) (foo []Foo, err error){}
func (foo *Foo) UpdateFoo(db *sql.DB) (result Foo, err error){}
func (foo *Foo) DeleteFoo(db *sql.DB) (err error){}
func DeleteAllFoo(db *sql.DB) (err error){}
Лексический анализатор
Лексический анализ — это процесс обнаружения лексем в потоке символов. Это так же просто, как поиск слов, разделенных пробелами, или более распространенное распознавание определенных ключевых слов и идентификаторов. В Go есть пакеты для сканирования и лексического анализа. Я выбрал хорошо документированный пакет Lexmachine
Тима Хендерсона.
Используя отличный пример Тима Dot lexer в качестве шаблона, мы можем построить лексер для нашего упрощенного подмножества SQL. Ключевые слова и литералы довольно просты: CREATE
, TABLE
, IF
, NOT
, EXISTS
и наш список статических типов данных.
Опять же, мы должны обнаружить опцию IF NOT EXISTS
, чтобы устранить двусмысленность table_name
. Литералы — это просто (),;
.
Идентификаторы немного сложнее. Они включают в себя типы CHAR(n), VARCHAR(n), FLOAT(n), NUMERIC(p,s), ID(ID), table_name, column_name
. Lexmachine
использует регулярные выражения для идентификаторов. Вот наш код:
VARCHAR(n): [vV][aA][rR][cC][hH][aA][rR]\([0-9]+\)
CHAR(n): [cC][hH][aA][rR]\([0-9]+\)
FLOAT(n): [fF][lL][oO][aA][tT]\([0-9]+\)
NUMERIC(p,s): [nN][uU][mM][eE][rR][iI][cC]\([0-9]+,[0-9]+\)
ID(ID): ([a-z]|[A-Z]|_|#|@)([a-z]|[A-Z]|[0-9]|_|#|@|$)*
\(([a-z]|[A-Z]|_|#|@)([a-z]|[A-Z]|[0-9]|_|#|@|$)*\)
ID: ([a-z]|[A-Z]|_|#|@)([a-z]|[A-Z]|[0-9]|_|#|@|$)*
Обратите внимание, что Lexmachine пока не поддерживает такие режимы, как (?i), что объясняет приведенное выше регулярное выражение. ID требуется, чтобы разрешить ссылки, которые в противном случае были бы ошибками из-за скобок. ID может использоваться как для table_name
, так и для column_name
. Лексер пропустит любой пробел. Вот и всё, он готов.
metaapi/metasql/lexer.go:
// Основано на https://hackthology.com/writing-a-lexer-in-go-with-lexmachine.html
package metasqlimport (
"strings"lex "github.com/timtadh/lexmachine"
"github.com/timtadh/lexmachine/machines"
)var Literals []string
var Keywords []string
var Tokens []string
var TokenIds map[string]int
var Lexer *lex.Lexer // Вызывается при инициализации. Создаёт лексер и списки токенов.
func init() {
initTokens()
var err error
Lexer, err = initLexer()
if err != nil {
panic(err)
}
}
func initTokens() {
Tokens = []string{
"VARCHARID",
"CHARID",
"FLOATID",
"NUMERICID",
"REFID",
"ID",
}
Keywords = []string{
"CREATE",
"TABLE",
"IF",
"NOT",
"EXISTS",
"BOOLEAN",
"BOOL",
"TEXT",
"SMALLINT",
"INTEGER",
"BIGINT",
"INT",
"SMALLSERIAL",
"BIGSERIAL",
"SERIAL",
"REAL",
"FLOAT8",
"DECIMAL",
"NUMERIC",
"DOUBLE",
"PRECISION",
"DATE",
"TIMESTAMPTZ",
"TIMESTAMP",
"TIME",
"INTERVAL",
"JSONB",
"JSON",
"UUID",
}
Literals = []string{
"(",
")",
",",
";",
}
Tokens = append(Tokens, Keywords...)
Tokens = append(Tokens, Literals...)
TokenIds = make(map[string]int)
for i, tok := range Tokens {
TokenIds[tok] = i
}
}
// Создаёт объект лексера и компилирует недетерминизированный конечный автомат.
func initLexer() (*lex.Lexer, error) {
lexer := lex.NewLexer()for _, lit := range Literals {
r := "\\" + strings.Join(strings.Split(lit, ""), "\\")
lexer.Add([]byte(r), token(lit))
}
for _, name := range Keywords {
lexer.Add([]byte(strings.ToLower(name)), token(name))
}
lexer.Add([]byte(`[vV][aA][rR][cC][hH][aA][rR]\([0-9]+\)`), token("VARCHARID"))
lexer.Add([]byte(`[cC][hH][aA][rR]\([0-9]+\)`), token("CHARID"))
lexer.Add([]byte(`[fF][lL][oO][aA][tT]\([0-9]+\)`), token("FLOATID"))
lexer.Add([]byte(`[nN][uU][mM][eE][rR][iI][cC]\([0-9]+,[0-9]+\)`), token("NUMERICID"))
lexer.Add([]byte(`([a-z]|[A-Z]|_|#|@)([a-z]|[A-Z]|[0-9]|_|#|@|$)*\(([a-z]|[A-Z]|_|#|@)([a-z]|[A-Z]|[0-9]|_|#|@|$)*\)`), token("REFID"))
lexer.Add([]byte(`([a-z]|[A-Z]|_|#|@)([a-z]|[A-Z]|[0-9]|_|#|@|$)*`), token("ID"))
lexer.Add([]byte("( |\t|\n|\r)+"), skip) err := lexer.Compile()
if err != nil {
return nil, err
}
return lexer, nil
}
// lex.Action - функция, которая пропускает совпадения.
func skip(*lex.Scanner, *machines.Match) (interface{}, error) {
return nil, nil
}
// lex.Action конструирует токен данного типа по имени типа.
func token(name string) lex.Action {
return func(s *lex.Scanner, m *machines.Match) (interface{}, error) {
return s.Token(TokenIds[name], string(m.Bytes), m), nil
}
}
Парсер
Синтаксический анализ — это процесс обнаружения частей определенной грамматики в потоке лексем, созданных лексером.
Создадим простой синтаксический анализатор для нашей ограниченной грамматики SQL. Для сложной грамматики мы склонялись бы к реальному анализатору, основанному на рекурсивном спуске или сгенерированному генератором синтаксических анализаторов. Наша задача достаточно проста, чтобы конечная машина состояний справилась с ней.
Простая машина состояний следует шаблону: если я нахожусь в состоянии CurState
и вижу какой-то вход, то перемещаюсь в следующее состояние и выполняю какое-то действие. И так до достижения некоторого терминального состояния, обычно EOF
. В нашем случае вводом будет тип токена, а действием — некоторая функция, которую мы вызываем для сохранения табличных данных или их игнорирования. Ниже таблица состояний. Вы заметите, что большая ее часть связана с обнаружением различных типов данных:
Cur Next
State Input State Action
---------------------------------------
0 Error 0 error_state
1 CREATE 2 create_table
2 TABLE 3 nop
2 ID 2 some_stuff
3 IF 4 nop
4 NOT 5 nop
5 EXISTS 3 nop
3 ID 6 table_name
6 ( 7 nop
7 ID 8 column_name
7 UUID 8 column_name
8 BOOLEAN 9 data_type
8 BOOL 9 data_type
8 CHARID 9 data_type
8 VARCHARID 9 data_type
8 TEXT 9 data_type
8 SMALLINT 9 data_type
8 INT 9 data_type
8 INTEGER 9 data_type
8 BIGINT 9 data_type
8 SMALLSERIAL 9 data_type
8 SERIAL 9 data_type
8 BIGSERIAL 9 data_type
8 FLOATID 9 data_type
8 REAL 9 data_type
8 FLOAT8 9 data_type
8 DECIMAL 9 data_type
8 NUMERIC 9 data_type
8 NUMERICID 9 data_type
8 DOUBLE 10 nop
10 PRECISION 9 data_type
8 DATE 9 data_type
8 TIME 9 data_type
8 TIMESTAMPTZ 9 data_type
8 TIMESTAMP 9 data_type
8 INTERVAL 9 data_type
8 JSON 9 data_type
8 JSONB 9 data_type
8 UUID 9 data_type
9 , 7 nop
9 ) 11 nop
9 REFID 9 some_stuff
9 NOT 9 some_stuff
9 ID 9 some_stuff
11 ; 1 end_table
11 ID 11 some_stuff
В дополнение к пониманию и применению нашей грамматики синтаксический анализатор должен также захватывать все данные, необходимые для генерации кода. В реальном компиляторе для этого можно построить абстрактное синтаксическое дерево, которое затем анализируется генератором кода. В нашем случае мы просто построим список таблиц и их имена столбцов и типы, используемые для генератора. Наша машина состояний сама по себе является просто картой Go, принимающей строку в качестве входных данных и возвращающей структуру следующего состояния и действия (метода) для запуска. Строка карты содержит как текущее состояние, так и входной токен, соединенные вместе, чтобы сформировать одну строку для карты. "CurState, InToken": {NextState, FunctionToCall}
.
metaapi/metasql/parse.go:
package metasqlimport (
"errors"
"fmt"
"log" lex "github.com/timtadh/lexmachine"
)
type Column struct {
Name string
Type string
}
type Table struct {
Name string
Query string
Columns []Column
}
type StateMachine struct {
FName string
CurState int
Tables []Table
}
type NextAction struct {
State int
Fn func(*StateMachine, *lex.Token)
}
func getColumn(sm *StateMachine) *Column {
if len(sm.Tables) > 0 {
table := &(sm.Tables[len(sm.Tables)-1])
if len(table.Columns) > 0 {
return &(table.Columns[len(table.Columns)-1])
} else {
return nil
}
} else {
return nil
}}
func InitState(fname string) *StateMachine {
sm := new(StateMachine)
sm.FName = fname
sm.CurState = 1
return sm
}
func error_state(sm *StateMachine, token *lex.Token) {
//состояние не найдено
log.Fatal("Error in SQL Syntax!")
}
func nop(sm *StateMachine, token *lex.Token) {
//nop
}
func create_table(sm *StateMachine, token *lex.Token) {
sm.Tables = append(sm.Tables, Table{})
}
func table_name(sm *StateMachine, token *lex.Token) {
if len(sm.Tables) > 0 {
sm.Tables[len(sm.Tables)-1].Name = string(token.Lexeme)
}
}
func column_name(sm *StateMachine, token *lex.Token) {
if len(sm.Tables) > 0 {
table := &(sm.Tables[len(sm.Tables)-1])
table.Columns = append(table.Columns, Column{})
table.Columns[len(table.Columns)-1].Name = string(token.Lexeme)
}
}
func data_type(sm *StateMachine, token *lex.Token) {
column := getColumn(sm)
column.Type = Tokens[token.Type]
}
func some_stuff(sm *StateMachine, token *lex.Token) {
//nop
}
func end_table(sm *StateMachine, token *lex.Token) {
//nop
}
func appendQuery(sm *StateMachine, st string) {
if len(sm.Tables) > 0 {
(&(sm.Tables[len(sm.Tables)-1])).Query += st + " "
}
}
func printQuery(sm *StateMachine) {
if len(sm.Tables) > 0 {
fmt.Println("query: ", (&(sm.Tables[len(sm.Tables)-1])).Query, " <<")
}
}
func ProcessState(sm *StateMachine, token *lex.Token) (err error) { //Машина состояний, формат:
//"CurState, InToken": {NextState, FunctionToCall} stateMap := map[string]NextAction{
"Error": {0, error_state},
"1,CREATE": {2, create_table},
"2,TABLE": {3, nop},
"2,ID": {2, some_stuff},
"3,IF": {4, nop},
"4,NOT": {5, nop},
"5,EXISTS": {3, nop},
"3,ID": {6, table_name},
"6,(": {7, nop},
"7,ID": {8, column_name},
"7,UUID": {8, column_name},
"8,BOOLEAN": {9, data_type},
"8,BOOL": {9, data_type},
"8,CHARID": {9, data_type},
"8,VARCHARID": {9, data_type},
"8,TEXT": {9, data_type},
"8,SMALLINT": {9, data_type},
"8,INT": {9, data_type},
"8,INTEGER": {9, data_type},
"8,BIGINT": {9, data_type},
"8,SMALLSERIAL": {9, data_type},
"8,SERIAL": {9, data_type},
"8,BIGSERIAL": {9, data_type},
"8,FLOATID": {9, data_type},
"8,REAL": {9, data_type},
"8,FLOAT8": {9, data_type},
"8,DECIMAL": {9, data_type},
"8,NUMERIC": {9, data_type},
"8,NUMERICID": {9, data_type},
"8,DOUBLE": {10, nop},
"10,PRECISION": {9, data_type},
"8,DATE": {9, data_type},
"8,TIME": {9, data_type},
"8,TIMESTAMPTZ": {9, data_type},
"8,TIMESTAMP": {9, data_type},
"8,INTERVAL": {9, data_type},
"8,JSON": {9, data_type},
"8,JSONB": {9, data_type},
"8,UUID": {9, data_type},
"9,,": {7, nop},
"9,)": {11, nop},
"9,REFID": {9, some_stuff},
"9,NOT": {9, some_stuff},
"9,ID": {9, some_stuff},
"11,;": {1, end_table},
"11,ID": {11, some_stuff},
}
mapStr := fmt.Sprintf("%d,%s", sm.CurState, Tokens[token.Type])
nextState := stateMap[mapStr]
//отображение нулей во все поля структуры, если они не найдены
if nextState.State == 0 {
nextState = stateMap["Error"]
printQuery(sm)
err = errors.New("Syntax Error: " + Tokens[token.Type])
return
}
sm.CurState = nextState.State
nextState.Fn(sm, token)
appendQuery(sm, string(token.Lexeme))
return nil
}
Генератор
Генератор отвечает за генерацию кода нашего “компилятора”. Он принимает внутреннее представление таблиц SQL, созданное на предыдущих стадиях, и генерирует CRUD API. Если все пойдет хорошо, полученный код должен быть готов к компиляции и запуску в другом проекте.
Подход к созданию генератора такой: мы начинаем с некоторого известного и работающего кода, представляющего нашу цель — API. Мы переименуем этот код в файл txt, подаваемый на вход генератору в качестве шаблона, и медленно преобразуем его до полного соответствия шаблону. Мы напишем соответствующие методы приемника в generate.go
и пройдём по шаблону. При запуске генератора необходимо воссоздать исходную цель. Этот процесс позволяет действительно легко сравнивать генерируемый код рядом с исходной целью и исправлять любые ошибки.
В моём случае целевым API будет todo CRUD API из проекта govueintro
. todo.go
, которые я буду конвертировать, чтобы использовать автоматически сгенерированный todo_generated.go
из метаапи. crud.txt ниже начинали свою жизнь как todo_generated.go
, и я итеративно преобразовал его в crud.txt
. Я заменил разделы методами приемника в generate.go
. crud.txt
сейчас выглядит уродливо, но поверьте мне, итеративный процесс преобразования прост. generate.go
просто использует шаблоны go, чтобы сложить определенные табличные данные в универсальный crud.txt
metaapi/metasql/generate.go:
package metasqlimport (
"errors"
"io/ioutil"
"os"
"strconv"
"strings"
"text/template"
)
// Generate предполагает, что первичный идентификатор находится в первом столбце (индекс 0)
func Generate(sm *StateMachine, txtFile string) error {
if sm.FName == "" {
return (errors.New("No file name"))
}
dot := strings.Index(sm.FName, ".")
var prefix string
if dot > 0 {
prefix = sm.FName[:dot]
} else {
prefix = sm.FName
}
dat, err := ioutil.ReadFile("./" + txtFile)
if err != nil {
return err
} tt := template.Must(template.New(prefix).Parse(string(dat)))
dest := prefix + "_generated.go"
file, err := os.Create(dest)
if err != nil {
return err
}
tt.Execute(file, sm)
file.Close()
return nil
}
//======== строковые помощники
// должны использовать: https://github.com/blakeembrey/pluralize
func singularize(s string) string {
if strings.HasSuffix(strings.ToLower(s), "s") {
return strings.TrimSuffix(strings.ToLower(s), "s")
} else {
return strings.ToLower(s)
}
}
func capitalize(s string) string {
return strings.Title(s)
}
func lowerize(s string) string {
return strings.ToLower(s)
}
//преобразует submitted_at в SubmittedAt и не только
func camelize(s string) string {
return strings.ReplaceAll(strings.Title(strings.ReplaceAll(strings.ToLower(s), "_", " ")), " ", "")
}
func comma(i int, length int) string {
if i < (length - 1) {
return ","
} else {
return ""
}
}
//======== template methodsfunc (sm *StateMachine) Package() string {
return os.Getenv("GOPACKAGE")
}
// Написание для расширения
func (sm *StateMachine) Import() string {var s string
var includeTime boolincludeTime = false
for _, table := range sm.Tables {
for _, column := range table.Columns {
switch column.Type {
case "DATE", "TIME", "TIMESTAMP", "TIMESTAMPTZ", "INTERVAL":
includeTime = true
default:
}
}
}
s += "import (\n\t\"database/sql\"\n"
if includeTime {
s += "\t\"time\"\n"
}
s += ")"
return s
}
func (table Table) SingName() string {
return singularize(table.Name)
}
func (table Table) CapName() string {
return capitalize(lowerize(table.Name))
}
func (table Table) CapSingName() string {
return capitalize(singularize(table.Name))
}
func (table Table) DropTableStatement() string {
var s string
s += "(\"DROP TABLE IF EXISTS " + table.Name + "\")"
return s
}
func (table Table) CreateTableStatement() string {
var s string
s += "(`" + table.Query + "`)"
return s
}
func (table Table) StructFields() string {var typeMap = map[string]string{
"BOOLEAN": "bool",
"BOOL": "bool",
"CHARID": "string",
"VARCHARID": "string",
"TEXT": "string",
"SMALLINT": "int16",
"INT": "int32",
"INTEGER": "int32",
"BIGINT": "int64",
"SMALLSERIAL": "int16",
"SERIAL": "int32",
"BIGSERIAL": "int64",
"FLOATID": "float64",
"REAL": "float32",
"FLOAT8": "float32",
"DECIMAL": "float64",
"NUMERIC": "float64",
"NUMERICID": "float64",
"PRECISION": "float64",
"DATE": "time.Time",
"TIME": "time.Time",
"TIMESTAMPTZ": "time.Time",
"TIMESTAMP": "time.Time",
"INTERVAL": "time.Time",
"JSON": "[]byte",
"JSONB": "[]byte",
"UUID": "string",
}
var s string for _, column := range table.Columns {
s += "\t" + camelize(column.Name)
s += " " + typeMap[column.Type]
s += "`xml:\"" + camelize(column.Name) + "\" json:\"" + lowerize(camelize(column.Name)) + "\"`"
s += "\n"
}
return s
}
func (table Table) Star() string {
var s string
for i, column := range table.Columns {
s += " " + column.Name
s += comma(i, len(table.Columns))
}
return s
}
func (table Table) ScanAll() string {var s string
s += ".Scan("
for i, column := range table.Columns {
s += " &result." + camelize(column.Name)
s += comma(i, len(table.Columns))
}
s += ")"
return s
}
func (table Table) CreateStatement() string {
var s string
s += "(\"INSERT INTO " + table.Name + " ("for i, column := range table.Columns {
if i == 0 {
continue
}
s += " " + column.Name
s += comma(i, len(table.Columns))
}
s += ") VALUES ("
index := 1
for i, _ := range table.Columns {
if i == 0 {
continue
}
s += "$"
s += strconv.Itoa(index)
s += comma(i, len(table.Columns))
index++
}
s += ") RETURNING"
for i, column := range table.Columns {
s += " " + column.Name
s += comma(i, len(table.Columns))
}
s += "\")"
return s
}
func (table Table) CreateQuery() string {
var s string
s += "("
for i, column := range table.Columns {
if i == 0 {
continue
}
s += " " + table.SingName() + "." + camelize(column.Name)
s += comma(i, len(table.Columns))
}
s += ")"
s += table.ScanAll()
return s
}
func (table Table) RetrieveStatement() string {
var s string
s += "(\"SELECT" + table.Star() + " FROM " + table.Name + " WHERE (" index := 1
for i, column := range table.Columns {
if i == 0 {
s += column.Name + " = $" + strconv.Itoa(index)
s += ")\", " + table.SingName() + "." + camelize(column.Name) + ")"
}
break
}
s += table.ScanAll()
return s
}
func (table Table) RetrieveAllStatement() string {
var s string
s += "(\"SELECT" + table.Star() + " FROM " + table.Name + " ORDER BY " for i, column := range table.Columns {
if i == 0 {
s += column.Name
}
break
}
s += " DESC\")"
return s
}
func (table Table) UpdateStatement() string {
var s string
s += "(\"UPDATE " + table.Name + " SET" index := 2
for i, column := range table.Columns {
if i == 0 {
continue
}
s += " " + column.Name + " = $" + strconv.Itoa(index)
index++
s += comma(i, len(table.Columns))
}
s += " WHERE ("
index = 1
for i, column := range table.Columns {
if i == 0 {
s += column.Name + " = $" + strconv.Itoa(index)
s += ") RETURNING"
}
break
}
for i, column := range table.Columns {
s += " " + column.Name
s += comma(i, len(table.Columns))
}
s += "\")"
return s
}
func (table Table) UpdateQuery() string { var s string
s += "("
for i, column := range table.Columns {
s += " " + table.SingName() + "." + camelize(column.Name)
s += comma(i, len(table.Columns))
}
s += ")"
s += table.ScanAll()
return s
}
func (table Table) DeleteStatement() string {
var s string
s += "(\"DELETE FROM " + table.Name + " WHERE (" index := 1
for i, column := range table.Columns {
if i == 0 {
s += column.Name + " = $" + strconv.Itoa(index)
}
break
}
s += ")\")"
return s
}
func (table Table) DeleteQuery() string { var s string
for i, column := range table.Columns {
if i == 0 {
s += "(" + table.SingName() + "." + camelize(column.Name) + ")"
}
break
}
return s
}
func (table Table) DeleteAllStatement() string {
var s string
s += "(\"DELETE FROM " + table.Name + "\")"
return s
}
metaapi/metasql/crud.txt
//Сгенерировано с помощью MetaApi https://github.com/exyzzy/metaapi
package {{ .Package }}{{ .Import }}{{ range $index, $table := .Tables }}
// CREATE TABLE
func CreateTable{{ $table.CapName }}(db *sql.DB) (err error) {
_, err = db.Exec{{ $table.DropTableStatement }}
if err != nil {
return
}
_, err = db.Exec{{ $table.CreateTableStatement }}
return
}
// Структура
type {{ $table.CapSingName }} struct {
{{ $table.StructFields }}
}
// Create
func ({{ $table.SingName }} *{{ $table.CapSingName }}) Create{{ $table.CapSingName }}(db *sql.DB) (result {{ $table.CapSingName }}, err error) {
stmt, err := db.Prepare{{ $table.CreateStatement }}
if err != nil {
return
}
defer stmt.Close()
err = stmt.QueryRow{{ $table.CreateQuery }}
return
}
// Извлечение
func ({{ $table.SingName }} *{{ $table.CapSingName }}) Retrieve{{ $table.CapSingName }}(db *sql.DB) (result {{ $table.CapSingName }}, err error) {
result = {{ $table.CapSingName }}{}
err = db.QueryRow{{ $table.RetrieveStatement }}
return
}// Извлечение всего
func ({{ $table.SingName }} *{{ $table.CapSingName }}) RetrieveAll{{ $table.CapName }}(db *sql.DB) ({{ $table.Name }} []{{ $table.CapSingName }}, err error) {
rows, err := db.Query{{ $table.RetrieveAllStatement }}
if err != nil {
return
}
for rows.Next() {
result := {{ $table.CapSingName }}{}
if err = rows{{ $table.ScanAll }}; err != nil {
return
}
{{ $table.Name }} = append({{ $table.Name }}, result)
}
rows.Close()
return
}//Update
func ({{ $table.SingName }} *{{ $table.CapSingName }}) Update{{ $table.CapSingName }}(db *sql.DB) (result {{ $table.CapSingName }}, err error) {
stmt, err := db.Prepare{{ $table.UpdateStatement }}
if err != nil {
return
}
defer stmt.Close()err = stmt.QueryRow{{ $table.UpdateQuery }}
return
}//Delete
func ({{ $table.SingName }} *{{ $table.CapSingName }}) Delete{{ $table.CapSingName }}(db *sql.DB) (err error) {
stmt, err := db.Prepare{{ $table.DeleteStatement }}
if err != nil {
return
}
defer stmt.Close()_, err = stmt.Exec{{ $table.DeleteQuery }}
return
}
//DeleteAll
func DeleteAll{{ $table.CapSingName }}s(db *sql.DB) (err error) {
stmt, err := db.Prepare{{ $table.DeleteAllStatement}}
if err != nil {
return
}
defer stmt.Close()_, err = stmt.Exec()
return
}
{{ end }}
Короткий main держит все вместе и использует флаги go для передачи имен файлов.
metaapi/main.go:
package mainimport (
"flag"
"fmt"
"io/ioutil"
"log"
"strings" "github.com/exyzzy/metaapi/metasql" lex "github.com/timtadh/lexmachine"
)
// Включаем печать отладки
var DEBUG = falsefunc main() {
sqlPtr := flag.String("sql", "", ".sql input file to parse")
txtPtr := flag.String("txt", "crud.txt", "go template as .txt file")
flag.Parse()
sqlFile := strings.ToLower(*sqlPtr)
txtFile := strings.ToLower(*txtPtr) if (sqlFile == "") || (!strings.HasSuffix(sqlFile, ".sql")) {
log.Fatal("No .sql File")
}
if (txtFile == "") || (!strings.HasSuffix(txtFile, ".txt")) {
log.Fatal("No .txt File")
} dat, err := ioutil.ReadFile("./" + sqlFile)
if err != nil {
log.Fatal(err)
} s, err := metasql.Lexer.Scanner([]byte(dat))
if err != nil {
log.Fatal(err)
} sm := metasql.InitState(sqlFile)
for tok, err, eof := s.Next(); !eof; tok, err, eof = s.Next() {
if err != nil {
log.Fatal(err)
}
token := tok.(*lex.Token)
if DEBUG {
fmt.Printf("%-10v | %-12v | %v:%v-%v:%v\n",
metasql.Tokens[token.Type],
string(token.Lexeme),
token.StartLine,
token.StartColumn,
token.EndLine,
token.EndColumn)
}
err = metasql.ProcessState(sm, token)
if err != nil {
log.Fatal(err)
}
}
err = metasql.Generate(sm, txtFile)
if err != nil {
log.Fatal(err)
}
if DEBUG {
fmt.Printf("Table Capture:\n%+v\n", sm)
}
}
Компилятор работает?
Чтобы использовать его, пройдите шаги ниже:
- Клонируйте и установите проект metaapi.
- Создайте каталог для нового проекта.
- Скопируйте
crud.txt
, вашsql.todo
и опционально файл todo.go в новый проект. - Выполните
go.generate
или вручную запустите metaapi.
Для своего проекта я использовал такой код:
metasql/todo.go:
//go:generate metaapi -sql=todo.sql -txt=crud.txt
package metasql
//Посмотрите: todo_generated.go
metasql/todo.sql:
create table todos (
id integer generated always as identity primary key,
updated_at timestamptz,
done boolean,
title text
);
Давайте запустим его и посмотрим, что получится:
go get github.com/exyzzy/metaapi
go install $GOPATH/src/github.com/exyzzy/metaapi
mkdir myproj
cd myproj
cp $GOPATH/src/github.com/exyzzy/metaapi/metasql/crud.txt .
cp $GOPATH/src/github.com/exyzzy/metaapi/metasql/todo.sql .
cp $GOPATH/src/github.com/exyzzy/metaapi/metasql/todo.go .
go generate
todo_generated.go
создан автоматически:
//Auto generated with MetaApi https://github.com/exyzzy/metaapi
package metasqlimport (
"database/sql"
"time"
)
//Create Table
func CreateTableTodos(db *sql.DB) (err error) {
_, err = db.Exec("DROP TABLE IF EXISTS todos")
if err != nil {
return
}
_, err = db.Exec(`create table todos ( id integer generated always as identity primary key , updated_at timestamptz , done boolean , title text ) ; `)
return
}//Структура
type Todo struct {
Id int32`xml:"Id" json:"id"`
UpdatedAt time.Time`xml:"UpdatedAt" json:"updatedat"`
Done bool`xml:"Done" json:"done"`
Title string`xml:"Title" json:"title"`}//Create
func (todo *Todo) CreateTodo(db *sql.DB) (result Todo, err error) {
stmt, err := db.Prepare("INSERT INTO todos ( updated_at, done, title) VALUES ($1,$2,$3) RETURNING id, updated_at, done, title")
if err != nil {
return
}
defer stmt.Close()
err = stmt.QueryRow( todo.UpdatedAt, todo.Done, todo.Title).Scan( &result.Id, &result.UpdatedAt, &result.Done, &result.Title)
return
}
//Извлечение
func (todo *Todo) RetrieveTodo(db *sql.DB) (result Todo, err error) {
result = Todo{}
err = db.QueryRow("SELECT id, updated_at, done, title FROM todos WHERE (id = $1)", todo.Id).Scan( &result.Id, &result.UpdatedAt, &result.Done, &result.Title)
return
}
//Извлечение всего
func (todo *Todo) RetrieveAllTodos(db *sql.DB) (todos []Todo, err error) {
rows, err := db.Query("SELECT id, updated_at, done, title FROM todos ORDER BY id DESC")
if err != nil {
return
}
for rows.Next() {
result := Todo{}
if err = rows.Scan( &result.Id, &result.UpdatedAt, &result.Done, &result.Title); err != nil {
return
}
todos = append(todos, result)
}
rows.Close()
return
}
//Update
func (todo *Todo) UpdateTodo(db *sql.DB) (result Todo, err error) {
stmt, err := db.Prepare("UPDATE todos SET updated_at = $2, done = $3, title = $4 WHERE (id = $1) RETURNING id, updated_at, done, title")
if err != nil {
return
}
defer stmt.Close() err = stmt.QueryRow( todo.Id, todo.UpdatedAt, todo.Done, todo.Title).Scan( &result.Id, &result.UpdatedAt, &result.Done, &result.Title)
return
}
//Delete
func (todo *Todo) DeleteTodo(db *sql.DB) (err error) {
stmt, err := db.Prepare("DELETE FROM todos WHERE (id = $1)")
if err != nil {
return
}
defer stmt.Close() _, err = stmt.Exec(todo.Id)
return
}
//DeleteAll
func DeleteAllTodos(db *sql.DB) (err error) {
stmt, err := db.Prepare("DELETE FROM todos")
if err != nil {
return
}
defer stmt.Close() _, err = stmt.Exec()
return
}
Миссия выполнена. Теперь у нас есть инструмент, который может генерировать базовый CRUD API для произвольной таблицы SQL. Мы также можем расширить или настроить лексер, парсер, генератор и шаблон для создания новых файлов. Не нравится мой API? Клонируйте репозиторий, измените шаблон и сделайте свой собственный CRUD!
Читайте также:
- Удалённые вызовы процедур в Golang
- Полиморфизм с интерфейсами в Golang
- Объектно-ориентированное программирование в Golang
Перевод статьи Eric Lang: Metaprogram in Go