學習:SQL 語句到結構體的轉換 | Go 語言編程之旅 (eddycjy.com) 目標:SQL表轉換為Go語言結構體 可以線上體驗這個過程:SQL生成GO語言結構體 - 支持批量處理 (tl.beer) MySQL資料庫中的表結構,本質上是SQL語句。 CREATE TABLE `USER`( ...
學習: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}}
在模板中,會把管道符前面的運算結果作為參數傳遞給管道符後面的函數;
database/sql簡要應用
用Go語言鏈接MySQL資料庫,並查詢下表。
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\n", 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\n", 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語言結構體;數據源: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"
測試,大概如下:
-
用
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"
}