写后台业务的同学经常调侃自己的工作就是围绕数据表CRUD. 虽然实际工作并不会如此简单,但是日常中的确有很多类似的重复、缺乏创造性的工作。而这种工作上是可以在一定程度上自动化的。为了提供业务研发人员开发效率,前段时间我们开发了一个后端开发工作流工具,主要提供以下功能:
- 生成服务器API基础代码以及Swagger文档注释 (只支持gin框架)
- 生成服务器API客户端代码
- go struct 批量添加 tag
- 生成 gorm model struct
- model struct 生成 sql
因为这些功能跟我们内部的公共库有一定耦合,因此整个工具可能无法开源出来。这里,我们以model struct 生成 sql
功能为例,聊聊我们在做这个工具的思路和使用到的工具。
任务
这里以我们在项目中使用的jinzhu同学的gorm作为orm库。如果你在使用golang的其他orm lib,实现方式应该大同小异。
我们的任务是从下面的这个model struct定义:
生成 mysql 建表语句(文件):
思路
从model struct
生成 sql
是一个将语言A翻译为语言B
的问题。而这个过程跟我们平时将源代码编译为二进制可执行程序从原理上说是没有区别的。因此,这个问题本质上是一个编译问题。一个完整的编译包含以下步骤:
对于本文要完成的任务来说,主要完成词法分析、语法分析、目标代码生成即可。
工具
要完成词法分析和语法分析,我们有上古神器 Lex 和 Yacc, Yet Another Compiler-Compiler. 而我们只是想完成一个建表文件的生成任务而已,使用者两个工具有时候要自定义语法,又是要自己写lex和yacc文件,累觉不爱……
Golang 有很多其他语言羡慕不来的工具,例如 go pprof, go list, go vet 等。在语言元编程方面,go 1.4实现了自举;而编译时候涉及到的词法分析和语法分析很早前就放在了标准库 go/ast
中。AST
是abstract syntax tree的缩写,直译过来是抽象语法树。通过AST,我们可以编写一个go程序解析go源代码。具体到本文要完成的任务,要编写一个这样的程序解析定义数据表的model struct, 然后生成sql建表语句。
实现
具体到我们的任务实现,可以拆分为如下几个步骤:
- 加载源代码,生成 AST Tree
- 获取和解析 model struct AST
- 根据struct field name/tag 生成create_definition, table_options
完整代码实现,可以移步github gorm2sql.
实现效果:
user_email.go
:
type UserBase struct {
UserId string `sql:"index:idx_ub"`
Ip string `sql:"unique_index:uniq_ip"`
}
type UserEmail struct {
Id int64 `gorm:"primary_key"`
UserBase
Email string
Sex bool
Age int
Score float64
UpdateTime time.Time `sql:"default:CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"`
CreateTime time.Time `sql:"default:CURRENT_TIMESTAMP"`
}
gorm2sql sql -f user_email.go -s UserEmail -o db.sql
Result:
CREATE TABLE `user_email`
(
`id` bigint AUTO_INCREMENT NOT NULL ,
`user_id` varchar(128) NOT NULL ,
`ip` varchar(128) NOT NULL ,
`email` varchar(128) NOT NULL ,
`sex` boolean NOT NULL ,
`age` int NOT NULL ,
`score` double NOT NULL ,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_ub (`user_id`),
UNIQUE INDEX uniq_ip (`ip`),
PRIMARY KEY (`id`)
) engine=innodb DEFAULT charset=utf8mb4;