
1.4 SQL语句到结构体的转换
在项目初始化或添加新数据表时,我们常常需要添加模型(Model)结构,这时就会遇到一个新问题,即需要写Model结构体。如果是手写Model结构体,则太低效了,因此本节我们实现数据库表到Go结构体的转换。
从本节起,我们将用到 MySQL 数据库,笔者的 MySQL 数据库版本为 5.7。读者需了解MySQL数据库的一些基本知识,并注意MySQL的安装,以保证对本书后续内容的正常学习。
1.4.1 需要转换的数据结构
需要转换的数据结构其实是MySQL数据库中的表结构,本质上是SQL语句,例子如下:

我们希望最终能得到表中的所有列信息,并根据所有列信息生成所期望的结构体。
1.4.2 生成结构体
是不是要去指定的数据库里先遍历一遍所有的表,然后一个个分析表的SQL语句,接着再进行转换呢?其实不需要那么麻烦,因为在 MySQL 数据库中,有一个神奇的“东西”,那就是 information_schema 数据库。它可以非常便捷地帮助我们实现这个功能,下面就来了解一下其所提供的功能。
1.确定数据源
(1)information_schema数据库。
information_schema数据库在MySQL中提供了对数据库元数据的访问,可以获得MySQL服务器自身相关的信息,如数据库、表名称、列数据类型、访问权限等。
● SCHEMATA:提供有关数据库的信息,可与SHOW DATABASES语句等效。
● COLUMNS:提供有关表中列的信息,可与SHOW COLUMNS语句等效。
● TABLES:提供有关数据库中表的信息,可与SHOW FULL TABLES语句等效。
● STATISTICS:提供有关表索引的信息,可与SHOW INDEX语句等效。
● USER_PRIVILEGES:提供有关全局权限的信息,从mysql.user系统表中获取值。
● CHARACTER_SETS:提供数据库可用字符集的信息,可与SHOW CHARACTER SET语句等效。
(2)COLUMNS表。
COLUMNS表提供了整个数据库中列的信息,其包含以下几个常用字段。
● TABLE_NAME:列所属的表名称。
● COLUMN_NAME:列的名称。
● COLUMN_DEFAULT:列的默认值。
● IS_NULLABLE:列是否允许为NULL,值为YES或NO。
● DATA_TYPE:列的数据类型,仅包含类型信息。
● COLUMN_TYPE:列的数据类型,包含类型名称和可能的其他信息。例如,精度、长度、是否无符号等。
● COLUMN_KEY:列是否被索引。
● COLUMN_COMMENT:列的注释信息。
在Go语言中,一个结构体的最小包含为字段名和字段类型。实际上,可以看到COLUMNS表中基本都具备了,它能够直接帮助程序进行表到结构体的映射转换。
2.转换和生成
在确定了获取表结构的列信息数据源后,接下来需要思考的问题就是如何转换并生成 Go结构体。在把表转为Go结构体时,数据类型比较简单,一般来说,不存在过多层级Go结构体的问题,也就是说,不会出现如下所示的多层级嵌套的情况:

因此,在面对这类数据类型比较简单的基本转换时,是不必大费周章的做递归循环嵌套判断的,可以直接使用Go的template实现相关逻辑,非常的方便。
(1)template.
template是Go语言的文本模板引擎,它提供了两个标准库。这两个标准库使用了同样的接口,但功能略有不同,具体如下:
● text/template:基于模板输出文本内容。
● html/template:基于模板输出安全的HTML格式的内容,可以理解为其进行了转义,以避免受某些注入攻击。
下面我们对template进行一个快速入门,以便更好地理解和使用它,示例代码如下:

输出结果如下:

在上述代码中,首先调用标准库text/template中的New方法,其根据我们给定的名称标识创建了一个全新的模板对象。接下来调用Parse方法,将常量templateText(预定义的待解析模板)解析为当前文本模板的主体内容。最后调用 Execute 方法,进行模板渲染。简单来说,就是将传入的data动态参数渲染到对应的模板标识位上。因为我们将Execute方法的io.Writer指定到了os.Stdout中,所以其最终输出到标准控制台中。
(2)template模板定义。
前文提到了预定义的待解析模板和模板的标识位,下面对它们进行具体讲解。
● 双层大括号:也就是{{和}}标识符,在template中,所有的动作(Actions)、数据评估(Data Evaluations)、控制流转都需要用标识符双层大括号包裹,其余的模板内容均全部原样输出。
● 点(DOT):会根据点(DOT)标识符进行模板变量的渲染,其参数可以为任何值,但特殊的复杂类型需进行特殊处理。例如,当为指针时,内部会在必要时自动表示为指针所指向的值。如果执行结果生成了一个函数类型的值,如结构体的函数类型字段,那么该函数不会自动调用。
● 函数调用:在前面的代码中,通过FuncMap方法注册了名title的自定义函数。在模板渲染中一共用了两类处理方法,即使用{{title.Name1}}和管道符(|)对.Name3 进行处理。在template中,会把管道符前面的运算结果作为参数传递给管道符后面的函数,最终,命令的输出结果就是这个管道的运算结果。
1.4.3 表到结构体的转换
在项目的internal下新建sql2struct目录,用于存储表转为结构体的工具库代码,目录结构如下:

