為hade增加model自動生成功能

軒脈刃發表於2022-02-15

大家好,我是軒脈刃。

我們寫業務的時候和db接觸是少不了的,那麼要生成model也是少不了的,如何自動生成model,想著要給hade框架增加個這樣的命令。

看了下網上的幾個開源專案,最終聚焦在兩個專案中:

https://github.com/go-gorm/gen

https://github.com/xxjwxc/gormt

gormt的gui是非常強大的,看文件都支援終端gui和windows的gui。但是gormt是一個工具,無法在另外一個專案中引入。

但是gen專案是gorm官方推出的,有jinzhu作者的參與。

所以我嘗試選擇gen專案來。

gen

gen其實不只是工具,它更像一個全新的orm封裝。gen專案生成出來的檔案有其實有兩個部分,一個是model,就是db的表和對應的model,以xxx.gen.go 命名。而另一個部分是每個model對應一套gen函式,這套gen函式基本上是對orm的二次封裝了。

image-20220211091540150

當然這套函式是基於gorm來封裝的,不過你可以完全脫離gorm來使用這套函式。

生成的方法示例如下:

g := gen.NewGenerator(gen.Config{
			OutPath:      "/Users/yejianfeng/Documents/workspace/gohade/hade/app/dal",
			ModelPkgPath: "/Users/yejianfeng/Documents/workspace/gohade/hade/app/dal/model",
			WithUnitTest: true,

			FieldNullable:     false,
			FieldCoverable:    true,
			FieldWithIndexTag: false,
			FieldWithTypeTag:  false,

			Mode: gen.WithDefaultQuery,
		})
		gormService := container.MustMake(contract.ORMKey).(contract.ORMService)
		db, err := gormService.GetDB(orm.WithConfigPath("database.default"))
		if err != nil {
			return err
		}

		g.UseDB(db)
		g.WithDataTypeMap(dataMap)
		//g.WithJSONTagNameStrategy(func(c string) string { return "-" })

		//g.ApplyBasic(model.Customer{})
		//g.ApplyBasic(g.GenerateAllTable()...)
		//g.GenerateModel("users")
		//g.GenerateModel("answers")
		//g.GenerateAllTable()
		g.ApplyBasic(g.GenerateAllTable()...)
		g.Execute()

使用起來像是這樣:

u := query.Use(db).User

// Get first matched record
user, err := u.WithContext(ctx).Where(u.Name.Eq("modi")).First()
// SELECT * FROM users WHERE name = 'modi' ORDER BY id LIMIT 1;

// Get all matched records
users, err := u.WithContext(ctx).Where(u.Name.Neq("modi")).Find()
// SELECT * FROM users WHERE name <> 'modi';

// IN
users, err := u.WithContext(ctx).Where(u.Name.In("modi", "zhangqiang")).Find()
// SELECT * FROM users WHERE name IN ('modi','zhangqiang');

// LIKE
users, err := u.WithContext(ctx).Where(u.Name.Like("%modi%")).Find()
// SELECT * FROM users WHERE name LIKE '%modi%';

最終生成的檔案如下:

image-20220211092157272

gen有一些高階的功能:

  • 自定義模型函式,且提供了通過sql語句的註釋自實現函式的方法
  • 提供了單元測試框架,你可以自己定義TestCase,來實現對某個自實現函式的單測
  • 智慧欄位查詢,在select語句的時候,可以根據輸出物件自動生成select的field

其實gen更像是另一個orm框架了,和facebook的ent類似,為每個model生成一套orm方法。gen是位元組跳動的無恆實驗室開發的產品,據說位元組內部正在將gorm切換到gen。gen的主打是安全,意思是,如果你的orm是完全使用gen來生成的,通過註釋sql而不是自己裸寫sql來生成,它更能保證安全性。(當然,因為所有的實現程式碼都是gen來自動生成的)。

我目前的認知還是覺得這套東西太重了一些,整個熟悉下來無異於需要了解另外一個orm框架的語法了。在使用gorm和gen上並沒有什麼太大的區別。

// 插入一條資料
	email := "foo@gmail.com"
	name := "foo"
	user := &model.User{
		ID:        0,
		Username:  name,
		Password:  "",
		Email:     email,
		CreatedAt: time.Time{},
	}
	dal.SetDefault(db)
	err := dal.User.WithContext(c).Create(user)
	if err != nil {
		c.AbortWithError(50001, err)
		return
	}
