CRUD是贯穿整个程序员日常开发的基本工作,占据了我们绝大多数的coding时间。
作为一名程序员,我们总是希望能有更简单的开发方式来解决重复性的工作问题。在这个小版本中,我将结合自己的工作,来给出一套自动生成代码的完整方案,供大家借鉴。
v0.5.1:Gormer-自动生成代码的初体验 项目链接 https://github.com/Junedayday/micro_web_service/tree/v0.5.1
目标 自动生成一套可用的Dao层代码,兼容原始版本。
关键技术点
Go Template技术概览
gormer工具核心思路
gormer的模板填充
目录构造 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 --- micro_web_service 项目目录 |-- gen 从idl文件夹中生成的文件,不可手动修改 |-- idl 对应idl文件夹 |-- demo 对应idl/demo服务 |-- demo.pb.go demo.proto的基础结构 |-- demo.pb.gw.go demo.proto的HTTP接口,对应gRPC-Gateway |-- demo_grpc.pb.go demo.proto的gRPC接口代码 |-- order 对应idl/order服务 |-- order.pb.go order.proto的基础结构 |-- order.pb.gw.go order.proto的HTTP接口,对应gRPC-Gateway |-- order_grpc.pb.go order.proto的gRPC接口代码 |-- idl 原始的idl定义 |-- demo 业务package定义 |-- demo.proto protobuffer的原始定义 |-- order 业务order定义 |-- order.proto protobuffer的原始定义 |-- internal 项目的内部代码,不对外暴露 |-- config 配置相关的文件夹 |-- viper.go viper的相关加载逻辑 |-- dao Data Access Object层 |-- order.go Order对象,订单表,实现model层的OrderRepository |-- order_test.go Order的单元测试 |-- gormer 新增:从pkg/gormer中生成的相关代码,不允许更改 |-- order.go 新增:gormer从orders表中获取的真实Gorm结构体 |-- model model层,定义对象的接口方法 |-- order.go OrderRepository接口,具体实现在dao层 |-- mysql MySQL连接 |-- init.go 初始化连接到MySQL的工作 |-- server 服务器的实现 |-- demo.go server中对demo这个服务的接口实现 |-- server.go server的定义,须实现对应服务的方法 |-- service service层,作为领域实现的核心部分 |-- order.go Order相关的服务,目前仅简单的CRUD |-- zlog 封装日志的文件夹 |-- zap.go zap封装的代码实现 |-- pkg 新增:开放给第三方的工具库 |-- gormer 新增:gormer工具,用于生成Gorm相关Dao层代码 |-- buf.gen.yaml buf生成代码的定义,从v1beta升到v1 |-- buf.yaml buf工具安装所需的工具,从v1beta升到v1 |-- gen.sh 更新:生成代码的脚本:buf+gormer |-- go.mod Go Module文件 |-- main.go 项目启动的main函数
1.Go Template技术概览 Go的标准库提供了Template功能,但网上的介绍很零散,我建议大家可以阅读以下两篇资料:
这里,为了方便大家阅读下面的内容,我简要概括下:
结构体中字段填充 {{ .FieldName }}
条件语句 {{if .FieldName}} // action {{ else }} // action 2 {{ end }}
循环 {{range .Member}} ... {{end}}
流水线 {{ with $x := <^>result-of-some-action<^> }} {{ $x }} {{ end }}
很多资料会很自然地将Go Template和HTML结合起来,但这只是模板的其中一个用法。
HTML的结构用模板化的方式可以减少大量重复性的代码,但这种思路是前后单不分离的,个人不太推荐。
2.gormer工具核心思路 在pkg/gormer目录下提供了一个gormer工具,用于自动生成代码,我对主流程进行简单地讲解:
解析各种关键性的参数
连接测试数据库,获取表信息
逐个处理每个表
读取数据库中的表结构
根据表结构生成对应的Go语言结构体,放在internal/gormer下
生成相关的Dao层代码,放在internal/dao下
执行go fmt格式化代码
其中最关键的是3-b与3-c,它们是生成代码的最关键步骤。我们来看一个关键性的结构体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 type StructLevel struct { TableName string Name string SmallCamelName string Columns []FieldLevel } type FieldLevel struct { FieldName string FieldType string GormName string }
3.gormer的模板填充 结合1、2,我们可以开始生成模板的部分,具体的Template代码如下,它会将StructLevel这个结构体中的字段填充到下面内容中,生成go文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 var gormerTmpl = ` // Table Level Info const {{.Name}}TableName = "{{.TableName}}" // Field Level Info type {{.Name}}Field string const ( {{range $item := .Columns}} {{$.Name}}Field{{$item.FieldName}} {{$.Name}}Field = "{{$item.GormName}}" {{end}} ) var {{$.Name}}FieldAll = []{{$.Name}}Field{ {{range $k,$item := .Columns}}"{{$item.GormName}}", {{end}}} // Kernel struct for table for one row type {{.Name}} struct { {{range $item := .Columns}} {{$item.FieldName}} {{$item.FieldType}} ` + "`" + `gorm:"column:{{$item.GormName}}"` + "`" + ` {{end}} } // Kernel struct for table operation type {{.Name}}Options struct { {{.Name}} *{{.Name}} Fields []string } // Match: case insensitive var {{$.Name}}FieldMap = map[string]string{ {{range $item := .Columns}}"{{$item.FieldName}}":"{{$item.GormName}}","{{$item.GormName}}":"{{$item.GormName}}", {{end}} } func New{{.Name}}Options(target *{{.Name}}, fields ...{{$.Name}}Field) *{{.Name}}Options{ options := &{{.Name}}Options{ {{.Name}}: target, Fields: make([]string, len(fields)), } for index, field := range fields { options.Fields[index] = string(field) } return options } func New{{.Name}}OptionsAll(target *{{.Name}}) *{{.Name}}Options{ return New{{.Name}}Options(target, {{$.Name}}FieldAll...) } func New{{.Name}}OptionsRawString(target *{{.Name}}, fields ...string) *{{.Name}}Options{ options := &{{.Name}}Options{ {{.Name}}: target, } for _, field := range fields { if f,ok := {{$.Name}}FieldMap[field];ok { options.Fields = append(options.Fields, f) } } return options } `
生成的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 package gormerimport "time" const OrderTableName = "orders" type OrderField string const ( OrderFieldId OrderField = "id" OrderFieldName OrderField = "name" OrderFieldPrice OrderField = "price" OrderFieldCreateTime OrderField = "create_time" ) var OrderFieldAll = []OrderField{"id" , "name" , "price" , "create_time" }type Order struct { Id int64 `gorm:"column:id"` Name string `gorm:"column:name"` Price float64 `gorm:"column:price"` CreateTime time.Time `gorm:"column:create_time"` } type OrderOptions struct { Order *Order Fields []string } var OrderFieldMap = map [string ]string { "Id" : "id" , "id" : "id" , "Name" : "name" , "name" : "name" , "Price" : "price" , "price" : "price" , "CreateTime" : "create_time" , "create_time" : "create_time" , } func NewOrderOptions (target *Order, fields ...OrderField) *OrderOptions { options := &OrderOptions{ Order: target, Fields: make ([]string , len (fields)), } for index, field := range fields { options.Fields[index] = string (field) } return options } func NewOrderOptionsAll (target *Order) *OrderOptions { return NewOrderOptions(target, OrderFieldAll...) } func NewOrderOptionsRawString (target *Order, fields ...string ) *OrderOptions { options := &OrderOptions{ Order: target, } for _, field := range fields { if f, ok := OrderFieldMap[field]; ok { options.Fields = append (options.Fields, f) } } return options }
dao层的代码逻辑类似,我就不重复填写了。
这里,我将代码拆分成了gormer与dao两层,主要是:
internal/gormer整个目录是不可变的、只能自动生成,对应基础的数据库表结构
internal/dao层会添加其余的文件,如定制化的sql。
至此,再将引用的相关代码简单修改,就实现了这一整块功能.
总结 本章重点介绍了Go Template在高度重复的代码模块中的应用,结合数据库实现了一个高度自动化的工具gormer。
gormer目前实现的功能比较单一,但只要有了初步自动化的思路,我们可以在后续迭代中慢慢优化,让它适应更多的场景。
Github: https://github.com/Junedayday/code_reading
Blog: http://junes.tech/
Bilibili: https://space.bilibili.com/293775192
公众号: golangcoding