Share This
Связаться со мной
Крути в низ
Categories
//🛠 Взаимодействие MySQL и Go: подводные камни автоматической кодогенерации

🛠 Взаимодействие MySQL и Go: подводные камни автоматической кодогенерации

Большинство статей про использование MySQL в Golang повторяет примеры из официального руководства. Реальная разработка далека от простых примеров: из-за строгой типизации часто возникают проблемы. Разбираемся с их решением, если вам необходимо создать много однотипных функций. Обсудить

vzaimodejstvie mysql i go podvodnye kamni avtomaticheskoj kodogeneracii 4e19e4f - 🛠 Взаимодействие MySQL и Go: подводные камни автоматической кодогенерации

Введение в проблему

Для пользователя всё очень просто в скриптовых языках, вроде PHP или Python: данные из базы мапятся в словарь, доступ к которому можно организовать как по имени поля, так и по его номеру в датасете.

В 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("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:

  • sql2String
  • sql2Int
  • sql2Float

Заключение

Предложенный подход сокращает время написания и отладки кода чуть ли не вдвое, а может и втрое, если вам потребуется написать API для импорта более десятка таблиц (от 10 до 20 и более полей). Более подробный работающий код можно найти в репозитории на GitHub: https://github.com/akalend/mysql-golang-generator

Приведенный пример описывает только извлечение информации по первичному ключу, но так можно реализовать генератор для вставки и обновления записей, используя конструкцию ON DUPLICATE KEY UPDATE. Можно развивать код в направлении составного первичного ключа или вообще не используя первичный ключ – все зависит от вашей фантазии. Всегда есть куда развиваться: например, прикрутить к коду шаблонизатор, чтоб проще генерировать шаблоны функций. Надеюсь, материал моей статьи и приведенный в ней кодогенератор кому-то сократит время разработки. Удачи!

  • 0 views
  • 0 Comment

Leave a Reply

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.

Свежие комментарии

    Рубрики

    About Author 01.

    Roman Spiridonov
    Roman Spiridonov

    Привет ! Мне 38 лет, я работаю в области информационных технологий более 4 лет. Тут собрано самое интересное.

    Categories 05.

    © Speccy 2020 / All rights reserved

    Связаться со мной
    Close