GORM 強大的代碼生成工具 — gorm-gen

GORM 的那些困擾

GORM 進入到 2.0 時代之後解決了很多 jinzhu/gorm 時代的問題,整體的擴展性以及功能也更強大。但總有一些繞不開的問題困擾着我們。

SQL 注入

Object Relational Mapping 的定位就是幫助開發者減輕心智負擔,你不用再去思考業務 object 和 數據表 relation 之間的對應,ORM 框架來幫你完成。我們只需要簡單的在 object 上加上 tag,剩下怎麼拼 SQL,怎麼 Scan 數據後寫入 object 就交給 ORM 來完成。業務開發者不需要操心這個。

問題就在這裏,這樣的定位勢必導致 ORM 被反射和 interface{} 滿天飛,你既然要通用,按照 Golang 目前的能力,就勢必要在運行時做類型轉換,用各種反射黑科技。

但是,反射頂多能告訴你當前是什麼,不能來校驗。因爲 ORM 是不感知業務的。要求它來校驗輸入數據的類型,格式,合法性是不現實的。使用方法十分靈活的查詢接口很容易造成研發對接口的誤用,從而導致 SQL 注入。

複雜 SQL

GORM 作爲 ORM 框架並沒有提供任何輔助代碼開發的功能,導致面對較爲複雜的數據庫表查詢場景時,開發者需逐條手寫數據表中的列與對應結構體的成員變量,單調且重複的查詢功能也需要手動複製,稍不注意就會造成不易察覺的拼寫錯誤。

其實在 Golang 泛型比較弱的情況下,使用【代碼生成】依然是解決個性化場景的經典方案,這樣繞開了 interface{},我們就可以做更多校驗,也省去了斷言。GORM 其實也是基於這個思路,推出了自己的【代碼生成工具】:Gen。

gorm-gen

Gen: Friendly & Safer GORM powered by Code Generation

這裏需要說明,Gen 並不是一個三方突發奇想做的庫,而是作爲 GORM 的官方工具,在 go-gorm 組織下提供的。本身也是由 jinzhu 大佬和相關同學一起維護,所以大家可以放心,這是個官方的解決方案。

我們可以使用 GORM,也可以用 Gen 來生成代碼,只是 API 層的兩種實現,底層的能力都是一樣的。

gen[1] 對自己的定位就是通過代碼生成,讓 GORM 更加友好(針對複雜 SQL 場景也能處理),也更加安全(增加類型校驗)。

  • CRUD or DIY query method code generation

  • Auto migration from database to code

  • Transactions, Nested Transactions, Save Point, RollbackTo to Saved Point

  • Competely compatible with GORM

  • Developer Friendly

  • Multiple Generate modes

從真正使用上來說,我覺得最核心的 feature 在於:

  1. 字段類型校驗,過濾參數錯誤,爲數字、字符串、布爾類型、時間類型硬編碼制定差異化類型安全的表達式方法,杜絕了 SQL 注入的風險,能跑就安全;

  2. 映射數據庫表像,DB 裏面有數據表就能生成對應的 Golang 結構體;

  3. 用註釋的形式描述查詢的邏輯後,一鍵即可生成對應的安全可靠查詢 API。

此外還有一個好處是,我們用 GORM 來 Find 數據時,總還是要先聲明結果,然後把指針傳入 API,由 GORM 進行填充,而有了 Gen 之後,直接返回對應的數據結構,免於提前實例化數據後在注入 API 的繁瑣。

複雜 SQL 怎麼解

通過 interface 指明我們希望查詢的語義,自動生成查詢代碼,這個可以說是 gorm-gen 最香的能力了。原因很簡單:

所以,大家最關心的能力還是,能不能我定義個接口,說清楚我需要什麼數據(或者 sql 提供出來),你自己來生成查詢代碼,gorm 的封裝,類型安全,數據轉換等等,一切都由工具搞定,作爲業務開發者,我只管調用從你生成的方法就行。能不能做到?

能!這就是 gorm-gen 帶來的能力。這一節我們直接來實戰演練一下。

本節實例源碼在 db-demo[2] 感興趣的同學可以看一下 gendemo 目錄下的代碼。

我們還是通過 go get 添加 gen 的依賴

go get -u gorm.io/gen

然後在項目中 import "gorm.io/gen" 進來即可。

首先我們創建一個 gendemo 目錄,準備一些業務結構體,這些就是我們的 PO(需要持久化的對象)。目錄結構如下:

