π ΠΠ·Π°ΠΈΠΌΠΎΠ΄Π΅ΠΉΡΡΠ²ΠΈΠ΅ MySQL ΠΈ Go: ΠΏΠΎΠ΄Π²ΠΎΠ΄Π½ΡΠ΅ ΠΊΠ°ΠΌΠ½ΠΈ Π°Π²ΡΠΎΠΌΠ°ΡΠΈΡΠ΅ΡΠΊΠΎΠΉ ΠΊΠΎΠ΄ΠΎΠ³Π΅Π½Π΅ΡΠ°ΡΠΈΠΈ
Большинство статей про использование MySQL в Golang повторяет примеры из официального руководства. Реальная разработка далека от простых примеров: из-за строгой типизации часто возникают проблемы. Разбираемся с их решением, если вам необходимо создать много однотипных функций. Обсудить Для пользователя всё очень просто в скриптовых языках, вроде PHP или Python: данные из базы мапятся в словарь, доступ к которому можно организовать как по имени поля, так и по его номеру в датасете. В Go все устроено немного иначе: это язык строгой типизации и данные должны соответствовать типам, а количество переменных должно совпадать с количеством принятых данных. Запросы типа Вот пример из руководства: При использовании функции В MySQL есть конструкция DESCRIBE, которая описывает структуру таблицы. Используя эту конструкцию, можно сгенерировать: Данные из запроса Про последние два поля поговорим чуть попозже. Из этой структуры можно сгенерировать список переменных и список полей безо всякого анализа и построения AST (abstract syntax structure): Для примера взята таблица отзывов Когда мы выполняем команду Попытаемся прочитать этот датасет приведенным ниже кодом: Мы получим ошибку: Ошибка преобразования возникла из-за того, что значение поля Чтобы таких ошибок не возникало, необходимо использовать следующие типы: Все они представляют приблизительно одинаковую структуру (на примере Если поле Аналогичные функции преобразования можно создать для каждого типа. Если вернуться к структуре нашей таблицы, переменные примут тип: Упрощенный код анализа таблицы представлен ниже: Обратите внимание В нашем примере бизнес логика была такова, что тип Далее – дело техники. Из среза полей В этом примере используется хрестоматийная функция Также используются функции обработки Предложенный подход сокращает время написания и отладки кода чуть ли не вдвое, а может и втрое, если вам потребуется написать API для импорта более десятка таблиц (от 10 до 20 и более полей). Более подробный работающий код можно найти в репозитории на GitHub: https://github.com/akalend/mysql-golang-generator Приведенный пример описывает только извлечение информации по первичному ключу, но так можно реализовать генератор для вставки и обновления записей, используя конструкцию Введение в проблему
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? На практике используются таблицы и с полусотней полей, тогда написание кода превращается в ад… И тут нам на помощь приходит кодогенерация.Как заставить машину написать повторяющийся код?
DESCRIBE <имя таблицы>
заносим в структуру:
type Field struct { name string // имя поля ftype string // тип поля sqltype string // тип SQL поля fn_conv string // имя функции преобразования }
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("ttvar(") fmt.Printf("tttret %sn", strings.Title(strings.ToLower(dg.tablename ))) for _,field := range dg.fields { if field.name == dg.pk { continue } fieldList = append(fieldList, field.name) fmt.Printf("ttt%s %sn", field.name, field.sqltype) } fmt.Println("tt)") 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("ttsql_s := " %s "n" , sql_txt) fmt.Printf("ttrows, err := db.Query(sql_s, %s)n ", dg.pk) fmt.Println("tterrorCheck(err)") fmt.Println("ttdefer rows.Close()") fmt.Println("ttfor rows.Next() {") fmt.Printf("ttterr = rows.Scan(&%s)n", out_vars) fmt.Println("ttterrorCheck(err)") for _,field := range dg.fields { if field.name == dg.pk { fmt.Printf("tttret.%s=%sn", field.name, field.name) } else { fmt.Printf("tttret.%s=%s(%s)n", field.name, field.fn_conv, field.name) } } fmt.Println("tt}") fmt.Println("ttreturn ret") fmt.Println("t}") }
errorCheck(err)
:
func errorCheck(err) { if err != nil { panic(err.Error()) } }
NULL
:Заключение
ON DUPLICATE KEY UPDATE
. Можно развивать код в направлении составного первичного ключа или вообще не используя первичный ключ – все зависит от вашей фантазии. Всегда есть куда развиваться: например, прикрутить к коду шаблонизатор, чтоб проще генерировать шаблоны функций. Надеюсь, материал моей статьи и приведенный в ней кодогенератор кому-то сократит время разработки. Удачи!
- 0 views
- 0 Comment
Π‘Π²Π΅ΠΆΠΈΠ΅ ΠΊΠΎΠΌΠΌΠ΅Π½ΡΠ°ΡΠΈΠΈ