《Go语言编程之旅》学习04--SQL转GoStruct

日之朝矣

我们继续使用学习单词格式转换的项目目录

这篇文章是跟着《Go编程之旅》第1.4节 ,编写一个从已存在的数据库表中,将其列与类型直接转换为go的结构体的工具,但是连接数据库时改用了GORM ,默认使用MySQL数据库

需要转换的数据结构

如果你的数据库里已经有自己创建的表,那么可以用自己的表

如果没有的话,也可使用如下SQL创建一个新表

1
2
3
4
5
6
7
8
9
10
11
12
create TABLE `blog_tag`(
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(100) DEFAULT '' COMMENT '标签名称',
`creare_on` int(10) unsigned DEFAULT '0' COMMENT'创建时间',
`created_by` varchar(100) DEFAULT '' COMMENT '创建人',
`modified_on` int(10) unsigned DEFAULT '0' COMMENT'修改时间',
`modified_by` varchar(100) DEFAULT '' COMMENT '修改人',
`deleted_on` int(10) unsigned DEFAULT'0' COMMENT'删除时间',
`is_del` tinyint(3) unsigned DEFAULT '0' COMMENT '是否删除,0为未删除、1为已删除',
`state` tinyint(3) unsigned DEFAULT '1' COMMENT'状态,0为禁用、1为启用',
PRIMARY KEY (`id`)
)ENGINE=InnODB DEFAULT CHARSET=utf8mb4 COMMENT='标签管理';

如何生成结构体

我们先来确定一下整体的思路,再来写代码

首先是获取到指定数据库中指定表中列的所有信息,然后将信息通过操作转换成结构体

获取数据肯定没问题,但重点是将数据转换为结构体,这里我们使用了text/template,文档:https://studygolang.com/pkgdoc 左侧往下翻,找到text/template

获取指定表所有列的信息

在我们的MySQL中,有information_schema这样一个数据库

information_schema 是 MySQL 数据库的一个系统数据库,用于存储关于 MySQL 数据库和表的元数据信息。information_schema 数据库包含了一系列的表,用于存储不同层次和角度的元数据信息,这些信息包括:

  1. 数据库层次的信息:如数据库名、数据库表名、列名、列数据类型、列长度、列默认值等。
  2. 存储引擎层次的信息:如存储引擎名、存储引擎版本、表行数、表大小、索引等。
  3. 用户权限层次的信息:如用户、用户密码、用户权限等。

下面是 information_schema 数据库中一些常用的表:

  1. SCHEMATA:存储所有数据库的信息,包括数据库名、默认字符集、默认排序规则等。
  2. TABLES:存储所有表的信息,包括表名、数据库名、表类型、表引擎、表行数等。
  3. COLUMNS:存储所有列的信息,包括列名、列数据类型、列长度、列默认值等。
  4. STATISTICS:存储所有索引的信息,包括索引名、索引类型、索引长度等。
  5. USER_PRIVILEGES:存储用户权限的信息,包括用户、主机名、权限等。

通过查询 information_schema 数据库中的COLUMNS表,我们便可以获取到列的各项信息

正式开始

请确定已经创建好表,并确保数据可以正常连接

连接数据库

在internal文件夹下新建文件夹sql2struct,并在sql2struct文件夹下新建文件mysql.go

我们使用的是GORM来连接数据库,因此需要安装一下

1
2
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql

首先写入以下代码

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
type DBModel struct {
DBEngine *gorm.DB
DBInfo *DBInfo
}

// 数据库信息
type DBInfo struct {
DBType string
Host string
UserName string
Password string
Charset string
}

// information_schema中COLUMNS表需要的一些列名
type TableColumn struct {
COLUMN_NAME string // 列名
DATA_TYPE string // 列的数据类型
IS_NULLABLE string // 列是否允许为NULL,值为YES或NO
COLUMN_KEY string // 列的键类型,如 PRIMARY KEY、FOREIGN KEY 等。
COLUMN_TYPE string // 列的类型和长度
COLUMN_COMMENT string // 列的注释信息
}