我們來看看每個文件幹了什麼。

這裏很簡單,只是維護了內存中的數據庫連接,完成初始化,和業務無關。

package dal

import (
 "fmt"
 "sync"

 "gorm.io/gorm"

 "gorm.io/driver/sqlite"

 "github.com/ag9920/db-demo/gendemo/dal/model"
)

var DB *gorm.DB
var once sync.Once

func init() {
 once.Do(func() {
  DB = ConnectDB().Debug()
  _ = DB.AutoMigrate(&model.User{}&model.Passport{})
 })
}

func ConnectDB() (conn *gorm.DB) {
 conn, err := gorm.Open(sqlite.Open(":memory:")&gorm.Config{})
 if err != nil {
  panic(fmt.Errorf("cannot establish db connection: %w", err))
 }
 return conn
}

這裏我們定義了兩個業務模型:User 以及 Passport。

package model

import (
 "database/sql/driver"
 "fmt"
 "strings"
 "time"

 "gorm.io/gorm"
)

type Username string

type Password string

func (p *Password) Scan(src interface{}) error {
 *p = Password(fmt.Sprintf("@%v@", src))
 return nil
}

func (p *Password) Value() (driver.Value, error) {
 *p = Password(strings.Trim(string(*p)"@"))
 return p, nil
}

type User struct {
 gorm.Model        // ID uint CreatAt time.Time UpdateAt time.Time DeleteAt gorm.DeleteAt If it is repeated with the definition will be ignored
 ID         uint   `gorm:"primary_key"`
 Name       string `gorm:"column:name"`
 Age        int    `gorm:"column:age;type:varchar(64)"`
 Role       string `gorm:"column:role;type:varchar(64)"`
 Friends    []User `gorm:"-"` // only local used gorm ignore
}

type Passport struct {
 ID        int
 Username  Username // will be field.String
 Password  Password // will be field.Field because type Password set Scan and Value
 LoginTime time.Time
}

這裏定義了我們希望實現的接口定義。這裏本質上就是通過【註釋】告訴 gen,我們希望獲取什麼樣的數據,sql 怎麼生成。所以註釋的寫法很重要。大家先看下代碼,我們下面會說:

package model

import "gorm.io/gen"

type Method interface {
 // Where("name=@name and age=@age")
 FindByNameAndAge(name string, age int) (gen.T, error)
 //sql(select id,name,age from users where age>18)
 FindBySimpleName() ([]gen.T, error)

 //sql(select id,name,age from @@table where age>18
 //{{if cond1}}and id=@id {{end}}
 //{{if name == ""}}and @@col=@name{{end}})
 FindByIDOrName(cond1 bool, id int, col, name string) (gen.T, error)

 //sql(select * from users)
 FindAll() ([]gen.M, error)

 //sql(select * from users limit 1)
 FindOne() gen.M

 //sql(select address from users limit 1)
 FindAddress() (gen.T, error)
}

// only used to User
type UserMethod interface {
 //where(id=@id)
 FindByID(id int) (gen.T, error)

 //select * from users where age>18
 FindAdult() ([]gen.T, error)

 //select * from @@table
 // {{where}}
 //  {{if role=="user"}}
 //   id=@id
 //  {{else if role=="admin"}}
 //   role="user" or rule="normal-admin"
 //  {{else}}
 //   role="user" or role="normal-admin" or role="admin"
 //  {{end}}
 // {{end}}
 FindByRole(role string, id int)

 //update users
 // {{set}}
 //  update_time=now(),
 //  {{if name != ""}}
 //   name=@name
 //  {{end}}
 // {{end}}
 // where id=@id
 UpdateUserName(name string, id int) error
}

註釋的內容可以描述 gorm 的Where查詢內容,也可以是一個完整的 SQL 查詢語句。

// Where("name=@name and age=@age")
FindByNameAndAge(name string, age int) (gen.T, error)
//sql(select id,name,age from users where age>18)
FindBySimpleName() ([]gen.T, error)
//sql(select id,name,age from @@table where age>18
//{{if cond1}}and id=@id {{end}}
//{{if name == ""}}and @@col=@name{{end}})
FindByIDOrName(cond1 bool, id int, col, name string) (gen.T, error)

下面兩個小節我們來看一下注釋的規則:

佔位符

子句

