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.

    blank
    Roman Spiridonov

    Моя специальность - Back-end Developer, Software Engineer Python. Мне 39 лет, я работаю в области информационных технологий более 5 лет. Опыт программирования на Python более 3 лет. На Django более 2 лет.

    Categories 05.

    © Speccy 2022 / All rights reserved

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