// 数据库中列类型与Golang之间类型对应关系
var DBTypeToStructType = map[string]string{
"int": "int32",
"tinyint": "int8",
"smallint": "int",
"mediumint": "int64",
"bigint": "int64",
"bit": "int",
"bool": "bool",
"enum": "string",
"set": "string",
"varchar": "string",
"char": "string",
"tinytext": "string",
"mediumtext": "string",
"text": "string",
"longtext": "string",
"blob": "string",
"tinyblob": "string",
"mediumblob": "string",
"longblob": "string",
"date": "time.Time",
"datetime": "time.Time",
"timestamp": "time.Time",
"time": "time.Time",
"float": "float64",
"double": "float64",
}

func NewDBModel(info *DBInfo) *DBModel {
return &DBModel{DBInfo: info}
}

接着就是链接数据库与查询的具体方法,同样是写在mysql.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
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)

func (m *DBModel) Connect() error {
var err error
s := "%s:%s@tcp(%s)/information_schema?" + "charset=%s&parseTime=True&loc=Local"
dsn := fmt.Sprintf(s, m.DBInfo.UserName, m.DBInfo.Password, m.DBInfo.Host, m.DBInfo.Charset)
m.DBEngine, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
return err
}

// GetColumns 获取列信息
func (m *DBModel) GetColumns(dbName, tableName string) (*[]TableColumn, error) {
var columns []TableColumn
err := m.DBEngine.Table("COLUMNS").Where("TABLE_SCHEMA = ? and TABLE_NAME = ?", dbName, tableName).Scan(&columns).Error
if err != nil {
return nil, err
}

return &columns, nil
}

这里专门把import放出来,确定一下,不要导错了包

模板

在internal/sql2struct下新建文件template.go,写入以下内容

1
2
3
4
5
6
7
8
const structTpl = `type {{.TableName | ToCamelCase}} struct {
{{range .Columns}}{{ $length := len .Comment}}{{ if gt $length 0}} // {{.Comment}}{{else}} // {{.Name}}{{end}}
{{ $typeLen := len .Type }}{{ if gt $typeLen 0 }}{{.Name | ToCamelCase}} {{.Type}} {{.Tag}}{{ else }}{{.Name}}{{ end }}
{{end}}}

func (model {{.TableName | ToCamelCase}}) TableName() string{
return "{{.TableName}}"
}`

稍微解释下里面的内容

双层大括号:也就是{{ }},”Action”—数据运算和控制单位—由”和“界定;在Action之外的所有文本都不做修改的拷贝到输出中。

点(DOT):会根据点(DOT)标识符进行模板变量的渲染,其参数可以为任何值,但特殊的复杂类型需进行特殊处理。例如,当为指针时,内部会在必要时自动表示为指针所指向的值。如果执行结果生成了一个函数类型的值,如结构体的函数类型字段,那么该函数不会自动调用。

这个模板生成的原型大概如下

1
2
3
4
5
6
7
8
9
10
11
type 大写驼峰的表名称 struct{
// 注释
字段名 字段类型
// 注释
字段名 字段类型
...
}

func (model 大写驼峰的表名称) TableName() string{
return "表名称"
}

接着我们来渲染模板,还是在template.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
type StructTemplate struct {
structTpl string
}

type StructColumn struct {
Name string
Type string
Tag string
Comment string
}

//
type StructTemplateDB struct {
TableName string
Columns []*StructColumn
}

func NewStructTemplate() *StructTemplate {
return &StructTemplate{structTpl: structTpl}
}


// 组合列信息
func (t *StructTemplate) AssemblyColumns(tbColumns *[]TableColumn) []*StructColumn {
tplColumns := make([]*StructColumn, 0, len(*tbColumns))
for _, column := range *tbColumns {
tag := fmt.Sprintf("`json:"+"\"%s\""+"`", column.COLUMN_NAME)
tplColumns = append(tplColumns, &StructColumn{
Name: column.COLUMN_NAME,
Type: DBTypeToStructType[column.DATA_TYPE],
Tag: tag,
Comment: column.COLUMN_COMMENT,
})
}
return tplColumns
}

// 模板生成
func (t *StructTemplate) Generate(tableName string, tplColumns []*StructColumn) error {
tpl := template.Must(template.New("sql2struct").Funcs(template.FuncMap{
"ToCamelCase": word.UnderscoreToUpperCamelCase,
}).Parse(t.structTpl)) // 初始化template对象,映射方法,解析字符串为模板

tplDB := StructTemplateDB{
TableName: tableName,
Columns: tplColumns,
}
err := tpl.Execute(os.Stdout, tplDB)
return err
}

