1. 目录
2. 背景
最近在工作中会有依据 mysql 表在 go 中编写一个对应的构造体这样的 coding,尽管数据表并不是简单,字段不是很多,代码写起来也比拟快,为了疾速的实现工作我一开始就是依照数据表的列一个接着一个的来写。但我是个懒人,反复的工作心愿能够通过代码帮我实现,因为前面也有相似的工作,如果我有对应的代码生成工具会不便很多,并且用本人做进去的工具内心中或多或少会有一些成就感。所以我心生一个想法,为什么我不搞一个简略的工具,来依据表的构造生成构造体呢?所以我就钻研了一下,看了一些材料和,包含 mysql 的 information_schame 数据库,sqlx,go 规范库里的 text/template 等内容,特此写下文章分享给读者敌人们,心愿读者敌人们有所播种。话不多说,让咱们开始本次的探索之旅。
3. 怎么找到 mysql 的表构造信息
在装置 mysql 的时候,会发现除了本人创立的数据库之外,还会有一些别的数据库是默认给你创立好的。咱们能够登陆 mysql 应用上面语句查看。
mysql> SHOW DATABASES;
+--------------------+
| Database |
+--------------------+
| elliot_test |
| information_schema |
| mysql |
| performance_schema |
| sys |
+--------------------+
5 rows in set (0.01 sec)
大家能够看下我本地的 mysql,其中 elliot_test 是我创立的数据库,其余的 information_schema, mysql, performance_schema, sys(5.7 以上的叫 sys,5.6 的自带的是 test), 这些是 mysql 自带的。那么这些数据库有什么用呢,记录的都是什么信息呢?
- information_schema:保留了 MySQl 服务所有数据库的信息。具体 MySQL 服务有多少个数据库,各个数据库有哪些表,各个表中的字段是什么数据类型,各个表中有哪些索引,各个数据库要什么权限能力拜访。
- mysql:保留 MySQL 的权限、参数、对象和状态信息。如哪些 user 能够拜访这个数据,DB 的参数。
- performance_schema:次要用于收集数据库服务器性能参数,提供过程期待的详细信息,保留历史的事件汇总信息,为提供 MySQL 服务器性能做出具体的判断。
- test:5.6 自带,没有什么货色。
- sys:Sys 库所有的数据源来自:performance_schema。指标是把 performance_schema 的把复杂度升高。
在理解了 mysql 自带数据库的性能之后,这时候咱们就晓得了要查看 mysql 的表构造信息其实咱们只须要在 information_schema 这外面找就好了,因为这里蕴含了所有数据库的信息,包含有哪些表,什么表有什么字段,这正是咱们须要的。因为 information_schema 外面表比拟多,这里就不作展现并且一一介绍了,感兴趣的读者敌人们能够登陆 mysql 应用 use information_schema; 命令切换数据库,而后应用 show tables;命令查看这上面有什么表。这里就介绍几个比拟重要的,或者说咱们可能会用到的。
- SCHEMATA 表:提供了以后 mysql 实例中所有数据库的信息。是 show databases 的后果取之此表。
- TABLES 表:提供了对于数据库中的表的信息(包含视图)。具体表述了某个表属于哪个 schema,表类型,表引擎,创立工夫等信息。是 show tables from schemaname 的后果取之此表。
- COLUMNS 表:提供了表中的列信息。具体表述了某张表的所有列以及每个列的信息。是 show columns from schemaname.tablename 的后果取之此表。
咱们这里关注的是 COLUMNS 这张表,这张表记录的是每张表的列信息,咱们应用 desc 命令看下这张表都有些什么。
能够看到外面记录的货色还是很多的,如果聚焦于咱们的需要:依据表构造构建构造体。那咱们的关注点只须要晓得他的列名(COLUNM_NAME), 以及对应的数据类型(DATA_TYPE)就能够了。所以咱们很容易写出上面这条 sql 查问到咱们须要的信息。
不过咱们也能够用下图这种形式去查看。
取得了这些信息之后,就能够开始咱们的编码工作了。
4. 技术实现
在这一章节会探讨如何实现这个需要,会探讨如何简略实现一个 mysql 的 client,也就是一个简略的 mysql 驱动,当然并不是说在这里我要实现这个货色,只是探讨一下如果要写一个的话大略须要怎么做。另外会讲到实现这个需要用到的一些次要技术,包含 sqlx,go 规范库的 text/template,还有实现这个需要的外围逻辑解说。
4.1 以何种形式与数据库交互
其实这里个别是没有探讨的必要的,抉择一个开源的库去和执行下面提到的获取数据表信息的 sql 语句就好了。不过如果我想把这个工具当作一个开源的我的项目去做,可能会思考为了轻量化而尽量减少这个我的项目的依赖,以及开源的 mysql 驱动库对于这个我的项目来说会提供一些咱们自身并不需要的性能。不过在这里我并不打算要本人入手实现一个简略的查问语句解决的工具,因为想要疾速的实现这个小工具,如果破费大量的工夫在实现别的货色下面,那么我工作就失去了焦点,这并不是我想要的。不过之前看过不少 go-mysql 这个开源库的源码,对数据库驱动库是怎么实现的还是有肯定理解的,实际上实现起来也并不是很难。这里的话能够和读者敌人们分享一下如果我要手动实现的话,我要怎么做。
这里思路能够简略的概括一下,就是我要吧本人模拟成 mysql 的 client 端,只有咱们遵循 mysql 的通信协定就能够了。就如同 go 的构造与其实现类,实现类只有实现了接口的办法就能够看作是这个接口的实现类。而咱们只须要遵循 mysqld 的 client 端的一些通信协定,mysql 天然也会把咱们看成是一个 client 端,当初支流的 CDC 组件个别的做法就是把本人伪装成 mysql 的从节点去获取 mysql 的数据变更,其实也是一样的情理。那么要把本人变成 mysql 的 client 端咱们都须要遵循哪些协定呢?
首先在 TCP 层面的客户端与服务端三次握手建设了 TCP 连贯之后,mysql 的服务端会被动发送一个握手初始化的包给客户端,这里的次要内容是服务器的一些信息,通知客户端要遵循什么什么协定,打个比方说如果 mysql 的 binlog 协定就有好几个版本,mysql 5.15 之前是是 v0,5.6 之前是 v1,5.6 之后是 v2 版本,握手的作用就是通知客户端一些服务端的信息,前面的通信要依照一些标准去进行。在握手初始化之后要进行认证登陆的过程,这里客户端会把账户和明码给服务端,让服务端去验证,最初服务器把认证后果发送回来,要是胜利了,前面就能够开始执行咱们的发送的 sql 语句了。在这个过程中波及到的协定内容和格局我贴在下方参考资料中(connect_phase packet)
在执行 sql 语句这一段,咱们这个需要其实只是执行一条 query 语句,所以依照 COM_QUERY 协定从客户端构建一个数据包,而后等服务端返回的时候依照 ResultSet 协定去解析返回的内容就好了。
这一系列流程我是在 go-mysql 这个开源库上看到的,之前学习过这个库,具体看了不少代码的实现,我把代码的地位放在参考资料上,感兴趣的敌人能够看看~看的过程中如果有不太明确的中央能够留言分割我,咱们能够交换交换。
这里就不打算把所有的协定的格局都开展讲了,不过我会在下方参考资料那边贴上 mysql 的官网文档,外面会有对协定内容具体的介绍。这里我想要说的是一个实现的大抵流程。能够看到其实这里并不是说很难,不过实现起来的确也是须要工夫的。既然讲到这里了,说句题外话,既然实现了协定就能够被当作是 mysql 的客户端,那么我可不可以实现一些协定,被当作一个 mysql 的服务端呢?当然也是能够的,之前我也做过相似的尝试。所以说很多时候网络的确不是百分百平安的,对方只须要满足一些行为就能够获取信赖,然而有时候咱们并不能齐全确认对方是不是真的值得信赖。
4.2 代码实现
其实实现的流程比较简单,没用多长时间就写完了,这里次要记录一些应用到的技术和一些值得注意或者值得分享的中央。
4.2.1 与数据库的交互 –sqlx
我次要用了 sqlx 这个库和数据库做交互,感觉这个库写的还不错,应用起来也很不便,github 地址贴在这里:https://github.com/jmoiron/sqlx,感兴趣的敌人能够看看 readme 外面对于它应用办法的具体介绍,这里略微贴一段代码吧。
type Person struct {
FirstName string `db:"first_name"`
LastName string `db:"last_name"`
Email string
}
// Query the database, storing results in a []Person (wrapped in []interface{})
people := []Person{}
db.Select(&people, "SELECT * FROM person ORDER BY first_name ASC")
不过说切实的,就我集体而言,其实感觉 orm 框架的用法都大同小异,用哪个其实不是问题的要害。
4.2.2 如何生成代码 — go text/template
一开始是想着用字符串拼接去做的,然而感觉这样子写进去的代码有点点丑。其实丑是一方面,另外一方面是,因为不同 orm 框架生成的 struct 可能是不一样的,最显著的就是 tag 外面的标签是不一样的,如果后续要拓展的话,难道判断生成什么 orm 框架上面的 struct 这样的逻辑要嵌入到字符串拼接外面吗?这显然不是一个好的方法。所以我想到了用 template 模版去解析的形式去做。
go 规范库中提供了 text/template 这个包,实现了数据驱动的用于生成文本输入的模板,其实这个很像前端的 mvvm 那一套,咱们定义好模版,而后传入参数,他就会解析模版,将咱们传入的参数替换到模版对应的地位,从而生成咱们想要的文本。在模版外面还能够写逻辑呢,比方简略的逻辑判断,循环遍历之类的。话不多说,这里展现一个简略的 demo。对于 text/template 具体的用法和解说文章,我贴到参考资料中,感兴趣的敌人能够看看。
package main
import(
"os"
"text/template"
)
type Inventory struct {
Material string
Count uint
}
func main() {sweaters := Inventory{"wool", 17}
tmpl, err := template.New("test").Parse("{{.Count}} of {{.Material}}\n")//{{.Count}}获取的是 struct 对象中的 Count 字段的值
if err != nil {panic(err) }
err = tmpl.Execute(os.Stdout, sweaters)// 返回 17 of wool
if err != nil {panic(err) }
}
这个例子是将 Inventory 的对象传进模版中做解析,其中 {{.Count}},{{.Material}} 就是获取这个对象的属性,而后替换。
好了,讲到这里实现这个需要的所有技术,所有基础知识咱们都晓得了,上面咱们看看外围逻辑。
4.2.3 外围逻辑
外围的流程如下:
- 获取列数据。包含列的名字以及数据类型。
- 数据类型转化,因为 mysql 和 go 的类型其实不大一样的,如果咱们要生成 go 的 struct 就须要做一个 mysql 数据类型和 go 数据类型的转化,我这里的做法比拟毛糙,写了一个 map 配置,key 是 mysql 数据类型,value 是 go 数据类型。间接拿就完事了。然而其实这样并不是很正当,比方 mysql 的 TINYINT 类型对应的是 go 的 int8,然而在 go 中如果用 int32,int64,去示意可不可以呢?其实是能够的。这里后续能够作为一个优化点,或者这个配置的能力向用户凋谢更好。
- 执行模版引擎。会提前写好一个模版文件,而后用拿到的数据去解析。不过这个模版文件是可配置的,这样能够提供比拟灵便的形式去生成本人想要的代码。比方我当初写的这个模版文件比拟淳厚,除了生成 struct 之外就什么都没有了,然而在一些环境下,可能用户会想着我生成某某个接口的实现类或者有一些特定的正文,那他能够自定义模版文件去生成,不过解析模版文件须要对应的数据,这里数据的构造体还是在我的掌控之内。所以前面能够思考把这个数据也凋谢进来,这样就比拟完满了。
- 格式化生成的文件。因为模版生成进去的文件不是特地难看。所以我这里手动执行了一些 go fmt 去格式化代码,看起来会难受很多。
func (g *Generator) Gen(config *GenInfo) (isSuccess bool, err error) {if err := checkGenInfo(config); err != nil {return false, err}
tableInfos, err := g.executeQuery(config.Schema, config.Table)
if err != nil {return false, err}
templateMetaDatas, err := convertTableInfoToMeta(tableInfos)
templateData := &TemplateData{
PackageName: config.PackageName,
StructName: config.StructName,
Meta: templateMetaDatas,
}
var genPath string
if config.ExportFolder == "" {genPath = config.FileName} else {genPath = fmt.Sprintf("%s/%s", config.ExportFolder, config.FileName)
}
isSuccess, err = genCodeByTemplate(genPath, config.TemplatePath, templateData)
if err == nil && isSuccess {_, _ = exec.Command("go", "fmt", genPath).Output()}
return isSuccess, err
}
template 文件:
package {{.PackageName}}
type {{.StructName}} struct {{{- range $i, $v := .Meta}}
{{$v.CamelName}} {{$v.DataTypeInGo}}
{{- end}}
}
测试:
func TestGenerator_Gen(t *testing.T) {
config := &Config{
Host: "127.0.0.1",
Port: 3306,
Username: "root",
Password: "", // 我才不通知你我的明码呢。}
g, err := NewGenerator(config)
if err != nil {t.Errorf("have err during NewGenerator, err is %s", err)
return
}
genInfo := &GenInfo{
Schema: "elliot_test",
Table: "test_table",
ExportFolder: "",
TemplatePath: "struct_gen_test_template",
FileName: "test_gen.go",
PackageName: "table_gen",
StructName: "TestGenStruct",
}
isSuccess, err := g.Gen(genInfo)
if err != nil {t.Errorf("have err during Gen file, err is %s", err)
return
}
t.Logf("does it gen file successully? %v", isSuccess)
}
后果:
package table_gen
type TestGenStruct struct {
Name string
Age int
}
完满,这就达到了咱们最后的幻想。这里就不对细节上的货色做过多的介绍了,感觉该讲的货色大部分都讲了。具体的 code 我曾经开源到我的 github 上了。地址:https://github.com/elliotchen…。感兴趣的小伙伴能够看看。有问题或者发现代码中的谬误,能够留言分割我,相互交流学习。
5. 总结
做这个货色是一时衰亡,做完之后还是有很多中央感到有余和可优化,前面有工夫能够缓缓优化一下。就我集体而言有工夫的话还是比拟喜爱倒腾一些小东西。一方面做成一个货色的过程中摸索求知这个过程是很高兴的,另一方面做成之后的喜悦就像攀登了一座座小山峰最终达到指标,那霎时扑面而来的高兴是人生少有的。好了,明天这个摸索的故事就讲到这里了,感激各位读者敌人们赏脸读到开端处 hhh。
6. 参考资料
- mysql information_schema 详解:https://zhuanlan.zhihu.com/p/…
- mysql connection_phase packet :https://dev.mysql.com/doc/int…
- mysql COM_QUERY packet: https://dev.mysql.com/doc/int…
- mysql COM_QUERY Response :https://dev.mysql.com/doc/int…
- go text/template :https://www.cnblogs.com/wangh…
- go-mysql client 模块代码:https://github.com/go-mysql-o…