基于 Golang AST 自动生成建表 sql

写后台业务的同学经常调侃自己的工作就是围绕数据表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的问题。而这个过程跟我们平时将源代码编译为二进制可执行程序从原理上说是没有区别的。因此,这个问题本质上是一个编译问题。一个完整的编译包含以下步骤:

对于本文要完成的任务来说,主要完成词法分析、语法分析、目标代码生成即可。

工具

要完成词法分析和语法分析,我们有上古神器 LexYacc, 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;

扩展阅读

Golang中使用gorm小结

Golang中使用gorm小结

项目中使用orm的好处很多:

  1. 防止直接拼接sql语句引入sql注入漏洞
  2. 方便对modle进行统一管理
  3. 专注业务,加速开发

坏处也是显而易见的:

  1. 开发者与最终的sql语句隔了一层orm,因此可能会不慎引入烂sql
  2. 依赖于orm的成熟度,无法进行一些「复杂」的查询。当然,复杂的查询一大半都是应该从设计上规避的

留意不合法的时间值

MySQL的DATE/DATATIME类型可以对应Golang的time.Time。但是,如果DATE/DATATIME不慎插入了一个无效值,例如2016-00-00 00:00:00, 那么这条记录是无法查询出来的。会返回gorm.RecordNotFound类型错误。零值0000-00-00 00:00:00是有效值,不影响正常查询。

留意tagsql:"default:null"

gorm对各种tag的支持非常完善。但是有些行为跟直觉不太一致,需要注意。当对某字段设置tagsql:"default:null"时,你想通过update设置该字段为null就不可能了,只能通过raw sql。这是gorm设计的取向问题。

如何通过gorm设置字段为null

字段允许为null值肯定是设计存在问题。但是,往往前人埋下的坑需要你去填。gorm作者给出了两种方法,以string为例:

  1. 在golang中,声明该字段为*string
  2. 使用sql.NullString类型

推荐使用后者。

留意连接串中的loc

例如通过如下连接串打开mysql连接:

parseTime=true&loc=Local说明会解析时间,时区是机器的local时区。机器之间的时区可能不一致会设置有问题,这导致从相同库的不同实例查询出来的结果可能解析以后就不一样。因此推荐将loc统一设置为一个时区,如parseTime=true&loc=America%2FChicago