// 插入一條資料
	email := "foo@gmail.com"
	name := "foo"
	age := uint8(25)
	birthday := time.Date(2001, 1, 1, 1, 1, 1, 1, time.Local)
	user := &User{
		Name:         name,
		Email:        &email,
		Age:          age,
		Birthday:     &birthday,
		MemberNumber: sql.NullString{},
		ActivatedAt:  sql.NullTime{},
		CreatedAt:    time.Now(),
		UpdatedAt:    time.Now(),
	}
	err = db.Create(user).Error
	logger.Info(c, "insert user", map[string]interface{}{
		"id":  user.ID,
		"err": err,
	})

而且如果要寫出官網給出的這麼複雜的語句:

p := query.Use(db).Pizza

pizzas, err := p.WithContext(ctx).Where(
    p.WithContext(ctx).Where(p.Pizza.Eq("pepperoni")).
        Where(p.WithContext(ctx).Where(p.Size.Eq("small")).Or(p.Size.Eq("medium"))),
).Or(
    p.WithContext(ctx).Where(p.Pizza.Eq("hawaiian")).Where(p.Size.Eq("xlarge")),
).Find()

// SELECT * FROM `pizzas` WHERE (pizza = "pepperoni" AND (size = "small" OR size = "medium")) OR (pizza = "hawaiian" AND size = "xlarge")

我相信對新手來說真是一個不大容易的事情。

所以目前我還只傾向於使用gen的生成model的部分。

自動生成model命令設計

首先設計一下這個命令的產品形態。

./hade model gen --output=document/app/model/ --database=database.default

在命令列中的引數:

output: 必選,表示輸出的路徑
database: 可選,預設使用database.default

如果沒有設定db,或者output沒有設定,直接返回錯誤。

第一步是一個互動命令列工具,首先展示要生成的表列表選擇:

請選擇要生成模型的表格:
[] *
[] users
[] answers
[] questions

第二步確認要生成的目錄和檔案,以及覆蓋提示:

繼續下列操作會在目錄(xxxx)生成下列檔案:
user.gen.go(覆蓋)
answer.gen.go(新檔案)

請確認是否繼續?(Y/N)

第三步選擇後是一個生成模型的選項:

請選擇模型規則:
[] FieldNullable, 對於資料庫的可null欄位設定指標
[] FieldCoverable, 根據資料庫的Default設定欄位的預設值
[] FieldWithIndexTag, 根據資料庫的索引關係設定索引標籤
[] FieldWithTypeTag, 生成型別欄位

最後一步就是生成模型檔案了。

自動生成model命令實現

瞭解了gen和命令的設計,實現就很簡單了。

大概就分幾步吧:

  • 獲取資料庫中的所有表
  • 讓使用者多選要生成model的表格
    • 和現有的目錄中的檔案進行比對
  • 讓使用者多選要生成的model的選項,比如是否可null,是否有default設定等
  • 使用gen生成模型檔案

具體程式碼在 https://github.com/gohade/hade/blob/feature/model-gen/framework/command/model/model.go

其中程式碼實現方便稍微有幾個地方要注意下:

如何查詢一個資料庫中的所有表

使用gorm很方便就實現了

dbTables, err := db.Migrator().GetTables()

當使用者選擇了要生成的表格,要和硬碟中已有的檔案進行比對,如何操作

這裡其實涉及到兩個集合的交集和差集

我發現collection庫之前已經實現了差集,但是沒有實現交集。

這裡我補充實現了colleciton的交集,Intersect,並且將collection庫升級到1.4.1

// Intersect 比較兩個陣列,獲取兩個陣列交集,僅對基礎元素生效
Intersect (arr ICollection) ICollection

gen 庫如何只生成model不生成gen檔案?

g.UseDB(db)

for _, table := range genTables {
	g.GenerateModel(table)
}
g.Execute()

model命令驗證

驗證一下要model/gen命令

第一步,使用 ./hade model gen --output=app/model

image-20220215091522181

選擇其中的兩個表,answers和questions,提示目錄檔案

image-20220215091541908

下一步確認y繼續

image-20220215091643242

最後生成模型成功

image-20220215091659104

檢視檔案,確實生成了model

image-20220215091735434

功能完結。

相關文章