OK,現在我們有了業務 Model,有了我們希望生成的接口。該讓 gorm-gen 出場了!

首先我們切換到 cmd/generate 包,看看我們需要做什麼來告訴 gorm-gen 如何生成:

package main

import (
 "github.com/ag9920/db-demo/gendemo/dal/model"
 "gorm.io/gen"
)

func main() {

 g := gen.NewGenerator(gen.Config{
  OutPath: "../../dal/query",
  Mode:    gen.WithDefaultQuery,
 })

 g.ApplyBasic(model.Passport{}, model.User{})

 g.ApplyInterface(func(model.Method) {}, model.User{})
 g.ApplyInterface(func(model.UserMethod) {}, model.User{})

 g.Execute()
}

好了,我們切換到外層的 generate.sh

#!/bin/bash

PROJECT_DIR=$(dirname "$0")
GENERATE_DIR="$PROJECT_DIR/cmd/generate"

cd "$GENERATE_DIR" || exit

echo "Start Generating"
go run .

這裏來調用 go run 啓動我們的 main 函數即可。

萬事俱備,我們來執行一下:

$ ./generate.sh

Start Generating
2022/08/18 17:12:01 Start generating code.
2022/08/18 17:12:01 generate query file: /Users/ag9920/go/src/github.com/ag9920/db-demo/gendemo/dal/query/passports.gen.go
2022/08/18 17:12:01 generate query file: /Users/ag9920/go/src/github.com/ag9920/db-demo/gendemo/dal/query/users.gen.go
2022/08/18 17:12:01 generate query file: /Users/ag9920/go/src/github.com/ag9920/db-demo/gendemo/dal/query/gen.go
2022/08/18 17:12:01 Generate code done.

Bingo,任務完成。此時,我們再來看看 dal 目錄,你會發現多了個 query 文件夾

其中,passports.gen.go 以及 users.gen.go 分別對應我們的兩個 model,很直觀。而 gen.go 則是通用的查詢代碼。我們來看看裏面有什麼:

// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.

package query

import (
 "context"
 "database/sql"

 "gorm.io/gorm"
)

var (
 Q        = new(Query)
 Passport *passport
 User     *user
)

func SetDefault(db *gorm.DB) {
 *Q = *Use(db)
 Passport = &Q.Passport
 User = &Q.User
}

func Use(db *gorm.DB) *Query {
 return &Query{
  db:       db,
  Passport: newPassport(db),
  User:     newUser(db),
 }
}

type Query struct {
 db *gorm.DB

 Passport passport
 User     user
}

func (q *Query) Available() bool { return q.db != nil }

func (q *Query) clone(db *gorm.DB) *Query {
 return &Query{
  db:       db,
  Passport: q.Passport.clone(db),
  User:     q.User.clone(db),
 }
}

type queryCtx struct {
 Passport *passportDo
 User     *userDo
}

func (q *Query) WithContext(ctx context.Context) *queryCtx {
 return &queryCtx{
  Passport: q.Passport.WithContext(ctx),
  User:     q.User.WithContext(ctx),
 }
}

func (q *Query) Transaction(fc func(tx *Query) error, opts ...*sql.TxOptions) error {
 return q.db.Transaction(func(tx *gorm.DB) error { return fc(q.clone(tx)) }, opts...)
}

func (q *Query) Begin(opts ...*sql.TxOptions) *QueryTx {
 return &QueryTx{q.clone(q.db.Begin(opts...))}
}

type QueryTx struct{ *Query }

func (q *QueryTx) Commit() error {
 return q.db.Commit().Error
}

func (q *QueryTx) Rollback() error {
 return q.db.Rollback().Error
}

func (q *QueryTx) SavePoint(name string) error {
 return q.db.SavePoint(name).Error
}

func (q *QueryTx) RollbackTo(name string) error {
 return q.db.RollbackTo(name).Error
}

這裏很好理解,其實就是把我們的 DAL 操作都封裝了起來,提供了常見的 WithContext, Transaction 等方法。業務只需要構造出一個 gorm 鏈接,傳入 SetDefault 就能使用。

自動生成的數據訪問方法比較多,而且還有我們指定的兩個接口實現。這裏我們就不貼完整代碼了,感興趣的同學可以到上面的源碼倉庫瞭解。這裏我們抽出幾個典型的代碼片段看一下。

