起因
很多年以前,当我第一次接触到ORM的时候,我就有一点疑惑:这玩意用起来倒是方便,就是模型结构得一个字段一个字段的写,非常枯燥也非常累人,而且如果表结构修改了,比如增加、减少或者修改了一个字段,就得修改模型文件。那个时候也没有想到可以从数据库中读取到目标表的表结构数据自动生成ORM需要的模型结构。直到有一天我看到一个根据模板自动生成ORM的模型文件的代码,然后我就用golang也写了这么一个玩意。完整的代码在这里。
生成过程
准备工作
假设我在mysql中创建了一个名为dbnote的库,并且创建了一个名为msg的表,创建语句如下:
CREATE TABLE msg (
id int(11) NOT NULL AUTO_INCREMENT,
sender_id int(11) NOT NULL COMMENT '发送者',
receiver_id int(11) NOT NULL COMMENT '接收者',
content varchar(256) NOT NULL COMMENT '内容',
status tinyint(4) NOT NULL,
createtime timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);
那么对应的ORM模型则是
type Msg struct {
ID int `db:"id" json:"id"` //
SenderID int `db:"sender_id" json:"sender_id"` // 发送者
ReceiverID int `db:"receiver_id" json:"receiver_id"` // 接收者
Content string `db:"content" json:"content"` // 内容
Status int8 `db:"status" json:"status"` //
Createtime *time.Time `db:"createtime" json:"createtime"` //
}
获取表结构信息
为了生成这个struct以及相关的增删改查代码,我需要先获得这个表的结果信息,以及编写对应的模板文件用于代码生成。
通过查询语句
SELECT table_name from tables where table_schema='dbnote'
可以获取到这个库中所有的表名(当然,也可以增加过滤条件筛选目标表)。
用目标表的名称通过查询语句
SELECT COLUMN_NAME,DATA_TYPE,COLUMN_KEY,COLUMN_COMMENT from COLUMNS
where TABLE_NAME='msg' and table_schema = 'dbnote'
可以获取这个表的结构信息。
表结构的信息用这样一个struct存储
type TABLE_SCHEMA struct {
COLUMN_NAME string `db:"COLUMN_NAME" json:"column_name"`
DATA_TYPE string `db:"DATA_TYPE" json:"data_type"`
COLUMN_KEY string `db:"COLUMN_KEY" json:"column_key"`
COLUMN_COMMENT string `db:"COLUMN_COMMENT" json:"COLUMN_COMMENT"`
}
生成模型及操作函数需要的全部信息,则用这样一个struct存储
type ModelInfo struct {
BDName string
DBConnection string
TableName string
PackageName string
ModelName string
TableSchema *[]TABLE_SCHEMA
}
生成struct
初始化模板对象的代码是这样的
data, _ := ioutil.ReadFile("../modeltool/model.tpl")
render := template.Must(template.New("model").
Funcs(template.FuncMap{
"FirstCharUpper": modeltool.FirstCharUpper,
"TypeConvert": modeltool.TypeConvert,
"Tags": modeltool.Tags,
"ExportColumn": modeltool.ExportColumn,
"Join": modeltool.Join,
"MakeQuestionMarkList": modeltool.MakeQuestionMarkList,
"ColumnAndType": modeltool.ColumnAndType,
"ColumnWithPostfix": modeltool.ColumnWithPostfix,
}).
Parse(string(data)))
函数的定义参见代码。
填充好一个ModelInfo对象后,就可以生成代码文件了
if err := render.Execute(f, model); err != nil {
log.Fatal(err)
}
fmt.Println(fileName)
cmd := exec.Command("goimports", "-w", fileName)
cmd.Run()
最后用goimports添加需要import的package,并且goimports竟然连format工作都做了,简直太爽。
相关模板语法说明
- 以下语句将ModelName的值用函数FirstCharUpper处理为首字面大写然后赋值给$exportModelName变量
{{$exportModelName := .ModelName | FirstCharUpper}}
- 以下语句使用变量$exportModelName
type {{$exportModelName}} struct
- 以下语句通过函数ExportColumn将字段COLUMN_NAME的值中的下画线去掉并将单词的首字面大写,并且将id处理成ID。ExportColumn内容参见代码,可以根据实际需要调整。
{{.COLUMN_NAME | ExportColumn}}
- 以下语句循环处理ModelInfo.TableSchema中的元素
{{range .TableSchema}} {{.COLUMN_NAME | ExportColumn}} {{.DATA_TYPE | TypeConvert}} {{.COLUMN_NAME | Tags}} // {{.COLUMN_COMMENT}}
{{end}}}
- 以下语句用3个参数 .PkColumns,=?, and调用函数ColumnWithPostfix
{{ColumnWithPostfix .PkColumns "=?" " and "}}"
函数的定义是
func ColumnWithPostfix(columns []string, Postfix, sep string) string {
result := make([]string, 0, len(columns))
for _, t := range columns {
result = append(result, t+Postfix)
}
return strings.Join(result, sep)
}
- 以下语句是另外一种风格的循环
{{range $K:=.PkColumns}}{{$K}},
{{end}}
增删改查代码的自动生成没有什么需要特别说明的,具体参见代码。
一点说明
代码的目录结构是这样的
|____dbnotes
| |____dbhelper
| | |____dbhelper.go
| |____dbnote.go
| |____init.go
| |____model
| | |____mail.go
| | |____msg.go
| | |____notice.go
| |____modelgenerator
| | |____modelgenerator.go
| |____modeltool
| | |____model.tpl
| | |____modeltool.go
modelgenerator.go 编译后,运行modelgenerator可根据model.tpl将struct及操作函数生成源码文件,存放在model目录下mail.go、msg.go、notice.go的这3个源码文件就是自动生成的。3个表的创建命令在init.go的注释代码中。
数据库IP地址,用户名及密码在dbhelper.go的init()函数中,DB实例用于连接mail、msg、notice所在的库,SYSDB实例用于连接information_schema库获取表结构信息。
dbnote.go是示例代码,示范对mail、msg、notice3个表数据的增删改查。
一点问题
由于golang的基本类型都有一个默认的初始值,不存在定义后没有初始化的变量。所以对于数据库中的NULL就没有一个比较好的直接处理的方式,如果将struct中的数据类型定义为类似这样
Content *string
倒是可以接收有内容的值以及NULL,但是这样以来,对于Content的取值和赋值就没那么方便了,当然也可以用NullString。鉴于golang的基本类型都有一个默认的初始值,我个人觉得表里面还是设定为不接受NULL值比较好。