1.连接MySQL数据库
想要获取表中列的信息,即需要访问information_schema数据库中的COLUMNS表,在程序中进行连接、查询、数据组装等处理,那么就需要在项目的 sql2struct 目录下新建 mysql.go文件,写入如下声明代码:


上述代码声明了初始化方法 NewDBModel 和三个核心的结构体对象。DBModel 是整个数据库连接的核心对象,结构体DBInfo用于存储连接MySQL的一些基本信息,TableColumn用于存储COLUMNS表中我们需要的一些字段,其字段含义在1.4.2节曾介绍过。
下面编写连接MySQL数据库的具体方法,在mysql.go文件中新增如下代码:

在连接MySQL数据库时使用的是标准库database/sql的Open方法,第一个参数为驱动名称(如mysql),第二个参数为驱动连接数据库的连接信息。需要注意的是,在程序中必须导入github.com/go-sql-driver/mysql进行MySQL驱动程序的初始化,否则会出现错误。
2.获取表中列的信息
在编写完连接MySQL数据库的连接方法后,由于需要针对COLUMNS表进行查询和数据组装,所以继续在mysql.go文件中新增如下代码:


3.表字段类型映射
由于DataType字段的类型与Go 结构体中的类型不是完全一致的(如varchar、longtext、TimeStamp 等),因此需要做一层简单的类型转换。这里用的是最简单的枚举,然后再用 map作映射获取。继续在mysql.go中声明全局变量,代码如下:

这里并没有把所有类型的代码段都展示出来,更多代码可到线上示例代码处获取。
4.模板对象声明
在编写完数据库相关的查询、映射的方法后,接下来就需要把得到的列信息按照特定的规则转为Go结构体。这里采用的是模板渲染的方案,在项目的sql2struct目录下新建template.go文件,写入如下预定义模板:

在上述预定义模板中,其基本结构由一个Go结构体(type struct)和其所属的TableName方法组成,生成后的原型大致如下:

在定义完预定义模板后,我们需要对后续模板渲染对象进行声明,继续在template.go文件中写入如下代码:


上述代码一共定义了三个结构体,分别是承担主轴的 StructTemplate、StructColumn 和StructTemplateDB。StructColumn 用来存储转换后的 Go 结构体中的所有字段信息,StructTemplateDB用来存储最终用于渲染的模板对象信息。
5.模板渲染
前文代码中的StructColumn和StructTemplateDB结构体,实际上对应的是不同阶段的模板对象信息。下面围绕这两者编写相关方法。打开template.go文件,写入如下代码:

在上述代码中,对通过查询COLUMNS表所组装得到的tbColumns进行进一步的分解和转换。例如,数据库类型到Go结构体的转换和对JSON Tag的处理,都在这一层完成了。
在处理完模板对象后,接下来对模块渲染的自定义函数和模板对象进行处理,继续写入如下代码:


在上述代码中,首先声明了一个名为 sql2struct 的新模板对象,接着定义了自定义函数ToCamelCase,并与word.UnderscoreToUpperCamelCase方法进行了绑定,最后组装符合预定义模板的模板对象,再调用Execute方法进行渲染。
1.4.4 初始化子命令
下面将其集成到我们的子命令中。打开项目的cmd目录,新建sql.go文件。
首先声明7个cmd全局变量,用于接收外部的命令行参数,新增代码如下:

分别用于指定数据库的账号、密码、HOST、编码、数据库类型、数据库名称及表名称。接下来定义其对应的子命令,继续在sql.go文件中写入如下代码:


在上述代码中,声明了sql子命令和sql子命令对应的子命令struct,并在sql2structCmd中完成了对数据库的查询、模板对象的组装、渲染等动作的流转,相当于一个控制器。这部分逻辑也可以抽离为独立的一层进行调用,根据具体情况编写即可。
最后进行默认的cmd初始化动作和命令行参数的绑定,继续在sql.go文件中写入如下代码:

在完成sql2structCmd子命令的注册后,必须将sqlCmd注册到root.go中,代码如下:

1.4.5 验证
在编写完程序后,进行转换的手动验证,代码如下:

这里用的是1.4.1节提到的示例表结构,读者可以根据实际数据库中存在的表进行转换,若遇到工具中并没有完美支持的情况,也可以自行尝试改写。
1.4.6 小结
本节首先访问了MySQL数据库中的information_schema数据库,读取了COLUMNS表中对应的所需表的列信息,并基于标准库database/sql和text/template实现了表列信息到Go语言结构体的转换。
此后再新建表时,只需调用该工具,直接转换就可以获取对应的模型结构了,非常方便。