//Where("name=@name and age=@age")
func (u userDo) FindByNameAndAge(name string, age int) (result *model.User, err error) {
 params := make(map[string]interface{}, 0)

 var generateSQL strings.Builder
 params["name"] = name
 params["age"] = age
 generateSQL.WriteString("name=@name and age=@age ")

 var executeSQL *gorm.DB
 if len(params) > 0 {
  executeSQL = u.UnderlyingDB().Where(generateSQL.String(), params).Take(&result)
 } else {
  executeSQL = u.UnderlyingDB().Where(generateSQL.String()).Take(&result)
 }
 err = executeSQL.Error
 return
}

//sql(select id,name,age from users where age>18)
func (u userDo) FindBySimpleName() (result []*model.User, err error) {
 var generateSQL strings.Builder
 generateSQL.WriteString("select id,name,age from users where age>18 ")

 var executeSQL *gorm.DB
 executeSQL = u.UnderlyingDB().Raw(generateSQL.String()).Find(&result)
 err = executeSQL.Error
 return
}

//sql(select id,name,age from @@table where age>18
//{{if cond1}}and id=@id {{end}}
//{{if name == ""}}and @@col=@name{{end}})
func (u userDo) FindByIDOrName(cond1 bool, id int, col string, name string) (result *model.User, err error) {
 params := make(map[string]interface{}, 0)

 var generateSQL strings.Builder
 generateSQL.WriteString("select id,name,age from users where age>18 ")
 if cond1 {
  params["id"] = id
  generateSQL.WriteString("and id=@id ")
 }
 if name == "" {
  params["name"] = name
  generateSQL.WriteString("and " + u.Quote(col) + "=@name ")
 }

 var executeSQL *gorm.DB
 if len(params) > 0 {
  executeSQL = u.UnderlyingDB().Raw(generateSQL.String(), params).Take(&result)
 } else {
  executeSQL = u.UnderlyingDB().Raw(generateSQL.String()).Take(&result)
 }
 err = executeSQL.Error
 return
}

看到了麼?這就是基於我們的 interface 生成的實現。gorm-gen 很貼心的把我們的註釋也搬了過來,我們可以參照着對比一下。

其實本質就是用我們給的 SQL, 對變量進行填充,通過 GORM 提供的 Raw 和 Exec 接口拿到最後的結果。屬於最上層的封裝,但可以大大減輕我們的負擔,簡單的 SQL 可能還看不出來,我們對比一下複雜的:

//select * from @@table
// {{where}}
//  {{if role=="user"}}
//   id=@id
//  {{else if role=="admin"}}
//   role="user" or rule="normal-admin"
//  {{else}}
//   role="user" or role="normal-admin" or role="admin"
//  {{end}}
// {{end}}
func (u userDo) FindByRole(role string, id int) {
 params := make(map[string]interface{}, 0)

 var generateSQL strings.Builder
 generateSQL.WriteString("select * from users ")
 var whereSQL0 strings.Builder
 if role == "user" {
  params["id"] = id
  whereSQL0.WriteString("id=@id ")
 } else if role == "admin" {
  whereSQL0.WriteString("role=\"user\" or rule=\"normal-admin\" ")
 } else {
  whereSQL0.WriteString("role=\"user\" or role=\"normal-admin\" or role=\"admin\" ")
 }
 helper.JoinWhereBuilder(&generateSQL, whereSQL0)

 if len(params) > 0 {
  _ = u.UnderlyingDB().Exec(generateSQL.String(), params)
 } else {
  _ = u.UnderlyingDB().Exec(generateSQL.String())
 }
 return
}

//update users
// {{set}}
//  update_time=now(),
//  {{if name != ""}}
//   name=@name
//  {{end}}
// {{end}}
//where id=@id
func (u userDo) UpdateUserName(name string, id int) (err error) {
 params := make(map[string]interface{}, 0)

 var generateSQL strings.Builder
 generateSQL.WriteString("update users ")
 var setSQL0 strings.Builder
 setSQL0.WriteString("update_time=now(), ")
 if name != "" {
  params["name"] = name
  setSQL0.WriteString("name=@name ")
 }
 helper.JoinSetBuilder(&generateSQL, setSQL0)
 params["id"] = id
 generateSQL.WriteString("where id=@id ")

 var executeSQL *gorm.DB
 if len(params) > 0 {
  executeSQL = u.UnderlyingDB().Exec(generateSQL.String(), params)
 } else {
  executeSQL = u.UnderlyingDB().Exec(generateSQL.String())
 }
 err = executeSQL.Error
 return
}

