golang利用模板生成資料庫表對應的模型及操作函式

changjixiong發表於2016-11-13

起因


  很多年以前,當我第一次接觸到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值比較好。

相關文章