Go语言SQL语句到结构体的转换命令行工具

Go语言SQL语句到结构体的转换命令行工具

学习:SQL 语句到结构体的转换 | Go 语言编程之旅 (eddycjy.com)

目标:SQL表转换为Go语言结构体

可以在线体验这个过程:SQL生成GO语言结构体 – 支持批量处理 (tl.beer)

MySQL数据库中的表结构,本质上是SQL语句。

CREATE TABLE `USER`(
    `id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT "primary key",
    `ip_address` INT  NOT NULL DEFAULT 0 COMMENT "ip_address",
    `nickname`    VARCHAR(128) NOT NULL DEFAULT "" COMMENT "user note",
    `description` VARCHAR(256) NOT NULL DEFAULT "" COMMENT "user description",
    `creator_email` VARCHAR(64) NOT NULL DEFAULT "" COMMENT "creator email",
    `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT "create time",
    `deleted_at` TIMESTAMP NULL DEFAULT NULL COMMENT "delete time",
    PRIMARY KEY(`id`)
)ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT="user table";

大概目标就是要把此Table转换为Go语言结构体语句,

type USER struct {
	Id           uint      `comment:"primary key"`
	IpAddress    int       `comment:"ip_address"`
	Nickname     string    `comment:"user note"`
	Description  string    `comment:"user description"`
	CreatorEmail string    `comment:"creator email"`
	CreatedAt    time.Time `comment:"create time"`
	DeletedAt    time.Time `comment:"delete time"`
}

结构体变量后面的是`结构体标签 `:Go系列:结构体标签 – 掘金 (juejin.cn)

数据源:MySQL中的information_schema库中有个COLUMNS表,里面记录了mysql所有库中所有表的字段信息。

text/template简要应用

package main

import (
	"os"
	"strings"
	"text/template"
)

const templateText = `
Output 0: {{title .Name1}}
Output 1: {{title .Name2}}
Output 2: {{.Name3 | title}}
`

func main1() {
	funcMap := template.FuncMap{"title": strings.Title} // type FuncMap map[string]any FuncMap类型定义了函数名字符串到函数的映射
	tpl := template.New("go-programing-tour")           //创建一个名为"..."的模板。
	tpl, _ = tpl.Funcs(funcMap).Parse(templateText)
	data := map[string]string{
		"Name1": "go",
		"Name2": "programing",
		"Name3": "tour",
	}
	_ = tpl.Execute(os.Stdout, data)
}
  • 模板内内嵌的语法支持,全部需要加{{ }}来标记;

  • {{.}}表示当前作用域内的当前对象 data (_ = tpl.Execute(os.Stdout, data)),Execute()方法执行的时候,会将{{.Name1}}替换成data.Name1

  • 在模板中调用函数,{{函数名 传入参数}},因为在模版中,传入参数一般都是string类型;

  • template.FuncMap创建自定义函数,在模板作用域中生效:

    • funcMap := template.FuncMap{"title": strings.Title}作用域中的title,就意味着调用此函数,把后面的作为参数传入函数中;
  • {{.Name3 | title}} 在模板中,会把管道符前面的运算结果作为参数传递给管道符后面的函数;

  • more:[译]Golang template 小抄 (colobu.com)

database/sql简要应用

用Go语言链接MySQL数据库,并查询下表。

image-20221122155923274

package main

import (
	"database/sql"
	"fmt"

	_ "github.com/go-sql-driver/mysql" //导入包但不使用,init()
)

