Введение в проблему
В Go все устроено немного иначе: это язык строгой типизации и данные должны соответствовать типам, а количество переменных должно совпадать с количеством принятых данных. Запросы типа SELECT *
подходят далеко не всегда, а когда полей становится больше десяти, отлаживание запроса превращается в муку.
Вот пример из руководства:
// выполнение запроса
results, err := db.Query("SELECT id, name FROM tags")
if err != nil {
panic(err.Error()) // обработка ошибки
}
for results.Next() {
var id int
var name string
// тут переписываем результат в наши переменные
err = results.Scan(&id, &name)
if err != nil {
panic(err.Error())
}
}
При использовании функции Row.Scan
необходимо извлечь из базы данных два поля, т.е. нам потребуется объявить всего две переменные. Если необходимо извлечь 10 полей, нужно уже 10 переменных. А если полей более 20? На практике используются таблицы и с полусотней полей, тогда написание кода превращается в ад… И тут нам на помощь приходит кодогенерация.
Как заставить машину написать повторяющийся код?
В MySQL есть конструкция DESCRIBE, которая описывает структуру таблицы. Используя эту конструкцию, можно сгенерировать:
- объявление списка полей;
- списки полей для операторов SELECT, INSERT или UPDATE;
- список переменных для функции Scan;
- готовые типовые функции для выборки/вставки данных.
Данные из запроса DESCRIBE <имя таблицы>
заносим в структуру:
type Field struct {
name string // имя поля
ftype string // тип поля
sqltype string // тип SQL поля
fn_conv string // имя функции преобразования
}
Про последние два поля поговорим чуть попозже. Из этой структуры можно сгенерировать список переменных и список полей безо всякого анализа и построения AST (abstract syntax structure):
Для примера взята таблица отзывов review
со следующей структурой:
CREATE TABLE `review` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`model` varchar(30) DEFAULT NULL,
`url` varchar(126) DEFAULT NULL,
`rate` int(11) DEFAULT NULL,
`positive` varchar(510) DEFAULT NULL,
`negative` varchar(510) DEFAULT NULL,
`review` text,
`created` int(10) unsigned DEFAULT NULL,
`title` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
);
Когда мы выполняем команду DESCRIBE review
, то получаем следующий результат:
+----------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------+------------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| model | varchar(30) | YES | | NULL | |
| url | varchar(126) | YES | | NULL | |
| rate | int(11) | YES | | NULL | |
| positive | varchar(510) | YES | | NULL | |
| negative | varchar(510) | YES | | NULL | |
| review | text | YES | | NULL | |
| created | int(10) unsigned | YES | | NULL | |
| title | varchar(255) | YES | | NULL | |
+----------+------------------+------+-----+---------+----------------+
9 rows in set (0.00 sec)
Попытаемся прочитать этот датасет приведенным ниже кодом:
var fieldName string
var fieldType string
var fieldIsNull string
var fieldDefault string
var fieldComment string
var isKey string
sql_1 := "DESCRIBE " + tabName
rows, err := dg.Db.Query(sql_1)
errorCheck(err)
defer rows.Close()
for rows.Next() {
err = rows.Scan(&fieldName, &fieldType, &fieldIsNull, &isKey, &fieldDefault, &fieldComment)
Мы получим ошибку:
panic: sql: Scan error on column index 4, name "Default": converting NULL to string is unsupported
Ошибка преобразования возникла из-за того, что значение поля NULL
не может быть явно преобразовано в тип string
.
Как обработать NULL?
Чтобы таких ошибок не возникало, необходимо использовать следующие типы:
sql.NullString
sql.NullFloat64
sql.NullInt32 или sql.NullInt64
sql.NullBool
sql.NullTime
Все они представляют приблизительно одинаковую структуру (на примере sql.NullString
):
type NullString struct {
String string // данные строки, если не NULL, иначе пусто
Valid bool // значение true если String имеет значение NULL
}
Если поле Valid
имеет значение true
, то в поле String
находится значение, иначе NULL
. Мы будем использовать представленную ниже функцию преобразования:
func sql2String(str sql.NullString) string {
if str.Valid {
return str.String
}
return ""
}
Аналогичные функции преобразования можно создать для каждого типа. Если вернуться к структуре нашей таблицы, переменные примут тип:
var fieldIsNull sql.NullString
var fieldDefault sql.NullString
Упрощенный код анализа таблицы представлен ниже:
for rows.Next() {
err = rows.Scan(&fieldName, &fieldType, &fieldIsNull, &isKey, &fieldDefault, &fieldComment)
errorCheck(err)
type_out := "string"
sql_type := "sql.NullString"
fn_conv := "sql2String"
if strings.Index(fieldType, "int") >= 0 {
type_out = "int64"
sql_type = "sql.NullInt64"
fn_conv = "sql2Int"
} else if strings.Index(fieldType, "char") >= 0 {
type_out = "string"
sql_type = "sql.NullString"
fn_conv = "sql2String"
} else if strings.Index(fieldType, "date") >= 0 {
type_out = "string"
sql_type = "sql.NullString"
fn_conv = "sql2String"
} else if strings.Index(fieldType, "double") >= 0 {
type_out = "float"
sql_type = "sql.NullFloat64"
fn_conv = "sql2Float"
} else if strings.Index(fieldType, "text") >= 0 {
type_out = "string"
sql_type = "sql.NullString"
fn_conv = "sql2String"
}
fields = append(fields, Field{fieldName, type_out, sql_type, fn_conv} )
}
DateTime
или Date
преобразовывались в строку. При необходимости можно изменить тип на sql.Time
.Далее – дело техники.
Кодогенерация – это очень просто
Из среза полей fields
можно сгенерировать любой код. В качестве примера взят код функции, которая сохраняет все данные структуру (структура тоже сгенерирована этим кодом):
func (dg *DbGen) generate() {
var fieldList []string
fmt.Printf("\tfunc get%s(db *sql.DB, %s %s) %s {\n", strings.Title(strings.ToLower(dg.tablename)),
dg.pk, dg.type_pk, strings.Title(strings.ToLower(dg.tablename )))
fmt.Println("\t\tvar(")
fmt.Printf("\t\t\tret %s\n", strings.Title(strings.ToLower(dg.tablename )))
for _,field := range dg.fields {
if field.name == dg.pk {
continue
}
fieldList = append(fieldList, field.name)
fmt.Printf("\t\t\t%s %s\n", field.name, field.sqltype)
}
fmt.Println("\t\t)")
out_fieldList := strings.Join(fieldList, ",")
out_vars := strings.Join(fieldList, ", &")
sql_txt := fmt.Sprintf( "SELECT %s FROM %s WHERE %s=?", out_fieldList, dg.tablename, dg.pk)
fmt.Printf("\t\tsql_s := \" %s \"\n" , sql_txt)
fmt.Printf("\t\trows, err := db.Query(sql_s, %s)\n ", dg.pk)
fmt.Println("\t\terrorCheck(err)")
fmt.Println("\t\tdefer rows.Close()")
fmt.Println("\t\tfor rows.Next() {")
fmt.Printf("\t\t\terr = rows.Scan(&%s)\n", out_vars)
fmt.Println("\t\t\terrorCheck(err)")
for _,field := range dg.fields {
if field.name == dg.pk {
fmt.Printf("\t\t\tret.%s=%s\n", field.name, field.name)
} else {
fmt.Printf("\t\t\tret.%s=%s(%s)\n", field.name, field.fn_conv, field.name)
}
}
fmt.Println("\t\t}")
fmt.Println("\t\treturn ret")
fmt.Println("\t}")
}
В этом примере используется хрестоматийная функция errorCheck(err)
:
func errorCheck(err) {
if err != nil {
panic(err.Error())
}
}
Также используются функции обработки NULL
:
- sql2String
- sql2Int
- sql2Float
Заключение
Приведенный пример описывает только извлечение информации по первичному ключу, но так можно реализовать генератор для вставки и обновления записей, используя конструкцию ON DUPLICATE KEY UPDATE
. Можно развивать код в направлении составного первичного ключа или вообще не используя первичный ключ – все зависит от вашей фантазии. Всегда есть куда развиваться: например, прикрутить к коду шаблонизатор, чтоб проще генерировать шаблоны функций. Надеюсь, материал моей статьи и приведенный в ней кодогенератор кому-то сократит время разработки. Удачи!
Комментарии