Where,Set 現在都可以根據實際的數據情況進行調整。只要我們把註釋寫對,生成的代碼就是安全的,非常方便。

這裏也可以看出,gorm-gen 提供的【SQL 模板】 => 【接口實現】的能力還是非常靈活的,子句和佔位符同時使用,基本上大部分場景都可以覆蓋。

基礎 API

除此之外,我們通過 ApplyBasic 生成的基礎的訪問代碼也非常有用,這是對 GORM API 的加強,還是基於 users.gen.go,我們看一下生成的代碼什麼樣:

func (u userDo) Create(values ...*model.User) error {
 if len(values) == 0 {
  return nil
 }
 return u.DO.Create(values)
}

func (u userDo) CreateInBatches(values []*model.User, batchSize int) error {
 return u.DO.CreateInBatches(values, batchSize)
}

// Save : !!! underlying implementation is different with GORM
// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
func (u userDo) Save(values ...*model.User) error {
 if len(values) == 0 {
  return nil
 }
 return u.DO.Save(values)
}

func (u userDo) First() (*model.User, error) {
 if result, err := u.DO.First(); err != nil {
  return nil, err
 } else {
  return result.(*model.User), nil
 }
}

func (u userDo) Take() (*model.User, error) {
 if result, err := u.DO.Take(); err != nil {
  return nil, err
 } else {
  return result.(*model.User), nil
 }
}

func (u userDo) Last() (*model.User, error) {
 if result, err := u.DO.Last(); err != nil {
  return nil, err
 } else {
  return result.(*model.User), nil
 }
}

func (u userDo) Find() ([]*model.User, error) {
 result, err := u.DO.Find()
 return result.([]*model.User), err
}

func (u userDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.User, err error) {
 buf := make([]*model.User, 0, batchSize)
 err = u.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
  defer func() { results = append(results, buf...) }()
  return fc(tx, batch)
 })
 return results, err
}

func (u userDo) FindInBatches(result *[]*model.User, batchSize int, fc func(tx gen.Dao, batch int) error) error {
 return u.DO.FindInBatches(result, batchSize, fc)
}

調用生成的代碼

其實這一步就更簡單了,我們在 dal/query 目錄下已經有了生成的代碼,回憶一下,在 gen.go 裏面我們還有對外暴露的方法來獲取到這個 DAO:

import (
 "context"
 "database/sql"

 "gorm.io/gorm"
)

var (
 Q        = new(Query)
 Passport *passport
 User     *user
)

func SetDefault(db *gorm.DB) {
 *Q = *Use(db)
 Passport = &Q.Passport
 User = &Q.User
}

func Use(db *gorm.DB) *Query {
 return &Query{
  db:       db,
  Passport: newPassport(db),
  User:     newUser(db),
 }
}

type Query struct {
 db *gorm.DB

 Passport passport
 User     user
}

最終我們是靠這個 Query 對象來作爲 DAO,對外提供查詢,更新能力。所以這裏我們有兩種方案:

  1. 調用 SetDefault 之後,直接引用兩個對象對應的分別的 DAO:Passport 或 User。

  2. 通過 Use 方法,傳入一個 gorm.DB 鏈接,拿到一個 *Query 對象,這裏已經包含了兩個模型的 DAO,也可以直接使用。

這裏我們引用官方的最佳實踐,來看看結合生成的代碼,可以如何完成增刪改查,非常方便:

import (
 "context"
 "fmt"

 "gorm.io/hints"

 "github.com/ag9920/db-demo/gendemo/dal"
 "github.com/ag9920/db-demo/gendemo/dal/model"
 "github.com/ag9920/db-demo/gendemo/dal/query"
)

var q = query.Use(dal.DB.Debug())