func main() {
	// DB不是连接,并且只有当需要使用时才会创建连接;
	DB, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/DBName")
	if err != nil {
		fmt.Printf("DB:%v invalid,err:%v
", DB, err)
		return
	}
	// defer DB.Close()
	// It is rare to Close a DB, as the DB handle is meant to be long-lived and shared between many goroutines.
	// Closing a DB is useful if you don"t plan to use the database again. It does all the cleanup that would be done at program termination but allows the program to continue to run.

	// 如果想立即验证连接,需要用Ping()方法;
	if err = DB.Ping(); err != nil {
		fmt.Println("open database fail")
		return
	}
	fmt.Println("connnect success")

	// 读取DB
	var (
		id         int
		areacode   string
		cityname   string
		citypinyin string
	)
	// db.Query()表示向数据库发送一个query
	rows, err := DB.Query("SELECT * FROM businesscities;")
	if err != nil {
		fmt.Printf("DB.Query:%v invalid,err:%v
", rows, err)
	}
	if rows == nil {
		fmt.Println("没有数据")
	}
	defer rows.Close() // 很重要;

	for rows.Next() {
		err := rows.Scan(&id, &areacode, &cityname, &citypinyin)
		if err != nil {
			fmt.Println(err)
		}
		fmt.Println(id, areacode, cityname, citypinyin)
	}

	// 遍历完成后检查error;
	err = rows.Err()
	if err != nil {
		fmt.Println(err)
	}
}
  • DB, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/DBName")
    • sql.Open的第一个参数是driver名称,其他的driver还有如sqlite3等;
    • 第二个参数是driver连接数据库的信息;
    • DB不是连接,并且只有当需要使用时才会创建连接;
    • sql.DB的设计就是用来作为长连接使用的。不要频繁Open, Close。比较好的做法是,为每个不同的datastore建一个DB对象,保持这些对象Open。另外,sql.Open()的Close()可有可无的原因:
      • 官方说明文档:It is rare to Close a DB, as the DB handle is meant to be long-lived and shared between many goroutines.
      • Closing a DB is useful if you don”t plan to use the database again. It does all the cleanup that would be done at program termination but allows the program to continue to run.
    • 如果想立即验证连接,需要用Ping()方法;DB.Ping()
  • rows, err := DB.Query("SELECT * FROM businesscities;"): db.Query()表示向数据库发送一个query代码;
  • 对于rows来说,defer rows.Close()非常重要
  • 遍历rows使用rows.Next(), 把遍历到的数据存入变量使用rows.Scan()
  • 遍历完成后再检查下是否有error,rows.Err()

搭建子命令“架子”

本文不再赘述子命令的”架子“

  • Go语言单词格式转换命令行工具 – KpHang – 云海天 (cnblogs.com)
  • Go语言便捷的时间命令行工具 – KpHang – 云海天 (cnblogs.com)

目标:把某个数据库内的某一个表转换为Go语言结构体;数据源:MySQL中的information_schema库中有个COLUMNS表,里面记录了mysql所有库中所有表的字段信息;想一想需要什么功能函数?

  • 与数据库建立链接;
  • 数据库查询,获取想要的信息;
  • 解析查询结果,转换为结构体字符串,输出;

功能函数放在internal包中,不对外公布;

├── internal
│   ├── sql2struct
│   │   ├── mysql.go
│   │   └── template.go

链接数据库并查询

internal/sql2struct/mysql.go中。

定义结构体

面向对象编程,要思考需要定义那些结构体。

// 整个数据库连接的核心对象;
type DBModel struct {
	DBEngine *sql.DB
	DBInfo   *DBInfo
}

// 连接MySQL的一些基本信息;
type DBInfo struct {
	DBType   string
	Host     string
	Username string
	Password string
	Charset  string
}

// TableColumn用来存放COLUMNS表中我们需要的一些字段;
type TableColumn struct {
	ColumnName    string
	DataType      string
	IsNullable    string
	ColumnKey     string
	ColumnType    string
	ColumnComment string
}
  • DBModel:整个数据库连接的核心对象,包括DB主体,DBInfo;
  • DBInfo:数据库链接信息,用此信息链接数据库,赋值给DBEngin;

链接数据库前,先创建DBModel核心对象:

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

链接数据库

// (m *DBModel)  有两个东西,此函数是获取第一个东西	DBEngine *sql.DB
func (m *DBModel) Connect() error {
	var err error
	s := "%s:%s@tcp(%s)/information_schema?" +
		"charset=%s&parseTime=True&loc=Local"
	dsn := fmt.Sprintf( // dsn dataSourceName
		s,
		m.DBInfo.Username,
		m.DBInfo.Password,
		m.DBInfo.Host,
		m.DBInfo.Charset,
	)
	m.DBEngine, err = sql.Open(m.DBInfo.DBType, dsn)
	// 第一个参数为驱动名称,eg mysql;
	// 第二个参数为驱动连接数据库的连接信息;dsn dataSourceName
	if err != nil {
		return err
	}
	return nil
}
  • m.DBEngine, err = sql.Open(m.DBInfo.DBType, dsn)

数据库查询

func (m *DBModel) GetColumns(dbName, tableName string) ([]*TableColumn, error) {
	query := "SELECT COLUMN_NAME, DATA_TYPE, COLUMN_KEY, " +
		"IS_NULLABLE, COLUMN_TYPE, COLUMN_COMMENT " +
		"FROM COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? "
		// SELECT COLUMN_NAME, DATA_TYPE, COLUMN_KEY, IS_NULLABLE, COLUMN_TYPE, COLUMN_COMMENT FROM COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
	rows, err := m.DBEngine.Query(query, dbName, tableName)
	// SELECT COLUMN_NAME, DATA_TYPE, COLUMN_KEY, IS_NULLABLE, COLUMN_TYPE, COLUMN_COMMENT FROM COLUMNS WHERE TABLE_SCHEMA = "dbName" AND TABLE_NAME = "tableName"
	if err != nil {
		return nil, err
	}
	if rows == nil {
		return nil, errors.New("没有数据")
	}
	defer rows.Close()

	var columns []*TableColumn
	for rows.Next() {
		var column TableColumn
		err := rows.Scan(&column.ColumnName, &column.DataType,
			&column.ColumnKey, &column.IsNullable, &column.ColumnType, &column.ColumnComment)
		if err != nil {
			return nil, err
		}

		columns = append(columns, &column)
	}
	return columns, nil
}
  • rows, err := m.DBEngine.Query(query, dbName, tableName),Query会把query中的?,替换成后面的参数,以字符串的形式;

    SELECT COLUMN_NAME, DATA_TYPE, COLUMN_KEY, IS_NULLABLE, COLUMN_TYPE, COLUMN_COMMENT FROM COLUMNS WHERE TABLE_SCHEMA = "dbName" AND TABLE_NAME = "tableName"

    测试,大概如下:

    image-20221122203503619

  • rows.Next()rows.Scan()遍历查询结果,每一列信息都放在一个TableColumn结构体中,最终返回一个包括所有列的[]*TableColumn

转换为结构体模板

将上面查询返回的[]*TableColumn,转化为结构体模板,最终输出结构如下:

$ go run ./main.go sql struct --username user --password password --db=dbName --table=tableName
# Output:
type Businesscities struct {
                 // areacode 
                 Areacode       string  `json:"areacode"`
                 // cityname 
                 Cityname       string  `json:"cityname"`
                 // citypinyin 
                 Citypinyin     string  `json:"citypinyin"`
                 // id 
                 Id     int32   `json:"id"`
        }

func (model Businesscities) TableName() string {
return "businesscities"
}
  • ` ` 是结构体标签;Go系列:结构体标签 – 掘金 (juejin.cn)

定义结构体

最终的结构体模板:

这个结构体是最终转化后,在终端输出的结构体格式化字符串;

const strcutTpl = `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}}"
}`

// 结构体模板对象;
type StructTemplate struct {
	structTpl string
}

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

数据表的某一列信息,转换为如下格式:

// 存储转化后的Go结构体对象;
type StructColumn struct {
	Name    string
	Type    string
	Tag     string
	Comment string
}

模板渲染用的数据对象:

// 用来存储最终用于渲染的模版对象信息;
type StructTemplateDB struct {
	TableName string
	Columns   []*StructColumn
}
  • TableName -> 结构体名字;
  • Columns -> 结构体内的变量;

模板渲染前的数据处理

上面的数据库查询,获取到了一个[]*TableColumn,要把此数据,转换为[]*StructColumn:

func (t *StructTemplate) AssemblyColumns(tbColumns []*TableColumn) []*StructColumn {
	tplColumns := make([]*StructColumn, 0, len(tbColumns))
	for _, column := range tbColumns {
		tag := fmt.Sprintf("`"+"json:"+""%s""+"`", column.ColumnName)
		tplColumns = append(tplColumns, &StructColumn{
			Name:    column.ColumnName,
			Type:    DBTypeToStructType[column.DataType],
			Tag:     tag,
			Comment: column.ColumnComment,
		})
	}

	return tplColumns
}
  • []*StructColumn 每一个元素,最终转化为输出结构体模板中的一个成员变量;

  • // DataType字段的类型与Go结构体中的类型不是完全一致的;
    var DBTypeToStructType = map[string]string{
    	"int":        "int32",
    	"tinyint":    "int8",
    	"smallint":   "int",
    	"mediumint":  "int64",
      ...
    

渲染模板

  • 模板:structTpl
  • 用到的数据:[]*StructColumn
func (t *StructTemplate) Generate(tableName string, tplColumns []*StructColumn) error {
	tpl := template.Must(template.New("sql2struct").Funcs(template.FuncMap{
		"ToCamelCase": word.UnderscoreToUpperCamelCase, // 大驼峰
	}).Parse(t.structTpl))

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

	return nil
}
  • template包使用详情:[译]Golang template 小抄 (colobu.com)

  • tpl.Execute(os.Stdout, tplDB)后,对structTpl解析:

    const strcutTpl = `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}}"
    }`
    
    • 	// 遍历切片(tplDB.Columns)
      	{{range .Columns}}	
      	
      	{{end}}}
      
    • // 设置结构体成员变量的注释;
      // 定义变量length,if length > 0 注释用comment,else 注释用Name;
      {{ $length := len .Comment}} {{ if gt $length 0 }} // {{.Comment}} {{else}}// {{.Name}} {{ end }}
      
    • // 设置结构体成员变量;
      // type字符串长度 大于0,就正常设置大驼峰Name,类型,Tag;else 只设置Name;
      {{ $typeLen := len .Type }} {{ if gt $typeLen 0 }}{{.Name | ToCamelCase}}	{{.Type}}	{{.Tag}}{{ else }}{{.Name}}{{ end }}
      

sql子命令测试

$ go run ./main.go sql struct --username user --password password --db=dbName --table=tableName
# Output:
type TableName struct {
                 // areacode 
                 Areacode       string  `json:"areacode"`
                 // cityname 
                 Cityname       string  `json:"cityname"`
                 // citypinyin 
                 Citypinyin     string  `json:"citypinyin"`
                 // id 
                 Id     int32   `json:"id"`
        }

func (model Businesscities) TableName() string {
return "businesscities"
}
  • 此处有个问题:go语言编程之旅 sql语句到结构体的转换,”号打印出 &,#,3,4,; 问题 – Go语言中文网 – Golang中文社区 (studygolang.com)
hmoban主题是根据ripro二开的主题,极致后台体验,无插件,集成会员系统
自学咖网 » Go语言SQL语句到结构体的转换命令行工具