命令编写

在cmd目录下创建sql.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
import (
"github.com/spf13/cobra"
"log"
"wordConvertTool/internal/sql2struct"
)

var (
username string
password string
host string
charset string
dbType string
dbName string
tableName string
)

var sqlCmd = &cobra.Command{
Use: "sql",
Short: "sql转换和处理",
Long: "sql转换和处理",
Run: func(cmd *cobra.Command, args []string) {},
}

func init() {}

首先就是一些变量的声明与sqlCmd的声明

然后就是将sqlCmd添加到命令中,在cmd/root.go文件的init函数中写入

1
2
3
4
func init() {
...
rootCmd.AddCommand(sqlCmd)
}

接着来书写sqlCmd的子命令,在cmd/sql.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
var sql2structCmd = &cobra.Command{
Use: "struct",
Short: "sql转换",
Long: "sql转换",
Run: func(cmd *cobra.Command, args []string) {
// 根据参数信息初始化连接数据库
dbInfo := &sql2struct.DBInfo{
DBType: dbType,
Host: host,
UserName: username,
Password: password,
Charset: charset,
}
dbModel := sql2struct.NewDBModel(dbInfo)
err := dbModel.Connect()
if err != nil {
log.Fatalf("dbModel.Connect err: %v", err)
}

// 获取指定表的列信息
columns, err := dbModel.GetColumns(dbName, tableName)
if err != nil {
log.Fatalf("dbModel.GetColumns err: %v", err)
}

// 生成模板文件
template := sql2struct.NewStructTemplate()
templateColumns := template.AssemblyColumns(columns)
err = template.Generate(tableName, templateColumns)
if err != nil {
log.Fatalf("template.Generate err : %v", err)
}
},
}

func init() {
// 将sql2structCmd添加为sqlCmd的子命令
sqlCmd.AddCommand(sql2structCmd)
// 参数设置
sql2structCmd.Flags().StringVarP(&username, "username", "", "", "请输入数据库账号")
sql2structCmd.Flags().StringVarP(&password, "password", "", "", "请输入数据库密码")
sql2structCmd.Flags().StringVarP(&host, "host", "", "127.0.0.1:3306", "请输入数据库Host")
sql2structCmd.Flags().StringVarP(&charset, "charset", "", "utf8mb4", "请输入数据库的编码")
sql2structCmd.Flags().StringVarP(&dbType, "type", "", "mysql", "请输入数据库的类型")
sql2structCmd.Flags().StringVarP(&dbName, "db", "", "", "请输入数据库名称")
sql2structCmd.Flags().StringVarP(&tableName, "table", "", "", "请输入表名")
}

到这里代码就写完了,我们测试一下

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
go run main.go sql struct --username 数据库账号 --password 数据库密码 --host 主机地址 --db 数据库名 --table 需要转换的表名

type BlogTag struct {
// 创建时间
CreareOn int32 `json:"creare_on"`
// 创建人
CreatedBy string `json:"created_by"`
// 删除时间
DeletedOn int32 `json:"deleted_on"`
// id
Id int32 `json:"id"`
// 是否删除,0为未删除、1为已删除
IsDel int8 `json:"is_del"`
// 修改人
ModifiedBy string `json:"modified_by"`
// 修改时间
ModifiedOn int32 `json:"modified_on"`
// 标签名称
Name string `json:"name"`
// 状态,0为禁用、1为启用
State int8 `json:"state"`
}

func (model BlogTag) TableName() string{
return "blog_tag"
}

如果你发现,字段tag的"都变成了",那就是template的包导入错了,应该导入text/template

总结

经过这几个命令行工具案例的学习,基本上了解了cobra的使用方式,以及这种目录结构的妙用

  • 标题: 《Go语言编程之旅》学习04--SQL转GoStruct
  • 作者: 日之朝矣
  • 创建于 : 2023-02-28 11:05:31
  • 更新于 : 2023-10-10 08:35:51
  • 链接: https://rzzy.fun/2023/02/28/go-sql2struct-tool/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
 评论