func Create(ctx context.Context) {
 var err error
 ud := q.User.WithContext(ctx)

 userData := &model.User{ID: 1, Name: "modi"}
 // INSERT INTO `users` (`created_at`,`updated_at`,`deleted_at`,`name`,`age`,`role`,`id`) VALUES ('2021-09-13 20:05:51.389','2021-09-13 20:05:51.389',NULL,'modi',0,'',1)
 err = ud.Create(userData)

 userDataArray := []*model.User{{ID: 2, Name: "A"}{ID: 3, Name: "B"}}
 err = ud.CreateInBatches(userDataArray, 2)
 // INSERT INTO `users` (`created_at`,`updated_at`,`deleted_at`,`name`,`age`,`role`,`id`) VALUES ('2021-09-13 20:05:51.403','2021-09-13 20:05:51.403',NULL,'A',0,'',2),('2021-09-13 20:05:51.403','2021-09-13 20:05:51.403',NULL,'B',0,'',3)

 userData.Name = "new name"
 err = ud.Save(userData)
 // INSERT INTO `users` (`created_at`,`updated_at`,`deleted_at`,`name`,`age`,`role`,`id`) VALUES ('2021-09-13 20:05:51.389','2021-09-13 20:05:51.409',NULL,'new name',0,'',1) ON DUPLICATE KEY UPDATE `updated_at`=VALUES(`updated_at`),`deleted_at`=VALUES(`deleted_at`),`name`=VALUES(`name`),`age`=VALUES(`age`),`role`=VALUES(`role`)
}

func Delete(ctx context.Context) {
 var err error
 u, ud := q.User, q.User.WithContext(ctx)

 _, err = ud.Where(u.ID.Eq(1)).Delete()
 // UPDATE `users` SET `deleted_at`='2021-09-13 20:05:51.418' WHERE `users`.`id` = 1 AND `users`.`deleted_at` IS NULL

 _, err = ud.Where(u.ID.In(2, 3)).Delete()
 // UPDATE `users` SET `deleted_at`='2021-09-13 20:05:51.428' WHERE `users`.`id` IN (2,3) AND `users`.`deleted_at` IS NULL

 _, err = ud.Where(u.ID.Gt(100)).Unscoped().Delete()
 // DELETE FROM `users` WHERE `users`.`id` > 100
}


func Query(ctx context.Context) {
 var err error
 var user *model.User
 var users []*model.User

 u, ud := q.User, q.User.WithContext(ctx)

 /*--------------Basic query-------------*/
 user, err = ud.Take()
 // SELECT * FROM `users` WHERE `users`.`deleted_at` IS NULL LIMIT 1
 fmt.Printf("query 1 item: %+v", user)

 user, err = ud.Where(u.ID.Gt(100), u.Name.Like("%T%")).Take()
 // SELECT * FROM `users` WHERE `users`.`id` > 100 AND `users`.`name` LIKE '%T%' AND `users`.`deleted_at` IS NULL LIMIT 1
 fmt.Printf("query conditions got: %+v", user)

 user, err = ud.Where(ud.Columns(u.ID).In(ud.Select(u.ID.Min()))).First()
 // SELECT * FROM `users` WHERE `users`.`id` IN (SELECT MIN(`users`.`id`) FROM `users` WHERE `users`.`deleted_at` IS NULL) AND `users`.`deleted_at` IS NULL
 // ORDER BY `users`.`id` LIMIT 1
 fmt.Printf("subquery 1 got item: %+v", user)

 user, err = ud.Where(ud.Columns(u.ID).Eq(ud.Select(u.ID.Max()))).First()
 // SELECT * FROM `users` WHERE `users`.`id` = (SELECT MAX(`users`.`id`) FROM `users` WHERE `users`.`deleted_at` IS NULL) AND `users`.`deleted_at` IS NULL
 // ORDER BY `users`.`id` LIMIT 1
 fmt.Printf("subquery 2 got item: %+v", user)

 users, err = ud.Distinct(u.Name).Find()
 // SELECT DISTINCT `users`.`name` FROM `users` WHERE `users`.`deleted_at` IS NULL
 fmt.Printf("select distinct got: %d", len(users))

 /*--------------Diy query-------------*/
 user, err = ud.FindByNameAndAge("tom", 29)
 // SELECT * FROM `users` WHERE name='tom' and age=29 AND `users`.`deleted_at` IS NULL
 fmt.Printf("FindByNameAndAge: %+v", user)
}

總結

今天我們通過定義接口,生成實現代碼這個場景作爲切入點,瞭解了 gorm-gen 的最核心功能。其實生成的代碼還是非常簡潔,且功能強大的。並且支持從 table 直接生成業務結構。建議大家仔細看看我們的 demo 以及官方文檔 [1],相信對於 gorm-gen 熟練會幫助業務開發提效,安全。

參考資料

[1]

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

[2]

db-demo: https://github.com/ag9920/db-demo

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/z9O-E_5jCOx50FLWZbFTLw